<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>树下集熵</title>
  
  <subtitle>the truth of life</subtitle>
  <link href="https://www.entropy-tree.top/atom.xml" rel="self"/>
  
  <link href="https://www.entropy-tree.top/"/>
  <updated>2025-10-19T02:59:08.462Z</updated>
  <id>https://www.entropy-tree.top/</id>
  
  <author>
    <name>Entropy Tree</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>异构数据库迁移</title>
    <link href="https://www.entropy-tree.top/2025/10/19/heterogeneous-database-migration/"/>
    <id>https://www.entropy-tree.top/2025/10/19/heterogeneous-database-migration/</id>
    <published>2025-10-19T02:23:53.000Z</published>
    <updated>2025-10-19T02:59:08.462Z</updated>
    
    <content type="html"><![CDATA[<p>以 Oracle 19c 迁移 MySQL 8.0 为例，寻求一种心智负担最小的可行方案。<span id="more"></span></p><p>阅前须知：</p><ul><li>本文只考虑 Oracle、MySQL、PostgreSQL 三种比较知名的关系型数据库管理系统。</li><li>本文不考虑依托于庞大云服务的云数据库，云服务供应商应该会提供更强大的方案。</li><li>对于视图、物化视图、触发器、存储过程以及其他高级特性的异构数据库迁移不在本文能够处理的范围内。</li><li>本文的视角以 Oracle 往 MySQL 的迁移为主，涉及到 Oracle 往 PostgreSQL 的迁移，可能有其他更好的方案。</li><li>由于授权问题，商业版工具不在本文考虑范围内。</li><li>未经过实际生产验证，本文所有方案不适用于线上生产环境。</li><li>若无特别说明，本文提到的“表结构”主要包含字段类型、索引、约束等信息。</li><li>本文虽然明面考虑的是异构数据库迁移，但实际上更倾向于考虑长期同时支持多个异构数据库运作。</li><li>本文提到的大部分框架，本人没有深度使用经验，在实际应用中使用这些框架可能会带来一些挑战。</li></ul><h3 id="oracle-快速搭建"><a class="markdownIt-Anchor" href="#oracle-快速搭建"></a> Oracle 快速搭建</h3><p>使用 docker 搭建 oracle</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull registry.cn-hangzhou.aliyuncs.com/zhuyijun/oracle:19c</span><br></pre></td></tr></table></figure></div><p>运行容器</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">docker run -itd --name oracle19c \</span><br><span class="line">--privileged=<span class="literal">true</span> \</span><br><span class="line">--restart=always \</span><br><span class="line">-p 1521:1521 \</span><br><span class="line">-p 5500:5500 \</span><br><span class="line">-e ORACLE_SID=orcl \</span><br><span class="line">-e ORACLE_PDB=orclPDB1 \</span><br><span class="line">-e ORACLE_PWD=123456 \</span><br><span class="line">-e ORACLE_EDITION=standard \</span><br><span class="line">-e ORACLE_CHARACTERSET=UTF8 \</span><br><span class="line">-e TZ=Asia/Shanghai \</span><br><span class="line">registry.cn-hangzhou.aliyuncs.com/zhuyijun/oracle:19c</span><br></pre></td></tr></table></figure></div><blockquote><p>Oracle 属于重量级数据库，docker 启动可能需要等待一定时间</p></blockquote><p>这个镜像可能存在 initorcl.ora 文件缺失的问题，可以从 pfile 下拷贝 init.ora.xxxx 文件至 dbs 目录下重命名为 initorcl.ora 文件，并设置为 644 权限。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it oracle19c bash</span><br><span class="line"></span><br><span class="line"><span class="built_in">cp</span> /opt/oracle/admin/ORCL/pfile/init.ora.* /opt/oracle/product/19c/dbhome_1/dbs/initorcl.ora</span><br><span class="line"><span class="built_in">chmod</span> 644 initorcl.ora</span><br></pre></td></tr></table></figure></div><h3 id="mysql-快速搭建"><a class="markdownIt-Anchor" href="#mysql-快速搭建"></a> MySQL 快速搭建</h3><p>使用以下 docker-compose.yaml 快速搭建一个 MySQL 实例</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:8.0</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;3306:3306&quot;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="number">12345</span></span><br></pre></td></tr></table></figure></div><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br></pre></td></tr></table></figure></div><h3 id="客户端连接"><a class="markdownIt-Anchor" href="#客户端连接"></a> 客户端连接</h3><p>Oracle 自带的 sqlplus 工具使用体验较差，为了方便，可以使用 Navicat premium lite 17.3.4 版本（初次使用需要注册登录）或者考虑使用 <a class="link"   href="https://www.oracle.com/database/sqldeveloper/technologies/sqlcl/" >Oracle SQLcl<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（Oracle 官方推出的相对于 sqlplus 的现代化命令行工具）。</p><p>Oracle 连接配置用户名为SYSTEM、密码为 docker 指定的<code>ORACLE_PWD</code>参数值、服务名称为 orclPDB1。</p><p>在 Navicat 中需要依次新建表空间（相当于MySQL中的数据库）、新建用户并关联表空间、授权，然后再以新用户的身份连接，才能使用其关联的表空间。</p><p>如果使用 sqlcl，可参考以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接到 PDB</span></span><br><span class="line">sql SYSTEM/123456@172.17.0.1:1521/orclPDB1</span><br></pre></td></tr></table></figure></div><p>进入 SQL Shell 内，创建表空间、用户和关联用户表空间、授权连接权限</p><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> TABLESPACE apps </span><br><span class="line">  DATAFILE <span class="string">&#x27;/opt/oracle/product/19c/dbhome_1/dbs/apps01&#x27;</span> SIZE <span class="number">500</span>M AUTOEXTEND <span class="keyword">ON</span>;</span><br><span class="line">  </span><br><span class="line">#  <span class="keyword">DROP</span> TABLESPACE apps INCLUDING CONTENTS <span class="keyword">AND</span> DATAFILES;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> appuser IDENTIFIED <span class="keyword">BY</span> &quot;Secure123&quot;</span><br><span class="line">  <span class="keyword">DEFAULT</span> TABLESPACE apps</span><br><span class="line">  TEMPORARY TABLESPACE temp</span><br><span class="line">  QUOTA UNLIMITED <span class="keyword">ON</span> apps;</span><br><span class="line"></span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">CONNECT</span>, RESOURCE <span class="keyword">TO</span> appuser;</span><br></pre></td></tr></table></figure></div><p>切换连接</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">conn appuser/Secure123@172.17.0.1:1521/orclPDB1</span><br></pre></td></tr></table></figure></div><h3 id="准备工作"><a class="markdownIt-Anchor" href="#准备工作"></a> 准备工作</h3><p>本人并不是 Oracle 的深度用户，本着“以核心高频使用的数据类型为主，剔除过时/不推荐的数据类型，保留必要的边界测试”的理念来设计测试表，同时考虑不规范设计导致的隐式行为对迁移工作带来的问题。</p><h4 id="表结构定义"><a class="markdownIt-Anchor" href="#表结构定义"></a> 表结构定义</h4><p>以下是基于 LLM 生成的测试用例，不代表真实业务</p><h5 id="核心业务数据类型测试表"><a class="markdownIt-Anchor" href="#核心业务数据类型测试表"></a> 核心业务数据类型测试表</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 核心业务数据类型测试表 (高频场景)</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> &quot;CORE_DATA_TEST&quot; (</span><br><span class="line">  id NUMBER(<span class="number">10</span>) <span class="keyword">PRIMARY KEY</span>,          <span class="comment">-- 主键必须显式指定精度</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 数值类型 (必须显式精度) ===</span></span><br><span class="line">  price NUMBER(<span class="number">15</span>,<span class="number">4</span>) <span class="keyword">NOT NULL</span>,       <span class="comment">-- 金融场景精度要求</span></span><br><span class="line">  rating BINARY_DOUBLE,               <span class="comment">-- 64位浮点 (保留科学计算场景)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 字符串类型 (必须显式字符集语义) ===</span></span><br><span class="line">  username VARCHAR2(<span class="number">50</span> <span class="type">CHAR</span>) <span class="keyword">NOT NULL</span>, <span class="comment">-- 字符语义 (避免字节陷阱)</span></span><br><span class="line">  description NVARCHAR2(<span class="number">100</span>) <span class="keyword">NOT NULL</span>,  <span class="comment">-- Unicode支持</span></span><br><span class="line">  json_data <span class="type">CLOB</span> <span class="keyword">CHECK</span> (json_data <span class="keyword">IS</span> JSON), <span class="comment">-- JSON存储 (CLOB存储大JSON)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 日期时间 (必须明确精度和时区) ===</span></span><br><span class="line">  created_at <span class="type">TIMESTAMP</span>(<span class="number">6</span>) <span class="keyword">DEFAULT</span> SYSTIMESTAMP, <span class="comment">-- 创建时间 (微秒精度)</span></span><br><span class="line">  expires_at <span class="type">TIMESTAMP</span> <span class="keyword">WITH</span> <span class="type">TIME</span> ZONE,          <span class="comment">-- 带时区时间 (国际化场景)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 二进制数据 (限制使用) ===</span></span><br><span class="line">  avatar <span class="type">BLOB</span>,                        <span class="comment">-- 小头像 (仅示例，生产应存OSS链接)</span></span><br><span class="line">  metadata RAW(<span class="number">200</span>),                  <span class="comment">-- 固定长度二进制 (如加密密钥)</span></span><br><span class="line">  <span class="keyword">CONSTRAINT</span> core_data_test_username_unq <span class="keyword">UNIQUE</span> (username)  <span class="comment">-- 用户名唯一性约束</span></span><br><span class="line">);</span><br><span class="line"><span class="comment">-- 过期时间索引</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_core_expires_at <span class="keyword">ON</span> &quot;CORE_DATA_TEST&quot; (expires_at);</span><br><span class="line"><span class="comment">-- 创建时间索引</span></span><br><span class="line"><span class="keyword">CREATE</span> INDEX idx_core_created_at <span class="keyword">ON</span> &quot;CORE_DATA_TEST&quot; (created_at);</span><br></pre></td></tr></table></figure></div><h5 id="边界与规范陷阱测试表"><a class="markdownIt-Anchor" href="#边界与规范陷阱测试表"></a> 边界与规范陷阱测试表</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 边界与规范陷阱测试表 (迁移关键验证点)</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> &quot;EDGE_CASE_TEST&quot; (</span><br><span class="line">  id NUMBER(<span class="number">10</span>) <span class="keyword">PRIMARY KEY</span>,</span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 数值边界 ===</span></span><br><span class="line">  max_scale NUMBER(<span class="number">38</span>,<span class="number">0</span>),             <span class="comment">-- 大整数 (如订单ID)</span></span><br><span class="line">  nan_value BINARY_FLOAT,              <span class="comment">-- 浮点NaN (需特殊处理)</span></span><br><span class="line">  infinity_value BINARY_DOUBLE,         <span class="comment">-- 浮点Infinity (需转换NULL)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 字符编码陷阱 ===</span></span><br><span class="line">  byte_semantic_str VARCHAR2(<span class="number">10</span> BYTE), <span class="comment">-- 字节语义 (Oracle默认！)</span></span><br><span class="line">  emoji_string NVARCHAR2(<span class="number">20</span>),          <span class="comment">-- Emoji存储测试 (😊)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 时间边界 ===</span></span><br><span class="line">  min_date <span class="type">DATE</span> <span class="keyword">DEFAULT</span> <span class="type">DATE</span><span class="string">&#x27;0001-01-01&#x27;</span>, <span class="comment">-- 最小日期</span></span><br><span class="line">  max_timestamp <span class="type">TIMESTAMP</span>(<span class="number">9</span>),          <span class="comment">-- 纳秒精度 (MySQL最高支持微秒)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 空值/大对象陷阱 ===</span></span><br><span class="line">  empty_clob <span class="type">CLOB</span> <span class="keyword">DEFAULT</span> EMPTY_CLOB(), <span class="comment">-- 空CLOB (非NULL！)</span></span><br><span class="line">  null_blob <span class="type">BLOB</span>                       <span class="comment">-- 显式NULL二进制</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div><h5 id="高频不规范类型测试表"><a class="markdownIt-Anchor" href="#高频不规范类型测试表"></a> 高频不规范类型测试表</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 高频不规范类型测试表 (真实业务常见但危险的写法)</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> &quot;ANTIPATTERN_TEST&quot; (</span><br><span class="line">  id NUMBER,                          <span class="comment">-- 致命！未指定精度 (Oracle默认38位)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 数值型不规范 ===</span></span><br><span class="line">  price NUMBER,                       <span class="comment">-- 无精度金融字段 (灾难性存储浪费)</span></span><br><span class="line">  discount <span class="type">FLOAT</span>,                     <span class="comment">-- 模糊的浮点类型 (Oracle会转为BINARY_FLOAT)</span></span><br><span class="line">  quantity NUMBER(<span class="operator">*</span>,<span class="number">0</span>),               <span class="comment">-- 不规范：变长整数 (存储空间不可控)</span></span><br><span class="line">  tax_rate NUMBER(<span class="number">10</span>),                <span class="comment">-- 不规范：未指定小数位 (实际为NUMBER(10,0))</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 字符串型不规范 ===</span></span><br><span class="line">  name VARCHAR2(<span class="number">4000</span>),                <span class="comment">-- 不规范：滥用最大长度 (索引失效)</span></span><br><span class="line">  address NVARCHAR2(<span class="number">2000</span>),            <span class="comment">-- 不规范：超长Unicode (性能低下)</span></span><br><span class="line">  remark VARCHAR2(<span class="number">4000</span> <span class="type">CHAR</span>),         <span class="comment">-- 滥用最大长度 (影响索引性能)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 日期型不规范 ===</span></span><br><span class="line">  created_date <span class="type">DATE</span>,                  <span class="comment">-- 仅日期但存储了时间 (业务误解)</span></span><br><span class="line">  updated_time <span class="type">TIMESTAMP</span>,             <span class="comment">-- 未指定精度 (默认6位但业务可能误用)</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- === 隐式转换陷阱 ===</span></span><br><span class="line">  numeric_string VARCHAR2(<span class="number">10</span>),        <span class="comment">-- 存储数字的字符串 (如&#x27;123.45&#x27;)</span></span><br><span class="line">  date_string VARCHAR2(<span class="number">20</span>)            <span class="comment">-- 存储日期的字符串 (如&#x27;2023-01-01&#x27;)</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure></div><h4 id="测试数据生成"><a class="markdownIt-Anchor" href="#测试数据生成"></a> 测试数据生成</h4><p>具体的测试数据可以使用以下 LLM 提供的 SQL 脚本自动生成</p><h5 id="core_data_test"><a class="markdownIt-Anchor" href="#core_data_test"></a> CORE_DATA_TEST</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 生成 CORE_DATA_TEST 表测试数据</span></span><br><span class="line"><span class="keyword">DECLARE</span></span><br><span class="line">  v_rows NUMBER :<span class="operator">=</span> <span class="number">20</span>;  <span class="comment">-- 生成20行核心业务数据</span></span><br><span class="line"><span class="keyword">BEGIN</span></span><br><span class="line">  <span class="keyword">EXECUTE</span> IMMEDIATE <span class="string">&#x27;TRUNCATE TABLE CORE_DATA_TEST&#x27;</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">INSERT INTO</span> CORE_DATA_TEST (</span><br><span class="line">    id, price, rating, username, description, json_data,</span><br><span class="line">    created_at, expires_at, avatar, metadata</span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">SELECT</span></span><br><span class="line">    ROWNUM <span class="keyword">AS</span> id,</span><br><span class="line">    <span class="comment">-- 金融价格：范围0.0001-99999.9999，带边界值</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">1</span> <span class="keyword">THEN</span> <span class="number">0.0001</span></span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">2</span> <span class="keyword">THEN</span> <span class="number">99999.9999</span></span><br><span class="line">      <span class="keyword">ELSE</span> ROUND(DBMS_RANDOM.VALUE(<span class="number">0.01</span>, <span class="number">1000</span>), <span class="number">4</span>)</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> price,</span><br><span class="line">    <span class="comment">-- 科学计算评分：包含NaN和Infinity</span></span><br><span class="line">    <span class="keyword">CASE</span></span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">3</span> <span class="keyword">THEN</span> BINARY_DOUBLE_NAN</span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">4</span> <span class="keyword">THEN</span> BINARY_DOUBLE_INFINITY</span><br><span class="line">      <span class="keyword">ELSE</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">5</span>)</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> rating,</span><br><span class="line">    <span class="comment">-- 用户名：不同长度的字符语义</span></span><br><span class="line">    DBMS_RANDOM.STRING(<span class="string">&#x27;A&#x27;</span>, TRUNC(DBMS_RANDOM.VALUE(<span class="number">5</span>, <span class="number">50</span>))) <span class="keyword">AS</span> username,</span><br><span class="line">    <span class="comment">-- Unicode描述：包含多语言字符</span></span><br><span class="line">    <span class="keyword">CASE</span> <span class="built_in">MOD</span>(ROWNUM, <span class="number">4</span>)</span><br><span class="line">      <span class="keyword">WHEN</span> <span class="number">0</span> <span class="keyword">THEN</span> <span class="string">&#x27;描述-&#x27;</span> <span class="operator">||</span> CHR(<span class="number">20320</span>) <span class="operator">||</span> CHR(<span class="number">22909</span>)  <span class="comment">-- 中文</span></span><br><span class="line">      <span class="keyword">WHEN</span> <span class="number">1</span> <span class="keyword">THEN</span> <span class="string">&#x27;Описание-&#x27;</span> <span class="operator">||</span> CHR(<span class="number">1087</span>) <span class="operator">||</span> CHR(<span class="number">1088</span>) <span class="comment">-- 俄文</span></span><br><span class="line">      <span class="keyword">WHEN</span> <span class="number">2</span> <span class="keyword">THEN</span> <span class="string">&#x27;説明-&#x27;</span> <span class="operator">||</span> CHR(<span class="number">12371</span>) <span class="operator">||</span> CHR(<span class="number">12428</span>)   <span class="comment">-- 日文</span></span><br><span class="line">      <span class="keyword">ELSE</span> <span class="string">&#x27;Description-&#x27;</span> <span class="operator">||</span> CHR(<span class="number">128522</span>)                <span class="comment">-- 英文+表情</span></span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> description,</span><br><span class="line">    <span class="comment">-- JSON数据：不同结构的JSON</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> <span class="built_in">MOD</span>(ROWNUM, <span class="number">3</span>) <span class="operator">=</span> <span class="number">0</span> <span class="keyword">THEN</span> <span class="string">&#x27;&#123;&quot;id&quot;:&#x27;</span> <span class="operator">||</span> ROWNUM <span class="operator">||</span> <span class="string">&#x27;, &quot;name&quot;:&quot;user&#x27;</span> <span class="operator">||</span> ROWNUM <span class="operator">||</span> <span class="string">&#x27;&quot;&#125;&#x27;</span></span><br><span class="line">      <span class="keyword">WHEN</span> <span class="built_in">MOD</span>(ROWNUM, <span class="number">3</span>) <span class="operator">=</span> <span class="number">1</span> <span class="keyword">THEN</span> <span class="string">&#x27;&#123;&quot;product&quot;: &#123;&quot;id&quot;: &#x27;</span> <span class="operator">||</span> ROWNUM <span class="operator">||</span> <span class="string">&#x27;, &quot;price&quot;: &#x27;</span> <span class="operator">||</span> ROUND(DBMS_RANDOM.VALUE(<span class="number">10</span>,<span class="number">100</span>),<span class="number">2</span>) <span class="operator">||</span> <span class="string">&#x27;&#125;&#125;&#x27;</span></span><br><span class="line">      <span class="keyword">ELSE</span> <span class="string">&#x27;&#123;&quot;array&quot;: [1, 2, 3], &quot;nested&quot;: &#123;&quot;level&quot;: 2&#125;&#125;&#x27;</span></span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> json_data,</span><br><span class="line">    <span class="comment">-- 创建时间：带微秒精度</span></span><br><span class="line">    SYSTIMESTAMP <span class="operator">-</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">365</span>) <span class="keyword">AS</span> created_at,</span><br><span class="line">    <span class="comment">-- 带时区的过期时间</span></span><br><span class="line">    SYSTIMESTAMP <span class="keyword">AT</span> <span class="type">TIME</span> ZONE <span class="string">&#x27;UTC&#x27;</span> <span class="operator">+</span> DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">30</span>) <span class="keyword">AS</span> expires_at,</span><br><span class="line">    <span class="comment">-- 小头像二进制：生成小尺寸二进制数据</span></span><br><span class="line">    UTL_RAW.CAST_TO_RAW(DBMS_RANDOM.STRING(<span class="string">&#x27;X&#x27;</span>, <span class="number">100</span>)) <span class="keyword">AS</span> avatar,</span><br><span class="line">    <span class="comment">-- 元数据：固定长度二进制</span></span><br><span class="line">    UTL_RAW.CAST_TO_RAW(DBMS_RANDOM.STRING(<span class="string">&#x27;X&#x27;</span>, <span class="number">200</span>)) <span class="keyword">AS</span> metadata</span><br><span class="line">  <span class="keyword">FROM</span> DUAL</span><br><span class="line">  <span class="keyword">CONNECT</span> <span class="keyword">BY</span> LEVEL <span class="operator">&lt;=</span> v_rows;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">COMMIT</span>;</span><br><span class="line">  DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;成功生成 &#x27;</span> <span class="operator">||</span> v_rows <span class="operator">||</span> <span class="string">&#x27; 行 CORE_DATA_TEST 数据&#x27;</span>);</span><br><span class="line">EXCEPTION</span><br><span class="line">  <span class="keyword">WHEN</span> OTHERS <span class="keyword">THEN</span></span><br><span class="line">    DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;CORE_DATA_TEST 错误: &#x27;</span> <span class="operator">||</span> SQLERRM);</span><br><span class="line">    <span class="keyword">ROLLBACK</span>;</span><br><span class="line"><span class="keyword">END</span>;</span><br><span class="line"><span class="operator">/</span></span><br></pre></td></tr></table></figure></div><p>CORE_DATA_TEST 表数据特点：</p><ol><li><strong>数值类型</strong>：<ul><li>金融价格包含边界值(0.0001和99999.9999)</li><li>科学评分包含NaN和Infinity特殊值</li></ul></li><li><strong>字符串类型</strong>：<ul><li>用户名使用字符语义(VARCHAR2 CHAR)</li><li>描述包含多语言字符(中文、俄文、日文)</li><li>JSON数据验证JSON约束</li></ul></li><li><strong>日期时间</strong>：<ul><li>创建时间使用微秒精度TIMESTAMP(6)</li><li>过期时间带时区信息</li></ul></li><li><strong>二进制数据</strong>：<ul><li>小头像使用小尺寸BLOB</li><li>元数据使用固定长度RAW(200)</li></ul></li></ol><h5 id="edge_case_test"><a class="markdownIt-Anchor" href="#edge_case_test"></a> EDGE_CASE_TEST</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 生成 EDGE_CASE_TEST 表测试数据</span></span><br><span class="line"><span class="keyword">DECLARE</span></span><br><span class="line">  v_rows NUMBER :<span class="operator">=</span> <span class="number">15</span>;  <span class="comment">-- 生成15行边界测试数据</span></span><br><span class="line"><span class="keyword">BEGIN</span></span><br><span class="line">  <span class="keyword">EXECUTE</span> IMMEDIATE <span class="string">&#x27;TRUNCATE TABLE EDGE_CASE_TEST&#x27;</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">INSERT INTO</span> EDGE_CASE_TEST (</span><br><span class="line">    id, max_scale, nan_value, infinity_value, </span><br><span class="line">    byte_semantic_str, emoji_string, min_date, max_timestamp,</span><br><span class="line">    empty_clob, null_blob</span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">SELECT</span></span><br><span class="line">    ROWNUM <span class="keyword">AS</span> id,</span><br><span class="line">    <span class="comment">-- 大整数边界</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">1</span> <span class="keyword">THEN</span> <span class="number">0</span></span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">2</span> <span class="keyword">THEN</span> <span class="built_in">POWER</span>(<span class="number">10</span>, <span class="number">38</span>)<span class="number">-1</span>  <span class="comment">-- 最大38位整数</span></span><br><span class="line">      <span class="keyword">ELSE</span> TRUNC(DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="built_in">POWER</span>(<span class="number">10</span>, <span class="number">18</span>)))</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> max_scale,</span><br><span class="line">    <span class="comment">-- 浮点特殊值</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">3</span> <span class="keyword">THEN</span> BINARY_FLOAT_NAN</span><br><span class="line">      <span class="keyword">ELSE</span> <span class="keyword">NULL</span> </span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> nan_value,</span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">4</span> <span class="keyword">THEN</span> BINARY_DOUBLE_INFINITY</span><br><span class="line">      <span class="keyword">ELSE</span> <span class="keyword">NULL</span> </span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> infinity_value,</span><br><span class="line">    <span class="comment">-- 字节语义字符串：包含多字节字符</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">5</span> <span class="keyword">THEN</span> <span class="string">&#x27;汉字&#x27;</span>  <span class="comment">-- 测试多字节截断</span></span><br><span class="line">      <span class="keyword">ELSE</span> DBMS_RANDOM.STRING(<span class="string">&#x27;X&#x27;</span>, TRUNC(DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">10</span>)))</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> byte_semantic_str,</span><br><span class="line">    <span class="comment">-- Emoji字符串：混合文本和表情</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">6</span> <span class="keyword">THEN</span> <span class="string">&#x27;Hello&#x27;</span> <span class="operator">||</span> CHR(<span class="number">128512</span>) <span class="operator">||</span> CHR(<span class="number">128525</span>) <span class="comment">-- 😀😍</span></span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">7</span> <span class="keyword">THEN</span> CHR(<span class="number">128640</span>) <span class="operator">||</span> CHR(<span class="number">128641</span>) <span class="operator">||</span> CHR(<span class="number">128642</span>) <span class="comment">-- 🚀🚁🚂</span></span><br><span class="line">      <span class="keyword">ELSE</span> <span class="string">&#x27;Text&#x27;</span> <span class="operator">||</span> CHR(<span class="number">128522</span>) <span class="comment">-- 🙂</span></span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> emoji_string,</span><br><span class="line">    <span class="comment">-- 日期边界</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">8</span> <span class="keyword">THEN</span> <span class="type">DATE</span> <span class="string">&#x27;0001-01-01&#x27;</span>  <span class="comment">-- 最小日期</span></span><br><span class="line">      <span class="keyword">ELSE</span> SYSDATE <span class="operator">-</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">365</span> <span class="operator">*</span> <span class="number">10</span>)</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> min_date,</span><br><span class="line">    <span class="comment">-- 时间戳边界：纳秒精度</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> ROWNUM <span class="operator">=</span> <span class="number">9</span> <span class="keyword">THEN</span> <span class="type">TIMESTAMP</span> <span class="string">&#x27;9999-12-31 23:59:59.999999999&#x27;</span> <span class="comment">-- 最大时间戳</span></span><br><span class="line">      <span class="keyword">ELSE</span> SYSTIMESTAMP <span class="operator">+</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">365</span>)</span><br><span class="line">    <span class="keyword">END</span> <span class="keyword">AS</span> max_timestamp,</span><br><span class="line">    <span class="comment">-- 空CLOB（非NULL）</span></span><br><span class="line">    EMPTY_CLOB() <span class="keyword">AS</span> empty_clob,</span><br><span class="line">    <span class="comment">-- 显式NULL二进制</span></span><br><span class="line">    <span class="keyword">NULL</span> <span class="keyword">AS</span> null_blob</span><br><span class="line">  <span class="keyword">FROM</span> DUAL</span><br><span class="line">  <span class="keyword">CONNECT</span> <span class="keyword">BY</span> LEVEL <span class="operator">&lt;=</span> v_rows;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">COMMIT</span>;</span><br><span class="line">  DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;成功生成 &#x27;</span> <span class="operator">||</span> v_rows <span class="operator">||</span> <span class="string">&#x27; 行 EDGE_CASE_TEST 数据&#x27;</span>);</span><br><span class="line">EXCEPTION</span><br><span class="line">  <span class="keyword">WHEN</span> OTHERS <span class="keyword">THEN</span></span><br><span class="line">    DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;EDGE_CASE_TEST 错误: &#x27;</span> <span class="operator">||</span> SQLERRM);</span><br><span class="line">    <span class="keyword">ROLLBACK</span>;</span><br><span class="line"><span class="keyword">END</span>;</span><br><span class="line"><span class="operator">/</span></span><br></pre></td></tr></table></figure></div><p>EDGE_CASE_TEST 表边界测试：</p><ol><li><p><strong>数值边界</strong>：</p><ul><li>最大38位整数(10³⁸-1)</li><li>浮点NaN和Infinity特殊值</li></ul></li><li><p><strong>字符编码陷阱</strong>：</p><ul><li><p>字节语义字符串包含多字节字符(测试截断)</p></li><li><p>Emoji字符串混合文本和表情符号</p></li></ul></li><li><p><strong>时间边界</strong>：</p><ul><li>最小日期(0001-01-01)</li><li>最大时间戳(9999-12-31 23:59:59.999999999)</li></ul></li><li><p><strong>空值/大对象</strong>：</p><ul><li>空CLOB(EMPTY_CLOB()非NULL)</li><li>显式NULL二进制数据</li></ul></li></ol><h5 id="antipattern_test"><a class="markdownIt-Anchor" href="#antipattern_test"></a> ANTIPATTERN_TEST</h5><div class="code-container" data-rel="Sql"><figure class="iseeu highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 生成 ANTIPATTERN_TEST 表测试数据</span></span><br><span class="line"><span class="keyword">DECLARE</span></span><br><span class="line">  <span class="comment">-- 生成1000行测试数据</span></span><br><span class="line">  v_rows NUMBER :<span class="operator">=</span> <span class="number">1000</span>;</span><br><span class="line"><span class="keyword">BEGIN</span></span><br><span class="line">  <span class="comment">-- 清空表（可选）</span></span><br><span class="line">  <span class="keyword">EXECUTE</span> IMMEDIATE <span class="string">&#x27;TRUNCATE TABLE ANTIPATTERN_TEST&#x27;</span>;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">-- 插入测试数据</span></span><br><span class="line">  <span class="keyword">INSERT INTO</span> ANTIPATTERN_TEST (</span><br><span class="line">    id, price, discount, quantity, tax_rate, </span><br><span class="line">    name, address, remark, </span><br><span class="line">    created_date, updated_time, </span><br><span class="line">    numeric_string, date_string</span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">SELECT</span></span><br><span class="line">    <span class="comment">-- id: 序列号</span></span><br><span class="line">    ROWNUM,</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- price: 随机金融数值（无精度控制）</span></span><br><span class="line">    DBMS_RANDOM.VALUE(<span class="number">0.01</span>, <span class="number">10000</span>),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- discount: 随机浮点数（精度问题）</span></span><br><span class="line">    DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">1</span>),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- quantity: 变长整数（存储不可控）</span></span><br><span class="line">    TRUNC(DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">1000</span>)),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- tax_rate: 未指定小数位的税率</span></span><br><span class="line">    TRUNC(DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">30</span>)),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- name: 随机长度字符串（滥用最大长度）</span></span><br><span class="line">    DBMS_RANDOM.STRING(<span class="string">&#x27;A&#x27;</span>, TRUNC(DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">4000</span>))),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- address: 随机Unicode字符串（性能低下）</span></span><br><span class="line">    DBMS_RANDOM.STRING(<span class="string">&#x27;X&#x27;</span>, TRUNC(DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">2000</span>))),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- remark: 滥用最大长度的备注</span></span><br><span class="line">    <span class="keyword">CASE</span> </span><br><span class="line">      <span class="keyword">WHEN</span> <span class="built_in">MOD</span>(ROWNUM, <span class="number">10</span>) <span class="operator">=</span> <span class="number">0</span> <span class="keyword">THEN</span> RPAD(<span class="string">&#x27;长备注&#x27;</span>, <span class="number">4000</span>, <span class="string">&#x27;x&#x27;</span>)  <span class="comment">-- 10%为超长备注</span></span><br><span class="line">      <span class="keyword">ELSE</span> DBMS_RANDOM.STRING(<span class="string">&#x27;A&#x27;</span>, TRUNC(DBMS_RANDOM.VALUE(<span class="number">10</span>, <span class="number">100</span>)))</span><br><span class="line">    <span class="keyword">END</span>,</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- created_date: 包含时间的日期（业务只需日期）</span></span><br><span class="line">    SYSDATE <span class="operator">-</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">365</span>),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- updated_time: 高精度时间戳（业务可能误用）</span></span><br><span class="line">    SYSTIMESTAMP <span class="operator">-</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">86400</span>),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- numeric_string: 存储数字的字符串</span></span><br><span class="line">    TO_CHAR(DBMS_RANDOM.VALUE(<span class="number">1</span>, <span class="number">1000</span>), <span class="string">&#x27;9999.99&#x27;</span>),</span><br><span class="line">    </span><br><span class="line">    <span class="comment">-- date_string: 存储日期的字符串</span></span><br><span class="line">    TO_CHAR(SYSDATE <span class="operator">-</span> DBMS_RANDOM.VALUE(<span class="number">0</span>, <span class="number">365</span>), <span class="string">&#x27;YYYY-MM-DD&#x27;</span>)</span><br><span class="line">  <span class="keyword">FROM</span> DUAL</span><br><span class="line">  <span class="keyword">CONNECT</span> <span class="keyword">BY</span> LEVEL <span class="operator">&lt;=</span> v_rows;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">COMMIT</span>;</span><br><span class="line">  DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;成功生成 &#x27;</span> <span class="operator">||</span> v_rows <span class="operator">||</span> <span class="string">&#x27; 行测试数据&#x27;</span>);</span><br><span class="line">EXCEPTION</span><br><span class="line">  <span class="keyword">WHEN</span> OTHERS <span class="keyword">THEN</span></span><br><span class="line">    DBMS_OUTPUT.PUT_LINE(<span class="string">&#x27;错误: &#x27;</span> <span class="operator">||</span> SQLERRM);</span><br><span class="line">    <span class="keyword">ROLLBACK</span>;</span><br><span class="line"><span class="keyword">END</span>;</span><br><span class="line"><span class="operator">/</span></span><br></pre></td></tr></table></figure></div><p>ANTIPATTERN_TEST 表数据说明：</p><ol><li><strong>体现数值型不规范</strong>：<ul><li><code>price</code>：生成带小数的随机值，展示无精度控制的问题</li><li><code>discount</code>：生成0-1之间的随机浮点数，展示精度问题</li><li><code>quantity</code>：生成整数，但使用变长存储</li><li><code>tax_rate</code>：生成整数税率，但未指定小数位</li></ul></li><li><strong>体现字符串型不规范</strong>：<ul><li><code>name</code>：生成长度1-4000的随机字符串（滥用最大长度）</li><li><code>address</code>：生成长度1-2000的Unicode字符串（性能问题）</li><li><code>remark</code>：90%为短字符串，10%为4000字符超长字符串（索引问题）</li></ul></li><li><strong>体现日期型不规范</strong>：<ul><li><code>created_date</code>：生成带时间分量的日期（业务只需日期）</li><li><code>updated_time</code>：生成高精度时间戳（默认6位小数）</li></ul></li><li><strong>体现隐式转换陷阱</strong>：<ul><li><code>numeric_string</code>：存储格式化的数字字符串</li><li><code>date_string</code>：存储格式化的日期字符串</li></ul></li><li><strong>数据多样性</strong>：<ul><li>使用<code>DBMS_RANDOM</code>包生成随机数据</li><li>包含边界值（如超长字符串）</li><li>生成1000行代表性数据</li></ul></li></ol><h3 id="迁移方案"><a class="markdownIt-Anchor" href="#迁移方案"></a> 迁移方案</h3><p>迁移步骤分成两个阶段，结构迁移和数据迁移。结构迁移的主要问题是异构数据库数据类型的兼容。由于 SQL 标准并不负责规定各种数据库的具体类型定义，Oracle、MySQL 等的数据类型并不一致，不过 Oracle 中实际高频使用的类型基本在 MySQL 中有等价或近似的类型，但即便有一定程度的兼容性，也仍有可能存在一些细节问题，甚至还得考虑重新设计源数据库数据类型，以满足兼容性。</p><h4 id="废案历史"><a class="markdownIt-Anchor" href="#废案历史"></a> 废案历史</h4><p>主要讨论结构迁移部分。</p><p>第一版废案：liquibase(community) + 脚本处理，得到 MySQL DDL。liquibase(community) 生成的 changelog 无法解决 Oracle 与 MySQL 之间的 DDL 差异性问题。DDL 相差较大，脚本处理可靠性存疑且需要耗费精力验证。</p><blockquote><p>考虑 liquibase(community) 而非 flyway，主要原因是 diff-changelog 的功能，flyway 的类似 diff 功能仅在商业版提供。但在能够获取到 DDL 的情况下，两者都能很好地实现 SQL 版本控制。</p><p>此外，另一个轻量工具 <a class="link"   href="https://github.com/sqldef/sqldef" >sqldef/sqldef<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 能够导出数据库 DDL，并能将修改过后的 DDL 与当前数据库进行对比生成差异的 DDL，但仅限于同构的数据库，且不支持 Oracle。</p></blockquote><p>第二版废案：AWS SCT。AWS SCT 能够对比异构数据库差异并提供建议，但是还是需要人工干预处理，且 AWS SCT 主要为 AWS 的云服务工作，对本地自建数据库支持较弱。</p><p>第三版废案：ora2pg + PostgreSQL，利用 PostgreSQL 作为中间者，间接实现 Oracle 到 MySQL 的迁移。但是实际上围绕 PostgreSQL 的工具基本都是单向迁移工具，且只能往 PostgreSQL 迁移。推测 PostgreSQL 相关的工具之所以只能实现单向迁移，是因为 PostgreSQL 本身有足够大的能力可以“容纳另一个数据库的所有常用功能”，反之则不成立。进行迁移的过程中，底层实现的不同导致“元信息”发生不可逆的转变，因此也不可能再进行反向迁移了。</p><h4 id="目前方案"><a class="markdownIt-Anchor" href="#目前方案"></a> 目前方案</h4><p>经过漫长的考虑，这里最终决定采用一种特别的方案：利用具备自动建表功能（或者是能够自动生成 DDL 方言）的框架基于实体类、结构体代码来实现对 MySQL 的自动建表。</p><blockquote><p>在该方案的上一版中，其实还有一个步骤就是基于 Oracle 的已有表结构生成实体类、结构体代码。但是综合多种因素考虑，该步骤可能并不是必要的。</p><p>基于 Oracle 表结构提取并生成代码这一点，Java 可能是最省力的方案（然而实际上它所生成的实体类缺少必要的注解信息，无法与支持自动建表的框架集成），而不少出现在这之后的新语言例如 Go、Rust 等可能都无法很好地与 Oracle 适配。但这一点也并非是硬性要求，详情见下文说明。</p><p>另一方面，这些新语言也大概率不会选择 Oracle 作为开发环境，而是选择更开放的 PostgreSQL 等，因此也就不存在与 Oracle 相关的迁移需求。</p></blockquote><p>自动建表框架选型：</p><p>Java</p><ul><li><p><a class="link"   href="https://github.com/babyfish-ct/jimmer" >babyfish-ct/jimmer<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> (未来有计划支持，截止目前 2025.10 不支持)</p><blockquote><p>有相关计划支持 DDL 生成，不过目前仍在规划中</p><p><a class="link"   href="https://github.com/babyfish-ct/jimmer/issues/286" >Feature request: SQL DDL generation · Issue #286 · babyfish-ct/jimmer<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>但可能不会考虑直接在代码运行的过程中改动数据表结构</p><p><a class="link"   href="https://github.com/babyfish-ct/jimmer/issues/3" >是否可以支持将表关系定义持久化到数据库里？ · Issue #3 · babyfish-ct/jimmer<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>相关讨论表示未来某个版本可能会上线 DDL 生成功能</p><p><a class="link"   href="https://github.com/babyfish-ct/jimmer/discussions/996" >关于 DDL 的一些想法。Some thoughts on DDL. · babyfish-ct/jimmer · Discussion #996<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></blockquote></li><li><p><a class="link"   href="https://github.com/dromara/auto-table" >dromara/auto-table<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> (dromara 开源社区孵化项目)</p><blockquote><p>支持在没有 JPA 环境的项目中也能实现自动维护表结构，并提供了对 mybatis-plus 的支持。</p><p><a class="link"   href="https://github.com/dromara/mybatis-plus-ext" >dromara/mybatis-plus-ext<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>: mybatis-plus框架的拓展包，在框架原有基础上做了进一步的轻度封装，增强内容：多数据源自动建表、数据自动填充、自动关联查询、冗余数据自动更新、动态查询条件等。</p></blockquote></li><li><p><a class="link"   href="https://github.com/hibernate/hibernate-orm" >hibernate/hibernate-orm<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> (传统 JPA 规范实现框架)</p><blockquote><p>可能是最早支持自动维护表结构的框架，也是 <a class="link"   href="https://github.com/spring-projects/spring-data-jpa" >spring-projects/spring-data-jpa<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 的默认底层实现。但是依赖 JPA 环境，在已经使用 Mybatis 相关框架的项目中还需要额外配置 JPA。</p></blockquote></li></ul><p>Golang</p><ul><li><p><a class="link"   href="https://github.com/go-gorm/gorm" >go-gorm/gorm<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><blockquote><p>原生支持自动表结构维护的 orm 框架。</p></blockquote></li><li><p><a class="link"   href="https://gitea.com/xorm/xorm" >xorm/xorm<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><blockquote><p>Simple and Powerful ORM for Go, support mysql,postgres,tidb,sqlite3,sqlite,mssql,oracle,cockroach.</p><p>这个似乎实验性支持 Oracle 数据库。</p></blockquote></li></ul><h4 id="可能的伪需求基于已有的表结构生成实体类-结构体代码"><a class="markdownIt-Anchor" href="#可能的伪需求基于已有的表结构生成实体类-结构体代码"></a> 可能的伪需求：基于已有的表结构生成实体类、结构体代码</h4><p>在项目生命周期里，结构体或实体类总是与数据表同步进行的，通常也不会出现“数据表一万张，实体类或结构体一个没写”的情况（除非是要将项目代码和数据库一并更换，那这也就不在本文的考虑范围内了）。</p><p>在实体类、结构体代码都已经齐全的情况下，大部分语言可以利用各自的框架来实现对 MySQL 的自动建表。因此，直接基于当前项目所使用的语言来进行自动建表即可，并不存在从 Oracle 中提取表结构来生成代码的需求。</p><p>此外需要注意判断框架能否支持在已有表的基础上通过修改实体类、结构体代码来实现对数据表结构的新增、删除和修改等，以决定框架的使用时机，否则会造成代码与数据表的不一致。</p><p>但是出于某种原因考虑，本人还是决定使用 mybatis-generator 尝试生成 Java 实体类代码。</p><p>快速新建一个 maven 项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mvn archetype:generate \</span><br><span class="line">  -DgroupId=com.example \</span><br><span class="line">  -DartifactId=entity-generator \</span><br><span class="line">  -DarchetypeArtifactId=maven-archetype-quickstart \</span><br><span class="line">  -DinteractiveMode=<span class="literal">false</span></span><br></pre></td></tr></table></figure></div><p>pom.xml 补充内容</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">java.version</span>&gt;</span>17<span class="tag">&lt;/<span class="name">java.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">mybatis-generator.version</span>&gt;</span>1.4.2<span class="tag">&lt;/<span class="name">mybatis-generator.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">mysql.version</span>&gt;</span>8.0.33<span class="tag">&lt;/<span class="name">mysql.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">ojdbc.version</span>&gt;</span>19.3.0.0<span class="tag">&lt;/<span class="name">ojdbc.version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- MySQL 驱动 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.mysql<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mysql-connector-j<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;mysql.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">&lt;!-- Oracle 驱动 --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.oracle.database.jdbc<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>ojdbc8<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;ojdbc.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- MyBatis Generator Plugin --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.mybatis.generator<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-generator-maven-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;mybatis-generator.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">configurationFile</span>&gt;</span>src/main/resources/generatorConfig.xml<span class="tag">&lt;/<span class="name">configurationFile</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">includeCompileDependencies</span>&gt;</span>true<span class="tag">&lt;/<span class="name">includeCompileDependencies</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">overwrite</span>&gt;</span>true<span class="tag">&lt;/<span class="name">overwrite</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">verbose</span>&gt;</span>true<span class="tag">&lt;/<span class="name">verbose</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>src/main/resources/generatorConfig.xml 配置</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">generatorConfiguration</span> <span class="keyword">PUBLIC</span></span></span><br><span class="line"><span class="meta">  <span class="string">&quot;-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN&quot;</span></span></span><br><span class="line"><span class="meta">  <span class="string">&quot;http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">generatorConfiguration</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">context</span> <span class="attr">id</span>=<span class="string">&quot;OracleGenerator&quot;</span> <span class="attr">targetRuntime</span>=<span class="string">&quot;MyBatis3Simple&quot;</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 生成内容配置 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">commentGenerator</span>&gt;</span></span><br><span class="line">            <span class="comment">&lt;!-- 是否去除自动生成的注释 --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;suppressAllComments&quot;</span> <span class="attr">value</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">commentGenerator</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 配置数据库连接 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">jdbcConnection</span> <span class="attr">driverClass</span>=<span class="string">&quot;oracle.jdbc.OracleDriver&quot;</span></span></span><br><span class="line"><span class="tag">                        <span class="attr">connectionURL</span>=<span class="string">&quot;jdbc:oracle:thin:@//localhost:1521/ORCLPDB1&quot;</span></span></span><br><span class="line"><span class="tag">                        <span class="attr">userId</span>=<span class="string">&quot;APPUSER&quot;</span></span></span><br><span class="line"><span class="tag">                        <span class="attr">password</span>=<span class="string">&quot;Secure123&quot;</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">jdbcConnection</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- Java模型生成器 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">javaModelGenerator</span> <span class="attr">targetPackage</span>=<span class="string">&quot;com.example.entity&quot;</span></span></span><br><span class="line"><span class="tag">                            <span class="attr">targetProject</span>=<span class="string">&quot;src/main/java&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;enableSubPackages&quot;</span> <span class="attr">value</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;trimStrings&quot;</span> <span class="attr">value</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">javaModelGenerator</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 表配置 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">&quot;%&quot;</span> <span class="attr">schema</span>=<span class="string">&quot;APPUSER&quot;</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">context</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">generatorConfiguration</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>生成代码</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn mybatis-generator:generate</span><br></pre></td></tr></table></figure></div><p>运行过程中发现了以下日志</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[WARNING] Unsupported Data Type 101 <span class="keyword">in</span> table APPUSER.CORE_DATA_TEST, column: RATING, property defaults to Object <span class="built_in">type</span>.</span><br><span class="line">[WARNING] Unsupported Data Type -101 <span class="keyword">in</span> table APPUSER.CORE_DATA_TEST, column: EXPIRES_AT, property defaults to Object <span class="built_in">type</span>.</span><br><span class="line">[WARNING] Unsupported Data Type 100 <span class="keyword">in</span> table APPUSER.EDGE_CASE_TEST, column: NAN_VALUE, property defaults to Object <span class="built_in">type</span>.</span><br><span class="line">[WARNING] Unsupported Data Type 101 <span class="keyword">in</span> table APPUSER.EDGE_CASE_TEST, column: INFINITY_VALUE, property defaults to Object <span class="built_in">type</span>.</span><br></pre></td></tr></table></figure></div><p>Oracle 中的 BINARY_FLOAT、BINARY_DOUBLE、TIMESTAMP WITH TIME ZONE 属于非标准类型，没有在 mybatis-generator 中包含，所以被默认处理为了 Object 类型，但实际上这些类型可以对应到 Java 中更具体的类型。通过拓展 mybatis-generator 的 resolver 可以自定义实现对这些类型的处理。</p><p>另外新建一个项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mvn archetype:generate \</span><br><span class="line">  -DgroupId=com.example.extension \</span><br><span class="line">  -DartifactId=mybatis-generator-extension \</span><br><span class="line">  -DarchetypeArtifactId=maven-archetype-quickstart \</span><br><span class="line">  -DinteractiveMode=<span class="literal">false</span></span><br></pre></td></tr></table></figure></div><p>修改 pom.xml</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">java.version</span>&gt;</span>17<span class="tag">&lt;/<span class="name">java.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">project.build.sourceEncoding</span>&gt;</span>UTF-8<span class="tag">&lt;/<span class="name">project.build.sourceEncoding</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.mybatis.generator<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-generator-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.4.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-compiler-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.14.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">release</span>&gt;</span>$&#123;java.version&#125;<span class="tag">&lt;/<span class="name">release</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>自定义拓展 mybatis-generator 的 JavaTypeResolverDefaultImpl</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.extension;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;</span><br><span class="line"><span class="keyword">import</span> org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OracleTypeResolver</span> <span class="keyword">extends</span> <span class="title class_">JavaTypeResolverDefaultImpl</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">OracleTypeResolver</span><span class="params">()</span> &#123;</span><br><span class="line">        addReplacement(<span class="number">100</span>, <span class="string">&quot;BINARY_FLOAT&quot;</span>, Double.class);</span><br><span class="line">        addReplacement(<span class="number">101</span>, <span class="string">&quot;BINARY_DOUBLE&quot;</span>, Double.class);</span><br><span class="line">        addReplacement(-<span class="number">101</span>, <span class="string">&quot;TIMESTAMP WITH TIME ZONE&quot;</span>, <span class="string">&quot;java.time.ZonedDateTime&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">addReplacement</span><span class="params">(<span class="type">int</span> code, String dbType, String javaType)</span> &#123;</span><br><span class="line">        typeMap.put(code, <span class="keyword">new</span> <span class="title class_">JdbcTypeInformation</span>(dbType, <span class="keyword">new</span> <span class="title class_">FullyQualifiedJavaType</span>(javaType)));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">addReplacement</span><span class="params">(<span class="type">int</span> code, String dbType, Class&lt;?&gt; javaType)</span> &#123;</span><br><span class="line">        typeMap.put(code, <span class="keyword">new</span> <span class="title class_">JdbcTypeInformation</span>(dbType, <span class="keyword">new</span> <span class="title class_">FullyQualifiedJavaType</span>(javaType.getName())));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>安装到本地仓库</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn clean install -DskipTests</span><br></pre></td></tr></table></figure></div><p>在原来的项目 pom.xml 中补充依赖</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.example.extension<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-generator-extension<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>在 src/main/resources/generatorConfig.xml 中加入自定义映射配置。注意配置有顺序要求，需要放在 <code>jdbcConnection</code> 与 <code>javaModelGenerator</code> 之间</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 自定义映射配置 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">javaTypeResolver</span> <span class="attr">type</span>=<span class="string">&quot;com.example.extension.OracleTypeResolver&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">&quot;useJSR310Types&quot;</span> <span class="attr">value</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">javaTypeResolver</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>重新生成代码</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn mybatis-generator:generate</span><br></pre></td></tr></table></figure></div><p>有一点值得考虑的是，当代码生成器无法准确地判断数据字段类型到底对应哪种代码内置类型时，是否能够认为该数据字段类型是一个数据库专有类型或者是一个不被推荐使用的类型？</p><h4 id="结构迁移"><a class="markdownIt-Anchor" href="#结构迁移"></a> 结构迁移</h4><p>本文采用 mybatis-plus-ext(auto-table 扩展) 的方案来实现 Oracle 往 MySQL 的表结构迁移。</p><p>初始化项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">spring init -d=mysql,lombok --build=maven demo</span><br></pre></td></tr></table></figure></div><p>补充依赖</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.dromara.mpe<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>mybatis-plus-ext-spring-boot3-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.5.14-EXT881<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></div><p>application.yaml</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">demo</span></span><br><span class="line">  <span class="attr">profiles:</span></span><br><span class="line">    <span class="attr">active:</span> <span class="string">dev</span></span><br></pre></td></tr></table></figure></div><p>application-dev.yaml</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">driver-class-name:</span> <span class="string">com.mysql.cj.jdbc.Driver</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://localhost:3306/test</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="number">12345</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 开发环境 application-dev.yml</span></span><br><span class="line"><span class="attr">auto-table:</span></span><br><span class="line">  <span class="comment"># 开启自动建表</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">record-sql:</span></span><br><span class="line">    <span class="comment"># 开启SQL记录</span></span><br><span class="line">    <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">    <span class="comment"># 当前版本（此处可保持与计划上线的版本号一致，方便管理SQL文件）</span></span><br><span class="line">    <span class="attr">version:</span> <span class="number">1.0</span><span class="number">.0</span></span><br><span class="line">    <span class="comment"># 记录到文件</span></span><br><span class="line">    <span class="attr">record-type:</span> <span class="string">file</span></span><br><span class="line">    <span class="comment"># 文件夹位置</span></span><br><span class="line">    <span class="attr">folder-path:</span> <span class="string">./sqlLogs</span></span><br></pre></td></tr></table></figure></div><p>启动类上增加 @EnableAutoTable 注解</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableAutoTable</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DemoApplication</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(DemoApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>CoreDataTest 实体类示例，更多用法参考 <a class="link"   href="https://autotable.tangzc.com/" >Dromara AutoTable | Auto Table<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Table</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CoreDataTest</span> &#123;</span><br><span class="line">    <span class="meta">@ColumnId</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(notNull = true, length = 15, decimalLength = 4)</span></span><br><span class="line">    <span class="keyword">private</span> BigDecimal price;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(type = &quot;double&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Double rating;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(notNull = true, length = 50)</span></span><br><span class="line">    <span class="meta">@UniqueIndex</span></span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(notNull = true, length = 100)</span></span><br><span class="line">    <span class="keyword">private</span> String description;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(length = 6, defaultValue = &quot;CURRENT_TIMESTAMP(6)&quot;)</span></span><br><span class="line">    <span class="meta">@Index(name = &quot;idx_core_created_at&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> LocalDateTime createdAt;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(type = &quot;TIMESTAMP(6)&quot;)</span></span><br><span class="line">    <span class="meta">@Index(name = &quot;idx_core_expires_at&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> ZonedDateTime expiresAt;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(type = &quot;longtext&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String jsonData;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(type = &quot;longblob&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">byte</span>[] avatar;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Column(type = &quot;varbinary&quot;, length = 200)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">byte</span>[] metadata;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>CoreDataTest、EdgeCaseTest 根据前面的 DDL 可以提取出较多的数据表元信息，而 AntipatternTest 只能依靠框架的隐式行为或者人工手动规范。实际上 AntipatternTest 在前面的代码生成器中就已经生成了错误的类型，基本可以认定为无法直接用于迁移，是不能在数据表设计中出现的反规范设计。</p><h4 id="数据迁移"><a class="markdownIt-Anchor" href="#数据迁移"></a> 数据迁移</h4><p>解决了前面的类型兼容问题，数据迁移相对就没有那么麻烦。数据可以通过直接导出平台无关的通用数据格式文件，例如 csv 文件，再导入到异构数据库中。这对于小规模数据迁移很方便，对于大规模数据这里采用的是 <a class="link"   href="https://github.com/apache/seatunnel" >apache/seatunnel<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><blockquote><p>顺便一提，apache/seatunnel 也具备自动建表的功能，但是需要依赖已存在的、规范的数据源，并且无法完整迁移索引等结构。</p></blockquote><p>拉取 seatunnel 镜像，seatunnel 的镜像比较大，拉取需要等待一定时间</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull apache/seatunnel</span><br></pre></td></tr></table></figure></div><p>编写配置文件 o2m.config，更多配置可查询 <a class="link"   href="https://seatunnel.apache.org/zh-CN/" >官方文档<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line">env &#123;</span><br><span class="line">    job.mode = &quot;BATCH&quot;</span><br><span class="line">    parallelism = 4</span><br><span class="line">&#125;</span><br><span class="line">source &#123;</span><br><span class="line">    Jdbc &#123;</span><br><span class="line">        plugin_output = &quot;oracle&quot;</span><br><span class="line"></span><br><span class="line">        url = &quot;jdbc:oracle:thin:@172.17.0.1:1521/orclPDB1&quot;</span><br><span class="line">        driver = &quot;oracle.jdbc.OracleDriver&quot;</span><br><span class="line">        connection_check_timeout_sec = 100</span><br><span class="line">        username = &quot;APPUSER&quot;</span><br><span class="line">        password = &quot;Secure123&quot;</span><br><span class="line">        &quot;table_list&quot;=[</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;table_path&quot;=&quot;APPUSER.CORE_DATA_TEST&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;table_path&quot;=&quot;APPUSER.EDGE_CASE_TEST&quot;</span><br><span class="line">            &#125;,</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;table_path&quot;=&quot;APPUSER.ANTIPATTERN_TEST&quot;</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">        split.size = 10000</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">transform &#123;</span><br><span class="line">    TableRename &#123;</span><br><span class="line">        plugin_input = &quot;oracle&quot;</span><br><span class="line">        plugin_output = &quot;table_rename&quot;</span><br><span class="line"></span><br><span class="line">        convert_case = &quot;LOWER&quot;</span><br><span class="line">    &#125;</span><br><span class="line">    FieldRename &#123;</span><br><span class="line">    plugin_input = &quot;table_rename&quot;</span><br><span class="line">    plugin_output = &quot;trans_result&quot;</span><br><span class="line">    </span><br><span class="line">    convert_case = &quot;LOWER&quot;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">sink &#123;</span><br><span class="line">    Jdbc &#123;</span><br><span class="line">        plugin_output = &quot;trans_result&quot;</span><br><span class="line"></span><br><span class="line">        url = &quot;jdbc:mysql://172.17.0.1:3306/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;rewriteBatchedStatements=true&quot;</span><br><span class="line">        driver = &quot;com.mysql.cj.jdbc.Driver&quot;</span><br><span class="line">        username = &quot;root&quot;</span><br><span class="line">        password = &quot;12345&quot;</span><br><span class="line">        # Automatically generate sql statements based on database table names</span><br><span class="line">        generate_sink_sql = true</span><br><span class="line">        database = test</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>运行服务</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> -it -v ./:/config apache/seatunnel ./bin/seatunnel.sh -m <span class="built_in">local</span> -c /config/o2m.config</span><br></pre></td></tr></table></figure></div><p>不出所料，ANTIPATTERN_TEST 因为元信息的规范性问题，迁移的数据出现了因类型错误导致数据存储格式不正确的问题，比较明显的就是 NUMBER 类型没有明确指定精度，导致主键 ID 的类型变成了 DECIMAL(38, 18)。</p><p>而 CORE_DATA_TEST 和  EDGE_CASE_TEST 则出现了部分数据无法迁移的问题。以这里的测试数据为例，经过排查发现是因为 TIMESTAMP 类型的一个极端值超过了 MySQL 中 timestamp 类型的上限，以及 BINARY_FLOAT 和  BINARY_DOUBLE 类型的测试数据中包含了 nan 和 inf 这两个特殊值。</p><blockquote><p>这两个特殊值在 MySQL 中没有相应的支持，实际应用中出于兼容目的需要考虑使用其他方案替代。</p></blockquote><p>在除去了极端值以及 nan 和 inf 这两个特殊值后，CORE_DATA_TEST 和  EDGE_CASE_TEST 也顺利地完成了迁移。</p><h2 id="一些思考"><a class="markdownIt-Anchor" href="#一些思考"></a> 一些思考</h2><blockquote><p>个人观点</p></blockquote><h3 id="一个开发哲学问题先写代码还是先写-ddl"><a class="markdownIt-Anchor" href="#一个开发哲学问题先写代码还是先写-ddl"></a> 一个开发哲学问题：先写代码还是先写 DDL ？</h3><p>传统的项目开发中，DDL 总是必不可少的存在，项目起步通常是先在数据库执行 DDL，有了基础的数据库环境后，再着手于项目代码的编写，然后随着新功能的开发就是反复地执行 DDL、编写代码。然而这可能会存在以下问题：</p><ul><li>如果要同时支持多个异构数据库，设计成本巨大，且难以保证兼容性。</li><li>不利于处于需求频繁变化时期的项目的快速迭代。</li><li>需要人为去把控数据表设计的规范性，或者需要借助专门的审查平台。</li><li>后期改动数据表成本巨大。</li><li>容易将一部分本该由代码控制的业务逻辑泄露到数据库中 (定义了一系列与业务关联的触发器、存储过程等，会为开发带来巨大的心智负担，而且不利于异构数据库的兼容)。</li></ul><p>不过 DDL 优先设计仍有其存在的必要性，大部分时候可能还是最符合习惯与直觉的开发流程，只是需要基于一些影响因素来考虑使用。</p><ul><li><p>明确项目未来只会支持单一数据库类型，如果未来必须要支持多个异构数据库，则需要考虑牺牲一定的性能来换取兼容性。</p><blockquote><p>只是为了某一个数据库特有的性能优化，而放弃与其他数据库的兼容性，是否是一个战略上正确的决定？</p><p>如果有足够强大的资源能够同时支持多个异构数据库，并能为每个异构数据库做专门优化的，可以忽略这一点。</p></blockquote></li><li><p>明确是不需要快速迭代的大型项目，或者拥有一套强大的多 DDL 方言版本管理方案。</p></li><li><p>团队整体应该指定并遵循严格的数据表设计规范，不能出现不清晰的设计。可以借助 SQL 审查平台基于规则自动检查设计的规范性，同时保存改动记录，便于随时回滚。</p><blockquote><p>有的团队中可能会存在 DBA 或者其他由 DBA 转变而来的现代化新角色来处理数据库相关的工作，这与“每个团队成员需要具有一定数据表设计规范意识”并不冲突。</p></blockquote></li><li><p>小规模或个人项目，数据库变动不复杂、规模小、且成本完全可控。</p><blockquote><p>这种情况下，其实也更推荐去尝试新的开发方式。</p></blockquote></li></ul><hr /><p>现代项目的开发中，已经出现打破了这种 DDL 优先设计的技术。比较知名的就是以 golang 语言主导的项目中的 gorm 框架，能够通过定义结构体来实现对数据表结构的创建。</p><p>不过打破 DDL 优先设计并不是说要脱离 DDL。出于审计、可追溯或者版本控制的目的，DDL 仍会在项目开发中出现，只是说不再需要人为去设计编写，而是由框架自动生成。</p><blockquote><p>其实在更早的时候就已经出现了通过代码来自动生成数据表结构的技术（这里简称为 code first），典型的例子就是 hibernate，具有这种思维的各种技术后来被统一为 JPA 规范。</p><p>JPA 规范的核心目标：提供一套<strong>统一、标准化的API</strong>，将开发者从繁琐的 JDBC 和 SQL 细节中解放出来，实现用面向对象的方式操作数据库。</p><p>JPA 的设计理念是没问题的，并且契合了领域驱动设计的思想，但是可能过于理想化 (没有银弹)。JPA 规范的实现者们可能并没有真正理解 JPA 的目标，JPA 的使用者们可能也并没有学会如何正确地驾驭这套体系。</p><p>实际上后来的 mybatis-plus 也在通过插件增强，以获得与 JPA 类似的便利性。历史上有不少个人或社区孵化项目尝试将 JPA 的 code first 理念应用到 mybatis-plus 的框架上</p><ul><li><a class="link"   href="https://github.com/YoWenhow/mybatis-enhance-actable" >YoWenhow/mybatis-enhance-actable<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（可能已停止维护）</li><li><a class="link"   href="https://gitee.com/sweetxxin/mybatis-plus-auto-ddl" >mybatis-plus-auto-ddl<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（可能已停止维护）</li><li><a class="link"   href="https://gitee.com/donkey-design/auto-ddl" >auto-ddl<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（可能已停止维护）</li><li><a class="link"   href="https://gitee.com/sunchenbin/mybatis-enhance" >mybatis-enhance-actable<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（可能已停止维护）</li><li><a class="link"   href="https://github.com/dromara/auto-table" >dromara/auto-table<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>（活跃更新）</li></ul><p>也有将 JPA 与 Mybtais/Mybtais 配合使用的方式，集两者之长</p><ul><li><a class="link"   href="https://www.cnkirito.moe/jpa-and-mybatis/" >鱼和熊掌兼得：同时使用 JPA 和 Mybatis - 徐靖峰|个人博客<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li></ul><blockquote><p>从现在的视角来看，不妨可以尝试混合持久化：采用“<strong>80/20 原则</strong>”。即80%简单、标准的 CRUD 操作使用  JPA，提升开发效率；而对 20% 性能关键的复杂查询、报表或批量操作，使用原生 SQL 或更灵活的轻量级工具。或者说 mybatis-plus 本身就在实现这样一种思想。</p></blockquote><p>JPA 规范的实现与实际应用也许存在诸多问题（阻抗失衡、N+1 问题），但其 code first 的设计理念始终影响着后继者，并且与现在的 IaC 理念有着异曲同工之妙。</p></blockquote><h3 id="未来的范式代码定义结构"><a class="markdownIt-Anchor" href="#未来的范式代码定义结构"></a> 未来的范式：代码定义结构？</h3><blockquote><p>这里需要考虑两种情况：</p><p>A：在原有项目的基础上引入</p><p>B：在全新的项目里引入</p></blockquote><p>前面提到的 JPA、gorm 其实还是绑定于各自的生态，如果已有项目并没有使用这些框架，就需要花费额外的成本去引入，侵入性较大。</p><p>有一个值得关注的项目 <a class="link"   href="https://github.com/prisma/prisma" >prisma/prisma<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，号称是适用于 Node.js 和 TypeScript 的下一代 orm 框架，统一通过 prisma schema 来进行数据建模，不需要分别在关系型数据库层面和应用程序层面进行数据建模。配合社区客户端实现，能支持生成 go、rust、python 等语言的代码。</p><p>但是在 2025 年初，官网打算将未来的 prisma v7 进行重写，这导致一大片客户端决定停止维护。</p><ul><li><a class="link"   href="https://github.com/steebchen/prisma-client-go/issues/1542" >Deprecation of the Prisma Go client · Issue #1542 · steebchen/prisma-client-go<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li><li><a class="link"   href="https://github.com/Brendonovich/prisma-client-rust/discussions/476" >PCR is no longer being maintained · Brendonovich/prisma-client-rust · Discussion #476<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li><li><a class="link"   href="https://github.com/RobertCraigie/prisma-client-py/issues/1073" >Deprecation of the Python client · Issue #1073 · RobertCraigie/prisma-client-py<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li></ul><p>虽然理论上可以通过开发新的客户端来适配 prisma v7，但这对社区无偿贡献的开发者来说无疑是一个很大的负担。并且正如 prisma 官方博客中所说的：“支持多个客户端的能力，不再是我们的重点”。</p><blockquote><p>原文摘录：</p><p>Additionally, the core benefit of the query engine—the ability to support multiple clients—is no longer our focus. Prisma ORM is a TypeScript project and while we support our community clients, we won’t be developing them in house.</p><p>prisma 官方的博客文章地址：<a class="link"   href="https://www.prisma.io/blog/from-rust-to-typescript-a-new-chapter-for-prisma-orm" >From Rust to TypeScript: A New Chapter for Prisma ORM<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></blockquote><p>基于前面的思考，一种理想化的代码定义结构的基础设施可能是：</p><ul><li><p><strong>输入唯一性</strong>：仅允许实体类/结构体代码作为来源。</p><blockquote><p>实体类或结构体代码总是能与数据表结构相对应，这能从框架层面上反映项目代码实际需要什么样的表结构来支持运行。而人工手动维护的 DDL，尤其还需要维护多份异构数据库的方言，这个过程本身就很耗费精力，很容易产生人为失误。因此选择代码作为来源是最可靠的依据。(当然代码也会存在失误，但相比数据表层面，代码上的结构性失误可能更容易被发现)</p><p>同时这也变相地要求数据表的唯一来源是代码，以及要求代码具备能够控制一些数据库高级特性的能力。</p><p>前面提到的 prisma schema 可能也有潜力同时作为实体类/结构体代码和数据表的唯一来源，但是过于依赖社区实现。</p></blockquote></li><li><p><strong>零侵入性(?)</strong>：不需要改变已有代码。</p><blockquote><p>主要是针对情况 A 考虑。</p><p>这点其实存在悖论，因为现有的代码定义结构的方案中，都是通过修改代码以包含索引、约束等元信息。如果不对代码做修改，那么就无法实现代码定义结构，如果要实现代码定义结构，那就必须得修改已有代码。</p><p>按照前面的说法，数据表的唯一来源是代码，那么一开始数据表是不存在的，元信息自然也就不存在（不考虑隐式行为），代码不包含元信息且不能改动，那就必然无法实现元信息的创建，因为这个过程中元信息根本就不存在，是一个不可能三角。</p></blockquote></li><li><p><strong>零学习成本或低使用成本(?)</strong>：无需学习新的 DSL 语法或者 DSL 能够同时处理编程语言代码和数据表两个层面，提高开发效率。</p><blockquote><p>针对情况 A，新的 DSL 如果还是以人工手动编写的方式运作，那么它和人工维护 SQL 方言没有本质区别。</p><p>针对情况 B，考虑到 DSL 并不仅限于对数据表的控制，如果也能控制代码层面，避免手动编写大量的异构编程语言后端代码 (需要同时进行后端、数据库迁移的，并需要长期支持原系统和新系统)，也许是另一种可能。</p></blockquote></li><li><p><strong>全生命周期支持</strong>：空库创建→增量变更→表管理</p><blockquote><p>数据表结构不仅能通过代码自动创建，还能基于代码的变动来对比差异并自动调整表结构以适应代码。以此实现代码全面管理数据表。更进一步还能支持分布式集群的同构、异构数据库管理。</p></blockquote></li><li><p><strong>多方言自动化</strong>：消除 SQL 方言差异</p><blockquote><p>最核心的功能，基于目标数据库的类型自动处理表结构，解决人工处理异构数据库方言差异的问题。</p></blockquote></li></ul><p>不过上面的想法还是过于理想化。它们在实际应用中能否得以实现以及在实现过程中会有什么困难，还是无法确定的事情。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://juejin.cn/post/7460568798136926262" >Docker 安装 Oracle在Docker中快速使用各个版本的Oracle数据库（10g、11g、12c、18c、1 - 掘金<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.modb.pro/db/48925" >工欲善其事，必先利其器，用上AWS SCT迁移工具，其它的都是将就 - 墨天轮<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.cnblogs.com/hwxing/p/18699033" >Oracle迁移PostgreSQL方案 - Cody&amp; - 博客园<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blog.oldwu.top/index.php/archives/132/" >Springboot+mybatis-plus+ACTable实现自动创建表结构 - oldwu’s Blog<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://mybatis.org/generator/index.html" >Introduction to MyBatis Generator – MyBatis Generator Core<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://books.yyang.io/ddd-persistence-with-jpa/intro/orm/Mismatch.html" >第三节 O/R阻抗失配 · 使用JPA全面实现DDD持久化<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.toutiao.com/article/7492052303269167616/" >为什么你的MyBatis总产生N+1问题？原理与根治方案 - 今日头条<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://uaxe.github.io/geektime-docs/%E8%BF%90%E7%BB%B4-%E6%B5%8B%E8%AF%95/%E4%BA%91%E5%8E%9F%E7%94%9F%E5%9F%BA%E7%A1%80%E6%9E%B6%E6%9E%84%E5%AE%9E%E6%88%98%E8%AF%BE/01-%E5%8F%91%E5%B1%95%E5%8E%86%E7%A8%8B%EF%BC%9AIaC%E8%BF%87%E5%8E%BB%E3%80%81%E7%8E%B0%E5%9C%A8%E4%B8%8E%E6%9C%AA%E6%9D%A5%E5%B1%95%E6%9C%9B/#iac" >01 发展历程：IaC过去、现在与未来展望 - 极客时间文档<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://juejin.cn/post/7374299311935127578" >Prisma 的全部命令和 schema 语法本文详细介绍了 Prisma 的全部命令，包括初始化项目、配置数据库、生成 - 掘金<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.v2ex.com/t/1044717" >大家在工作中创建服务端项目的数据库表都是怎么设计怎么建的？手写 SQL，用 GUI 工具还是 ORM 框架？ - V2EX<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;以 Oracle 19c 迁移 MySQL 8.0 为例，寻求一种心智负担最小的可行方案。</summary>
    
    
    
    <category term="数据库" scheme="https://www.entropy-tree.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    
    
    <category term="oracle19c" scheme="https://www.entropy-tree.top/tags/oracle19c/"/>
    
    <category term="mysql" scheme="https://www.entropy-tree.top/tags/mysql/"/>
    
    <category term="code first" scheme="https://www.entropy-tree.top/tags/code-first/"/>
    
  </entry>
  
  <entry>
    <title>现代化环境管理</title>
    <link href="https://www.entropy-tree.top/2025/04/17/modern-multi-env-manager/"/>
    <id>https://www.entropy-tree.top/2025/04/17/modern-multi-env-manager/</id>
    <published>2025-04-17T05:48:22.000Z</published>
    <updated>2025-04-17T08:15:17.101Z</updated>
    
    <content type="html"><![CDATA[<p>聊聊一种可能是环境管理的现代化方案<span id="more"></span></p><p>在讨论环境管理之前，先看看环境搭建这个从古至今的问题。</p><blockquote><p>这里讨论的环境，主要是指 sdk 的环境、多版本 sdk 动态选择的环境，也可以是指一套容器化的环境。</p></blockquote><h2 id="环境搭建的传统方式"><a class="markdownIt-Anchor" href="#环境搭建的传统方式"></a> 环境搭建的传统方式</h2><p>在以前，搭建环境的一般步骤就是手动从官方网站下载具体的 sdk ，然后安装 sdk 、配置环境变量，最后验证环境的可用性。这样看来，搭建环境似乎不是一件很麻烦的事情，每个步骤都很清晰，搭建完环境后就可以专心投入新项目的开发。</p><p>后来，有了更多的新项目，并且这些项目对 sdk 的要求各不相同，于是按照这些项目的要求手动搭建环境。起初还好，每套 sdk 互不干扰，基本一套环境搭建完，后续可以一直使用。</p><p>不过，随着时间发展，sdk 也在不断迭代升级。出于对新特性的好奇，你再次从官方网站上手动重复了一遍环境搭建工作。有一部分 sdk 似乎变得“智能”了一些，环境变量配置的工作被内置在特定格式 sdk 文件中，但是升级 sdk 还是需要手动从官方网站上下载。</p><p>另一方面，云服务器的流行，使得你开始接触不同的操作系统，你可能会需要在不同的新机器中反复搭建环境。当你在新的操作系统上搭建环境时，可能会发现过去的经验并不能很好的工作。</p><h2 id="脚本化搭建与包管理器搭建"><a class="markdownIt-Anchor" href="#脚本化搭建与包管理器搭建"></a> 脚本化搭建与包管理器搭建</h2><p>在学习了终端 cli 安装之后，你可能意识到之前环境搭建的很多过程是可以自动化的，并且注意到一些新兴的 sdk 并非是以安装包的形式直接提供，而是给出了一串奇怪的命令。在终端执行这串命令之后，环境便完成了搭建。</p><p>你尝试理解了这串命令的含义，知道它包含了一个脚本，不过是通过网络的形式下载执行的。为了了解具体的脚本是如何做到自动化搭建环境的，你访问了这个脚本实际的内容，发现它是以代码的形式记录了一个环境搭建的行为。</p><p>因此，通过编写脚本记录环境搭建的步骤，你实现了自动化环境搭建，但是脚本的编写与维护工作可能是一件更耗费精力的事情：你可能希望这个脚本能实现安装、更新、卸载以及更加高级的功能。</p><p>脚本并非完全包含了环境搭建的所有可能性。一个脚本可能只在特定的操作系统上工作，针对不同的系统你可能还需要编写不同格式的脚本，且考虑环境配置的幂等性等情况，一部分配置工作还是设计为用户手动处理。</p><p>脚本虽然解决了一部分手动操作的问题，但是引入了新的问题。这些问题很大程度上受脚本编写质量的影响：如果一个脚本编写的非常出色，那么几乎不需要用户操劳。如果一个脚本编写的质量不好，甚至可能造成损失的，那么用户可能得自行为执行这个脚本的过失负责。因此，除非用户审阅并正确理解脚本的行为，不然无法估计执行脚本所带来的变数。（然而这和仔细阅读用户须知等协议是类似的，大部分用户并没有这么多的耐心）</p><p>你希望有一个相对规范统一的平台来处理这些事情，于是你接触了专属于操作系统的包管理器。包管理器相对于脚本提供了更好的保障和统一性，虽然这种保障并不是绝对的，但是这比纯粹的脚本管理带来了更多的高级功能和体验。尽管包管理器的运作需要特定的权限，但是目前你拥有最高权限。你使用包管理器在几条命令之后就完成了环境搭建，环境搭建基本上由包管理器统一承担。</p><p>但正因为包管理器是系统专属的，它的运作只适用于它所对应的操作系统，无法在多台系统各异的机器上重复相同的操作。此外，包管理器由于总是将环境置于一个全局的位置，导致系统可能会变得不太稳定。你或许需要一个具有隔离性的环境管理方案。</p><h2 id="容器化搭建与跨平台-sdk-管理器搭建"><a class="markdownIt-Anchor" href="#容器化搭建与跨平台-sdk-管理器搭建"></a> 容器化搭建与跨平台 sdk 管理器搭建</h2><p>云原生时代，容器化部署开始流行，环境的搭建可以转移到容器中实现。基于容器仓库可以快速获取定制好的各种环境，并且定制自己的环境使用。<br />容器化主要有应用容器和系统容器两个方向。</p><p>应用容器主要是以单个应用进程为中心。<a class="link"   href="https://github.com/89luca89/distrobox" >89luca89/distrobox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 是一个容器包装层，主要是提供不同的容器化 linux 版本，依赖 docker 或 podman 运作。本质上是一系列脚本，自动化处理复杂的容器集成逻辑，实现高度系统集成的容器环境。此外，还有类似的 <a class="link"   href="https://github.com/containers/toolbox" >containers/toolbox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><p>系统容器旨在模拟完整的操作系统环境，能同时运行多个进程与服务。<a class="link"   href="https://github.com/lxc/lxc" >lxc<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 是比较典型的系统级容器虚拟化工具。lxc 本身比较底层，基于 lxc 开发的高级管理工具有 <a class="link"   href="https://github.com/canonical/lxd" >lxd<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 和 <a class="link"   href="https://github.com/lxc/incus" >incus<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>(lxd 的社区分支)。</p><p>如果使用过基于 systemd 的操作系统，可能会知道 systemd-nspawn 这个工具，它是由 systemd 生态所提供的系统级容器。</p><p>容器化引入新的学习成本，定制符合个人需求的环境需要熟练掌握容器化技术的使用方法，似乎对于个人环境搭建来说有些沉重了。</p><p>在另外一条道路上，一些新兴的跨平台 sdk 管理器出现，它们不同于传统的包管理器，不绑定单一的系统。这些 sdk 管理器基本涵盖了常见的 sdk 包，能够爬取各种 sdk 的版本号，具有下载、安装、卸载、特定版本控制等一系列功能，可以快速地搭建环境，适应复杂多变的环境需求。</p><blockquote><p>我在<a href="https://www.entropy-tree.top/2023/03/12/more-env-control/">过去的文章</a>中有提到过一些 sdk 管理器，但是它们局限于各自的生态。<br />以下几个工具包含了相对全面的 sdk 管理：</p><ul><li><a class="link"   href="https://github.com/asdf-vm/asdf" >asdf-vm/asdf<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li><li><a class="link"   href="https://github.com/version-fox/vfox" >version-fox/vfox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li><li><a class="link"   href="https://github.com/gvcgo/version-manager" >gvcgo/version-manager<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li><li><a class="link"   href="https://github.com/jdx/mise" >jdx/mise<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li></ul></blockquote><p>直到多版本 sdk 管理器出现之前开发环境都是基于固定的环境变量 PATH 来识别。</p><h2 id="多版本环境管理问题"><a class="markdownIt-Anchor" href="#多版本环境管理问题"></a> 多版本环境管理问题</h2><p>前面说到，出于对新版本 sdk 的需求，你可能已经在系统中积累了多个版本的 sdk 或者是只留下了最新版本的 sdk。sdk 的迭代升级并不总是一帆风顺的，有时候迁移到新的 sdk 可能会付出较大的代价，因为新的 sdk 无法再兼容旧 sdk 的产物。</p><p>同时，多次重复的 sdk 配置工作，使得环境变量变得杂乱不堪。你可能需要一种能管理多版本 sdk 且不会污染环境变量的方案，例如 vfox.</p><p>在多版本 sdk 管理器的时代，为了实现多版本切换，引入了 shims 机制。最初是通过 shims 脚本动态切换软链接的指向实现 sdk 版本的切换，不过 shims 中间层的存在，造成了一定的性能损失。于是，后续出现了读取目录下特定文件动态改变环境变量的方案 <a class="link"   href="https://github.com/direnv/direnv" >direnv<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://github.com/jdx/mise" >mise<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，通过维护动态的环境变量来实现高效的 sdk 版本切换。</p><h2 id="声明式的环境管理"><a class="markdownIt-Anchor" href="#声明式的环境管理"></a> 声明式的环境管理</h2><p>在大部分多版本 sdk 管理方案还是基于动态选择思想设计时，一些不走寻常路的工具采取了基于声明式配置的静态解析。</p><p>声明式配置的一个例子就是 <a class="link"   href="https://github.com/NixOS/nix" >nix<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。nix 的设计受到了 haskell 函数式编程范式的影响，使用 nix 需要一定的函数式经验。不过基于 nix 已经出现了相对容易使用的工具：<a class="link"   href="https://github.com/jetify-com/devbox" >jetify-com/devbox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 和 <a class="link"   href="https://github.com/flox/flox" >flox/flox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>.</p><blockquote><p>devbox 可以在 <a class="link"   href="https://www.nixhub.io/" >Nixhub.io | A Nix Packages Registry<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 中搜索包，并支持导出 devcontainers 供其他容器使用。</p></blockquote><h2 id="现状"><a class="markdownIt-Anchor" href="#现状"></a> 现状</h2><p>传统的环境搭建方式，在简单场景下依然是最符合直觉的不二选择。但是环境的变化总是会趋向于更复杂的情况，越发庞大的环境，越发混乱的环境。于是开始使用容器管理环境，但是容器的初衷并不是提供开发用途的环境，更多的是提供长期的服务。</p><p>当然在容器发展过程中也出现了 <a class="link"   href="https://code.visualstudio.com/docs/devcontainers/containers" >Developing inside a Container<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 这种在容器内开发的形式。<br /><a class="link"   href="https://codesandbox.io/" >codesandbox<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 就是一个基于容器化环境开发的在线平台，并且很多开发工具提供了对容器化开发的支持。</p><blockquote><p><a class="link"   href="https://www.youtube.com/watch?v=SDa3v4Quj7Y" >My Favorite Way To Handle Dev Environments | VS Code Devcontainers - YouTube<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 介绍了如何在 vscode 中使用 devcontainers 插件来实现通过容器管理开发环境。</p></blockquote><p>但是环境并不总是单一地服务于开发、测试。很多程序也需要一个稳定运行的环境，而一些程序与容器化的相性不合，更多时候还是需要一个在本地可用的环境。于是又回到本地，但是这时候有了 sdk 管理器的选择。</p><p>而 sdk 管理器则从原本的单一 sdk 管理趋向于更加全面的 sdk 管理，并且通过与 direnv 等工具配合实现了更灵活的开发环境管理。sdk 管理器的出现，算是很好地解决了大部分问题。但是某天你需要在另一台新的机器上准备这些环境，你会发现你又需要重复做一篇历史工作。容器化一定程度上能解决这个问题，但是容器化并不适用所有的场景。你可能希望有一种方案，能像脚本一样记录、能像 sdk 管理器一样控制版本、能像容器一样迁移。于是声明式环境管理可能是一种较好的方案。</p><p>声明式环境管理使得环境可以被记录和控制，基于这些配置元信息，能便捷地将环境转移到其他地方。</p><p>那么最终的环境管理形态可能有两种：一种是基于声明式配置 + 容器化开发(devcontainers) + 容器化生产的组合，适合大规模的环境共享以及对环境有较高的一致性要求；另一种是基于 sdk 管理器，适合个人灵活使用和临时测试。</p><h2 id="参考文章"><a class="markdownIt-Anchor" href="#参考文章"></a> 参考文章</h2><p><a class="link"   href="https://wiki.archlinux.org/title/Toolbox" >Toolbox - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinux.org/title/Distrobox" >Distrobox - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinux.org/title/Linux_Containers" >Linux Containers - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinux.org/title/LXD" >LXD - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinux.org/title/Incus" >Incus - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinux.org/title/Systemd-nspawn" >systemd-nspawn - ArchWiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blog.mkacg.com/2025/01/02/use-docker-and-distrobx-and-nix-to-develop-uos/" >使用 distrobox 和 nix 加速 UOS 开发 | 小竹’s blog<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.v2ex.com/t/1036639" >SDK 版本管理器之 vmr 和 vfox 对比 - V2EX<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhaochunqi.com/posts/using-mise-to-manager-your-packages/" >Mise：跨语言开发环境的高效配置工具 | 小赵的技术手记<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhaochunqi.com/posts/direnv/" >利用 direnv 管理项目环境变量，提高开发效率 | 小赵的技术手记<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.jetify.com/blog/devbox-turn-a-1000-container-script-into-10-lines/" >Devbox vs Docker: light &amp; repeatable dev envs w/o container<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhuanlan.zhihu.com/p/391525180" >docker容器技术基础之linux cgroup、namespace - 知乎<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://code.visualstudio.com/docs/devcontainers/tutorial" >Dev Containers tutorial<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://sspai.com/post/89358" >devcontainer 容器化你的开发环境-理论篇 - 少数派<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;聊聊一种可能是环境管理的现代化方案</summary>
    
    
    
    <category term="环境管理" scheme="https://www.entropy-tree.top/categories/%E7%8E%AF%E5%A2%83%E7%AE%A1%E7%90%86/"/>
    
    
    <category term="shims" scheme="https://www.entropy-tree.top/tags/shims/"/>
    
    <category term="containers" scheme="https://www.entropy-tree.top/tags/containers/"/>
    
    <category term="declarative" scheme="https://www.entropy-tree.top/tags/declarative/"/>
    
  </entry>
  
  <entry>
    <title>自制 vscode 扩展包</title>
    <link href="https://www.entropy-tree.top/2025/03/04/make-your-own-vscode-extension-pack/"/>
    <id>https://www.entropy-tree.top/2025/03/04/make-your-own-vscode-extension-pack/</id>
    <published>2025-03-04T12:00:21.000Z</published>
    <updated>2025-03-04T13:47:52.940Z</updated>
    
    <content type="html"><![CDATA[<p>将已有的 vscode 扩展打包成为扩展包，方便快速分享给他人使用。<span id="more"></span></p><h2 id="必要的前置工具"><a class="markdownIt-Anchor" href="#必要的前置工具"></a> 必要的前置工具</h2><p>参考微软官方的文档 <a class="link"   href="https://code.visualstudio.com/api/get-started/your-first-extension" >Your First Extension<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，可以通过 <a class="link"   href="https://yeoman.io/" >Yeoman<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 和 <a class="link"   href="https://www.npmjs.com/package/generator-code" >VS Code Extension Generator<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 搭建基础环境。</p><p>安装这两个工具</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bun i yo generator-code -g</span><br></pre></td></tr></table></figure></div><p>生成项目基础环境</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yo code</span><br></pre></td></tr></table></figure></div><p>交互式生成</p><ul><li>指定扩展的类型，这里选择扩展包 <code>New Extension Pack</code></li><li>将当前已安装的扩展添加至扩展包 <code>Yes</code></li><li>扩展包命名</li><li>扩展包唯一标识</li><li>扩展包描述</li><li>使用 git 管理 <code>Yes</code></li></ul><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20250304172757460.png"                      alt="image-20250304172757460"                 ></p><p>生成的配置文件示例</p><p>package.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;oh-my-extension&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;displayName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;oh-my-extension&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;the collection of extensions for personal use&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.0.1&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;vscode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^1.97.0&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;categories&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;Extension Packs&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;extensionPack&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span> </span><br><span class="line">    <span class="string">&quot;alduncanson.react-hooks-snippets&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;astro-build.astro-vscode&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;aykutsarac.jsoncrack-vscode&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;biomejs.biome&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;bradlc.vscode-tailwindcss&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;dracula-theme.theme-dracula&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;dsznajder.es7-react-js-snippets&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;formulahendry.auto-close-tag&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;formulahendry.auto-complete-tag&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;formulahendry.auto-rename-tag&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;gruntfuggly.todo-tree&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;quicktype.quicktype&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;rooveterinaryinc.roo-cline&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;rust-lang.rust-analyzer&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;sdras.vue-vscode-snippets&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;simonsiefke.svg-preview&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;tamasfe.even-better-toml&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;usernamehw.errorlens&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;vscode-icons-team.vscode-icons&quot;</span><span class="punctuation">,</span> </span><br><span class="line">    <span class="string">&quot;wscats.vue&quot;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>到这里扩展包的配置基本制作完成，后续可以打包为 vsix 文件或发布到 Marketplace。</p><h2 id="打包-vsix-文件"><a class="markdownIt-Anchor" href="#打包-vsix-文件"></a> 打包 vsix 文件</h2><p>打包涉及到另外一个管理管理工具 vsce，能够将项目打包为 VsCode 可以使用的 vsix 文件，并支持通过 CLI 的方式将插件发布至 Marketplace。</p><blockquote><p>这个工具在微软官方文档 <a class="link"   href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension" >Publishing Extensions<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 中也提到了，需要注意的是通过 CLI 的方式发布是不需要手动打包 vsix 文件的。</p></blockquote><p>安装打包工具</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bun i @vscode/vsce -g</span><br></pre></td></tr></table></figure></div><p>进入到项目根目录打包</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> oh-my-extension</span><br><span class="line"></span><br><span class="line">vsce package</span><br></pre></td></tr></table></figure></div><p>此时在当前目录下已经生成了 vsix 文件，可以分享 vsix 文件给其他人使用。</p><h2 id="手动发布到-marketplace"><a class="markdownIt-Anchor" href="#手动发布到-marketplace"></a> 手动发布到 Marketplace</h2><p>默认生成的文件模块还无法直接发布，需要补充 publisher 信息，可以参考这份<a class="link"   href="https://code.visualstudio.com/api/get-started/extension-anatomy#extension-manifest" >官方模板<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><blockquote><p>publisher 的信息需要与在 Marketplace 创建的 publisher 一致，扩展包最终的 Identifier 是由 &lt;publisher&gt;.&lt;name&gt; 组成的。</p></blockquote><p>在 package.json 中补充完 publisher 信息后，需要重新打包 vsix 文件。手动发布的方式需要依赖 vsix 文件。</p><p>在 <a class="link"   href="https://marketplace.visualstudio.com/vscode" >Marketplace<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 中手动上传 vsix 文件进行发布。一般等待几分钟的验证之后，就能在 VsCode 的扩展市场搜索到发布的扩展包。</p><h2 id="cli-发布方式"><a class="markdownIt-Anchor" href="#cli-发布方式"></a> CLI 发布方式</h2><p>CLI 发布方式需要先使用 Microsoft 账号创建一个 Azure DevOps 的组织，然后才能创建获取 Personal access token (需要保管好，不能泄露)。</p><p>获取到 token 之后，创建新的 publisher。</p><blockquote><p>ID 信息需要和 package.json 中的 publisher 一致。如果之前已经创建过 publisher 了，这里不用重复创建。</p></blockquote><p>接下来，在终端登录 publisher 并输入 token</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">vsce login &lt;publisher <span class="built_in">id</span>&gt;</span><br><span class="line"></span><br><span class="line">https://marketplace.visualstudio.com/manage/publishers/</span><br><span class="line">Personal Access Token <span class="keyword">for</span> publisher <span class="string">&#x27;&lt;publisher id&gt;&#x27;</span>: ****************************************************</span><br><span class="line"></span><br><span class="line">The Personal Access Token verification succeeded <span class="keyword">for</span> the publisher <span class="string">&#x27;&lt;publisher id&gt;&#x27;</span>.</span><br></pre></td></tr></table></figure></div><p>通过 cli 发布，这种方式不需要打包 vsix 文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vsce publish</span><br></pre></td></tr></table></figure></div><p>等待 Marketplace 验证之后，就能在 VsCode 的扩展市场中搜索到相应的扩展。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;将已有的 vscode 扩展打包成为扩展包，方便快速分享给他人使用。</summary>
    
    
    
    <category term="工具链" scheme="https://www.entropy-tree.top/categories/%E5%B7%A5%E5%85%B7%E9%93%BE/"/>
    
    
    <category term="vscode" scheme="https://www.entropy-tree.top/tags/vscode/"/>
    
  </entry>
  
  <entry>
    <title>更换小米路由器 4A 千兆版的系统</title>
    <link href="https://www.entropy-tree.top/2024/10/04/flash-the-xiaomi-router/"/>
    <id>https://www.entropy-tree.top/2024/10/04/flash-the-xiaomi-router/</id>
    <published>2024-10-04T03:13:13.000Z</published>
    <updated>2024-10-04T03:24:43.296Z</updated>
    
    <content type="html"><![CDATA[<p>之前购入一台小米路由器用于研究学习。不过由于原生的路由器系统存在封闭、可定制性差的缺点，遂准备将原生系统更换。<span id="more"></span></p><blockquote><p>其实设备早在距离本文发布将近一年前就已经购入，不过当时还同时购入了另一台机器用于旁路由。</p><p>由于路由器本身的配置并不高，相比另一台机器有所欠缺，遂没有把研究更换路由器系统放在首位。</p></blockquote><p>提示：</p><ul><li><p>本文内容可能仅适用于小米路由器 4A 千兆版，系统版本 2.28.65。</p></li><li><p>路由器刷机需要足够充分的准备，以免造成损失。</p></li></ul><h2 id="准备工作"><a class="markdownIt-Anchor" href="#准备工作"></a> 准备工作</h2><p>准备工作分为硬件设备和软件资料等。</p><p>硬件设备主要是以下三个</p><ul><li>小米路由器 4A 千兆版</li><li>网线</li><li>个人电脑 (有网口或者用网口扩展器)</li></ul><p>软件资料获取如下</p><ul><li><p>访问相关论坛社区等查询该路由器设备的型号版本是否支持刷机。</p></li><li><p>访问路由器后台管理，查看系统信息。</p><blockquote><p>本人持有的路由器为小米路由器 4A 千兆版，当前系统版本为 2.28.65，处于可刷机的版本。</p><p>注意：2.30.x 版本与2.28.x 版本存在差异。</p><p>其他型号版本的路由器根据相关的资料判断是否支持刷机。</p></blockquote></li><li><p>查询路由器系统的相关资料并选择合适的路由器系统。</p><blockquote><p>比较知名的路由器系统就有 openWRT、Padavan 等等，可自行选择符合需求的系统。</p></blockquote></li></ul><h2 id="刷机"><a class="markdownIt-Anchor" href="#刷机"></a> 刷机</h2><p>刷机阶段主要是根据详细的教程，准备对应的固件、环境和工具等。这里就以小米路由器 4A 千兆版为例，记录下整个刷机过程。</p><h3 id="固件下载"><a class="markdownIt-Anchor" href="#固件下载"></a> 固件下载</h3><p>固件主要有两个 Breed 和 openWRT/Padavan 等 (本文演示的是 Padavan)</p><h4 id="breed"><a class="markdownIt-Anchor" href="#breed"></a> Breed</h4><p>Breed 是由 <a class="link"   href="https://github.com/hackpascal" >hackpascal<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 开发的闭源 Bootloader，称为“不死鸟”。根据参考资料，这个固件可以避免现有的 bootloader 在从官方固件升级时被覆盖而导致用户失去对设备的自定义和控制能力。</p><p>Breed 下载地址：<a class="link"   href="https://breed.hackpascal.net/" >https://breed.hackpascal.net/<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>适用于小米路由器 4A 千兆版的 Breed 固件：<a class="link"   href="https://breed.hackpascal.net/breed-mt7621-pbr-m1.bin" >https://breed.hackpascal.net/breed-mt7621-pbr-m1.bin<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h4 id="openwrt"><a class="markdownIt-Anchor" href="#openwrt"></a> openWRT</h4><p>适用于小米路由器 4A 千兆版的 openWRT 固件<a class="link"   href="https://downloads.openwrt.org/releases/21.02.1/targets/ramips/mt7621/openwrt-21.02.1-ramips-mt7621-xiaomi_mi-router-4a-gigabit-squashfs-sysupgrade.bin" >下载<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h4 id="padavan"><a class="markdownIt-Anchor" href="#padavan"></a> Padavan</h4><p>适用于小米路由器 4A 千兆版的 Padavan 固件 <a class="link"   href="https://opt.cn2qq.com/padavan/MI-R4A_3.4.3.9-099.trx" >下载<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="开启路由器-ssh"><a class="markdownIt-Anchor" href="#开启路由器-ssh"></a> 开启路由器 SSH</h3><blockquote><p>需要提前准备好 python3 环境</p></blockquote><p>一般的家用路由器不会默认开启 SSH 功能，现有的方案是通过破解获取 SSH 的功能。破解的方案借助了 OpenWRTInvasion 项目。</p><p>下载项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/acecilia/OpenWRTInvasion.git</span><br></pre></td></tr></table></figure></div><p>安装依赖</p><blockquote><p>注意 python 的版本，本人使用的是 3.10.4 版本</p></blockquote><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> OpenWRTInvasion</span><br><span class="line">pip install -r requirements.txt</span><br></pre></td></tr></table></figure></div><p>执行脚本</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">python remote_command_execution_vulnerability.py</span><br></pre></td></tr></table></figure></div><p>输入路由器管理后台密码以及管理后台页面 url 地址上的 stok 值，会看到如下输出</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241003234803267.png"                      alt="image-20241003234803267"                 ></p><p>使用其输出的 ssh 命令或其他命令连接到路由器，看到以下内容则表示连接成功</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241003235011323.png"                      alt="image-20241003235011323"                 ></p><h3 id="备份原始固件"><a class="markdownIt-Anchor" href="#备份原始固件"></a> 备份原始固件</h3><p>为了避免出现无法还原系统的问题，建议刷机之前将官方原始固件备份。</p><p>路由器固件一般存储在 MTD (Memory Technology Device) 分区上，通常在 /proc/mtd 文件中可以看到所有 MTD 分区的信息。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> /proc/mtd</span><br></pre></td></tr></table></figure></div><p>输出的示例参考如下</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">dev:    size   erasesize  name</span><br><span class="line">mtd0: 01000000 00010000 <span class="string">&quot;ALL&quot;</span></span><br><span class="line">mtd1: 00030000 00010000 <span class="string">&quot;Bootloader&quot;</span></span><br><span class="line">mtd2: 00010000 00010000 <span class="string">&quot;Config&quot;</span></span><br><span class="line">mtd3: 00010000 00010000 <span class="string">&quot;Bdata&quot;</span></span><br><span class="line">mtd4: 00010000 00010000 <span class="string">&quot;Factory&quot;</span></span><br><span class="line">mtd5: 00010000 00010000 <span class="string">&quot;crash&quot;</span></span><br><span class="line">mtd6: 00010000 00010000 <span class="string">&quot;cfg_bak&quot;</span></span><br><span class="line">mtd7: 00100000 00010000 <span class="string">&quot;overlay&quot;</span></span><br><span class="line">mtd8: 00d00000 00010000 <span class="string">&quot;OS1&quot;</span></span><br><span class="line">mtd9: 00b30000 00010000 <span class="string">&quot;rootfs&quot;</span></span><br><span class="line">mtd10: 00180000 00010000 <span class="string">&quot;disk&quot;</span></span><br></pre></td></tr></table></figure></div><p>这些文件都位于 /dev 目录下，每一个文件就是一个 MTD 分区，存储了重要的固件信息。</p><p>对于分区级别的备份，建议使用 dd 命令实现完整克隆。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="built_in">mkdir</span> backupB9AD</span><br><span class="line"><span class="built_in">cd</span> backupB9AD</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd0 of=/tmp/backupB9AD/mtd0-ALL.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd1 of=/tmp/backupB9AD/mtd1-Bootloader.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd2 of=/tmp/backupB9AD/mtd2-Config.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd3 of=/tmp/backupB9AD/mtd3-Bdata.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd4 of=/tmp/backupB9AD/mtd4-Factory.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd5 of=/tmp/backupB9AD/mtd5-crash.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd6 of=/tmp/backupB9AD/mtd6-cfg_bak.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd7 of=/tmp/backupB9AD/mtd7-overlay.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd8 of=/tmp/backupB9AD/mtd8-OS1.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd9 of=/tmp/backupB9AD/mtd9-rootfs.bin</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mtd10 of=/tmp/backupB9AD/mtd10-disk.bin</span><br></pre></td></tr></table></figure></div><blockquote><p>注意文件写入等操作只能在 /tmp 目录下完成，在其他位置写入会提示系统只读。</p></blockquote><p>备份完成后，将备份文件下载到本地</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz -e <span class="string">&quot;ssh -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa -c 3des-cbc -o UserKnownHostsFile=/dev/null&quot;</span> root@miwifi.com:/tmp/backupB9AD/ ./backupB9AD</span><br></pre></td></tr></table></figure></div><h3 id="上传新固件"><a class="markdownIt-Anchor" href="#上传新固件"></a> 上传新固件</h3><p>上传 Breed 固件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz -e <span class="string">&quot;ssh -o KexAlgorithms=+diffie-hellman-group1-sha1 -o HostKeyAlgorithms=+ssh-rsa -c 3des-cbc -o UserKnownHostsFile=/dev/null&quot;</span> breed-mt7621-pbr-m1.bin root@miwifi.com:/tmp/</span><br></pre></td></tr></table></figure></div><p>检查文件 md5 值是否为 <code>24e62762809c15ba3872e610a37451a3</code>，这一步是为了确保文件传输完整。md5 列表清单可参考 <a class="link"   href="https://breed.hackpascal.net/md5sum.txt" >https://breed.hackpascal.net/md5sum.txt<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>刷入 Breed 固件并自动重启</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mtd -r write /tmp/breed-mt7621-pbr-m1.bin Bootloader</span><br></pre></td></tr></table></figure></div><p>使用网线将路由器的 LAN 口与电脑连接，访问 192.168.1.1 可以看到如下页面</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241004021336675.png"                      alt="image-20241004021336675"                 ></p><blockquote><p>如果不能访问，请在断开电源和没有其他 WAN/LAN 口连接的情况下，按住路由器的 reset 按钮并重新接入电源 5-10 秒后再松开 reset 按钮</p></blockquote><p>在固件更新页面上传前面下载的 Padavan 固件</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241004022142624.png"                      alt="image-20241004022142624"                 ></p><p>上传完后点击更新，等待完成固件更新</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241004022325708.png"                      alt="image-20241004022325708"                 ></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241004022456732.png"                      alt="image-20241004022456732"                 ></p><p>访问 Padavan 系统的默认网关：192.168.123.1，网页会要求输入用户名和密码</p><blockquote><p>用户名和密码默认均为 admin</p><p>wifi 名称为 PDCN 或 PDCN_5G</p><p>wifi 密码默认为 1234567890</p></blockquote><p>看到如下页面即表示成功</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20241004083741971.png"                      alt="image-20241004083741971"                 ></p><p>将设备连接到 PDCN 或 PDCN_5G，测试能否上网。</p><h2 id="后续"><a class="markdownIt-Anchor" href="#后续"></a> 后续</h2><p>一些可能有用的初步设置</p><h3 id="自定义连接设备的名称"><a class="markdownIt-Anchor" href="#自定义连接设备的名称"></a> 自定义连接设备的名称</h3><p>在 “高级设置” ── “内部网络” ── “DHCP 服务器” 中手动指定 IP 的 DHCP 列表，找到对应设备的 MAC 地址，并修改主机名，点击右侧的 + 号即可。</p><blockquote><p>参考 <a class="link"   href="https://www.right.com.cn/FORUM/forum.php?mod=viewthread&amp;tid=4047691" >https://www.right.com.cn/FORUM/forum.php?mod=viewthread&amp;tid=4047691<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></blockquote><h3 id="启用-ssh"><a class="markdownIt-Anchor" href="#启用-ssh"></a> 启用 SSH</h3><p>在 “高级设置” ── “系统管理” ── “服务” 中可在终端服务里启用 SSH 服务。</p><p>其他设置和更多的用法等可自行上网搜索研究。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://www.right.com.cn/forum/thread-6776687-1-1.html" >小米路由器4A千兆版 折腾记录 完美刷入官方openwrt 2022-08-09更新<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>(出处: 恩山无线论坛)</p><p><a class="link"   href="https://blog.csdn.net/u013488276/article/details/141136009" >小米路由器4A千兆版刷openWRT保姆级教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blog.csdn.net/CoolBoySilverBullet/article/details/121077410" >【路由器】Breed 介绍、刷入和使用<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.cnblogs.com/travis-ge/articles/17740005.html" >小米路由器4A千兆版(xiaomi-R4A)刷breed以及其它系统<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.cnblogs.com/wangdac/p/13614220.html" >Padavan老毛子固件玩法入门<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://kjzjj.com/index.php/2021/07/06/padavan-pdcn/" >老毛子系统使用说明（padavan &amp; PDCN)<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;之前购入一台小米路由器用于研究学习。不过由于原生的路由器系统存在封闭、可定制性差的缺点，遂准备将原生系统更换。</summary>
    
    
    
    <category term="软硬件" scheme="https://www.entropy-tree.top/categories/%E8%BD%AF%E7%A1%AC%E4%BB%B6/"/>
    
    
    <category term="刷机" scheme="https://www.entropy-tree.top/tags/%E5%88%B7%E6%9C%BA/"/>
    
    <category term="openWRT" scheme="https://www.entropy-tree.top/tags/openWRT/"/>
    
    <category term="Padavan" scheme="https://www.entropy-tree.top/tags/Padavan/"/>
    
    <category term="breed" scheme="https://www.entropy-tree.top/tags/breed/"/>
    
  </entry>
  
  <entry>
    <title>浅析设计模式</title>
    <link href="https://www.entropy-tree.top/2024/07/09/design-pattern/"/>
    <id>https://www.entropy-tree.top/2024/07/09/design-pattern/</id>
    <published>2024-07-09T09:41:37.000Z</published>
    <updated>2024-07-09T09:41:37.322Z</updated>
    
    <content type="html"><![CDATA[<p>简单地分析一下 23 种设计模式之间的联系以及如何选择比较合适的设计模式。<span id="more"></span></p><p>对于设计模式的理解，首先从设计模式遵循的七大原则开始。</p><h2 id="设计模式的原则"><a class="markdownIt-Anchor" href="#设计模式的原则"></a> 设计模式的原则</h2><p>设计模式原则是设计模式的基础，其核心原则是 SOLID 原则。SOLID 原则最初是针对面向对象编程（OOP）提出的，它在形式上依赖于 OOP 的许多概念，例如类、继承、封装和多态。但是 SOLID 原则背后的思想——创建灵活、可维护和可扩展的软件——是可以跨编程范式应用的。</p><p>通常在讨论设计模式的原则时，还会包括另外两个原则 (LoD 和 CARP)，它们与 SOLID 原则一起被称为 “设计模式七大原则”。</p><blockquote><p>关于设计模式的原则，存在六大原则和七大原则的说法，两者的区别主要在于是否将“组合/聚合复用原则”单独列出。</p></blockquote><h3 id="solid-原则"><a class="markdownIt-Anchor" href="#solid-原则"></a> SOLID 原则</h3><blockquote><p>SOLID 原则是一组软件设计原则，用于指导软件开发人员设计和实现高质量的、易于维护和扩展的软件。它是由 Robert C. Martin 在其著作《Agile Software Development, Principles, Patterns, and Practices》中提出的，是目前软件工程界被广泛接受的一种软件设计理念。</p></blockquote><p>SOLID 原则包括以下五个原则，其名称就是由这五个原则的英文名称首字母组成</p><ul><li>Singe Responsibility Principle, SRP</li><li>Open-Closed Principle, OCP</li><li>Liskov Substitution Principle, LSP</li><li>Interface Segregation Principle, ISP</li><li>Dependency Inversion Principle, DIP</li></ul><h4 id="单一职责原则-srp"><a class="markdownIt-Anchor" href="#单一职责原则-srp"></a> 单一职责原则 SRP</h4><p><strong>说明</strong>：一个类应该只有一个引起它变化的原因。一个类只负责一项职责。</p><p><strong>例子</strong>：一个 <code>User</code> 类通常只负责记录用户的数据，另一个类 <code>UserValidator</code> 负责验证用户数据。</p><p>但是职责的划分有时并不是非常明确的，在不同的情况下有不同的划分，<code>User</code> 类也可以兼有验证数据的职责。个人认为某个职责是否应该单独划分，取决于职责的复杂程度。如果验证的逻辑足够复杂，那么可以将这部分职责单独分离到 <code>UserValidator</code> 中，如果验证的逻辑非常简单，那么 <code>User</code> 类可以自行实现，而不必单独划分。最终目的是确保代码易于理解和维护。</p><h4 id="开放封闭原则-ocp"><a class="markdownIt-Anchor" href="#开放封闭原则-ocp"></a> 开放封闭原则 OCP</h4><p><strong>说明</strong>：类应该对扩展开放，对修改关闭。软件实体应该可以扩展，但不可修改。</p><p><strong>例子</strong>：扩展 <code>Shape</code> 类，优先考虑继承 <code>Shape</code> 类，增加新的 <code>Rectangle</code> 和 <code>Circle</code> 类，而不是修改 <code>Shape</code> 类本身。</p><p>OCP 的核心思想是在不修改现有代码的基础上，允许扩展功能，从而提高系统的灵活性和可维护性。最常见的形式就是类的继承、接口的实现。</p><p>此外，策略模式和装饰者模式都是在不修改已有代码的基础上扩展系统功能的设计模式。</p><blockquote><p>子类重写父类中已经实现的方法，也违反了开闭原则，所以通常父类需要扩展的功能被定义为抽象方法。</p></blockquote><h4 id="里氏替换原则-lsp"><a class="markdownIt-Anchor" href="#里氏替换原则-lsp"></a> 里氏替换原则 LSP</h4><p><strong>说明</strong>：子类对象应该能够替换掉它们的父类对象，而不影响程序的行为。</p><p><strong>例子</strong>：<code>Bird</code> 类有 <code>fly()</code> 抽象方法，子类 <code>Sparrow</code> 也实现 <code>fly()</code> 方法，但 <code>Penguin</code> 类不应继承 <code>Bird</code> 类，因为它不能飞。</p><p>关于 LSP 的一个误解是，它似乎禁止子类对象拥有父类没有的方法 (这与开闭原则冲突)。实际上，LSP 所表达的是子类在重写或新增方法时，不应该改变父类方法的预期行为。<code>Sparrow</code> 类可以在 <code>fly()</code> 方法中进一步描述它如何 fly，但是不能把 “如何 fly” 描述为 “如何 run”，这已经篡改了方法原本的意图。</p><blockquote><p>子类不能完全替换父类，有时候可能是因为父类的抽象程度不够高。</p></blockquote><h4 id="接口隔离原则-isp"><a class="markdownIt-Anchor" href="#接口隔离原则-isp"></a> 接口隔离原则 ISP</h4><p><strong>说明</strong>：不应该强迫客户端依赖于它们不使用的接口，使用多个特定接口而非单个总接口。</p><p><strong>例子</strong>：<code>Printer</code> 类实现 <code>IPrinter</code> 接口和 <code>IScanner</code> 接口，而不是实现一个大的 <code>IMachine</code> 接口。</p><p>ISP 可以被视为是针对接口的单一职责原则，要求每个接口只包含一个客户端所需的方法集合，确保接口聚焦于特定功能来实现。将大型接口分解为小型、特定的接口，通过分离关注点来提高代码的可维护性。前前面的 <code>IMachine</code> 接口为了兼顾到所有客户端的功能实现，会尽可能地包含所有的方法。对于单个客户端来说，这些方法中可能只有一小部分是有意义的，但由于接口的特性，每个实现了接口的客户端必须编写大量无意义的代码去实现不属于自身的方法。而且在调用者在调用某个客户端的方法时需要在一大堆无意义的方法中找到几个有实际意义的方法，增加了代码的维护难度。</p><blockquote><p>ISP 中的 “客户端” 通常是指使用接口的类或组件，包括接口的实现类、接口的消费者、接口的调用者、接口的依赖者。</p></blockquote><h4 id="依赖倒置原则-dip"><a class="markdownIt-Anchor" href="#依赖倒置原则-dip"></a> 依赖倒置原则 DIP</h4><p><strong>说明</strong>：高层模块不应该依赖于低层模块，两者都应该依赖于抽象，而非具体实现。</p><p><strong>例子</strong>：<code>Service</code> 类依赖于 <code>RepositoryInterface</code> 接口，而不是具体的 <code>MySQLRepository</code> 实现。</p><p>DIP 通过接口将类与类进行解耦，<code>Service</code> 类如果直接依赖于 <code>MySQLRepository</code> 的实现，那么每新增一种实现，就需要修改 <code>Service</code> 类。依赖接口而非具体实现，可以使 <code>Service</code> 类只需要通过一个接口就能获取所有依赖于该接口的低层模块实例。</p><blockquote><p>DIP 一个广泛典型的应用就是 Spring 框架中的控制反转和依赖注入。在传统的程序设计中，高层模块通常负责创建和维护低层模块的实例。控制反转使得高层模块不再负责创建低层模块的实例，而是由其他特殊模块进行实例的创建和维护。高层模块只需要提供方法，接受它们需要的低层模块实例作为参数，而不是自己创建这些实例。</p></blockquote><h3 id="另外两个原则"><a class="markdownIt-Anchor" href="#另外两个原则"></a> 另外两个原则</h3><p>设计模式七大原则中除去 SOLID 五原则后，另外两个原则通常是 迪米特法则 (Law of Demeter, LoD) 与 合成/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP)</p><h4 id="迪米特法则-lod"><a class="markdownIt-Anchor" href="#迪米特法则-lod"></a> 迪米特法则 LoD</h4><p>迪米特法则，也称为最少知识原则</p><p><strong>说明</strong>：一个对象应尽量少地了解其他对象。</p><p><strong>例子</strong>：<code>Car</code> 类只调用 <code>Engine</code> 类的 <code>start()</code> 方法，而不直接调用 <code>Piston</code> 类的 <code>move()</code> 方法。</p><p><code>Car</code> 与 <code>Engine</code> 是直接关联，<code>Piston</code> 与 <code>Engine</code> 直接关联，但是 <code>Car</code> 不应该直接操作 <code>Piston</code> 而是通过调用 <code>Engine</code> 的 <code>start()</code> 方法，<code>start()</code> 方法内部再去调用 <code>move()</code> 方法。本质上是通过建立间接关系，而非直接依赖，从而降低耦合度。</p><h4 id="合成聚合复用原则-carp"><a class="markdownIt-Anchor" href="#合成聚合复用原则-carp"></a> 合成/聚合复用原则 CARP</h4><p><strong>说明</strong>：优先使用对象的组合/聚合，而不是通过继承来复用它们。</p><p><strong>例子</strong>：<code>Car</code> 类有一个 <code>Engine</code> 对象作为其成员，而不是从 <code>Engine</code> 类继承。</p><p>组合/聚合优于继承。优先考虑组合/聚合，而不是继承去实现类的复用。其最大的优势就是可以在运行时操作类的实例。</p><blockquote><p>组合/聚合与继承对应的就是面向对象的经典关系 HAS-A 和 IS-A。</p><p>HAS-A 表示了部分与整体的关系，一个类是另一个类的一部分。</p><p>IS-A 表示了抽象与具体的关系，子类是父类的特化。</p></blockquote><p>在对七大原则有了初步理解后，接下来就是分析具体的设计模式。设计模式有很多种，最经典的设计模式有 23 种。</p><h2 id="设计模式的应用"><a class="markdownIt-Anchor" href="#设计模式的应用"></a> 设计模式的应用</h2><p>设计模式被分为创建型模式、结构型模式、行为模式。</p><h3 id="创建型模式"><a class="markdownIt-Anchor" href="#创建型模式"></a> 创建型模式</h3><p>创建型模式提供了创建对象的机制，能够提升已有代码的灵活性和可复用性。该分类下有 5 种具体的设计模式。</p><h4 id="工厂方法"><a class="markdownIt-Anchor" href="#工厂方法"></a> 工厂方法</h4><p>工厂方法模式在父类中提供一个创建对象的方法，允许子类决定实例化对象的类型。</p><p>工厂方法模式包含了产品、具体产品、创建者、具体创建者。</p><ul><li><strong>产品 (Product)</strong> 将会对接口进行声明。对于所有由创建者及其子类构建的对象，这些接口都是通用的。</li><li><strong>具体产品 (Concrete Products)</strong> 是产品接口的不同实现。</li><li><strong>创建者 (Creator)</strong> 类声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。</li><li><strong>具体创建者 (Concrete Creators)</strong> 将会重写基础工厂方法，使其返回不同类型的产品。</li></ul><p>注意，并不一定每次调用工厂方法都会<strong>创建</strong>新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。</p><p>工厂方法模式的应用场景</p><ul><li>无法预知对象确切类别及其依赖关系时</li><li>希望其他用户能扩展软件库或框架的内部组件</li><li>希望复用现有对象来节省系统资源，而不是每次都重新创建对象。</li></ul><p>工厂方法可以避免创建者和具体产品之间的紧密耦合，但是应用工厂方法模式需要引入许多新的子类，代码可能会因此变得更复杂。最好的情况是将该模式引入创建者类的现有层次结构中 (简单工厂)</p><p>与其他模式的关系</p><ul><li><p>在许多设计工作的初期都会使用较为简单和方便通过子类定制的<a class="link"   href="https://refactoringguru.cn/design-patterns/factory-method" >工厂方法模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，随后演化为使用更灵活复杂的<a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>或<a class="link"   href="https://refactoringguru.cn/design-patterns/builder" >生成器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p></li><li><p>你可以同时使用<a class="link"   href="https://refactoringguru.cn/design-patterns/factory-method" >工厂方法<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/iterator" >迭代器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来让子类集合返回不同类型的迭代器，并使得迭代器与集合相匹配。</p></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/factory-method" >工厂方法<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>是<a class="link"   href="https://refactoringguru.cn/design-patterns/template-method" >模板方法模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的一种特殊形式。同时，工厂方法可以作为一个大型模板方法中的一个步骤。</p></li></ul><h4 id="抽象工厂"><a class="markdownIt-Anchor" href="#抽象工厂"></a> 抽象工厂</h4><p>抽象工厂模式能创建一系列相关的对象 (产品簇)，而无需指定其具体类。</p><blockquote><p>在抽象工厂模式中，产品簇是指工厂生产出的产品之间具备强关联。</p><p>产品簇的划分维度有很多，可以是同一种风格或品牌下的产品系列，例如，Windows 或 Mac 下的 GUI 风格组件。也可以基于产品的用途来分类，即使产品在视觉或技术上有所不同，例如不同类型的交通工具。在某些情况下，还可以按同一个产品的不同规格配置或生产年代来划分，例如不同年代生产的不同内存规格的 iPad 产品。</p></blockquote><p>抽象工厂模式包含了抽象产品、具体产品、抽象工厂、具体工厂、客户端。</p><ul><li><p><strong>抽象产品 (Abstract Product)</strong> 为构成系列产品的一组不同但相关的产品声明接口。</p></li><li><p><strong>具体产品 (Concrete Product)</strong> 是抽象产品的多种不同类型实现。所有变体都必须实现相应的抽象产品。</p></li><li><p><strong>抽象工厂 (Abstract Factory)</strong> 接口声明了一组创建各种抽象产品的方法。</p></li><li><p><strong>具体工厂 (Concrete Factory)</strong> 实现抽象工厂的构建方法。每个具体工厂都对应特定产品变体，且仅创建此种产品变体。</p><p>尽管具体工厂会对具体产品进行初始化，其构建方法签名必须返回相应的抽象产品。这样，使用工厂类的客户端代码就不会与工厂创建的特定产品变体耦合。</p></li><li><p><strong>客户端 (Client)</strong> 只需通过抽象接口调用工厂和产品对象，就能与任何具体工厂/产品变体交互。</p></li></ul><p>抽象工厂模式的应用场景</p><ul><li>代码需要与多个不同系列的相关产品交互，但是由于无法提前获取相关信息，或者出于对未来扩展性的考虑，不希望代码基于产品的具体类进行构建。</li><li>有一个基于一组抽象方法的类，且其主要功能因此变得不明确。</li></ul><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>通常基于一组<a class="link"   href="https://refactoringguru.cn/design-patterns/factory-method" >工厂方法<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，但你也可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来生成这些类的方法。</p></li><li><p>当只需对客户端代码隐藏子系统创建对象的方式时，你可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来代替<a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p></li><li><p>你可以将<a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/bridge" >桥接模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>搭配使用。如果由桥接定义的抽象只能与特定实现合作，这一模式搭配就非常有用。在这种情况下，抽象工厂可以对这些关系进行封装，并且对客户端代码隐藏其复杂性。</p></li></ul><p>抽象工厂与工厂方法最大的区别就是产品簇的划分。</p><p>工厂方法是简单的工厂与产品的一对一关系，一个工厂负责创建一个具体的产品类的实例。</p><p>抽象工厂是一种工厂与产品的一对多关系，一个抽象工厂可以创建多个属于同一产品簇的产品。</p><h4 id="生成器"><a class="markdownIt-Anchor" href="#生成器"></a> 生成器</h4><p>生成器模式能够将复杂对象分步骤创建，可以使用相同的创建代码生成不同类型的和形式的对象。</p><p>生成器模式包括生成器、具体生成器、产品、主管、客户端。</p><ul><li><strong>生成器 (Builder)</strong> 接口声明在所有类型生成器中通用的产品构造步骤。</li><li><strong>具体生成器 (Concrete Builders)</strong> 提供构造过程的不同实现。具体生成器也可以构造不遵循通用接口的产品。</li><li><strong>产品 (Products)</strong> 是最终生成的对象。由不同生成器构造的产品无需属于同一类层次结构或接口。</li><li><strong>主管 (Director)</strong> 类定义调用构造步骤的顺序，这样就可以创建和复用特定的产品配置。</li><li><strong>客户端 (Client)</strong> 必须将某个生成器对象与主管类关联。</li></ul><p>一般情况下，你只需通过主管类<strong>构造函数的参数</strong>进行一次性关联即可。此后主管类就能使用生成器对象完成后续所有的构造任务。但在客户端将生成器对象传递给主管类制造方法时还有另一种方式。在这种情况下，你在使用主管类生产产品时每次都可以<strong>使用不同的生成器</strong>。</p><p>生成器模式的应用场景</p><ul><li>使用生成器模式可避免 “重叠构造函数 (telescoping constructor)” 的出现。</li><li>希望使用代码创建不同形式的产品时。</li><li>构造组合树或其他复杂对象。</li></ul><p>生成器模式可以分步创建对象，暂缓创建步骤或递归运行创建步骤。生成不同形式的产品时，可以复用相同的制造代码。遵循单一职责原则，将复杂构造代码从产品的业务逻辑中分离出来。</p><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/builder" >生成器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>重点关注如何分步生成复杂对象。<a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>专门用于生产一系列相关对象。抽象工厂会马上返回产品，生成器则允许在获取产品前执行一些额外构造步骤。</p></li><li><p>可以在创建复杂<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树时使用<a class="link"   href="https://refactoringguru.cn/design-patterns/builder" >生成器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，因为这可使其构造步骤以递归的方式运行。</p></li><li><p>可以结合使用<a class="link"   href="https://refactoringguru.cn/design-patterns/builder" >生成器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/bridge" >桥接模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>：主管类负责抽象工作，各种不同的生成器负责实现工作。</p></li></ul><h4 id="原型"><a class="markdownIt-Anchor" href="#原型"></a> 原型</h4><p>原型模式能够复制已有对象，而又无需使代码依赖它们所属的类。</p><p>原型模式包括原型、具体原型、客户端。此外还有原型注册表进一步改造代码。</p><ul><li><strong>原型 (Prototype)</strong> 接口将对克隆方法进行声明。在绝大多数情况下，其中只会有一个名为 <code>clone</code>克隆的方法。</li><li><strong>具体原型 (Concrete Prototype)</strong> 类将实现克隆方法。除了将原始对象的数据复制到克隆体中之外，该方法有时还需处理克隆过程中的极端情况，例如克隆关联对象和梳理递归依赖等等 (深拷贝)。</li><li><strong>客户端 (Client)</strong> 可以复制实现了原型接口的任何对象。</li></ul><p>原型注册表</p><ul><li><strong>原型注册表 (Prototype Registry)</strong> 提供了一种访问常用原型的简单方法，其中存储了一系列可供随时复制的预生成对象。最简单的注册表原型是一个 <code>名称 → 原型</code> 的哈希表。但如果需要使用名称以外的条件进行搜索，你可以创建更加完善的注册表版本。</li></ul><blockquote><p>原型模式的一个显著特征就是原型具有 <code>clone()</code> 方法。</p><p>另外，通常提到原型模式中的 <code>clone()</code> 方法，默认情况下它指的是浅拷贝。在 Java 中，Object 类的 <code>clone()</code> 方法是一个 <code>native</code> 修饰的方法，这意味着它是由 JVM 底层实现的，是通过直接复制对象在内存中的数据来实现的一种浅拷贝。</p><p>因此，如果自定义实现 <code>clone()</code> 方法，而不是实现 <code>Cloneable</code> 接口，并不会获得 JVM 的性能优化效果。</p></blockquote><p>原型模式的应用场景</p><ul><li>需要复制一些对象，同时又希望代码独立于这些对象所属的具体类。</li><li>如果子类的区别仅在于其对象的初始化方式，那么可以使用的该模式来减少子类的数量。</li></ul><p>原型模式在克隆包含循环引用的复杂对象时可能会非常麻烦，需要手动处理循环引用。</p><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>可用于保存<a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的历史记录。</p></li><li><p>大量使用<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的设计通常可从对于<a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的使用中获益。你可以通过该模式来复制复杂结构，而非从零开始重新构造。</p></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>并不基于继承，因此没有继承的缺点。另一方面，原型需要对被复制对象进行复杂的初始化。<a class="link"   href="https://refactoringguru.cn/design-patterns/factory-method" >工厂方法<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>基于继承，但是它不需要初始化步骤。</p></li><li><p>有时候<a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>可以作为<a class="link"   href="https://refactoringguru.cn/design-patterns/memento" >备忘录模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的一个简化版本，其条件是你需要在历史记录中存储的对象的状态比较简单，不需要链接其他外部资源，或者链接可以方便地重建。</p></li></ul><h4 id="单例"><a class="markdownIt-Anchor" href="#单例"></a> 单例</h4><p>单例模式能够保证一个类只有一个实例，并提供一个访问该实例的全局节点。</p><p>单例模式包含了最重要的单例类，其中定义了获取实例的静态方法，该方法也是获取实例的唯一方式。</p><ul><li><p><strong>单例 (Singleton)</strong> 类声明了一个名为 <code>get­Instance</code> 获取实例的静态方法来返回其所属类的一个相同实例。</p><p>单例的构造函数必须对客户端 (Client) 代码隐藏。调用 <code>获取实例</code> 方法必须是获取单例对象的唯一方式。</p></li></ul><blockquote><p>单例模式的显著特征是私有化构造方法。</p></blockquote><p>单例模式的应用场景</p><ul><li>程序中的某个类对于所有客户端只有一个可用的实例。</li><li>需要更加严格地控制全局变量。</li></ul><p>单例模式是违反了单一职责原则的设计模式，它确保了实例的唯一性并提供了全局访问点。单例模式在多线程环境下需要进行特殊处理，避免多个线程多次创建对象，实现方式可参考<a class="link"   href="https://www.runoob.com/design-pattern/singleton-pattern.html" >菜鸟教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。单例模式的客户端代码由于私有化构造函数，难以使用单元测试。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/abstract-factory" >抽象工厂模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://refactoringguru.cn/design-patterns/builder" >生成器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/prototype" >原型模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>都可以用<a class="link"   href="https://refactoringguru.cn/design-patterns/singleton" >单例<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来实现。</li></ul><h3 id="结构型模式"><a class="markdownIt-Anchor" href="#结构型模式"></a> 结构型模式</h3><p>结构型模式介绍如何将对象和类组装成较大的结构，并同时保持结构的灵活和高效。该分类下有 7 种模式具体的设计模式。</p><h4 id="适配器"><a class="markdownIt-Anchor" href="#适配器"></a> 适配器</h4><p>适配器模式能使接口不兼容的对象能够相互合作。</p><p>适配器模式包含了客户端、客户端接口、服务、适配器。</p><ul><li><strong>客户端 (Client)</strong> 是包含当前程序业务逻辑的类。</li><li><strong>客户端接口 (Client Interface)</strong> 描述了其他类与客户端代码合作时必须遵循的协议。</li><li><strong>服务 (Service)</strong> 中有一些功能类（通常来自第三方或遗留系统）。客户端与其接口不兼容，因此无法直接调用其功能。</li><li><strong>适配器 (Adapter)</strong> 是一个可以同时与客户端和服务交互的类：它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用，并将其转换为适用于被封装服务对象的调用。</li></ul><p>客户端代码只需通过接口与适配器交互即可，无需与具体的适配器类耦合。因此，你可以向程序中添加新类型的适配器而无需修改已有代码。这在服务类的接口被更改或替换时很有用：你无需修改客户端代码就可以创建新的适配器类。</p><blockquote><p>关于适配器模式中的适配，被适配的一方通常指的是源代码不可修改的组件或系统，适配的一方则是我们可以控制源代码的组件。</p></blockquote><p>适配器模式的应用场景</p><ul><li>希望使用某个类，但是其接口与其他代码不兼容时。</li><li>需要复用这样一些类，他们处于同一个继承体系，并且他们又有了额外的一些共同的方法，但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。</li></ul><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/adapter" >适配器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>可以对已有对象的接口进行修改，<a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>则能在不改变对象接口的前提下强化对象功能。此外，装饰还支持递归组合，适配器则无法实现。</li></ul><h4 id="桥接"><a class="markdownIt-Anchor" href="#桥接"></a> 桥接</h4><p>桥接模式可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构，从而能在开发时分别使用。</p><p>桥接模式包含了抽象部分、实现部分、具体实现、精确抽象 (可选)、客户端。</p><ul><li><p><strong>抽象部分 (Abstraction)</strong> 提供高层控制逻辑，依赖于完成底层实际工作的实现对象。</p></li><li><p><strong>实现部分 (Implementation)</strong> 为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。</p><p>抽象部分可以列出和实现部分一样的方法，但是抽象部分通常声明一些复杂行为，这些行为依赖于多种由实现部分声明的原语操作。</p></li><li><p><strong>具体实现 (Concrete Implementations)</strong> 中包括特定于平台的代码。</p></li><li><p><strong>精确抽象 (Refined Abstraction)</strong> 提供控制逻辑的变体。与其父类一样，它们通过通用实现接口与不同的实现进行交互。</p></li><li><p>通常情况下，<strong>客户端 (Client)</strong> 仅关心如何与抽象部分合作。但是，客户端需要将抽象对象与一个实现对象连接起来。</p></li></ul><p>桥接模式的应用场景</p><ul><li>拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类) 时。</li><li>希望在几个独立维度上扩展一个类时。</li><li>需要在运行时切换不同实现方法时。</li></ul><p>桥接模式下可以创建与平台无关的类和程序，客户端代码仅与高层抽象部分进行互动，不会接触到平台的详细信息。桥接模式遵循开闭原则，可以新增抽象部分和实现，且它们之间不会相互影响。桥接模式遵循单一职责原则，抽象部分专注于处理高层逻辑，实现部分处理平台细节。但是对高内聚的类使用该模式可能会让代码更加复杂。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/bridge" >桥接模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>通常会于开发前期进行设计，使你能够将程序的各个部分独立开来以便开发。另一方面，<a class="link"   href="https://refactoringguru.cn/design-patterns/adapter" >适配器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>通常在已有程序中使用，让相互不兼容的类能很好地合作。</li></ul><h4 id="组合"><a class="markdownIt-Anchor" href="#组合"></a> 组合</h4><p>组合模式将对象组合成树状结构，并且能像使用独立对象一样使用它们。</p><p>组合模式包含了组件、叶节点、容器、客户端。</p><ul><li><p><strong>组件 (Component)</strong> 接口描述了树中简单项目和复杂项目所共有的操作。</p></li><li><p><strong>叶节点 (Leaf)</strong> 是树的基本结构，它不包含子项目。</p><p>一般情况下，叶节点最终会完成大部分的实际工作，因为它们无法将工作指派给其他部分。</p></li><li><p><strong>容器 (Container)</strong> ——又名 “组合 (Composite)” —— 是包含叶节点或其他容器等子项目的单位。容器不知道其子项目所属的具体类，它只通过通用的组件接口与其子项目交互。</p><p>容器接收到请求后会将工作分配给自己的子项目，处理中间结果，然后将最终结果返回给客户端。</p></li><li><p><strong>客户端 (Client)</strong> 通过组件接口与所有项目交互。因此，客户端能以相同方式与树状结构中的简单或复杂项目交互。</p></li></ul><p>组合模式的应用场景</p><ul><li>需要实现树状对象结构时。</li><li>希望客户端代码以相同方式处理简单和复杂元素时。</li></ul><p>对于功能差异较大的类，提供公共接口会有困难，因此不推荐使用组合模式。在特定情况下，你需要过度一般化组件接口，使其变得令人难以理解。</p><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/bridge" >桥接模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://refactoringguru.cn/design-patterns/state" >状态模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/strategy" >策略模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> (在某种程度上包括<a class="link"   href="https://refactoringguru.cn/design-patterns/adapter" >适配器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>) 的接口非常相似。实际上，它们都基于<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> —— 即将工作委派给其他对象，不过也各自解决了不同的问题。模式并不只是以特定方式组织代码的配方，你还可以使用它们来和其他开发者讨论模式所解决的问题。</p></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/chain-of-responsibility" >责任链模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>通常和<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>结合使用。在这种情况下，叶组件接收到请求后，可以将请求沿包含全体父组件的链一直传递至对象树的底部。</p></li><li><p>你可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/iterator" >迭代器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来遍历<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树。</p></li><li><p>你可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/visitor" >访问者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>对整个<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树执行操作。</p></li><li><p>你可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/flyweight" >享元模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>实现<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树的共享叶节点以节省内存。</p></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的结构图很相似，因为两者都依赖递归组合来组织无限数量的对象。</p><p>装饰类似于组合，但其只有一个子组件。此外还有一个明显不同：装饰为被封装对象添加了额外的职责，组合仅对其子节点的结果。进行了“求和”。</p><p>但是，模式也可以相互合作：你可以使用装饰来扩展组合树中特定对象的行为。</p></li></ul><h4 id="装饰"><a class="markdownIt-Anchor" href="#装饰"></a> 装饰</h4><p>装饰模式允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。</p><p>装饰模式包含了部件、具体部件、基础装饰、具体装饰类、客户端。</p><ul><li><strong>部件 (Component)</strong> 声明封装器和被封装对象的公用接口。</li><li><strong>具体部件 (Concrete Component)</strong> 类是被封装对象所属的类。它定义了基础行为，但装饰类可以改变这些行为。</li><li><strong>基础装饰 (Base Decorator)</strong> 类拥有一个指向被封装对象的引用成员变量。该变量的类型应当被声明为通用部件接口，这样它就可以引用具体的部件和装饰。装饰基类会将所有操作委派给被封装的对象。</li><li><strong>具体装饰类 (Concrete Decorators)</strong> 定义了可动态添加到部件的额外行为。具体装饰类会重写装饰基类的方法，并在调用父类方法之前或之后进行额外的行为。</li><li><strong>客户端 (Client)</strong> 可以使用多层装饰来封装部件，只要它能使用通用接口与所有对象互动即可。</li></ul><p>装饰模式的应用场景</p><ul><li>希望在无需修改代码的情况下即可使用对象，且希望在运行时为对象新增额外的行为。</li><li>如果用继承来扩展对象行为的方案难以实现或者根本不可行时，可以使用该模式。</li></ul><p>装饰模式无需创建新子类即可扩展对象的行为，可以在运行时添加或删除功能。可以用多个装饰封装对象来组合几种行为。可以将实现了许多不同行为的一个大类拆分为多个较小的类。但是在封装器栈中删除特定封装器比较困难，实现行为不受装饰栈顺序影响的装饰也比较困难，各层的初始化配置代码看上去可能难以理解。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>可让你更改对象的外表，<a class="link"   href="https://refactoringguru.cn/design-patterns/strategy" >策略模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>则让你能够改变其本质。</li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/proxy" >代理<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>有着相似的结构，但是其意图却非常不同。这两个模式的构建都基于组合原则，也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期，而装饰的生成则总是由客户端进行控制。</li></ul><h4 id="外观"><a class="markdownIt-Anchor" href="#外观"></a> 外观</h4><p>外观模式能为程序库、框架或其他复杂类提供一个简单的接口。</p><p>外观模式包含了外观、附加外观、复杂子系统、客户端。</p><ul><li><p><strong>外观 (Facade)</strong> 提供了一种访问特定子系统功能的便捷方式，其了解如何重定向客户端请求，知晓如何操作一切活动部件。</p></li><li><p>创建<strong>附加外观 (Additional Facade)</strong> 类可以避免多种不相关的功能污染单一外观，使其变成又一个复杂结构。客户端和其他外观都可使用附加外观。</p></li><li><p><strong>复杂子系统 (Complex Subsystem)</strong> 由数十个不同对象构成。如果要用这些对象完成有意义的工作，你必须深入了解子系统的实现细节，比如按照正确顺序初始化对象和为其提供正确格式的数据。</p><p>子系统类不会意识到外观的存在，它们在系统内运作并且相互之间可直接进行交互。</p></li><li><p><strong>客户端 (Client)</strong> 使用外观代替对子系统对象的直接调用。</p></li></ul><p>外观模式的应用场景</p><ul><li>需要一个指向复杂子系统的直接接口，且该接口的功能有限。</li><li>需要将子系统组织为多层结构，可以使用此模式。</li></ul><p>外观模式可以让自己的代码独立于复杂子系统，但是外观也可能成为与程序中所有类都耦合的上帝对象。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>为现有对象定义了一个新接口，<a class="link"   href="https://refactoringguru.cn/design-patterns/adapter" >适配器模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>则会试图运用已有的接口。适配器通常只封装一个对象，外观通常会作用于整个对象子系统上。</li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/mediator" >中介者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的职责类似：它们都尝试在大量紧密耦合的类中组织起合作<ul><li>外观为子系统中的所有对象定义了一个简单接口，但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。</li><li>中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象，无法直接相互交流。</li></ul></li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>类通常可以转换为<a class="link"   href="https://refactoringguru.cn/design-patterns/singleton" >单例模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>类，因为在大部分情况下一个外观对象就足够了。</li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>与<a class="link"   href="https://refactoringguru.cn/design-patterns/proxy" >代理模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口，使得自己和服务对象可以互换，在这一点上它与外观不同。</li></ul><h4 id="享元"><a class="markdownIt-Anchor" href="#享元"></a> 享元</h4><p>享元模式摒弃了在每个对象中保存所有数据的方式，通过共享多个对象的所共有的相同状态，让你能在有限的内存容量中载入更多对象。</p><p>享元模式包含了享元、情景、享元工厂、客户端。</p><ul><li><p><strong>享元 (Flyweight)</strong> 类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为 “内在状态”。传递给享元方法的状态被称为 “外在状态”。</p></li><li><p><strong>情景 (Context)</strong> 类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。</p><p>通常情况下，原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但你也可将行为移动到情景类中，然后将连入的享元作为单纯的数据对象。</p></li><li><p><strong>客户端 (Client)</strong> 负责计算或存储享元的外在状态。在客户端看来，享元是一种可在运行时进行配置的模板对象，具体的配置方式为向其方法中传入一些情景数据参数。</p></li><li><p><strong>享元工厂 (Flyweight Factory)</strong> 会对已有享元的缓存池进行管理。有了工厂后，客户端就无需直接创建享元，它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找，如果找到满足条件的享元就将其返回；如果没有找到就根据参数新建享元。</p></li></ul><p>享元模式只是一种优化。在应用该模式之前，你要确定程序中存在与大量类似对象同时占用内存相关的内存消耗问题，并且确保该问题无法使用其他更好的方式来解决。</p><p>享元模式的应用场景</p><ul><li>仅在程序必须支持大量对象且没有足够的内存容量时。</li></ul><p>享元模式的应用场景很明确，但是要考虑牺牲执行速度来换取内存。</p><p>与其他模式的关系</p><ul><li>可以使用<a class="link"   href="https://refactoringguru.cn/design-patterns/flyweight" >享元模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>实现<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树的共享叶节点以节省内存。</li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/flyweight" >享元<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>展示了如何生成大量的小型对象，<a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>则展示了如何用一个对象来代表整个子系统。</li><li>如果你能将对象的所有共享状态简化为一个享元对象，那么<a class="link"   href="https://refactoringguru.cn/design-patterns/flyweight" >享元模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>就和<a class="link"   href="https://refactoringguru.cn/design-patterns/singleton" >单例<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>类似了。但这两个模式有两个根本性的不同。<ol><li>单例类只会有一个单例实体，但是享元类可以有多个实体，各实体的内在状态也可以不同。</li><li>单例对象可以是可变的。享元对象是不可变的。</li></ol></li></ul><h4 id="代理"><a class="markdownIt-Anchor" href="#代理"></a> 代理</h4><p>代理模式能够提供对象的替代品或其占位符。代理控制着对于原对象的访问，并允许在将请求提交给对象前后进行一些处理。</p><p>代理模式包含了服务接口、服务、代理、客户端。</p><ul><li><p><strong>服务接口 (Service Interface)</strong> 声明了服务接口。代理必须遵循该接口才能伪装成服务对象。</p></li><li><p><strong>服务 (Service)</strong> 类提供了一些实用的业务逻辑。</p></li><li><p><strong>代理 (Proxy)</strong> 类包含一个指向服务对象的引用成员变量。代理完成其任务（例如延迟初始化、记录日志、访问控制和缓存等）后会将请求传递给服务对象。</p><p>通常情况下，代理会对其服务对象的整个生命周期进行管理。</p></li><li><p><strong>客户端 (Client)</strong> 能通过同一接口与服务或代理进行交互，所以你可在一切需要服务对象的代码中使用代理。</p></li></ul><p>代理模式的应用场景</p><p>使用代理模式的方式多种多样，以下最常见的几种</p><ul><li>延迟初始化 (虚拟代理)。如果你有一个偶尔使用的重量级服务对象，一直保持该对象运行会消耗系统资源时，可使用该模式。</li><li>访问控制 (保护代理)。如果你只希望特定客户端使用服务对象，这里的对象可以是操作系统中非常重要的部分，而客户端则是各种已启动的程序 (包括恶意程序)，此时可用代理模式。</li><li>本地执行远程服务 (远程代理)。适用于服务对象位于远程服务器上的情景。</li><li>记录日志请求 (日志记录代理)。适用于当你需要保存对于服务对象的请求历史记录时。</li><li>缓存请求结果 (缓存代理)。适用于是需要缓存客户端请求结果并对缓存生命周期进行管理时，特别是当返回结果的体积非常大时。</li><li>智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。</li></ul><p>代理模式可以在客户端毫无察觉的情况下控制服务对象，可以对其生命周期进行管理，即使服务对象还未准备好或不存在，代理也可以正常工作。代理模式遵循开闭原则，可以在不对服务或客户端做出修改的情况下创建新代理。但是代理模式下服务响应可能会延迟。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/adapter" >适配器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>能为被封装对象提供不同的接口，<a class="link"   href="https://refactoringguru.cn/design-patterns/proxy" >代理模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>能为对象提供相同的接口，<a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>则能为对象提供加强的接口。</li></ul><h3 id="行为模式"><a class="markdownIt-Anchor" href="#行为模式"></a> 行为模式</h3><p>行为模式负责对象间的高效沟通和职责委派。该分类下有 11 种具体的设计模式，但是由于解释器设计模式适用于编译原理或语法引擎等相关的领域，这里不会详细介绍此模式。</p><h4 id="责任链"><a class="markdownIt-Anchor" href="#责任链"></a> 责任链</h4><p>责任链模式允许你将请求沿着处理者链进行发送。收到请求后，每个处理者均可对请求进行处理，或将其传递给链上的下个处理者。</p><p>责任链模式包含了处理者、基础处理者、具体处理者、客户端。</p><ul><li><p><strong>处理者 (Handler)</strong> 声明了所有具体处理者的通用接口。该接口通常仅包含单个方法用于请求处理，但有时其还会包含一个设置链上下个处理者的方法。</p></li><li><p><strong>基础处理者 (Base Handler)</strong> 是一个可选的类，你可以将所有处理者共用的样本代码放置在其中。</p><p>通常情况下，该类中定义了一个保存对于下个处理者引用的成员变量。客户端可通过将处理者传递给上个处理者的构造函数或设定方法来创建链。该类还可以实现默认的处理行为：确定下个处理者存在后再将请求传递给它。</p></li><li><p><strong>具体处理者 (Concrete Handlers)</strong> 包含处理请求的实际代码。每个处理者接收到请求后，都必须决定是否进行处理，以及是否沿着链传递请求。</p><p>处理者通常是独立且不可变的，需要通过构造函数一次性地获得所有必要地数据。</p></li><li><p><strong>客户端 (Client)</strong> 可根据程序逻辑一次性或者动态地生成链。值得注意的是，请求可发送给链上的任意一个处理者，而非必须是第一个处理者。</p></li></ul><p>责任链模式的应用场景</p><ul><li>当程序需要使用不同方式处理不同种类请求，而且请求类型和顺序预先未知时，可以使用责任链模式。</li><li>当必须按顺序执行多个处理者时，可以使用该模式。</li><li>如果所需处理者及其顺序必须在运行时进行改变，可以使用责任链模式。</li></ul><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/chain-of-responsibility" >责任链<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的管理者可使用<a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>实现。在这种情况下，你可以对由请求代表的同一个上下文对象执行许多不同的操作。</p><p>还有另外一种实现方式，那就是请求自身就是一个命令对象。在这种情况下，你可以对由一系列不同上下文连接而成的链执行相同的操作。</p></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/chain-of-responsibility" >责任链模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/decorator" >装饰模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是，两者有几点重要的不同之处。</p><p><a class="link"   href="https://refactoringguru.cn/design-patterns/chain-of-responsibility" >责任链<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的管理者可以相互独立地执行一切操作，还可以随时停止传递请求。另一方面，各种装饰可以在遵循基本接口的情况下扩展对象的行为。此外，装饰无法中断请求的传递。</p></li></ul><h4 id="命令"><a class="markdownIt-Anchor" href="#命令"></a> 命令</h4><p>命令模式可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中，且能实现可撤销操作。</p><p>命令模式包含了发送者、命令、具体命令、接收者、客户端。</p><ul><li><p><strong>发送者 (Sender)</strong> ——亦称 “触发者 (Invoker)” —— 类负责对请求进行初始化，其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令，而不向接收者直接发送请求。注意，发送者并不负责创建命令对象：它通常会通过构造函数从客户端处获得预先生成的命令。</p></li><li><p><strong>命令 (Command)</strong> 接口通常仅声明一个执行命令的方法。</p></li><li><p><strong>具体命令 (Concrete Commands)</strong> 会实现各种类型的请求。具体命令自身并不完成工作，而是会将调用委派给一个业务逻辑对象。但为了简化代码，这些类可以进行合并。</p><p>接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变，仅允许通过构造函数对这些成员变量进行初始化。</p></li><li><p><strong>接收者 (Receiver)</strong> 类包含部分业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节，接收者自己会完成实际的工作。</p></li><li><p><strong>客户端 (Client)</strong> 会创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后，生成的命令就可以与一个或多个发送者相关联了。</p></li></ul><p>命令模式的应用场景</p><ul><li>如果你需要通过操作来参数化对象，可使用命令模式。</li><li>如果你想要将操作放入队列中、操作的执行或者远程执行操作，可使用命令模式。</li><li>如果你想要实现操作回滚功能，可使用命令模式。</li></ul><p>与其他模式的关系</p><ul><li>你可以同时使用<a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/memento" >备忘录模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来实现“撤销”。在这种情况下，命令用于对目标对象执行各种不同的操作，备忘录用来保存一条命令执行前该对象的状态。</li><li><a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/strategy" >策略模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>看上去很像，因为两者都能通过某些行为来参数化对象。但是，它们的意图有非常大的不同。<ul><li>你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。</li><li>另一方面，策略通常可用于描述完成某件事的不同方式，让你能够在同一个上下文类中切换算法。</li></ul></li><li>你可以将<a class="link"   href="https://refactoringguru.cn/design-patterns/visitor" >访问者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>视为<a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的加强版本，其对象可对不同类的多种对象执行操作。</li></ul><h4 id="迭代器"><a class="markdownIt-Anchor" href="#迭代器"></a> 迭代器</h4><p>迭代器模式让你能在不暴露集合底层表现形式（列表、栈和树等）的情况下遍历集合中所有的元素。</p><p>迭代器模式包含了迭代器、具体迭代器、集合、具体集合、客户端。</p><ul><li><p><strong>迭代器 (Iterator)</strong> 接口声明了遍历集合所需的操作：获取下一个元素、获取当前位置和重新开始迭代等。</p></li><li><p><strong>具体迭代器 (Concrete Iterators)</strong> 实现遍历集合的一种特定算法。迭代器对象必须跟踪自身遍历的进度。这使得多个迭代器可以相互独立地遍历同一集合。</p></li><li><p><strong>集合 (Collection)</strong> 接口声明一个或多个方法来获取与集合兼容的迭代器。请注意，返回方法的类型必须被声明为迭代器接口，因此具体集合可以返回各种不同种类的迭代器。</p></li><li><p><strong>具体集合 (Concrete Collections)</strong> 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。你可能会琢磨，剩下的集合代码在什么地方呢？不用担心，它也会在同一个类中。只是这些细节对于实际模式来说并不重要，所以我们将其省略了而已。</p></li><li><p><strong>客户端 (Client)</strong> 通过集合和迭代器的接口与两者进行交互。这样一来客户端无需与具体类进行耦合，允许同一客户端代码使用各种不同的集合和迭代器。</p><p>客户端通常不会自行创建迭代器，而是会从集合中获取。但在特定情况下，客户端可以直接创建一个迭代器（例如当客户端需要自定义特殊迭代器时）。</p></li></ul><p>迭代器模式的应用场景</p><ul><li>当集合背后为复杂的数据结构，且你希望对客户端隐藏其复杂性时（出于使用便利性或安全性的考虑），可以使用迭代器模式。</li><li>使用该模式可以减少程序中重复的遍历代码。</li><li>如果你希望代码能够遍历不同的甚至是无法预知的数据结构，可以使用迭代器模式。</li></ul><p>与其他模式的关系</p><ul><li>你可以同时使用<a class="link"   href="https://refactoringguru.cn/design-patterns/memento" >备忘录模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/iterator" >迭代器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来获取当前迭代器的状态，并且在需要的时候进行回滚。</li><li>可以同时使用<a class="link"   href="https://refactoringguru.cn/design-patterns/visitor" >访问者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/iterator" >迭代器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>来遍历复杂数据结构，并对其中的元素执行所需操作，即使这些元素所属的类完全不同。</li></ul><h4 id="中介者"><a class="markdownIt-Anchor" href="#中介者"></a> 中介者</h4><p>中介者模式能让你减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互，迫使它们通过一个中介者对象进行合作。</p><p>中介者模式包含了组件、中介者、具体中介者。</p><ul><li><strong>组件 (Component)</strong> 是各种包含业务逻辑的类。每个组件都有一个指向中介者的引用，该引用被声明为中介者接口类型。组件不知道中介者实际所属的类，因此你可通过将其连接到不同的中介者以使其能在其他程序中复用。</li><li><strong>中介者 (Mediator)</strong> 接口声明了与组件交流的方法，但通常仅包括一个通知方法。组件可将任意上下文（包括自己的对象）作为该方法的参数，只有这样接收组件和发送者类之间才不会耦合。</li><li><strong>具体中介者 (Concrete Mediator)</strong> 封装了多种组件间的关系。 具体中介者通常会保存所有组件的引用并对其进行管理， 甚至有时会对其生命周期进行管理。</li></ul><p>组件并不知道其他组件的情况。如果组件内发生了重要事件，它只能通知中介者。中介者收到通知后能轻易地确定发送者，这或许已足以判断接下来需要触发的组件了。</p><p>对于组件来说，中介者看上去完全就是一个黑箱。发送者不知道最终会由谁来处理自己的请求，接收者也不知道最初是谁发出了请求。</p><p>中介者模式的应用场景</p><ul><li>当一些对象和其他对象紧密耦合以致难以对其进行修改时，可使用中介者模式。</li><li>当组件因过于依赖其他组件而无法在不同应用中复用时，可使用中介者模式。</li><li>如果为了能在不同情景下，复用一些基本行为，导致你需要被迫创建大量组件子类时，可使用中介者模式。</li></ul><p>与其他模式的关系</p><ul><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/facade" >外观模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/mediator" >中介者<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的职责类似：它们都尝试在大量紧密耦合的类中组织起合作。</p><ul><li>外观为子系统中的所有对象定义了一个简单接口，但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。</li><li>中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象，无法直接相互交流。</li></ul></li><li><p><a class="link"   href="https://refactoringguru.cn/design-patterns/mediator" >中介者<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/observer" >观察者<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>之间的区别往往很难记住。在大部分情况下，你可以使用其中一种模式，而有时可以同时使用。</p><p>中介者的主要目标是消除一系列系统组件之间的相互依赖。这些组件将依赖于同一个中介者对象。观察者的目标是在对象之间建立动态的单向连接，使得部分对象可作为其他对象的附属发挥作用。</p><p>有一种流行的中介者模式实现方式依赖于观察者。中介者对象担当发布者的角色，其他组件则作为订阅者，可以订阅中介者的事件或取消订阅。当中介者以这种方式实现时，它可能看上去与观察者非常相似。</p><p>当你感到疑惑时，记住可以采用其他方式来实现中介者。例如，你可永久性地将所有组件链接到同一个中介者对象。这种实现方式和观察者并不相同，但这仍是一种中介者模式。</p><p>假设有一个程序，其所有的组件都变成了发布者，它们之间可以相互建立动态连接。这样程序中就没有中心化的中介者对象，而只有一些分布式的观察者。</p></li></ul><h4 id="备忘录"><a class="markdownIt-Anchor" href="#备忘录"></a> 备忘录</h4><p>备忘录模式允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。</p><p>备忘录模式包含了原发器、备忘录、负责人。</p><ul><li><p><strong>原发器 (Originator)</strong> 类可以生成自身状态的快照，也可以在需要时通过快照恢复自身状态。</p></li><li><p><strong>备忘录 (Memento)</strong> 是原发器状态快照的值对象（value object）。通常做法是将备忘录设为不可变的，并通过构造函数一次性传递数据。</p></li><li><p><strong>负责人 (Caretaker)</strong> 仅知道 “何时” 和 “为何” 捕捉原发器的状态，以及何时恢复状态。</p><p>负责人通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时，负责人将从栈中获取最顶部的备忘录，并将其传递给原发器的恢复（restoration）方法。</p></li></ul><p>在该实现方法中，备忘录类将被嵌套在原发器中。这样原发器就可访问备忘录的成员变量和方法，即使这些方法被声明为私有。另一方面，负责人对于备忘录的成员变量和方法的访问权限非常有限：它们只能在栈中保存备忘录，而不能修改其状态。</p><p>如果你不想让其他类有任何机会通过备忘录来访问原发器的状态， 那么还有另一种可用的实现方式</p><ul><li>这种实现方式允许存在多种不同类型的原发器和备忘录。每种原发器都和其相应的备忘录类进行交互。原发器和备忘录都不会将其状态暴露给其他类。</li><li>负责人此时被明确禁止修改存储在备忘录中的状态。但负责人类将独立于原发器，因为此时恢复方法被定义在了备忘录类中。</li><li>每个备忘录将与创建了自身的原发器连接。原发器会将自己及状态传递给备忘录的构造函数。由于这些类之间的紧密联系，只要原发器定义了合适的设置器（setter），备忘录就能恢复其状态。</li></ul><p>备忘录模式的应用场景</p><ul><li>当你需要创建对象状态快照来恢复其之前的状态时，可以使用备忘录模式。</li><li>当直接访问对象的成员变量、获取器或设置器将导致封装被突破时，可以使用该模式。</li></ul><p>备忘录模式可以在不破环对象封装情况的前提下创建对象状态快照，并可以通过让负责人维护原发器状态历史记录来简化原发器代码。但是如果客户端过于频繁地创建备忘录，程序将消耗大量内存。负责人必须完整跟踪原发器的生命周期，这样才能销毁弃用的备忘录。</p><blockquote><p>绝大部分动态编程语言（例如 PHP、Python 和 JavaScript）不能确保备忘录中的状态不被修改。</p></blockquote><p>与其他模式的关系</p><p>详见上文与备忘录相关的内容。</p><h4 id="观察者"><a class="markdownIt-Anchor" href="#观察者"></a> 观察者</h4><p>观察者模式允许你定义一种订阅机制，可在对象事件发生时通知多个 “观察” 该对象的其他对象。</p><p>观察者模式包含了发布者、订阅者、具体订阅者、客户端。</p><ul><li><p><strong>发布者 (Publisher)</strong> 会向其他对象发送值得关注的事件。事件会在发布者自身状态改变或执行特定行为后发生。发布者中包含一个允许新订阅者加入和当前订阅者离开列表的订阅构架。</p><p>当新事件发生时，发送者会遍历订阅列表并调用每个订阅者对象的通知方法。该方法是在订阅者接口中声明的。</p></li><li><p><strong>订阅者 (Subscriber)</strong> 接口声明了通知接口。在绝大多数情况下，该接口仅包含一个 <code>update</code> 更新方法。该方法可以拥有多个参数，使发布者能在更新时传递事件的详细信息。</p></li><li><p><strong>具体订阅者 (Concrete Subscribers)</strong> 可以执行一些操作来回应发布者的通知。所有具体订阅者类都实现了同样的接口，因此发布者不需要与具体类相耦合。</p><p>订阅者通常需要一些上下文信息来正确地处理更新。因此，发布者通常会将一些上下文数据作为通知方法的参数进行传递。发布者也可将自身作为参数进行传递，使订阅者直接获取所需的数据。</p></li><li><p><strong>客户端 (Client)</strong> 会分别创建发布者和订阅者对象，然后为订阅者注册发布者更新。</p></li></ul><p>观察者模式的应用场景</p><ul><li>当一个对象状态的改变需要改变其他对象，或实际对象是事先未知的或动态变化的时，可使用观察者模式。</li><li>当应用中的一些对象必须观察其他对象时，可使用该模式。但仅能在有限时间内或特定情况下使用。</li></ul><p>观察者模式遵循开闭原则，无需修改发布者代码就能引入新的订阅者类（如果是发布者接口则可轻松引入发布者类）。可以在运行时建立对象之间的联系。但是订阅者的通知顺序是随机的。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/chain-of-responsibility" >责任链模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://refactoringguru.cn/design-patterns/command" >命令模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://refactoringguru.cn/design-patterns/mediator" >中介者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>和<a class="link"   href="https://refactoringguru.cn/design-patterns/observer" >观察者模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>用于处理请求发送者和接收者之间的不同连接方式：<ul><li>责任链按照顺序将请求动态传递给一系列的潜在接收者，直至其中一名接收者对请求进行处理。</li><li>命令在发送者和请求者之间建立单向连接。</li><li>中介者清除了发送者和请求者之间的直接连接，强制它们通过一个中介对象进行间接沟通。</li><li>观察者允许接收者动态地订阅或取消接收请求。</li></ul></li></ul><h4 id="状态"><a class="markdownIt-Anchor" href="#状态"></a> 状态</h4><p>状态模式让你能在一个对象的内部状态变化时改变其行为，使其看上去就像改变了自身所属的类一样。</p><p>状态模式包含了上下文、状态、具体状态。</p><ul><li><p><strong>上下文 (Context)</strong> 保存了对于一个具体状态对象的引用，并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互，且会提供一个设置器用于传递新的状态对象。</p></li><li><p><strong>状态 (State)</strong> 接口会声明特定于状态的方法。这些方法应能被其他所有具体状态所理解，因为你不希望某些状态所拥有的方法永远不会被调用。</p></li><li><p><strong>具体状态 (Concrete States)</strong> 会自行实现特定于状态的方法。为了避免多个状态中包含相似代码，你可以提供一个封装有部分通用行为的中间抽象类。</p><p>状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息，并且能触发状态转移。</p><p>上下文和具体状态都可以设置上下文的下个状态，并可通过替换连接到上下文的状态对象来完成实际的状态转换。</p></li></ul><p>状态模式的应用场景</p><ul><li>如果对象需要根据自身当前状态进行不同行为，同时状态的数量非常多且与状态相关的代码会频繁变更的话，可使用状态模式。</li><li>如果某个类需要根据成员变量的当前值改变自身行为，从而需要使用大量的条件语句时，可使用该模式。</li><li>当相似状态和基于条件的状态机转换中存在许多重复代码时，可使用状态模式。</li></ul><p>如果状态机只有很少的几个状态，或者很少发生改变，那么应用该模式可能会显得小题大作。</p><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/state" >状态<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>可被视为<a class="link"   href="https://refactoringguru.cn/design-patterns/strategy" >策略<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的扩展。两者都基于组合机制：它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。策略使得这些对象相互之间完全独立，它们不知道其他对象的存在。但状态模式没有限制具体状态之间的依赖，且允许它们自行改变在不同情景下的状态。</li></ul><h4 id="策略"><a class="markdownIt-Anchor" href="#策略"></a> 策略</h4><p>策略模式能让你定义一系列算法，并将每种算法分别放入独立的类中，以使算法的对象能够相互替换。</p><p>策略模式包含了上下文、策略、具体策略、客户端。</p><ul><li><p><strong>上下文 (Context)</strong> 维护指向具体策略的引用，且仅通过策略接口与该对象进行交流。</p></li><li><p><strong>策略 (Strategy)</strong> 接口是所有具体策略的通用接口，它声明了一个上下文用于执行策略的方法。</p></li><li><p><strong>具体策略 (Concrete Strategies)</strong> 实现了上下文所用算法的各种不同变体。</p><p>当上下文需要运行算法时，它会在其已连接的策略对象上调用执行方法。上下文不清楚其所涉及的策略类型与算法的执行方式。</p></li><li><p><strong>客户端 (Client)</strong> 会创建一个特定策略对象并将其传递给上下文。上下文则会提供一个设置器以便客户端在运行时替换相关联的策略。</p></li></ul><p>策略模式的应用场景</p><ul><li>当你想使用对象中各种不同的算法变体，并希望能在运行时切换算法时，可使用策略模式。</li><li>当你有许多仅在执行某些行为时略有不同的相似类时，可使用策略模式。</li><li>如果算法在上下文的逻辑中不是特别重要，使用该模式能将类的业务逻辑与其算法实现细节隔离开来。</li><li>当类中使用了复杂条件运算符以在同一算法的不同变体中切换时，可使用该模式。</li></ul><p>策略模式可以在运行时切换对象内的算法，将算法的实现和使用算法的代码隔离开来。使用组合代替继承。策略模式遵循开闭原则，无需对上下文进行修改就能够引入新的策略。</p><p>但是如果你的算法极少发生改变，那么没有任何理由引入新的类和接口。使用该模式只会让程序过于复杂。客户端必须知晓策略间的不同 —— 它需要选择合适的策略。</p><blockquote><p>许多现代编程语言支持函数类型功能，允许你在一组匿名函数中实现不同版本的算法。这样，你使用这些函数的方式就和使用策略对象时完全相同，无需借助额外的类和接口来保持代码简洁。</p></blockquote><p>与其他模式的关系</p><ul><li><a class="link"   href="https://refactoringguru.cn/design-patterns/template-method" >模板方法模式<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>基于继承机制：它允许你通过扩展子类中的部分内容来改变部分算法。<a class="link"   href="https://refactoringguru.cn/design-patterns/strategy" >策略<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>基于组合机制：你可以通过对相应行为提供不同的策略来改变对象的部分行为。模板方法在类层次上运作，因此它是静态的。策略在对象层次上运作，因此允许在运行时切换行为。</li></ul><h4 id="模板方法"><a class="markdownIt-Anchor" href="#模板方法"></a> 模板方法</h4><p>模板方法模式在超类中定义了一个算法的框架，允许子类在不修改结构的情况下重写算法的特定步骤。</p><p>模板方法模式包含了抽象类、具体类。</p><ul><li><strong>抽象类 (Abstract­Class)</strong> 会声明作为算法步骤的方法，以及依次调用它们的实际模板方法。算法步骤可以被声明为抽象类型，也可以提供一些默认实现。</li><li><strong>具体类 (Concrete­Class)</strong> 可以重写所有步骤，但不能重写模板方法自身。</li></ul><p>模板方法的应用场景</p><ul><li>当你只希望客户端扩展某个特定算法步骤，而不是整个算法或其结构时，可使用模板方法模式。</li><li>当多个类的算法除一些细微不同之外几乎完全一样时，你可使用该模式。但其后果就是，只要算法发生变化，你就可能需要修改所有的类。</li></ul><p>模板方法模式可仅允许客户端重写一个大型算法中的特定部分，使得算法其他部分修改对其所造成的影响减小，将重复代码提取到一个超类中。但是部分客户端可能会受到算法框架的限制。通过子类抑制默认步骤实现可能会导致违反里氏替换原则。模板方法中的步骤越多，其维护工作就可能会越困难。</p><p>与其他模式的关系</p><p>详见上文与模板方法相关的内容。</p><h4 id="访问者"><a class="markdownIt-Anchor" href="#访问者"></a> 访问者</h4><p>访问者模式能将算法与其所作用的对象隔离开来。</p><p>访问者模式包含了访问者、具体访问者、元素、具体元素、客户端。</p><ul><li><strong>访问者 (Visitor)</strong> 接口声明了一系列以对象结构的具体元素为参数的访问者方法。如果编程语言支持重载，这些方法的名称可以是相同的，但是其参数一定是不同的。</li><li><strong>具体访问者 (Concrete Visitor)</strong> 会为不同的具体元素类实现相同行为的几个不同版本。</li><li><strong>元素 (Element)</strong> 接口声明了一个方法来 “接收” 访问者。该方法必须有一个参数被声明为访问者接口类型。</li><li><strong>具体元素 (Concrete Element)</strong> 必须实现接收方法。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。请注意，即使元素基类实现了该方法，所有子类都必须对其进行重写并调用访问者对象中的合适方法。</li><li><strong>客户端 (Client)</strong> 通常会作为集合或其他复杂对象（例如一个<a class="link"   href="https://refactoringguru.cn/design-patterns/composite" >组合<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>树）的代表。客户端通常不知晓所有的具体元素类，因为它们会通过抽象接口与集合中的对象进行交互。</li></ul><p>访问者模式的应用场景</p><ul><li>如果你需要对一个复杂对象结构（例如对象树）中的所有元素执行某些操作，可使用访问者模式。</li><li>可使用访问者模式来清理辅助行为的业务逻辑。</li><li>当某个行为仅在类层次结构中的一些类中有意义，而在其他类中没有意义时，可使用该模式。</li></ul><p>访问者模式遵循开闭原则和单一职责原则。访问者对象可以在与各种对象交互时收集一些有用的信息。当你想要遍历一些复杂的对象结构（例如对象树），并在结构中的每个对象上应用访问者时，这些信息可能会有所帮助。</p><p>但是每次在元素层次结构中添加或移除一个类时，你都要更新所有的访问者。在访问者同某个元素进行交互时，它们可能没有访问元素私有成员变量和方法的必要权限。</p><p>与其他模式的关系</p><p>详见上文与访问者相关的内容。</p><h2 id="在实际项目中应用设计模式"><a class="markdownIt-Anchor" href="#在实际项目中应用设计模式"></a> 在实际项目中应用设计模式</h2><blockquote><p>如果你还未开发过中大型项目，那么设计模式可能并不适合你直接学习。</p></blockquote><p>设计模式的学习与应用依赖于大量的项目编码实践。如何在恰当的情况下使用合适的设计模式，需要大量的历史经验积累。实际上，在一个个项目规范开发的流程中，你可能已经无意识地使用到了某几种比较常用的设计模式。</p><p>设计模式本身就是一种在代码层面提出的思想，是为了更好地服务于代码的可维护性和稳健性，它并不是一种针对业务需求的指导思想。如果要在项目中强行套用设计模式，那么很可能会造成过度设计或矫枉过正。</p><blockquote><p>如果你已经开发过了大大小小的项目，那么设计模式可能就是对于你过去有意或无意的编码思维进行了整体的总结。通过学习这些设计模式，你能对自己的编码思维有更清晰的认知，并利用设计模式去优化已有项目架构中的不足之处。</p></blockquote><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p>本文主要内容参考自<a class="link"   href="https://refactoringguru.cn/" >重构大师 refactoringguru<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>其他参考资料</p><p><a class="link"   href="https://www.explainthis.io/zh-hans/swe/solid" >什么是 SOLID 原则？ | ExplainThis<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://sourcemaking.com/design_patterns" >design pattern | sourcemaking.com<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://it-blog-cn.com/blogs/design_mode/seven_principle.html" >设计模式 | 程序员进阶<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;简单地分析一下 23 种设计模式之间的联系以及如何选择比较合适的设计模式。</summary>
    
    
    
    <category term="编程思维" scheme="https://www.entropy-tree.top/categories/%E7%BC%96%E7%A8%8B%E6%80%9D%E7%BB%B4/"/>
    
    
    <category term="代码优化" scheme="https://www.entropy-tree.top/tags/%E4%BB%A3%E7%A0%81%E4%BC%98%E5%8C%96/"/>
    
    <category term="七大原则" scheme="https://www.entropy-tree.top/tags/%E4%B8%83%E5%A4%A7%E5%8E%9F%E5%88%99/"/>
    
  </entry>
  
  <entry>
    <title>git 大型项目管理</title>
    <link href="https://www.entropy-tree.top/2024/06/28/git-work-with-large-repo/"/>
    <id>https://www.entropy-tree.top/2024/06/28/git-work-with-large-repo/</id>
    <published>2024-06-27T16:42:25.000Z</published>
    <updated>2024-08-14T08:08:02.754Z</updated>
    
    <content type="html"><![CDATA[<p>git 作为一个版本控制工具，管理的通常是一些占用磁盘空间不大的源代码文件等。但是实际应用中，git 也会被用于管理大型项目。<span id="more"></span></p><p>以下介绍了三种可能应用于大型项目管理的方案。</p><h2 id="git-lfs"><a class="markdownIt-Anchor" href="#git-lfs"></a> Git LFS</h2><p>Git LFS 主要用于管理大型文件和二进制文件，例如游戏开发中的美术资源、视频文件、大型数据集等。它通过将大文件的指针存储在 Git 仓库中，而将文件存储在远程的 LFS 存储中，从而避免 Git 仓库变得过于庞大。</p><h3 id="使用"><a class="markdownIt-Anchor" href="#使用"></a> 使用</h3><p>安装 git-lfs</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S git-lfs</span><br></pre></td></tr></table></figure></div><blockquote><p>其他发行版使用对应的包管理器安装</p></blockquote><p>在一个本地的 Git 仓库中</p><p>初始化 git-lfs 的 Git hooks</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs install</span><br></pre></td></tr></table></figure></div><p>查看文件状态</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs status</span><br></pre></td></tr></table></figure></div><p>创建一个大文件 2GB</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/random of=./large_file bs=1024M count=2</span><br></pre></td></tr></table></figure></div><p>添加文件到 lfs</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs track <span class="string">&quot;large_file&quot;</span></span><br></pre></td></tr></table></figure></div><p>支持通配符 (需要使用引号包围名称及通配符，否则会被 shell 解析为匹配的路径)</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git lfs track <span class="string">&quot;*.png&quot;</span> <span class="comment"># 当前目录下所有 png 文件</span></span><br><span class="line">git lfs track <span class="string">&quot;**/*.png&quot;</span> <span class="comment"># 所有目录下 png 文件</span></span><br><span class="line">git lfs track *.zip <span class="comment"># 当前目录下所有 zip 文件</span></span><br></pre></td></tr></table></figure></div><p>也可以指定添加文件夹</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git lfs track <span class="string">&quot;large_dir/**&quot;</span> <span class="comment"># large_dir 目录下所有文件和子目录</span></span><br><span class="line">git lfs track <span class="string">&quot;large_dir/*&quot;</span>  <span class="comment"># large_dir 目录下所有文件，不含子目录</span></span><br></pre></td></tr></table></figure></div><p>添加完成后会产生一个 .gitattributes 文件，记录了被 git-lfs 跟踪的大型文件</p><p>将大型文件添加到缓存区</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git add large_file</span><br></pre></td></tr></table></figure></div><p>查看 lfs 跟踪的文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs ls-files</span><br></pre></td></tr></table></figure></div><p>移除文件</p><p>取消对该文件或文件夹的跟踪</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git lfs untrack <span class="string">&quot;large_file&quot;</span></span><br><span class="line">git lfs untrack <span class="string">&quot;large_dir/**&quot;</span></span><br><span class="line">git lfs untrack <span class="string">&quot;large_dir/*&quot;</span></span><br></pre></td></tr></table></figure></div><p>从缓存区移除文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">rm</span> -r --cached large_file</span><br></pre></td></tr></table></figure></div><p>清理不再跟踪或“悬挂”的 LFS 对象</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs prune</span><br></pre></td></tr></table></figure></div><p>推送本地仓库到远程仓库，注意提交包含 LFS 跟踪信息的 gitattributes 文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git push</span><br></pre></td></tr></table></figure></div><p>当克隆一个包含 Git LFS 跟踪的大型仓库时</p><p><strong>git clone</strong></p><ul><li>对于 Git LFS 跟踪的文件，<code>git clone</code> 会下载这些文件的指针（即 <code>.git/lfs/objects</code> 中的引用），但不会立即下载文件的实际内容。</li><li><code>git clone</code> 可能会很快完成，因为大文件的指针相对较小。</li></ul><p><strong>git lfs pull</strong></p><ul><li>这个命令专门用于下载 LFS 跟踪文件的实际内容。</li><li><code>git lfs pull</code> 来确保这些文件的完整内容被下载到本地。</li></ul><p>通常 <code>git clone</code> 之后再执行 <code>git lfs pull</code> 以得到最新的完整仓库。</p><p>将已有的项目迁移到 LFS</p><p>将所有的 png 文件导入 LFS 中</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs migrate import --everything --include=<span class="string">&quot;*.png&quot;</span></span><br></pre></td></tr></table></figure></div><p>将 main 分支中所有大于 100Kb 的文件导入 LFS 中</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs migrate import --include-ref=main --above=100Kb</span><br></pre></td></tr></table></figure></div><p>推送</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git push --force --all</span><br></pre></td></tr></table></figure></div><p>远程服务端执行的 gc 命令，清理仓库，通常不需要手动执行</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git --git-dir=git-repo.git gc --prune=now</span><br></pre></td></tr></table></figure></div><h3 id="演示"><a class="markdownIt-Anchor" href="#演示"></a> 演示</h3><p>在本地搭建一个 git 远程仓库</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> large-repo</span><br><span class="line"><span class="built_in">cd</span> large-repo</span><br><span class="line">git init --bare large-repo.git</span><br></pre></td></tr></table></figure></div><p>克隆到本地仓库</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> /home/entropy/Public/large-repo/large-repo.git</span><br></pre></td></tr></table></figure></div><p>开启 Git LFS</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git lfs install</span><br></pre></td></tr></table></figure></div><p>开启后会在用户目录下的 .gitconfig 中增加一个 filter 配置</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> ~/.gitconfig</span><br></pre></td></tr></table></figure></div><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">[filter &quot;lfs&quot;]</span><br><span class="line">smudge = git-lfs smudge -- %f</span><br><span class="line">process = git-lfs filter-process</span><br><span class="line">required = true</span><br><span class="line">clean = git-lfs clean -- %f</span><br></pre></td></tr></table></figure></div><p>同时在 .git/hooks 中增加了几个 hook 脚本，增加了 .git/lfs 目录</p><p>增加对一个大文件的跟踪</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">git lfs track <span class="string">&#x27;*.bin&#x27;</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/random of=a.bin bs=1M  count=10</span><br><span class="line">git add .</span><br><span class="line">git lfs ls-files</span><br><span class="line">git commit -am <span class="string">&quot;lfs 1&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure></div><p>将文件修改为 1K 大小并提交推送</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/random of=a.bin bs=1024  count=1</span><br><span class="line">git add .</span><br><span class="line">git commit -am <span class="string">&quot;lfs 2&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure></div><p>重新克隆仓库，查看仓库大小</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ..</span><br><span class="line"><span class="built_in">rm</span> -rf large-repo</span><br><span class="line">git <span class="built_in">clone</span> /home/entropy/Public/large-repo/large-repo.git</span><br><span class="line"><span class="built_in">cd</span> large-repo</span><br><span class="line"><span class="built_in">du</span> -sh .</span><br></pre></td></tr></table></figure></div><p>可以发现仓库并没有下载原来 10MB 大小的文件</p><p>切换到上一个版本，这个版本中包含 10MB 大小的文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git checkout HEAD~1</span><br></pre></td></tr></table></figure></div><p>LFS 会在 checkout 之后从远程仓库拉取文件到本地，再拷贝到工作目录中，因此整个目录大小变成了 20MB</p><p>如果只想保留最新版本，可以执行 <code>git lfs prune</code> 删掉历史数据</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git checkout master</span><br><span class="line">git lfs prune</span><br></pre></td></tr></table></figure></div><p>使用不同的方式配置跟踪文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">git lfs track <span class="string">&#x27;**/*.png&#x27;</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.png bs=1024 count=1</span><br><span class="line">git add .</span><br><span class="line">git lfs ls-files</span><br><span class="line"></span><br><span class="line">git lfs track <span class="string">&#x27;res/**&#x27;</span></span><br><span class="line"><span class="built_in">mkdir</span> res &amp;&amp; <span class="built_in">cd</span> res</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.jpg bs=1024 count=1</span><br><span class="line"><span class="built_in">mkdir</span> subdir &amp;&amp; <span class="built_in">cd</span> subdir</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.xz bs=1024 count=1</span><br><span class="line"><span class="built_in">cd</span> ../..</span><br><span class="line">git add .</span><br><span class="line">git lfs ls-files</span><br><span class="line"></span><br><span class="line">git lfs track <span class="string">&#x27;assets/*&#x27;</span></span><br><span class="line"><span class="built_in">mkdir</span> assets &amp;&amp; <span class="built_in">cd</span> assets</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.xz bs=1024 count=1</span><br><span class="line"><span class="built_in">mkdir</span> subdir &amp;&amp; <span class="built_in">cd</span> subdir</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.jpg bs=1024 count=1</span><br><span class="line"><span class="built_in">cd</span> ../..</span><br><span class="line">git add .</span><br><span class="line">git lfs ls-files</span><br><span class="line"></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=a.zip bs=1024 count=1</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=b.zip bs=1024 count=1</span><br><span class="line">git lfs track *.zip <span class="comment"># 被 shell 解析为匹配的具体路径</span></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=c.zip bs=1024 count=1</span><br><span class="line">git add .</span><br><span class="line">git lfs ls-files</span><br></pre></td></tr></table></figure></div><p>迁移已有项目到 LFS</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=c.zip bs=1M count=10</span><br><span class="line">git add .</span><br><span class="line">git commit -am <span class="string">&quot;c.zip&quot;</span></span><br><span class="line">git push</span><br><span class="line"></span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/urandom of=c.zip bs=1024 count=1</span><br><span class="line">git add .</span><br><span class="line">git commit -am <span class="string">&quot;c.zip 2&quot;</span></span><br><span class="line">git push</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> ..</span><br><span class="line"><span class="built_in">rm</span> -rf large-repo</span><br><span class="line">git <span class="built_in">clone</span> large-repo.git</span><br><span class="line"><span class="built_in">cd</span> large-repo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 执行 migrate 命令将所有 zip 文件迁移到 LFS 中</span></span><br><span class="line">git lfs migrate import --everything --include=<span class="string">&quot;*.zip&quot;</span></span><br><span class="line">git lfs ls-files</span><br><span class="line">git config lfs.allowincompletepush <span class="literal">true</span></span><br><span class="line">git push --force --all</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> ../large-repo.git/lfs</span><br><span class="line"><span class="built_in">du</span> -sh .</span><br><span class="line"><span class="built_in">cd</span> ../..</span><br><span class="line"><span class="built_in">rm</span> -rf large-repo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 重新克隆仓库，由于没有在远程仓库执行垃圾清理，克隆下来的仓库还是很大</span></span><br><span class="line">git <span class="built_in">clone</span> large-repo.git</span><br><span class="line"><span class="built_in">cd</span> large-repo</span><br><span class="line"><span class="built_in">du</span> -sh .</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> ..</span><br><span class="line"><span class="built_in">rm</span> -rf large-repo</span><br><span class="line">git --git-dir=large-repo.git gc --prune=now</span><br><span class="line"></span><br><span class="line"><span class="comment"># 远程仓库清理垃圾后，克隆下来的仓库体积变小</span></span><br><span class="line">git <span class="built_in">clone</span> large-repo.git</span><br><span class="line"><span class="built_in">cd</span> large-repo</span><br><span class="line"><span class="built_in">du</span> -sh .</span><br></pre></td></tr></table></figure></div><h2 id="稀疏克隆"><a class="markdownIt-Anchor" href="#稀疏克隆"></a> 稀疏克隆</h2><p>稀疏克隆 (sparse-clone) 允许用户在克隆仓库之后，通过指定某个文件夹或某个文件的路径，仅下载这部分指定的内容，适用于只关注大型仓库中的部分内容的情况。</p><p>单独使用稀疏克隆还是会克隆整个仓库的历史和分支，只是不会立即检出。通常稀疏克隆可以和其他参数配合使用，以克隆一个比较精简的仓库。例如，配合 <code>--filter</code> 过滤和 <code>--depth</code> 克隆深度，可以明显减小大型仓库在克隆时的大小。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> --filter=blob:none --depth 1 --sparse &lt;git-repo-url&gt;</span><br></pre></td></tr></table></figure></div><h3 id="使用-2"><a class="markdownIt-Anchor" href="#使用-2"></a> 使用</h3><p>初始化稀疏检出，在克隆仓库时使用 <code>--sparse</code> 选项，表示稀疏克隆，不会下载所有内容，但是会下载位于项目根目录下的文件。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> --filter=blob:none --depth 1 --sparse &lt;git-repo-url&gt;</span><br></pre></td></tr></table></figure></div><p>查看 git 仓库中记录的文件和目录，用于后续设置稀疏检出路径</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git ls-files</span><br></pre></td></tr></table></figure></div><p>设置稀疏检出路径，设置 src 和 docs 目录，会自动下载这两个目录的内容</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git sparse-checkout <span class="built_in">set</span> src docs</span><br></pre></td></tr></table></figure></div><p>添加稀疏检出路径，在已有目录的基础上添加额外的目录，不会覆盖前面设置的稀疏检出路径，使用 <code>set</code> 则会重新设置稀疏检出路径。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git sparse-checkout add tests</span><br></pre></td></tr></table></figure></div><p>稀疏检出路径会记录在 .git/info/sparse-checkout 文件中。</p><p>恢复完整检出，将会下载所有内容</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git sparse-checkout <span class="built_in">disable</span></span><br></pre></td></tr></table></figure></div><h2 id="git-submodules"><a class="markdownIt-Anchor" href="#git-submodules"></a> Git Submodules</h2><p>子模块允许在一个 Git 仓库中嵌套另一个 Git 仓库，适用于项目需要依赖外部库或多个项目需要共享代码的情况。</p><h3 id="使用-3"><a class="markdownIt-Anchor" href="#使用-3"></a> 使用</h3><p>添加子模块</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git submodule add &lt;git-repo-url&gt; module</span><br></pre></td></tr></table></figure></div><p>添加完成后会在项目根目录下生成一个 .gitmodules 文件，记录了子模块的本地和远程地址信息</p><p>查看子模块信息</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git submodule</span><br></pre></td></tr></table></figure></div><p>更新子模块</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git submodule update</span><br><span class="line">git submodule update --remote</span><br></pre></td></tr></table></figure></div><p>修复头指针分离问题</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git branch -f main HEAD <span class="comment"># 将 main 分支强制指向当前头指针的位置</span></span><br><span class="line">git checkout main <span class="comment"># 重新检出 main 分支</span></span><br></pre></td></tr></table></figure></div><p>克隆包含子模块的项目</p><p>在子模块的目录内执行以下命令递归初始化并下载子模块的内容</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git submodule update --init --recursive</span><br></pre></td></tr></table></figure></div><p>或者分为初始化子模块和更新子模块两步</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git submodule init<span class="comment"># 初始化子模块</span></span><br><span class="line">git submodule update<span class="comment"># 更新子模块</span></span><br></pre></td></tr></table></figure></div><p>也可以在克隆时一步到位地下载所有内容</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> --recursive &lt;git-repo-url&gt;</span><br></pre></td></tr></table></figure></div><p>删除子模块的步骤比较繁琐</p><p>1.删除子模块文件夹</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">rm</span> -r --cached module</span><br><span class="line"><span class="built_in">rm</span> -rf module</span><br></pre></td></tr></table></figure></div><p>2.删除 .gitmodules 文件中相关子模块的信息 (如果存在)，类似于</p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[submodule &quot;module&quot;]</span><br><span class="line">path = module</span><br><span class="line">url = https://github.com/s-chance/module.git</span><br></pre></td></tr></table></figure></div><p>3.删除 .git/config 文件中相关子模块的信息 (如果存在)</p><p>4.删除 .git 文件夹中的相关子模块文件 (如果存在)</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf .git/modules/module</span><br></pre></td></tr></table></figure></div><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://blog.sinlov.cn/posts/2019/10/20/git-lfs-tutorials/" >git lfs 使用详解 | sinlov’s blog<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://ticktechman.tech/series/git/22/" >高效Git-如何快速下载代码 | Git技术盘点<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://iphysresearch.github.io/blog/post/programing/git/git_submodule/" >Git: submodule 子模块简明教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://ticktechman.tech/series/git/29/" >Git使用-多仓库管理之submodule | Git技术盘点<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;git 作为一个版本控制工具，管理的通常是一些占用磁盘空间不大的源代码文件等。但是实际应用中，git 也会被用于管理大型项目。</summary>
    
    
    
    <category term="git" scheme="https://www.entropy-tree.top/categories/git/"/>
    
    
    <category term="git lfs" scheme="https://www.entropy-tree.top/tags/git-lfs/"/>
    
    <category term="shallow clone" scheme="https://www.entropy-tree.top/tags/shallow-clone/"/>
    
    <category term="sparse-clone" scheme="https://www.entropy-tree.top/tags/sparse-clone/"/>
    
    <category term="git submodules" scheme="https://www.entropy-tree.top/tags/git-submodules/"/>
    
  </entry>
  
  <entry>
    <title>Figma 设计入门</title>
    <link href="https://www.entropy-tree.top/2024/05/09/figma-starter/"/>
    <id>https://www.entropy-tree.top/2024/05/09/figma-starter/</id>
    <published>2024-05-09T14:07:11.000Z</published>
    <updated>2024-05-09T14:20:37.610Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要介绍 Figma 工具的基本使用，以及简要说明 UI 设计在项目设计中的应用<span id="more"></span></p><p>本文参考的相关视频教程：<a class="link"   href="https://www.bilibili.com/video/BV1wa411V7mU" >https://www.bilibili.com/video/BV1wa411V7mU<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h2 id="基本使用"><a class="markdownIt-Anchor" href="#基本使用"></a> 基本使用</h2><h3 id="figma-安装与汉化"><a class="markdownIt-Anchor" href="#figma-安装与汉化"></a> Figma 安装与汉化</h3><p>学习如何使用 WEB 端以及客户端</p><p><strong>方法一：使用 web 端</strong></p><ol><li><p>打开 Figma 官网 <a class="link"   href="http://www.figma.com" >www.figma.com<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li><li><p>注册并登录账号即可</p></li><li><p>如果需要在浏览器中使用本地字体，可以下载 Figma 的 Font installers</p><p>下载链接：<a class="link"   href="https://www.figma.com/downloads/" >https://www.figma.com/downloads/<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li></ol><p><strong>方法二：使用客户端</strong></p><ol><li>为了更好的体验，可以下载 Figma 桌面客户端</li><li>采用此方案的话就可以不需要安装 Font installers</li></ol><p>官方客户端没有提供中文语言，可以在 <a class="link"   href="https://www.figma.cool/cn" >https://www.figma.cool/cn<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> (Figma 中文社区) 下载汉化的 Figma 客户端压缩包。</p><p>Figma 还提供了移动端的实时预览版本，可以在移动端上实时查看页面效果图。</p><p><strong>实时预览方案</strong></p><ol><li>移动端设备使用 Figma Mirror APP</li><li>网页端浏览器输入 <a class="link"   href="https://www.figma.com/mirror" >figma.com/mirror<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></li></ol><p>注意：移动端需要手机和电脑处于同一网络，并且登录同一个账户</p><h3 id="后台界面总览介绍"><a class="markdownIt-Anchor" href="#后台界面总览介绍"></a> 后台界面总览介绍</h3><p>认识下后台界面</p><p>参考视频：<a class="link"   href="https://www.bilibili.com/video/BV1wa411V7mU/?p=3" >https://www.bilibili.com/video/BV1wa411V7mU/?p=3<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p>基本操作：</p><ul><li>移动画布：按住 Space 空格键再用鼠标就可以移动界面中间的画布区域</li><li>缩放画布：按住 Ctrl 键再用鼠标中键滚轮，就可以缩小放大画布区域</li><li>百分比显示：可以使用 Shift/Ctrl + 0 组合快捷键快速调整到 100% 大小显示</li><li>显示所有内容：可以使用 Shift + 1 组合快捷键快速显示</li><li>隐藏/显示侧边栏：可以使用 Ctrl + \ 组合快捷键快速隐藏/显示侧边栏</li></ul><p>在界面右下角的 ? 按钮处还可以打开 Keyboard shortcuts (快捷键 <code>Ctrl + Shift + ?</code>) 将常用的快捷键显示在界面下方。</p><h4 id="小结"><a class="markdownIt-Anchor" href="#小结"></a> 小结</h4><ol><li>按住 <code>空格</code> 或 <code>中键滚轮</code> 可以移动画布</li><li>缩放画布按 <code>Ctrl+中键滚轮</code>，百分比显示画面 <code>Shift+0</code></li><li>显示所有画布，按 <code>Shift+1</code></li><li>隐藏左右两侧面板 <code>Ctrl+\</code></li></ol><h3 id="移动缩放工具"><a class="markdownIt-Anchor" href="#移动缩放工具"></a> 移动&amp;缩放工具</h3><p>对物体进行位置、大小的控制</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508135341142.png"                      alt="image-20240508135341142"                 ></p><p>移动工具快捷键是 <code>V</code>，缩放工具快捷键是 <code>K</code></p><h4 id="移动工具"><a class="markdownIt-Anchor" href="#移动工具"></a> 移动工具</h4><p><strong>对矩形进行复制操作</strong></p><p>按住 Alt 键，鼠标移动到需要复制的矩形上，点击拖动即可完成复制。</p><p>在拖动的过程中按住 Shift 键可以保持水平或垂直方向的约束。</p><p>使用 Ctrl + D 快捷键可以重复地复制。</p><p>在多个矩形间距和位置没有对齐的情况下，可以先选中这些矩形然后使用 Figma 的 “整理” 功能调整布局。之后在选中这些矩形的情况下调整布局时会自动适配每个矩形的大小与间距，相当于是在范围内调整整体大小和布局对齐方式。</p><h4 id="缩放工具"><a class="markdownIt-Anchor" href="#缩放工具"></a> 缩放工具</h4><p>缩放工具主要用于整体缩放，和使用移动工具进行缩放的主要区别是：</p><p>使用缩放工具缩放可以保持等比例缩放，不会出现变形的情况。</p><p>使用移动工具缩放则会因为非等比例的缩放而导致一些图形变形。</p><h4 id="小结-2"><a class="markdownIt-Anchor" href="#小结-2"></a> 小结</h4><p><strong>移动工具</strong></p><ol><li>用来选中和移动元素，快捷键 <code>V</code></li><li>按住 <code>Alt</code> 拖动复制元素，<code>Ctrl+D</code> 可以重复复制</li><li>可以修改形状的圆角，按 <code>Alt</code> 点击小圆点可以单独控制圆角</li><li>框选所有元素，可以使用整理功能，方便对间距进行整体控制</li></ol><p><strong>缩放工具</strong></p><ol><li>用来缩放多个元素不会变形，快捷键 <code>K</code></li><li>圆角使用移动工具缩放后会变形，借助缩放工具即可</li></ol><h3 id="frame-与组的认识"><a class="markdownIt-Anchor" href="#frame-与组的认识"></a> Frame 与组的认识</h3><p>学习更高级的“组”</p><h4 id="组"><a class="markdownIt-Anchor" href="#组"></a> 组</h4><p>组，主要的功能是在于协助我们去批量地选中一些元素，然后为这些元素设计效果。</p><p>在 Figma 中的 “组” 与其他设计软件中的 “组” 拥有更多的功能。</p><p><strong>相似矩形进行编组操作</strong></p><p>创建如下的矩形</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508145422746.png"                      alt="image-20240508145422746"                 ></p><p>框选住所有矩形，鼠标右键菜单，创建编组，快捷键是 Ctrl + G。</p><p>编组成功之后，移动其中任意一个矩形都可以批量移动所有在组内的矩形。</p><p>取消编组，可以在选中矩形后，使用鼠标右键菜单取消或使用快捷键 Ctrl + Shift + G。</p><p>如何使以上任意两个矩形成组？</p><p>加选操作，先点击第一个矩形，再按住 Shift 键，然后点击第二个矩形，就可以同时操作两个矩形。</p><p>取消加选，只需要先取消选中，再单独点击一个矩形即可。</p><p>单独调整一个矩形的位置，会影响分组整体大小。</p><h4 id="画框"><a class="markdownIt-Anchor" href="#画框"></a> 画框</h4><p><strong>用画框进行编组</strong></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508150908741.png"                      alt="image-20240508150908741"                 ></p><p>Frame 画框快捷键为 <code>F</code></p><p>同样选中所有的矩形，鼠标右键菜单或使用快捷键 Ctrl + Alt + G 添加画框编组。</p><p>取消画框的快捷键是 Ctrl + Shift + G。</p><p>画框和分组之间可以相互切换。</p><p>画框具有裁剪功能，可以在有内容的画框编组下使用移动工具单独调整画框，再使用裁剪功能保留一部分内容，类似于蒙版的效果。</p><p>单独调整画框中一个矩形的位置，不会影响画框整体大小，不同于分组。</p><p>在填充颜色时，画框不会影响其中矩形的颜色，相当于画框是一个背景色，而分组就是这些矩形组成的，填充颜色时按组批量调整矩形颜色。</p><p>画框可以无限嵌套，对外层画框的调整会影响到内部的画框。</p><p>强制嵌套可以在移动画框时按住 Ctrl 键，不需要强制嵌套可以按住空格键。</p><h4 id="小结-3"><a class="markdownIt-Anchor" href="#小结-3"></a> 小结</h4><p><strong>组</strong></p><ol><li>编组快捷键 <code>Ctrl+G</code>；取消编组 <code>Ctrl+Shift+G</code></li><li>配合移动工具按 <code>Shift</code> 为加选</li><li>当子元素调整边界或移动位置时，组的边界会自动调整</li><li>组相当于批量移动工具，不具备高级属性</li></ol><p><strong>什么情况下使用组？</strong></p><ol><li>你希望对整个组调整大小的同时也能够影响子级的大小缩放</li><li>你想对多个元素进行位置的移动</li></ol><p><strong>画框</strong></p><ol><li>添加画框快捷键 <code>Ctrl+Alt+G</code>；取消画框 <code>Ctrl+Shift+G</code></li><li>右侧属性栏中可以切换两种状态</li><li>勾选裁剪内容后，超出画框的内容将不再显示</li><li>弹性布局必备，子元素能够在父级 frame 变化时保持自身相对位置和大小</li><li>强制嵌套快捷键移动时按 <code>Ctrl</code>；如不需要嵌套移动时按 <code>空格</code></li></ol><p><strong>什么情况下使用 Frame</strong></p><ol><li>你的项目需要用到弹性布局/自适应</li><li>你想对超出范围的内容部分进行裁切</li><li>具备多个画框进行嵌套</li></ol><h3 id="形状工具"><a class="markdownIt-Anchor" href="#形状工具"></a> 形状工具</h3><p>学习常见造型的绘制</p><p><strong>绘制常用基础造型</strong></p><p>Figma 内置了以下这些基本的形状造型</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508160911108.png"                      alt="image-20240508160911108"                 ></p><ul><li><p>矩形工具 (快捷键 <code>R</code>) 。绘制时按住 Shift 键可以绘制正方形，不按 Shift 就是绘制长方形。绘制完后可以对它的圆角进行设置，按住 Alt 键可以对单边进行圆角的设置。</p></li><li><p>直线工具 (快捷键 <code>L</code>)。绘制时按住 Shift 键可以绘制水平或垂直方向的直线。</p></li><li><p>箭头工具和直线工具是类似，区别是提前添加好了箭头。</p></li><li><p>椭圆工具 (快捷键 <code>O</code>)。绘制时按住 Shift 键可以绘制圆形。还可以通过小圆点调整成扇形、饼形。</p></li><li><p>多边形工具。绘制时按住 Shift 键可以绘制正多边形。</p></li><li><p>星形工具。绘制时按住 Shift 键可以绘制正五角星。</p></li><li><p>放置图片工具。可以将图片快速剪切到任意图形中，在设计头像框的时候很有用。</p><p>此外还可以通过两个图层叠加的方式，在图片下方放置一个圆形框，通过蒙版的设置，裁剪出头像框。快捷键 Ctrl + Alt + M</p></li></ul><h4 id="小结-4"><a class="markdownIt-Anchor" href="#小结-4"></a> 小结</h4><p><strong>形状工具</strong></p><ol><li>编辑形状工具可以双击或者点击工具栏上方的按钮编辑对象</li><li>使用移动工具可以在编辑模式下对单独的锚点进行圆角修改</li><li>布尔运算使用方法和其他设计软件一样，合并形状快捷键 <code>Ctrl+E</code></li></ol><p><strong>蒙版工具</strong></p><ol><li>上层为图像，下层为遮罩，创建蒙版快捷键 <code>Ctrl+Alt+M</code> 再按一次就是取消蒙版</li><li>功能更像是对图像进行裁切，更便捷的操作可以使用 Frame 或者放置图片功能，快捷键 <code>Ctrl+Shift+K</code></li></ol><h3 id="钢笔工具"><a class="markdownIt-Anchor" href="#钢笔工具"></a> 钢笔工具</h3><p>自由的绘图工具</p><p><strong>使用钢笔绘制直线曲线</strong></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508182458655.png"                      alt="image-20240508182458655"                 ></p><p>钢笔工具快捷键 <code>P</code>，铅笔工具快捷键 <code>Shift+P</code></p><p>切换到钢笔工具，先点击一个点，再按 Shift 键点击另外一个点，就可以绘制水平、垂直的直线。绘制完成后，由于点线会持续跟踪鼠标，可以按 Esc 键断开当前的点线，然后就可以绘制不与当前直线相连的新的直线。</p><p>点击一个点后拖拽到一个点后松开鼠标，就可以调整这两点之间直线的弯曲度，从而绘制曲线。</p><p>点击曲线锚点会出现一个“控制手柄”的东西，主要用于调整曲线的弧度，按住 Alt 键可以单独调整一边的曲线弧度。</p><p>曲线和折线拥有不同的锚点，折线锚点可以在按住 Ctrl 键后鼠标点击转换成曲线锚点。</p><p>消除折线锚点可以在按住 Alt 键后点击折线锚点，会将这个折线锚点移除，并把折线变为最近的两个锚点之间的直线。</p><p>创建新的折线锚点可以直接使用移动工具拖动两个锚点连线之间的中心点。</p><h4 id="小结-5"><a class="markdownIt-Anchor" href="#小结-5"></a> 小结</h4><p><strong>钢笔工具</strong></p><ol><li>钢笔快捷键 <code>P</code></li><li>路径线绘制完毕后按 <code>Esc</code> 断线</li><li>弯曲点快捷键按 <code>Ctrl</code> 点击锚点</li><li>使用钢笔工具在路径线上可以直接添加点，按 <code>Alt</code> 键进行减点</li><li>Figma 中点与点之间会进行连接，从而构成一个完整的图形</li><li>双击绘制完的闭合路径线，可以使用颜料桶直接进行上色</li></ol><h3 id="文字工具"><a class="markdownIt-Anchor" href="#文字工具"></a> 文字工具</h3><p>智能的文字系统</p><p>选中文字工具后可以在画布上单击输入文本，也可以拖拽出文本框再输入，后者更适合段落文本输入。</p><p>Figma 默认只显示英文字体的预览，中文字体可以通过一个 Chinese Font Picker 的插件预览。</p><p>复制一个文本到另一个文本上：</p><p>在 Figma 中的两段文字，其中一段文本可以选择“复制为文本”，在粘贴到另一段文本时保留另一段文字原本的属性。也可以选择“复制属性”将文本的属性应用到其他文本上。</p><p>文本还可以添加超链接，只需要复制一个链接，选中文本后粘贴即可。具有超链接的文本会出现下划线，鼠标悬停在文本上时也会显示链接内容。</p><p>文本的属性和样式还可以保存发布到组件库，之后在其他文档中只要启用组件库就可以直接使用已发布的文本属性和样式等。</p><h4 id="小结-6"><a class="markdownIt-Anchor" href="#小结-6"></a> 小结</h4><p><strong>文本工具</strong></p><ol><li>文字快捷键 <code>T</code></li><li>Figma 属性栏数值调整区域都可以按 <code>Alt</code> 结合鼠标左键进行调整</li><li>直接复制文字会把文字属性一并复制过去，通过 <code>复制为文本</code> 解决此问题</li><li>复制属性快捷键 <code>Ctrl+Alt+C</code>；粘贴属性快捷键 <code>Ctrl+Alt+V</code></li><li>可通过保存文本样式进行复用或者推送给团队使用</li><li>网页链接可以在文字上直接粘贴，就可以快速制作跳转链接</li></ol><h3 id="预览以及分享"><a class="markdownIt-Anchor" href="#预览以及分享"></a> 预览以及分享</h3><p>让其他人看到自己的设计</p><ol><li>草稿中的文件可以分享和编辑，对方可以保存</li><li>只有付费团队中的项目可以设置不让对方保存</li><li>在画板上右键可以设置封面</li><li>通过评论功能进行团队之间的沟通</li></ol><h3 id="属性栏-功能详解上"><a class="markdownIt-Anchor" href="#属性栏-功能详解上"></a> 属性栏 功能详解(上)</h3><p>对齐、分布、坐标、图层</p><h4 id="对齐-平均分布"><a class="markdownIt-Anchor" href="#对齐-平均分布"></a> 对齐、平均分布</h4><p><strong>对图形进行对齐和平均分布</strong></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240508231524628.png"                      alt="image-20240508231524628"                 ></p><p>对齐方式有：</p><ul><li>左对齐，快捷键 Alt + A</li><li>水平中心对齐，快捷键 Alt + H</li><li>右对齐，快捷键 Alt + D</li><li>顶部对齐，快捷键 Alt + W</li><li>垂直中心对齐，快捷键 Alt + V</li><li>底部对齐，快捷键 Alt + S</li></ul><p>圆角的设置：</p><p>默认情况下，改动矩形的其中一个圆角，其他圆角也会一并改动。可以在右侧面板开启独立圆角，单独调整其中一个圆角，也可以使用 Alt 快捷键的方式调整。</p><p>平滑圆角是专门针对 ios 设备的功能，用于制作符合 ios 规范的圆角。</p><p>翻转的功能：</p><p>垂直翻转，选中图形后，快捷键 Shift + V</p><p>水平翻转，选中图形后，快捷键 Shift + H</p><h4 id="图层"><a class="markdownIt-Anchor" href="#图层"></a> 图层</h4><p><strong>操作滤色以及正片叠底</strong></p><p>图层有变暗、正片叠底、滤色、叠加等模式</p><p>对于黑色背景可以使用滤色隐藏，对于白色背景可以使用正片叠底隐藏。</p><p>图层顺序调整：</p><ul><li>上移当前选中图层，快捷键 Ctrl + ]</li><li>下移当前选中图层，快捷键 Ctrl + [</li><li>上移到顶层，快捷键 Ctrl + Shift + ]</li><li>下移到底层，快捷键 Ctrl + Shift + [</li></ul><h3 id="属性栏-功能详解下"><a class="markdownIt-Anchor" href="#属性栏-功能详解下"></a> 属性栏 功能详解(下)</h3><p>填充、描边、投影、导出</p><h4 id="填充"><a class="markdownIt-Anchor" href="#填充"></a> 填充</h4><p><strong>填充的不同类型</strong></p><p>纯色、线性渐变、径向渐变、角度渐变、菱形渐变、图片填充</p><h4 id="描边"><a class="markdownIt-Anchor" href="#描边"></a> 描边</h4><p>改变边的大小粗细</p><h4 id="阴影"><a class="markdownIt-Anchor" href="#阴影"></a> 阴影</h4><p>在右侧面板的“效果”一栏可以修改阴影效果，默认的就是“投影”，可以调整投影的大小、位置、聚焦等。</p><h4 id="导出"><a class="markdownIt-Anchor" href="#导出"></a> 导出</h4><p>选中某一个制作好的组件可以导出为图片，需要先将这些组件编组才能预览效果图。</p><p>切片工具，被切片包含的内容，可以一并导出。</p><h3 id="figma-插件安装与使用"><a class="markdownIt-Anchor" href="#figma-插件安装与使用"></a> Figma 插件安装与使用</h3><p>提升效率必备</p><p><strong>常用插件推荐</strong></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240509095801884.png"                      alt="image-20240509095801884"                 ></p><p>更多插件可访问官方社区 <a class="link"   href="https://www.figma.com/community/plugins" >https://www.figma.com/community/plugins<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="组件管理"><a class="markdownIt-Anchor" href="#组件管理"></a> 组件管理</h3><p>元素组件 &amp; 变体组件</p><p>常规的组件如何搭建？具有多状态的组件如何搭建？</p><p>组件分为模板组件和实例组件，模板相当于是最大的组件，实例组件是在复制模板文件后产生的。对实例组件的修改不会影响模板组件，但是对模板组件的修改可以影响所有基于它的实例组件。</p><p>对于开关按钮这类复杂组件需要通过变体功能进行管理。</p><p>选中同一按钮的多个不同状态的组件，合并为变体。之后使用这个变体组件时，就可以在右侧面板切换不同状态。</p><p>变体组件对于命名有一定要求，需要对一堆组件进行批量命名。批量重命名，可以使用快捷键 Ctrl + R。</p><p>命名示例 <code>版本=ios,状态=默认,开关=false/true</code></p><p>先选中所有的 ios 组件，重命名为 <code>版本=ios,</code> 这里的英文逗号 <code>,</code> 不可忽略。同理选中所有的 android 组件，重命名为 <code>版本=android,</code></p><p>再选中所有默认状态下的组件，包括 ios 和 android 的组件，点击“当前名称”或“current name”，这时输入框里会出现 <code>$&amp;</code> 的字样 (也可以手动输入 <code>$&amp;</code> 表示当前的名称)，接着在后面输入 <code>状态=默认,</code> 即可。</p><p>同理重命名其他状态的组件即可。</p><p>开关状态，可以先选中所有的关状态组件，重命名为 <code>$&amp;开关=false</code> (<code>$&amp;</code> 可以保留当前的名称)，再选中所有的开状态组件，重命名为 <code>$&amp;开关=true</code>。</p><p>最后把所有重命名好的组件合并为变体组件，就可以通过这一个组件观察不同版本，不同状态下的组件。</p><h4 id="小结-7"><a class="markdownIt-Anchor" href="#小结-7"></a> 小结</h4><p><strong>组件管理</strong></p><ol><li>添加组件快捷键 <code>Ctrl+Alt+K</code></li><li>组件分为初始组件和实例组件 (复制初始组件得到的)</li><li>修改初始组件会直接影响实例组件，如不需要影响，对实例组件进行分离实例即可 <code>Ctrl+Alt+B</code></li><li>项目中具有多种属性状态的组件，使用变体功能进行搭建。变体组件需要基于初始组件搭建。</li><li>操作变体组件时，对命名规则要求较高，需要特别注意</li><li>重命名快捷键 <code>Ctrl+R</code></li></ol><h3 id="组件库管理"><a class="markdownIt-Anchor" href="#组件库管理"></a> 组件库管理</h3><p>制作完成的组件可以发布到组件库，之后在其他文档中只要启用组件库，就可以快速使用组件库中的组件。</p><h3 id="约束和栅格"><a class="markdownIt-Anchor" href="#约束和栅格"></a> 约束和栅格</h3><p><strong>弹性布局的设置</strong></p><p>弹性布局需要对元素进行约束。</p><p>约束的是画框内的子级元素，不是画框本身。如果在画框中还有分组，则分组内的元素不直接受画框的影响。</p><p><strong>约束工具</strong></p><ol><li>约束常用于适合弹性布局的界面中</li><li>当父级画框进行大小拖动时，只会影响子级，不能跨级控制</li><li>按 <code>Shift</code> 可以加选或减选约束控制</li></ol><p><strong>布局网格</strong></p><p>布局网格就是栅格化。</p><p>在右侧面板添加布局网格，可以调整网格的大小，还可以设置按列、按行的栅格化。比较常用的是按列栅格化。</p><p>按行、按列栅格化中还有类型选项，默认是“拉伸”类型，也就是弹性布局，还有左、右、居中类型。</p><p>布局网格的样式也可以保存发布并在其他文档中使用。</p><p><strong>栅格工具</strong></p><ol><li>栅格工具可以应用在 UI (4/6 列网格) 以及网页设计中 (12/24 列网格)</li><li>需要弹性布局就选择 “拉伸” 模式，其余情况选择 “左” “右” “居中”</li><li>栅格支持保存样式</li></ol><h3 id="自动布局"><a class="markdownIt-Anchor" href="#自动布局"></a> 自动布局</h3><p>弹性布局的设置</p><p>文字在一个矩形框中如何保持与边框上下左右的间距？</p><p>可以使用自动布局实现。</p><p>先选中文字元素后按 Shift + A 可以快速添加自动布局，此时文字会被套上一层框。背景颜色可以在右侧面板的“填充”中设置，圆角也是在右侧面板中设置。这样不需要单独绘制矩形，而是直接通过自动布局添加背景。</p><p>在自动布局面板中的“下”和“右”选项可在复制多个自动布局组件时修改外层自动布局的方向，从而使内部的多个自动布局组件改变排列方向。</p><p>添加外层自动布局，可以在选中元素后，再按 Shift + A 键。修改复制方向的时候，需要选中外层自动布局。</p><p>自动布局面板中还可以调整多个自动布局组件的间距，同样在修改的时候需要选中外层自动布局。</p><p>在自动布局中还有适应内容、固定宽度、填充容器的设置。</p><h4 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h4><p><strong>自动布局</strong></p><ol><li>需要有一个大图层管理里面的小图层</li><li><strong>适应内容</strong>：根据文字数量变化自动调节</li><li><strong>固定宽度</strong>：组件的宽度和高度固定</li><li><strong>填充容器</strong>：根据外轮廓的大小变化，内部元素大小也随之改变</li></ol><h3 id="磨砂图标绘制"><a class="markdownIt-Anchor" href="#磨砂图标绘制"></a> 磨砂图标绘制</h3><p>借助背景模糊快速打造磨砂质感</p><ol><li>绘制造型</li><li>添加描边</li><li>添加渐变、背景模糊</li></ol><h3 id="微质感图标绘制"><a class="markdownIt-Anchor" href="#微质感图标绘制"></a> 微质感图标绘制</h3><p>轻盈且具有立体感的技法打造</p><p>此类图标在绘制过程中主要涉及两个知识点：</p><ol><li>外部阴影</li><li>内部阴影</li></ol><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240509185900176.png"                      alt="image-20240509185900176"                 ></p><p>在 Figma 中可以为图标同时添加多个外部阴影以及多个内部阴影。</p><p>这里气泡框可以先绘制圆形，再绘制小矩形。选中这两个图形，先点击 Union selection 连集使其成为一个组合图形 (内部没有其他边界线)，再点击 Flatten selection 拼合，相当于合并图层。</p><p>然后双击图层，调整图形交界处的圆角。</p><p>造型完成后，就是对图形添加投影黑内部阴影，然后调整阴影的颜色、位置以及模糊度，直到符合预期效果。</p><p><strong>总结</strong></p><ol><li>造型绘制</li><li>颜色添加</li><li>阴影、内阴影添加</li></ol><h3 id="螺旋图标绘制"><a class="markdownIt-Anchor" href="#螺旋图标绘制"></a> 螺旋图标绘制</h3><p>借助插件高效完成复制操作</p><p>Figma 本身不具备重复旋转的操作，可以借助插件的方式高效地完成此类图标的绘制。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240509194739890.png"                      alt="image-20240509194739890"                 ></p><p>可以绘制一个半径大小为 1 的圆形作为自定义旋转点的定位，配合这个插件实现螺旋图形的绘制。</p><h3 id="25d图标绘制"><a class="markdownIt-Anchor" href="#25d图标绘制"></a> 2.5D图标绘制</h3><p>使用插件巧妙绘制轴测图标</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240509200635568.png"                      alt="image-20240509200635568"                 ></p><p>这个插件适合制作简单的 2.5D 图形，如果涉及比较复杂的 2.5D 图形，不建议使用 Figma 绘制。</p><h2 id="项目应用"><a class="markdownIt-Anchor" href="#项目应用"></a> 项目应用</h2><h3 id="网页-ui-设计"><a class="markdownIt-Anchor" href="#网页-ui-设计"></a> 网页 UI 设计</h3><p>使用 Figma 绘制简单的 web 登录注册页面，使用弹性布局适配移动端和 PC 端。</p><p>可以尝试临摹现有的流行网站主页等，重点关注多端的适配。</p><p>实战案例相关视频参考：<a class="link"   href="https://www.bilibili.com/video/BV1FG4y1K7t1/" >https://www.bilibili.com/video/BV1FG4y1K7t1/<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="ui-设计与前端开发"><a class="markdownIt-Anchor" href="#ui-设计与前端开发"></a> UI 设计与前端开发</h3><p>前端开发者，如果想自己实现一些较为复杂的网页，总是要编写代码、收集各种素材、调整各种样式。</p><p>这些工作可能并不利于前端开发者专注于界面 UI 布局设计，因为前端开发在编写代码时无法预览整个网页最终的效果，必须要把代码以及样式编写完整。在这个过程中，代码以及样式的调整会分散开发者的精力，从而导致最终的网页视觉效果、交互效果不尽人意。</p><p>UI 设计工具如 Figma 虽然在操作上可能并不符合前端开发的习惯，但是 UI 设计图好比是前端开发的蓝图。</p><p>对于需要自行实现网页 UI 设计并且不希望依赖任何第三方组件库的前端开发者，或者是需要自行设计组件库组件的前端研发者来说，在编写代码之前，先设计出一份蓝图是很有帮助的。</p><p>这也可以减轻前端开发者在后续编写代码时的压力：前端开发者可以根据蓝图更加专注于代码上的工作，而不必一边思考代码的编写，一边纠结一些 UI 设计上的问题。</p><p>或者说，在大型自研项目中，UI 设计一般是不可或缺的一环。Figma 在团队协作中具有一定优势，而前端开发者往往是需要和 UI 设计师对接工作的，这也意味着前端开发者可能会接触到 Figma 这类工具，因此具备一定的认知也是有帮助的。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://figmachina.com/guide/" >FigmaChina<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://a82q809s0y.feishu.cn/wiki/RVV0wq1QUi6Dqykl3gjciO1GnOd" >写给前端的 Figma 教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文主要介绍 Figma 工具的基本使用，以及简要说明 UI 设计在项目设计中的应用</summary>
    
    
    
    <category term="设计" scheme="https://www.entropy-tree.top/categories/%E8%AE%BE%E8%AE%A1/"/>
    
    
    <category term="UI 设计" scheme="https://www.entropy-tree.top/tags/UI-%E8%AE%BE%E8%AE%A1/"/>
    
    <category term="原型设计" scheme="https://www.entropy-tree.top/tags/%E5%8E%9F%E5%9E%8B%E8%AE%BE%E8%AE%A1/"/>
    
    <category term="协作工具" scheme="https://www.entropy-tree.top/tags/%E5%8D%8F%E4%BD%9C%E5%B7%A5%E5%85%B7/"/>
    
    <category term="前端辅助技能" scheme="https://www.entropy-tree.top/tags/%E5%89%8D%E7%AB%AF%E8%BE%85%E5%8A%A9%E6%8A%80%E8%83%BD/"/>
    
  </entry>
  
  <entry>
    <title>SSH 隧道技术</title>
    <link href="https://www.entropy-tree.top/2024/04/18/ssh-tunneling-techniques/"/>
    <id>https://www.entropy-tree.top/2024/04/18/ssh-tunneling-techniques/</id>
    <published>2024-04-18T03:53:33.000Z</published>
    <updated>2024-07-26T09:17:05.118Z</updated>
    
    <content type="html"><![CDATA[<p>SSH (Secure Shell) 不仅是安全远程管理的工具，也是在主机之间创建安全隧道的强大工具。本文围绕 SSH 的隧道技术，探索隧道技术的原理与用途，并通过在多台主机上建立和使用正向隧道和反向隧道，实际体会隧道技术的作用。<span id="more"></span></p><h2 id="正向-ssh-隧道"><a class="markdownIt-Anchor" href="#正向-ssh-隧道"></a> 正向 SSH 隧道</h2><h3 id="原理简介"><a class="markdownIt-Anchor" href="#原理简介"></a> 原理简介</h3><p>正向 SSH 隧道 (本地端口转发) 是一种通过加密的 SSH 连接来转发本地机器上的端口到远程机器上的端口的技术。通过在本地机器上设置 SSH 隧道，用户可以安全地访问远程服务器上的资源。这种隧道的设置允许本地应用程序安全地连接到远程服务器上的服务，就像这些服务是在本地运行一样。</p><p>当设置了一个正向 SSH 隧道，就是在本地机器上创建了一个可以安全访问远程资源的“入口”。访问这个本地端口的所有请求都会通过加密的 SSH 连接被转发到远程服务器的指定端口上。这种机制常用于安全地访问部署在远程服务器上的数据库、Web 服务等。</p><h3 id="使用"><a class="markdownIt-Anchor" href="#使用"></a> 使用</h3><p>建立基本的正向 SSH 隧道的命令格式如下：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -L [local_address:]local_port:[remote_address]:remote_port username@ssh_server</span><br></pre></td></tr></table></figure></div><p><code>-L</code>：指定正向隧道，格式为 <code>[local_address:]local_port:[remote_address]:remote_port</code>，其中</p><ul><li><p><code>local_address</code>：可选参数，表示本地地址。</p><ul><li>通常可以缺省，默认只有本地可以连接远程端口，即 <code>localhost</code>。</li><li>也可以绑定一个地址例如 <code>0.0.0.0</code>，以便其他主机使用本机作为正向代理服务器。</li></ul></li><li><p><code>local_port</code>：表示本地机器上用于访问或发送数据的端口。</p></li><li><p><code>remote_address</code>：表示远程网络中的目标地址，可以是 IP 或主机名。</p><ul><li><p>缺省参数，默认就是远程网络的 <code>localhost</code>。</p></li><li><p>在目标服务器和 SSH 服务器不是同一台服务器时，需要明确指定目标地址，不可缺省。</p></li></ul></li><li><p><code>remote_port</code>：表示目标地址上用于连接服务的端口。</p></li></ul><p><code>username@ssh_server</code>：远程 SSH 服务器的登录用户名和地址。</p><p>注意：正向隧道中的 <code>local</code> 是请求者，<code>remote</code> 是响应者，而远程 SSH 服务器不一定是目标服务器，但是要求一定能访问目标服务器。</p><p>常用辅助参数：</p><ul><li><code>-f</code>：在后台执行 SSH 命令。</li><li><code>-N</code>：不执行远程命令，常与端口转发一起使用。</li><li><code>-v</code>, <code>-vv</code>, <code>-vvv</code>：增加命令的详细输出级别，用于调试。</li><li><code>-C</code>：启用压缩，可以提高传输速度。</li><li><code>-p 端口</code>：指定 SSH 服务器监听的端口 (如果不是默认的22端口)。</li><li><code>-i 私钥文件</code>：使用指定的私钥文件进行身份验证。</li></ul><h3 id="场景案例"><a class="markdownIt-Anchor" href="#场景案例"></a> 场景案例</h3><p>在正向 SSH 的场景中，目标服务器或服务通常位于客户端可以直接或间接访问的网络中。这意味着客户端可以通过 SSH 连接到一个中介服务器，然后通过 SSH 端口转发访问位于同一远程网络内的其他服务，即使这些服务没有直接对外开放。客户端使用 SSH 客户端软件建立连接，用于远程管理服务器或进行安全的文件传输。这样，正向 SSH 不仅适用于目标网络对外完全开放的情况，也适用于需要通过中介访问受限资源的场景。</p><p>案例</p><p>在服务器上启动一个 tomcat，在本地直接访问 <code>服务器IP:8080</code>，大致有两种情况：</p><ul><li>访问成功。在这种情况下使用正向 SSH 隧道更多是在开放或不受信任的网络中进行加密通信，防止中间人攻击等。</li><li>访问失败。在这种情况下使用正向 SSH 隧道更多是为了绕过网络限制和防火墙策略等。</li></ul><p>在本地执行命令，在本地与 SSH 服务器之间创建正向的 SSH 隧道，分为两种情况：</p><ul><li><p>SSH 服务器 (10.0.0.2) 和目标服务器 (10.0.0.2) 是同一台服务器</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -L 9090:localhost:8080 root@10.0.0.2</span><br></pre></td></tr></table></figure></div><p>这个命令将本地的 <code>9090</code> 端口转发到远程 SSH 服务器上的 <code>localhost</code> 的 <code>8080</code> 端口。</p></li><li><p>SSH 服务器 (10.0.0.2) 和目标服务器 (10.0.0.3) 不是同一台服务器，但是要确保 SSH 服务器能够访问目标服务器</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -L 9090:10.0.0.3:8080 root@10.0.0.2</span><br></pre></td></tr></table></figure></div><p>这个命令将本地的 <code>9090</code> 端口转发到远程 SSH 服务器上的目标地址 <code>10.0.0.3</code> 的 <code>8080</code> 端口。</p></li></ul><p>在本地访问 <code>localhost:9090</code>，正常情况下应该能够看到 tomcat 的主页面。</p><h2 id="反向-ssh-隧道"><a class="markdownIt-Anchor" href="#反向-ssh-隧道"></a> 反向 SSH 隧道</h2><h3 id="原理简介-2"><a class="markdownIt-Anchor" href="#原理简介-2"></a> 原理简介</h3><p>反向 SSH 隧道 (远程端口转发) 是将远程服务器上的端口转发到本地机器上的端口的过程。这允许在远程服务器上运行的应用程序或用户通过这个端口安全地访问本地机器上运行的服务或应用。</p><p>当设置一个反向 SSH 隧道时，实际上是在远程服务器上创建了一个端口，当访问这个端口时，流量会通过 SSH 隧道转发到连接的本地机器的指定端口。这通常用于开发和测试阶段，允许远程团队成员或系统访问仅在开发者的本地机器上运行的应用。</p><h3 id="使用-2"><a class="markdownIt-Anchor" href="#使用-2"></a> 使用</h3><p>建立基本的反向 SSH 隧道的命令格式如下：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -R [remote_address:]remote_port:[local_address]:local_port username@ssh_server</span><br></pre></td></tr></table></figure></div><p><code>-R</code>：指定反向隧道，格式为 <code>[remote_address:]remote_port:[local_address]:local_port</code>，其中</p><ul><li><p><code>remote_address</code>：可选参数，表示远程地址。</p><ul><li>可以缺省，默认就是远程网络的 <code>localhost</code>。</li><li>可以绑定一个地址例如 <code>0.0.0.0</code>，以便其他主机使用本机进行通信，但是实际效果取决于 <code>GatewayPorts</code> 的设置。</li></ul></li><li><p><code>remote_port</code>：表示远程地址上用于连接服务的端口。</p></li><li><p><code>local_address</code>：表示本地网络中的目标地址。</p><ul><li><p>如果确定本机就是服务提供者，可以缺省，默认就是本机地址。</p></li><li><p>在本地网络目标机器和本机不是同一台服务器时，需要明确指定目标地址，不可缺省。</p></li></ul></li><li><p><code>local_port</code>：表示本地网络上用于提供服务的端口。</p></li></ul><p><code>username@ssh_server</code>：远程 SSH 服务器的登录用户名和地址。</p><p>注意：在反向隧道中，<code>remote</code> 是请求者，<code>local</code> 是响应者，而远程 SSH 服务器则不再是一个关注重点，重点在于本地网络中提供具体服务的目标机器与执行反向 SSH 隧道命令的机器是否是同一台。</p><p>常用辅助参数：<code>-f</code>, <code>-N</code>, <code>-v</code>, <code>-C</code>, <code>-p</code>, <code>-i</code>：这些参数的功能与在<code>ssh -L</code>中的功能相同。</p><h3 id="服务端配置选项"><a class="markdownIt-Anchor" href="#服务端配置选项"></a> 服务端配置选项</h3><p>GatewayPorts 是反向 SSH 隧道中一个比较重要的服务端配置选项。</p><p>GatewayPorts 在 /etc/ssh/sshd_config 配置文件中设置。如果设置在远程服务器的配置中，可以允许或拒绝转发的端口被远程主机上的其他客户端访问。</p><p>GatewayPorts 的三种设置：</p><ul><li>默认情况下，即使指定了 <code>remote_address</code> 为 <code>0.0.0.0</code>（希望监听所有接口），如果远程服务器的 <code>GatewayPorts</code> 设置为 <code>no</code>（默认设置），则实际上端口只会在 <code>localhost</code> 上开放。这意味着无法从远程服务器之外的其他机器访问这个端口。</li><li>如果 <code>GatewayPorts</code> 设置为 <code>yes</code>，则 <code>remote_address</code> 可以被设置为 <code>0.0.0.0</code>（或特定的外部地址），这样端口就会在指定的地址上开放，允许从任何可以访问该服务器的机器访问这个端口。</li><li>如果 <code>GatewayPorts</code> 设置为 <code>clientspecified</code>，则 <code>remote_address</code> 可以指定一个特定的 IP 地址，表示仅允许从特定的 IP 地址访问这个端口。</li></ul><p>推荐使用 <code>GatewayPorts clientspecified</code> 来手动控制客户端的访问权限。</p><h3 id="场景案例-2"><a class="markdownIt-Anchor" href="#场景案例-2"></a> 场景案例</h3><p>在反向 SSH 通常用于目标计算机位于受限网络环境中，无法被外部网络直接访问的情况。例如，服务器位于一个防火墙保护的内网中，或者服务器使用了 NAT 配置，使得外部网络不能直接访问服务器。</p><p>在这种情况下，位于内网中的服务器可以主动建立一个可访问的外部 SSH 服务器的连接。一旦连接建立，外部服务器可以通过这个反向隧道反向访问内网中的服务器。</p><p>反向 SSH 隧道适用于需要通过远程服务器代理访问本地或内网资源的场景，特别是当这些资源不能直接暴露给外部网络时。这种技术通过使用远程服务器作为一个跳板，使得本地服务可以安全地被外部访问，同时保持网络的封闭和安全。与正向 SSH 隧道相似，反向隧道为数据提供了加密的通道，确保传输的安全性。这使得反向 SSH 隧道成为远程访问和网络管理中一个非常有价值的工具。</p><p>案例</p><p>在本地网络中启动一个 tomcat，如果让一台位于外网的机器直接访问，通常只有访问失败的情况。这个时候，反向 SSH 隧道就是一种非常简单高效的内网穿透方案，只需要确保本机能够访问本地网络的服务。</p><p>在本机执行命令，在本机与远程 SSH 服务器之间创建反向的 SSH 隧道，分为两种情况：</p><ul><li><p>本机 (192.168.1.2) 和在本地网络中提供具体服务的机器 (192.168.1.2) 是同一台</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -R 9090:localhost:8080 root@10.0.0.2</span><br></pre></td></tr></table></figure></div><p>这个命令将远程 SSH 服务器的 <code>9090</code> 端口转发到本机上的 <code>localhost</code> 的 <code>8080</code> 端口。</p></li><li><p>本机 (192.168.1.2) 和在本地网络中提供具体服务的机器 (192.168.1.3) 不是同一台，但是要确保本机能够访问目标机器</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -R 9090:192.168.1.3:8080 root@10.0.0.2</span><br></pre></td></tr></table></figure></div><p>这个命令将远程 SSH 服务器的 <code>9090</code> 端口转发到本地网络的 <code>192.168.1.3</code> 的 <code>8080</code> 端口。</p></li></ul><p>访问 <code>SSH服务器IP:9090</code>，正常情况下应该能够看到 tomcat 的主页面。</p><h2 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h2><p>SSH 隧道可能会因为 SSH 长时间不执行命令或者网络问题而断开连接，可以使用一些 SSH 客户端配置选项来维持长时间的连接</p><h3 id="1-serveraliveinterval"><a class="markdownIt-Anchor" href="#1-serveraliveinterval"></a> 1. ServerAliveInterval</h3><p><code>ServerAliveInterval</code> 用于配置 SSH 客户端向服务器发送消息的时间间隔，以保持连接活跃。如果设置了此选项，客户端将定期向服务器发送空包，以保持会话不被服务器因为超时而关闭。</p><h4 id="使用方法"><a class="markdownIt-Anchor" href="#使用方法"></a> 使用方法：</h4><p>在命令行中添加 <code>-o ServerAliveInterval=n</code>，其中 <code>n</code> 是以秒为单位的时间间隔。例如，每 60 秒向服务器发送一次保持连接的消息：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -o ServerAliveInterval=60 username@server.com</span><br></pre></td></tr></table></figure></div><h3 id="2-serveralivecountmax"><a class="markdownIt-Anchor" href="#2-serveralivecountmax"></a> 2. ServerAliveCountMax</h3><p><code>ServerAliveCountMax</code> 与 <code>ServerAliveInterval</code> 一起使用，用于定义在没有收到任何服务器响应的情况下，客户端尝试发送保持连接消息的最大次数。如果达到这个次数仍未收到响应，SSH 客户端将断开连接。</p><h4 id="使用方法-2"><a class="markdownIt-Anchor" href="#使用方法-2"></a> 使用方法：</h4><p>设置尝试次数，与 <code>ServerAliveInterval</code> 一起使用：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -o ServerAliveInterval=60 -o ServerAliveCountMax=3 username@server.com</span><br></pre></td></tr></table></figure></div><p>这个配置表明，SSH 客户端每 60 秒向服务器发送一次消息，尝试最多 3 次，如果三次都没有响应，则客户端断开连接。</p><p>提示：在客户端配置选项较多的情况下，可以改写为配置文件的形式</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Host LTS</span><br><span class="line">HostName server.com</span><br><span class="line">User username</span><br><span class="line">ServerAliveInterval 60</span><br><span class="line">ServerAliveCountMax 3</span><br><span class="line">...</span><br><span class="line">Host LTS2</span><br><span class="line">...</span><br></pre></td></tr></table></figure></div><p>使用 <code>-F</code> 参数指定配置文件，假设配置文件名为 <code>ssh_lts_config</code>，对应的 <code>HOST</code> 为 <code>LTS</code> 则使用以下命令即可</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -F ssh_lts_config LTS</span><br></pre></td></tr></table></figure></div><p>这样就简化了命令行的编写。</p><h3 id="3-tcpkeepalive"><a class="markdownIt-Anchor" href="#3-tcpkeepalive"></a> 3. TCPKeepAlive</h3><p><code>TCPKeepAlive</code> 选项用于设置是否使用 TCP 层的 keepalive 特性来检测和保持网络连接活跃。默认情况下，这个选项通常是开启的。</p><h4 id="使用方法-3"><a class="markdownIt-Anchor" href="#使用方法-3"></a> 使用方法：</h4><p>如果需要显式开启或关闭，可以这样设置：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -o TCPKeepAlive=<span class="built_in">yes</span> username@server.com</span><br></pre></td></tr></table></figure></div><p>或者关闭：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -o TCPKeepAlive=no username@server.com</span><br></pre></td></tr></table></figure></div><h3 id="4-实际应用"><a class="markdownIt-Anchor" href="#4-实际应用"></a> 4. 实际应用</h3><p>在一些长时间运行的 SSH 会话，如反向隧道或持续的远程监控中，合理设置这些参数可以显著提高连接的稳定性，减少由于网络间歇性问题导致的连接断开。</p><p>此外还可以考虑使用 autossh (自动重连)、mosh (针对网络不好的环境进行了优化)、Eternal Terminal (专注解决了断线重连的问题) 等更强大的工具，相关参考资料如下：</p><p><a class="link"   href="https://www.lixl.cn/2020/010618877.html" >利用AutoSSH建立SSH隧道，实现内网穿透<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinuxcn.org/wiki/Mosh" >Mosh | archlinuxcn<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://github.com/mobile-shell/mosh" >mosh | github<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/MisterTea/EternalTerminal" >EternalTerminal | github<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.isolves.com/it/wlyx/fwq/2020-05-25/18682.html" >用ssh连接服务器吗？这两个替代品了解一下<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>SSH 隧道技术中，反向 SSH 隧道的实际应用可能更广泛一些，特别是需要从外网访问内网时，反向隧道让外部环境可以安全访问位于私有网络或内网中的服务和资源。对个人来说，反向隧道可以让外网很方便地访问本地服务，而往往这些服务不会部署到公网服务器上，例如一些简单的游戏联机服务，可能并没有提供一个统一的服务端，而是 P2P 联机的模式。这就可以通过反向 SSH 隧道来建立外网到内网的通信，使得在另一个内网的机器可以通过本地内网 —&gt; 公网 —&gt; 远程内网的链路，跨内网访问服务。</p><p>而正向 SSH 隧道更注重的是加密安全通信，特别是需要访问一些不希望暴露给整个互联网的服务，例如远程服务器上的数据库存储了很重要的数据，是不对公网开放的，这个时候就可以使用 SSH 加密连接到服务器，并将本地端口与远程服务器数据库的端口进行映射。Navicat 工具使用 SSH 隧道连接数据库就是基于这个正向 SSH 隧道技术实现的。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://www.learndiary.com/2022/01/ssh-forward/" >Linux 下 ssh 正向和反向流量转发 |学习日记<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhuanlan.zhihu.com/p/148825449" >彻底搞懂SSH端口转发命令<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.ssh.com/academy/ssh/tunneling-example" >SSH Tunneling: Examples, Command, Server Config<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://linux.cn/article-8945-1.html" >通过 SSH 实现 TCP / IP 隧道（端口转发）：使用 OpenSSH 可能的 8 种场景 | linux.cn<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;SSH (Secure Shell) 不仅是安全远程管理的工具，也是在主机之间创建安全隧道的强大工具。本文围绕 SSH 的隧道技术，探索隧道技术的原理与用途，并通过在多台主机上建立和使用正向隧道和反向隧道，实际体会隧道技术的作用。</summary>
    
    
    
    
    <category term="ssh" scheme="https://www.entropy-tree.top/tags/ssh/"/>
    
    <category term="正向隧道" scheme="https://www.entropy-tree.top/tags/%E6%AD%A3%E5%90%91%E9%9A%A7%E9%81%93/"/>
    
    <category term="反向隧道" scheme="https://www.entropy-tree.top/tags/%E5%8F%8D%E5%90%91%E9%9A%A7%E9%81%93/"/>
    
  </entry>
  
  <entry>
    <title>搭建RSS信息聚合体系</title>
    <link href="https://www.entropy-tree.top/2024/03/02/rss-workflow/"/>
    <id>https://www.entropy-tree.top/2024/03/02/rss-workflow/</id>
    <published>2024-03-02T12:21:46.000Z</published>
    <updated>2024-03-03T11:31:59.526Z</updated>
    
    <content type="html"><![CDATA[<p>记录搭建 Miniflux + RSSHub + Wallabag + Fluent Reader 组合构成的一个较为完整的聚合信息管理和阅读体系<span id="more"></span></p><p>Miniflux 和 RSSHub 为服务端提供内容聚合和RSS源生成，Wallabag 允许保存和稍后阅读网页内容，而 Fluent Reader 作为本地客户端，提供丰富的阅读体验。这个体系涵盖了从内容获取、管理到阅读的全过程，为 RSS 阅读用户提供了一套全面的解决方案。</p><h2 id="基础环境"><a class="markdownIt-Anchor" href="#基础环境"></a> 基础环境</h2><p>本文使用 docker-compose 搭建所有环境，为了避免一些版本不兼容问题，使用以下方式安装 docker-compose</p><p>下载最新 release 版本</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> curl -L https://github.com/docker/compose/releases/download/v2.24.6/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure></div><p>其中的<code>2.24.6</code>是截至本文更新时间的 docker-compose 最新的 release，后续可以视情况访问 <a class="link"   href="https://github.com/docker/compose/releases" >docker compose 官方仓库<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>获取最新 release 的信息。</p><p>赋予可执行权限</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> <span class="built_in">chmod</span> +x /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure></div><p>添加环境变量(如果之前没有添加过)</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;PATH=$PATH:/usr/local/bin/docker-compose&#x27;</span> &gt;&gt; ~/.bashrc &amp;&amp; <span class="built_in">source</span> ~/.bashrc</span><br></pre></td></tr></table></figure></div><p>检查 docker-compose 版本</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker-compose -v</span><br></pre></td></tr></table></figure></div><p><strong>注意：本文的搭建操作均在 docker-compose 2.24.6 版本下完成，低版本会有兼容性问题，无法直接使用本文的 docker-compose.yaml</strong></p><h2 id="rsshub-订阅源生成"><a class="markdownIt-Anchor" href="#rsshub-订阅源生成"></a> RSSHub 订阅源生成</h2><p>RSSHub 是一个生成订阅源的平台，借助爬虫或类似的技术为那些不提供 RSS 服务的网站生成 RSS 订阅源。</p><h3 id="官方示例"><a class="markdownIt-Anchor" href="#官方示例"></a> 官方示例</h3><p>可参考官方部署文档，编写以下 docker-compose.yaml</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.9&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">    <span class="attr">rsshub:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">diygod/rsshub:chromium-bundled</span></span><br><span class="line">        <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;1200:1200&#x27;</span></span><br><span class="line">        <span class="attr">environment:</span></span><br><span class="line">            <span class="attr">NODE_ENV:</span> <span class="string">production</span></span><br><span class="line">            <span class="attr">CACHE_TYPE:</span> <span class="string">redis</span></span><br><span class="line">            <span class="attr">REDIS_URL:</span> <span class="string">&#x27;redis://redis:6379/&#x27;</span></span><br><span class="line">            <span class="attr">PROXY_URI:</span> <span class="string">&#x27;socks5h://warp-socks:9091&#x27;</span></span><br><span class="line">        <span class="attr">depends_on:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">        <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">        <span class="attr">volumes:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">redis-data:/data</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">warp-socks:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">monius/docker-warp-socks:latest</span></span><br><span class="line">        <span class="attr">privileged:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">        <span class="attr">volumes:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">/lib/modules:/lib/modules</span></span><br><span class="line">        <span class="attr">cap_add:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">NET_ADMIN</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">SYS_MODULE</span></span><br><span class="line">        <span class="attr">sysctls:</span></span><br><span class="line">            <span class="attr">net.ipv6.conf.all.disable_ipv6:</span> <span class="number">0</span></span><br><span class="line">            <span class="attr">net.ipv4.conf.all.src_valid_mark:</span> <span class="number">1</span></span><br><span class="line">        <span class="attr">healthcheck:</span></span><br><span class="line">            <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;curl&quot;</span>, <span class="string">&quot;-f&quot;</span>, <span class="string">&quot;https://www.cloudflare.com/cdn-cgi/trace&quot;</span>]</span><br><span class="line">            <span class="attr">interval:</span> <span class="string">30s</span></span><br><span class="line">            <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">            <span class="attr">retries:</span> <span class="number">5</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">    <span class="attr">redis-data:</span></span><br></pre></td></tr></table></figure></div><p>在对应目录下执行<code>docker-compose up -d</code>运行。启动完成之后可以访问<code>127.0.0.1:1200</code>或服务器 IP + 1200 端口观察能否访问。</p><h3 id="精简版"><a class="markdownIt-Anchor" href="#精简版"></a> 精简版</h3><p>去除了 WARP 服务及相关配置，可能导致无法访问严格反爬的订阅源。该版本的目的是为了尊重目标网站的反爬政策以及遵守相关的互联网规则等。</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.9&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">    <span class="attr">rsshub:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">diygod/rsshub:chromium-bundled</span></span><br><span class="line">        <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;1200:1200&#x27;</span></span><br><span class="line">        <span class="attr">environment:</span></span><br><span class="line">            <span class="attr">NODE_ENV:</span> <span class="string">production</span></span><br><span class="line">            <span class="attr">CACHE_TYPE:</span> <span class="string">redis</span></span><br><span class="line">            <span class="attr">REDIS_URL:</span> <span class="string">&#x27;redis://redis:6379/&#x27;</span></span><br><span class="line">        <span class="attr">depends_on:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">        <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">        <span class="attr">volumes:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">redis-data:/data</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">    <span class="attr">redis-data:</span></span><br></pre></td></tr></table></figure></div><p>如果自建的 rsshub 仅供个人使用也可以不对外开放端口，在 miniflux 中直接使用容器的 ip + port 即可，例如<code>172.17.0.1:1200</code>。</p><h2 id="miniflux-内容聚合"><a class="markdownIt-Anchor" href="#miniflux-内容聚合"></a> Miniflux 内容聚合</h2><p>Miniflux 是一个 RSS 订阅管理的服务端，可以方便地添加、管理各种 RSS 订阅，同时也可以作为一个网页版的 RSS 阅读器使用。此外，Miniflux 还提供了跨平台客户端软件订阅同步的服务。</p><h3 id="官方示例-2"><a class="markdownIt-Anchor" href="#官方示例-2"></a> 官方示例</h3><p>官方文档的基础示例如下</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">miniflux:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">miniflux/miniflux:latest</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;80:8080&quot;</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">db:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">RUN_MIGRATIONS=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">CREATE_ADMIN=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ADMIN_USERNAME=admin</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ADMIN_PASSWORD=test123</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:15</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_USER=miniflux</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=secret</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_DB=miniflux</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">miniflux-db:/var/lib/postgresql/data</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;pg_isready&quot;</span>, <span class="string">&quot;-U&quot;</span>, <span class="string">&quot;miniflux&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">start_period:</span> <span class="string">30s</span></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">miniflux-db:</span></span><br></pre></td></tr></table></figure></div><p>配置完成后，在对应目录下执行<code>docker-compose up -d</code>运行。</p><h3 id="修改版"><a class="markdownIt-Anchor" href="#修改版"></a> 修改版</h3><p>以下是笔者使用的参考自<a class="link"   href="https://blog.yfi.moe/post/rsshub-miniflux/" >完整的 RSS 解决方案：自建 RSSHub 与 Miniflux<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>一文的 docker-compose.yaml 配置，修改了映射端口和增加了一些可选配置，<strong>其中的默认用户名和密码由自己设定，最好在第一次登录后立即修改</strong>。</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.9&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">miniflux:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">miniflux/miniflux:latest</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">db:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">RUN_MIGRATIONS=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FETCH_YOUTUBE_WATCH_TIME=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">CREATE_ADMIN=1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ADMIN_USERNAME=admin</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">ADMIN_PASSWORD=test123</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">BASE_URL=http://127.0.0.1:8080</span> <span class="comment"># miniflux 的域名</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POLLING_FREQUENCY=60</span> <span class="comment"># 每个 feed 的刷新间隔</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POLLING_PARSING_ERROR_LIMIT=0</span> <span class="comment"># 拉取出错后不会停止拉去，还是会按计划继续拉</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">BATCH_SIZE=100</span> <span class="comment"># 每次拉取的 feed 数量</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POLLING_SCHEDULER=entry_frequency</span> <span class="comment"># 拉取类型，根据上周的平均更新周期来拉取</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL=30</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">PROXY_OPTION=all</span> <span class="comment"># 图片代理功能，Miniflux 先把源端的图片缓存到服务器上来，后续就不用客户端去源服务器拉了</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">PROXY_PRIVATE_KEY=password</span> <span class="comment"># 使用客户端缓存过文章，然后某个时间重启了 docker 实例，那么会导致图片无法显示的情况，这是因为每次重启会随机生成 PROXY_PRIVATE_KEY,所以可以增加参数 PROXY_PRIVATE_KEY 固化 key ，便于解决更新、重启等场景下的图片显示问题。</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DATABASE_MAX_CONNS=50</span> <span class="comment"># 增加数据库连接数，对于多图片的 feed 非常有效，可以大幅提升加载和访问速度</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">DATABASE_MIN_CONNS=5</span> <span class="comment"># 同上</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">WORKER_POOL_SIZE=10</span>  <span class="comment"># 默认，或适当加大</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:15</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_USER=miniflux</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=secret</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_DB=miniflux</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">miniflux-db:/var/lib/postgresql/data</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;pg_isready&quot;</span>, <span class="string">&quot;-U&quot;</span>, <span class="string">&quot;miniflux&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">start_period:</span> <span class="string">30s</span></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">miniflux-db:</span></span><br></pre></td></tr></table></figure></div><p>更多配置参数可以参考<a class="link"   href="https://miniflux.app/docs/configuration.html" >官方文档<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。启动完成之后可以访问<code>127.0.0.1:8080</code>或服务器 IP + 8080 端口，也可以使用反向代理服务配置一个域名访问。</p><p><strong>注意：如果后续需要在多个客户端使用 miniflux 的服务进行同步，则<code>BASE_URL</code>项一定要配置一个互联网可访问的地址。除非你所有的客户端与服务端都处于同一个局域网中，则可以配置一个局域网的地址。</strong></p><h3 id="基本操作"><a class="markdownIt-Anchor" href="#基本操作"></a> 基本操作</h3><p>登录之后，Miniflux 的默认页面是这样的</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240302171154106.png"                      alt="image-20240302171154106"                 ></p><p>先修改用户名和密码，在 Settings 中设置即可。</p><p>添加订阅源，例如 RSSHub 文档中提供了 Bing Daily Wallpaper 的路由为<code>/bing</code>，假设本地自建的 RSSHub 地址为<code>172.17.0.1:1200</code>，则订阅源为<code>http://172.17.0.1:1200/bing</code></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240302172042379.png"                      alt="image-20240302172042379"                 ></p><p>添加完成后就能在主页看到订阅信息</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240302172203629.png"                      alt="image-20240302172203629"                 ></p><p>这样就完成了基本的订阅，可以查看订阅信息。更多的操作可以自行探索。</p><h3 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h3><p>除了 Miniflux 以外还有<a class="link"   href="https://tt-rss.org/" >Tiny Tiny RSS (ttrss)<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>、<a class="link"   href="https://github.com/FreshRSS/FreshRSS" >FreshRSS<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>等 RSS 订阅管理服务端，Miniflux 是极简风格，如果想尝试更多增强配置，可以考虑使用其它的服务端。以下是一些相关的参考资料：</p><ul><li><p><a class="link"   href="https://blognas.hwb0307.com/linux/docker/788" >Docker系列 安装个人RSS服务TTRSS 手机完美适配<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li><li><p><a class="link"   href="https://blog.l3zc.com/2023/07/rsshub-freshrss-information-flow/" >重新捡起 RSS：RSSHub + FreshRSS 建立我的信息流<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li></ul><h2 id="wallabag-稍后阅读"><a class="markdownIt-Anchor" href="#wallabag-稍后阅读"></a> Wallabag 稍后阅读</h2><p>Wallabag 作为一个辅助工具可以和 Miniflux 集成使用，提供更好的使用体验。</p><h3 id="官方示例-3"><a class="markdownIt-Anchor" href="#官方示例-3"></a> 官方示例</h3><p>参考<a class="link"   href="https://github.com/wallabag/docker" >官方仓库<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的文档，一份示例的 docker-compose.yaml 配置如下，关于每个配置项的说明可以参考<a class="link"   href="https://hub.docker.com/r/wallabag/wallabag" >docker hub 仓库<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的文档。</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">wallabag:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wallabag/wallabag</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_ROOT_PASSWORD=wallaroot</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_DRIVER=pdo_mysql</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_HOST=db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PORT=3306</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_NAME=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_USER=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PASSWORD=wallapass</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_CHARSET=utf8mb4</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_TABLE_PREFIX=&quot;wallabag_&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__MAILER_DSN=smtp://127.0.0.1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__FROM_EMAIL=wallabag@example.com</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DOMAIN_NAME=https://your-wallabag-instance.wallabag.org</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__SERVER_NAME=&quot;Your</span> <span class="string">wallabag</span> <span class="string">instance&quot;</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;80&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/opt/wallabag/images:/var/www/wallabag/web/assets/images</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;wget&quot;</span> ,<span class="string">&quot;--no-verbose&quot;</span>, <span class="string">&quot;--tries=1&quot;</span>, <span class="string">&quot;--spider&quot;</span>, <span class="string">&quot;http://localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">1m</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mariadb</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_ROOT_PASSWORD=wallaroot</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/opt/wallabag/data:/var/lib/mysql</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span> ,<span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;redis-cli&quot;</span>, <span class="string">&quot;ping&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br></pre></td></tr></table></figure></div><p>配置完成后，在对应目录下执行<code>docker-compose up -d</code>运行。</p><h3 id="修改版-2"><a class="markdownIt-Anchor" href="#修改版-2"></a> 修改版</h3><p>以下是参考自<a class="link"   href="https://blognas.hwb0307.com/linux/docker/1833" >Docker系列 Wallabag助力个性化网页RSS化<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的配置</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">wallabag:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wallabag/wallabag</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_ROOT_PASSWORD=wallaroot</span> <span class="comment"># 自行设置密码</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_DRIVER=pdo_mysql</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_HOST=db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PORT=3306</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_NAME=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_USER=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PASSWORD=wallapass</span> <span class="comment"># 自行设置密码</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_CHARSET=utf8mb4</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_TABLE_PREFIX=&quot;wallabag_&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__MAILER_DSN=smtp://127.0.0.1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__FROM_EMAIL=example@example.com</span> <span class="comment"># 自行设置邮箱</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DOMAIN_NAME=http://127.0.0.1:8888</span> <span class="comment"># 自定义域名</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__SERVER_NAME=&quot;wallabag</span> <span class="string">service&quot;</span> <span class="comment"># 服务名称</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8888:80&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./images:/var/www/wallabag/web/assets/images</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;wget&quot;</span> ,<span class="string">&quot;--no-verbose&quot;</span>, <span class="string">&quot;--tries=1&quot;</span>, <span class="string">&quot;--spider&quot;</span>, <span class="string">&quot;http://localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">1m</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mariadb</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">MYSQL_ROOT_PASSWORD=wallaroot</span> <span class="comment"># 这里对应前面同名环境变量的密码</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/var/lib/mysql</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span> ,<span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;redis-cli&quot;</span>, <span class="string">&quot;ping&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br></pre></td></tr></table></figure></div><p>配置完成后，在对应目录下执行<code>docker-compose up -d</code>运行。启动完成之后可以访问<code>127.0.0.1:8888</code>或服务器 IP + 8888 端口，也可以使用反向代理服务配置一个域名访问。</p><p><strong>注意：wallabag 服务的启动需要一些时间，一开始可能还不能直接访问成功，建议等待几分钟后再尝试访问。</strong></p><h3 id="postgres-版本"><a class="markdownIt-Anchor" href="#postgres-版本"></a> postgres 版本</h3><p>这里额外提供一份以<code>postgresql</code>作为数据库的配置，用于替换<code>mariadb</code>以支持一些<code>mariadb</code>不支持的系统架构，例如<code>linux/arm/v7</code>架构。如果遇到架构不支持的问题，请尝试以下配置</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">wallabag:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">wallabag/wallabag</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=wallaroot</span> <span class="comment"># 自行设置密码  </span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_USER=wallabag_admin</span> <span class="comment"># 自行设置用户名</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_DRIVER=pdo_pgsql</span> <span class="comment"># 适用于 pgsql 的驱动</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_HOST=db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PORT=5432</span> <span class="comment"># pgsql 的默认端口</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_NAME=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_USER=wallabag</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_PASSWORD=wallapass</span> <span class="comment"># 自行设置密码</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DATABASE_TABLE_PREFIX=&quot;wallabag_&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__MAILER_DSN=smtp://127.0.0.1</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__FROM_EMAIL=example@example.com</span> <span class="comment"># 自行设置邮箱</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__DOMAIN_NAME=http://127.0.0.1:8888</span> <span class="comment"># 自定义域名</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">SYMFONY__ENV__SERVER_NAME=&quot;wallabag</span> <span class="string">service&quot;</span> <span class="comment"># 服务名称</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8888:80&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./images:/var/www/wallabag/web/assets/images</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;wget&quot;</span> ,<span class="string">&quot;--no-verbose&quot;</span>, <span class="string">&quot;--tries=1&quot;</span>, <span class="string">&quot;--spider&quot;</span>, <span class="string">&quot;http://localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">1m</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">db</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:9.6</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=wallaroot</span> <span class="comment"># 这里对应前面同名环境变量的密码  </span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_USER=wallabag_admin</span> <span class="comment"># 这里对应前面同名环境变量的用户名</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/var/lib/mysql</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span> ,<span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:alpine</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;redis-cli&quot;</span>, <span class="string">&quot;ping&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">20s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">3s</span></span><br></pre></td></tr></table></figure></div><h3 id="基础服务配置"><a class="markdownIt-Anchor" href="#基础服务配置"></a> 基础服务配置</h3><p>默认的用户名和密码都是<code>wallabag</code>（这个在 docker hub 文档中有说明）</p><p>登录之后，先去 Config 中修改 PASSWORD，修改完成后再进行 API 服务的配置。</p><h4 id="创建-api-服务"><a class="markdownIt-Anchor" href="#创建-api-服务"></a> 创建 API 服务</h4><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240302185752088.png"                      alt="image-20240302185752088"                 ></p><p>创建一个新的客户端，取一个名称，然后创建即可。</p><style>.dkfgugobwjpd{zoom: 50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/03/02/rss-workflow/image-20240302185856950.png"                      class="dkfgugobwjpd" alt="image-20240302185856950"                ><p>创建完客户端之后就会提供客户端 ID 和密钥，此时不要直接刷新网页避免重置 ID 和密钥。</p><h4 id="配置插件"><a class="markdownIt-Anchor" href="#配置插件"></a> 配置插件</h4><p>访问对应浏览器的插件商店，下载同名的 wallabag 插件进行配置，配置说明如下</p><style>.pqvnvsjvwhuf{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/03/02/rss-workflow/image-20240302191015666.png"                      class="pqvnvsjvwhuf" alt="image-20240302191015666"                ><p>填写完成后获取 token 即可使用。</p><h4 id="基础使用"><a class="markdownIt-Anchor" href="#基础使用"></a> 基础使用</h4><p>在任意的网页上右键，可以看到 wallabag 的选项，点击之后就会保存当前网页到 wallabage 后台。</p><blockquote><p>如果浏览器隐藏了插件图标，建议将 wallabag 插件图标显示在浏览器上，方便观察状态。</p><p>另外，有些网站无法使用 wallabag 保存，可能是采取了严格的反爬手段。</p></blockquote><h3 id="miniflux-集成-wallabag"><a class="markdownIt-Anchor" href="#miniflux-集成-wallabag"></a> Miniflux 集成 Wallabag</h3><p>在 Wallabag 后台 Config 的 FEEDS 里面进行设置</p><style>.sgbstlzxtjwt{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/03/02/rss-workflow/image-20240302192701866.png"                      class="sgbstlzxtjwt" alt="image-20240302192701866"                ><p>生成完 token 后，下面会提供 4 个订阅链接，初始的选项是未读、收藏、归档、全部，按需选择其中几个订阅导入 Miniflux 即可。后面的订阅包含最大项目数按需配置即可。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20240302192926038.png"                      alt="image-20240302192926038"                 ></p><p>配置完成后保存。</p><p>后续对于需要稍后阅读的网页，可以采取 wallabag 保存到后台并生成 RSS 订阅源，由 miniflux 同步 wallabag RSS 订阅源的方式来阅读网页信息。不需要在后台持续挂着网页，并且能很方便地跨平台同步。</p><h3 id="补充-2"><a class="markdownIt-Anchor" href="#补充-2"></a> 补充</h3><p>关于 miniflux 和 wallabag 的集成其实有两种方式：一种就是使用自建的 wallabag 服务和浏览器插件，然后将订阅源导入 miniflux (也就是前面介绍的方式)；另一种方式是在 miniflux 中直接配置 wallabag，这种配置主要用于实现 miniflux 的离线保存，不过这种方式可能使用得相对较少，大部分用户基本不需要考虑这种方式，简单了解即可。有离线阅读需求的用户可以考虑<a href="#%E5%8F%A6%E4%B8%80%E7%A7%8D%E6%96%B9%E6%A1%88">另一种方案</a>。</p><p>根据笔者的个人使用体验，wallabag 的导出功能对于某些格式存在乱码的问题，目前也没有解决方案，参考了网上的一些教程似乎也没有提到这一点，因此可以推断 wallabag 的导出功能几乎没什么使用需求。</p><blockquote><p>下面这部分内容感兴趣的可以看看，不感兴趣的可以直接转到<a href="#%E5%8F%A6%E4%B8%80%E7%A7%8D%E6%96%B9%E6%A1%88">另一种方案</a>。</p></blockquote><h4 id="使用-wallabag-服务和浏览器插件然后将订阅源导入-miniflux"><a class="markdownIt-Anchor" href="#使用-wallabag-服务和浏览器插件然后将订阅源导入-miniflux"></a> 使用 Wallabag 服务和浏览器插件，然后将订阅源导入 Miniflux</h4><p>这种方法涉及到在 wallabag 和 miniflux 之间手动创建一个连接，具体步骤如下：</p><ol><li><strong>wallabag 服务和浏览器插件</strong>：通过 wallabag 的浏览器插件，用户可以快速保存他们想要稍后阅读的网页到 wallabag 服务中。</li><li><strong>生成订阅源</strong>：wallabag 允许用户生成一个 RSS 订阅源，这个源包含了所有保存在 wallabag 中的文章。</li><li><strong>导入 miniflux</strong>：将 wallabag 生成的 RSS 订阅源导入到 miniflux 中，这样就可以在 miniflux 中阅读通过 wallabag 保存的文章了。</li></ol><p><strong>优点</strong>：这种方式允许用户在 miniflux 中阅读通过 wallabag 保存的文章，同时保持了两个应用的独立性。用户可以利用 wallabag 强大的保存和离线阅读功能，以及 miniflux 高效的 RSS 订阅管理能力。</p><p><strong>缺点</strong>：需要手动设置和维护两个应用之间的连接，这可能对一些用户来说稍显复杂。</p><h4 id="在-miniflux-中直接配置-wallabag"><a class="markdownIt-Anchor" href="#在-miniflux-中直接配置-wallabag"></a> 在 Miniflux 中直接配置 Wallabag</h4><p>miniflux 提供了一个内置的功能，允许用户直接在 miniflux 的设置中配置 wallabag，具体步骤如下：</p><ol><li><p><strong>配置 wallabag</strong>：在 miniflux 的设置中填写 wallabag 的服务地址和用户认证信息。</p><blockquote><p>这一步填写的信息其实和在浏览器插件配置时填写的信息基本一致。不过这里不需要使用浏览器插件。</p></blockquote></li><li><p><strong>一键保存</strong>：阅读 RSS 订阅时，用户可以通过一键操作将感兴趣的文章保存到 wallabag 中，无需离开 miniflux。</p></li></ol><p><strong>优点</strong>：这种方式提供了一个更为紧密的集成，使用户在使用 miniflux 阅读 RSS 订阅的同时，轻松地将文章保存到 wallabag。操作更为便捷，用户体验更佳。</p><p><strong>缺点</strong>：这种方式主要是将文章从 miniflux 保存到 wallabag，而不是在 miniflux 中阅读 wallabag 的内容。如果用户想要在 miniflux 中阅读通过 wallabag 保存的文章，这种方式不适用。</p><h4 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h4><p>选择哪种方式取决于你的具体需求：</p><ul><li>如果你希望在 miniflux 中阅读通过 wallabag 保存的文章，可以考虑第一种方法。</li><li>如果你主要使用 miniflux 阅读 RSS 订阅，并希望有一个快速方便的方式将文章保存到 wallabag 以供离线阅读，那么直接在 miniflux 中配置 wallabag 可能是更好的选择。</li></ul><p>当然其实也可以考虑同时使用这两种方式，但是需要明确需求以达成最佳的工具组合，让每一个工具专一地负责某一个具体需求，否则会有功能重叠以及增加不必要的管理工作量。以下是一些参考的思路和做法：</p><p>wallabag 的最主要的功能是可以<strong>快速地保存当前网页内容</strong>(如果网页不限制 wallabag )并且能在<strong>离线模式下阅读网页</strong>。因此，wallabag 可以负责以下几个需求：</p><ul><li>专门保存一些你感兴趣的但是并没有专门提供 RSS 订阅的网页或者是没有收录在 RSSHub 的小众网页，然后提供订阅源给 miniflux 使用。</li><li>导出你需要进一步阅读或者是需要供以后参考的内容。</li></ul><p>miniflux 的目的很简单，主要就是管理订阅源以及多平台同步。</p><p>关于 miniflux 中的订阅信息如果需要离线阅读，可以考虑以下几种方案：</p><ul><li>配置 miniflux 内置的 wallabag 集成，之后可以将感兴趣的订阅信息保存到 wallabag 中 (如果配置了 wallabag，会在订阅的位置显示“保存”的选项)。</li><li>使用本地客户端，本地客户端提供了一定的离线阅读功能。</li></ul><p>关于大量订阅信息内容的整理和分类，可以由 miniflux 集中管理，wallabag 负责快速生成一个“稍后阅读”的 RSS 源，然后在 miniflux 中对这个 RSS 源再进行具体的整理和分类。如果遇到希望离线保存的信息，则在 wallabag 中进行导出。</p><h4 id="另一种方案"><a class="markdownIt-Anchor" href="#另一种方案"></a> 另一种方案</h4><p>使用<a class="link"   href="https://github.com/Kenshin/simpread" >simpread<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>(一个浏览器插件)提供的网页导出功能来实现离线阅读的需求，不过这个插件本身需要一定学习成本，可以根据需求专门学习其中的几个功能。</p><p>这样就有一套明确的工作流：使用 RSSHub 生成一些知名网站的 RSS 源，借助 Wallabag 收藏一些没有 RSS 源提供的网页，两者组合提供较为丰富的 RSS 订阅源；使用 Miniflux 集中管理来自 RSSHub 和 Wallabag 的 RSS 订阅源，借助 simpread 插件保存 Miniflux 中的一些网页到本地以供离线阅读。</p><h2 id="fluent-reader-本地客户端"><a class="markdownIt-Anchor" href="#fluent-reader-本地客户端"></a> Fluent Reader 本地客户端</h2><p><a class="link"   href="https://github.com/yang991178/fluent-reader" >fluent-reader<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>是一个跨平台的 PC 客户端，可以使用 miniflux 的服务提供更好的客户端阅读体验。</p><p>对于移动设备，可以使用同样是由 fluent-reader 开发团队开发的<a class="link"   href="https://github.com/yang991178/fluent-reader-lite" >fluent-reader-lite<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><h3 id="miniflux-配置-api"><a class="markdownIt-Anchor" href="#miniflux-配置-api"></a> Miniflux 配置 API</h3><blockquote><p>先确保前面的<code>BASE_URL</code>项配置的是一个互联网可访问的地址，否则无法使用 miniflux 服务，除非 miniflux 和客户端处于同一个局域网中。</p></blockquote><p>在 miniflux 的 Settings 里面配置 API 密钥，在 API 密钥管理页面中已经包含了所有 fluent-reader 需要配置的信息。</p><h3 id="fluent-reader-启用-miniflux-服务"><a class="markdownIt-Anchor" href="#fluent-reader-启用-miniflux-服务"></a> Fluent Reader 启用 Miniflux 服务</h3><p>在 fluent-reader 的“设置”——”服务“中可以看到 miniflux 服务的选项，选择后会进入 miniflux 服务的配置，对照前面 miniflux 的 API 密钥管理页面中的信息，将配置项填写完后确认即可。</p><p>更新订阅源可能需要一定的时间，建议在使用 miniflux 服务更新订阅之前，先将 fluent-reader 中原来的订阅导出一份备份文件到本地。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://docs.rsshub.app/zh/install" >部署 | RSSHub<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://miniflux.app/" >Miniflux 官网<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://v2ex.com/t/963837" >2023 年，我为什么选择 Miniflux 作为 RSS 主力工具<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blog.yfi.moe/post/rsshub-miniflux/" >完整的 RSS 解决方案：自建 RSSHub 与 Miniflux<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blognas.hwb0307.com/linux/docker/1833" >Docker系列 Wallabag助力个性化网页RSS化<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blognas.hwb0307.com/linux/docker/788" >Docker系列 安装个人RSS服务TTRSS 手机完美适配<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://blog.l3zc.com/2023/07/rsshub-freshrss-information-flow/" >重新捡起 RSS：RSSHub + FreshRSS 建立我的信息流<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;记录搭建 Miniflux + RSSHub + Wallabag + Fluent Reader 组合构成的一个较为完整的聚合信息管理和阅读体系</summary>
    
    
    
    
    <category term="RSS" scheme="https://www.entropy-tree.top/tags/RSS/"/>
    
    <category term="miniflux" scheme="https://www.entropy-tree.top/tags/miniflux/"/>
    
    <category term="RSSHub" scheme="https://www.entropy-tree.top/tags/RSSHub/"/>
    
    <category term="Wallabag" scheme="https://www.entropy-tree.top/tags/Wallabag/"/>
    
    <category term="Fluent Reader" scheme="https://www.entropy-tree.top/tags/Fluent-Reader/"/>
    
  </entry>
  
  <entry>
    <title>Linux FireWall</title>
    <link href="https://www.entropy-tree.top/2024/02/27/linux-firewall/"/>
    <id>https://www.entropy-tree.top/2024/02/27/linux-firewall/</id>
    <published>2024-02-27T15:18:08.000Z</published>
    <updated>2024-03-04T05:15:12.930Z</updated>
    
    <content type="html"><![CDATA[<p>记录关于 Linux 防火墙的一些简单使用和说明<span id="more"></span></p><p>Linux  防火墙可能在很多时候对初学者部署网络服务造成了一定的“阻碍”。由于不清楚防火墙的重要性，许多用户选择直接关闭防火墙。对于一些大型云服务商来说，他们在防火墙之外还提供了安全组，能类似地起到防火墙的作用，使得 Linux 自带的防火墙似乎没有使用的必要性。但是对于 VPS 服务来说，他们是不提供安全组服务的，这意味着用户需要自己为服务器的安全负责，这时就很有必要学习 Linux 自带的防火墙技术——iptables。</p><h2 id="linux-防火墙"><a class="markdownIt-Anchor" href="#linux-防火墙"></a> Linux 防火墙</h2><p>以下内容大部分参考自<a class="link"   href="https://www.bilibili.com/video/BV1Jz4y1u7Lz/" >iptables核心运作原理和数据包过滤方法 | 技术蛋老师<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><h3 id="iptables-的由来"><a class="markdownIt-Anchor" href="#iptables-的由来"></a> iptables 的由来</h3><p>以常见的 linux 服务器为例，可以划分为<strong>硬件、内核空间、用户空间</strong>三部分。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="f825b099b593ad097431dba7b71d966e5cc3a9d1.png@690w_!web-note.webp"                      alt="img"                 ></p><p>流量的流动是先从网卡，再到 linux 内核，最后抵达到网络应用。</p><p><strong>过滤</strong>就是先让流量从网卡进来，然后在 linux 内核里进行过滤处理，最后进入网络应用。这样网络应用前面就有一道防火墙，防火墙就位于 linux 内核的实现中。</p><p>在 linux 内核中存在一个叫 <strong>netfilter</strong> 的框架，一旦有流量进来，就会触发 netfilter的机制对流量进行过滤。</p><p>为了方便操作 netfilter 框架， iptables 工具应运而生，iptables 相当于 netfilter 的接口。使用 iptables 命令就可以执行 netfilter 的复杂流量过滤逻辑。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="bdc4747c99a88429a6b10cc47f5a2d2d34985993.png@690w_!web-note.webp"                      alt="img"                 ></p><p>不过 iptables 对于初学者来说还是具有一定的学习难度。</p><h3 id="tables-表格"><a class="markdownIt-Anchor" href="#tables-表格"></a> tables 表格</h3><p>iptables 具有多个 table (表格)，即 filter、nat、mangle、raw、security，常见的是前 4 个表格，最常用的则是 <strong>filter</strong> 和 <strong>nat</strong></p><p>这两个表中，filter 负责过滤，nat 负责网络地址转换。nat 表可以支持负载均衡等强大功能。不过，绝大多数用户可能只需要关心 filter 表。</p><h3 id="chains-链路"><a class="markdownIt-Anchor" href="#chains-链路"></a> chains 链路</h3><p>数据包抵达网络应用程序，最简单的链路就是一进一出。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="dd704f8f01fdde09c64efda5937a02110f980c8c.png@690w_!web-note.webp"                      alt="img"                 ></p><p>但实际上 linux 系统本身也可以作为路由器存在，有些数据包会经过这台 linux 主机，并且被转发到其它设备，这样就会多一条转发链路，加上前面的一进一出链路，就可能发生很多情况。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="346ad17db7da14cb8b89af5117b0d9438cd2366c.png@690w_!web-note.webp"                      alt="img"                 ></p><p>不过处于内核的 netfilter 已经对链路的各个部分进行了管理。</p><p>当数据包一抵达到系统时，就由 <strong>PREROUTING</strong> 链负责。</p><p>如果数据包的目标是本机，就由 <strong>INPUT</strong> 链负责。</p><p>数据包如果从本机出去，就由 <strong>OUTPUT</strong> 链负责。</p><p>数据包如果不是要到本机，只是经过本机路由，就由 <strong>FORWARD</strong> 链负责。</p><p>数据包如果要离开本机，或者路由后，还有个 <strong>POSTROUTING</strong> 链负责。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="e71c211f7afef61efca10acde5a35cb042b5ecc0.png@690w_!web-note.webp"                      alt="img"                 ></p><p>单从过滤数据包的使用需求上，用户不需要了解所有的链。如果使用 filter 表，只需要管理 INPUT、OUTPUT 和 FORWARD 链。</p><p>filter 链路简单来说就是<strong>数据包的目标是本机可以过滤、数据包的转发可以过滤、数据包的目标是主机以外的其它地址可以过滤。</strong></p><p><strong>具体实现过滤就是在对应的链里加入规则。</strong></p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="0f693be352f258e9c231179c3463db57c03cbdc0.png@690w_!web-note.webp"                      alt="img"                 ></p><p>iptables 含有不同的表，表里面有着不同的链，链里面可以制定不同的规则。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="1b74a15809579860fd87b760eec1f502ce4cda5f.png@690w_!web-note.webp"                      alt="img"                 ></p><h3 id="命令使用"><a class="markdownIt-Anchor" href="#命令使用"></a> 命令使用</h3><p>使用 iptables 命令通常需要 root 权限，测试时为了方便可以切换到 root 操作。</p><h4 id="查看表内容"><a class="markdownIt-Anchor" href="#查看表内容"></a> 查看表内容</h4><p><strong>iptables --table filter --list</strong> 这个命令会列出 filter 表里面的内容，不使用 --table 指定任何表，则默认选用 filter 表。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="0056ade209dfeb03992da54355f3f9e18263b55f.png@690w_!web-note.webp"                      alt="img"                 ></p><p>可以看到这里的三条链，这里初始没有设置任何规则。使用另一台处于同一局域网内的主机可以 ping 通。</p><p>接下来，使用 iptables 阻止另一台主机直接访问。</p><h4 id="设置规则"><a class="markdownIt-Anchor" href="#设置规则"></a> 设置规则</h4><p>先指定表格，再指定链，然后设置具体的规则</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">iptables \</span><br><span class="line">--table filter \</span><br><span class="line">--append INPUT \</span><br><span class="line">--<span class="built_in">source</span> 192.168.3.20 \</span><br><span class="line">--jump REJECT</span><br></pre></td></tr></table></figure></div><p>上面的命令在 filter 表的 INPUT 链中添加了规则：所有来自 192.168.3.20 (同一局域网的另一台主机 IP) 源地址的数据包，会被 INPUT 链拒绝处理。</p><p>重新查看 filter 表的信息如下</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="159d1000070c56b00209091c0c9d9b006cbc7d54.png@690w_!web-note.webp"                      alt="img"                 ></p><p>这里还需要注意 prot (protocol 协议)，因为没有指定具体协议，所以默认为 all。</p><p>此时另一台主机应该无法顺利 ping 通，当本机去 ping 该主机也会收不到来自该主机的响应。</p><p>除了使用 <strong>REJECT</strong> 拒绝处理，还可以使用 <strong>DROP</strong> 进行丢弃。</p><p>可以在上面命令的基础上将 REJECT 替换为 DROP 执行。不过需要注意此时 INPUT 链中会先后包含 REJECT 和 DROP 两条规则，并且会根据先后顺序依次执行，但是 REJECT 规则会直接拒绝掉，导致 DORP 规则无法执行。需要先删除 REJECT 规则才能让 DROP 顺利执行。</p><p>查看表内容时可以加上 <strong>–line-numbers 参数</strong>方便查看规则</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="7df3b0f7b10e163649bb89a77c6e3de0470202f7.png@690w_!web-note.webp"                      alt="img"                 ></p><h4 id="删除规则"><a class="markdownIt-Anchor" href="#删除规则"></a> 删除规则</h4><p><strong>iptables --table filter --delete INPUT 1</strong></p><p>上面的命令会删除 INPUT 链的第一条规则，即 REJECT 规则。删除之后，原来的 DROP 会成为新的第一条规则</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="e2fa90c7f20c58be5196acdc1f3d1edc34a94976.png@690w_!web-note.webp"                      alt="img"                 ></p><p>因此，使用序号连续删除规则的时候需要注意。</p><p>重新 ping 会出现另外的失败提示。</p><h4 id="批量屏蔽-ip-地址"><a class="markdownIt-Anchor" href="#批量屏蔽-ip-地址"></a> 批量屏蔽 IP 地址</h4><p>可以在 <a class="link"   href="https://ipdeny.com" >https://ipdeny.com<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 上找到对应的地址段，使用 wget 下载对应的文件，查看文件内容，有大量的 IP 段。这里使用命令手动添加和普通的脚本添加都不是最推荐的方式。</p><p>使用 ipset 指令就可以将大量的 IP 段单独汇集为一个集合，生成唯一 的哈希值。</p><p>当为 iptables 添加 IP 地址时，只需要使用该哈希值来代表背后的 IP 地址段集合。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ipset create asan <span class="built_in">hash</span>:net</span><br></pre></td></tr></table></figure></div><p>上面的命令会创建一个名为 asan 的集合，使用 net 表示网络号，而非主机号。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ipset list asan</span><br></pre></td></tr></table></figure></div><p>上面的命令会列出 asan 集合的信息，此时集合里面还没有添加 IP 地址段。</p><p>接下来是将 IP 地址段添加到集合中，使用 iptables 和 ipset 编写脚本。</p><p>首先，创建一个名为 <a class="link"   href="http://block-india.sh" >block-india.sh<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 的文件，然后在脚本里填入以下内容</p><div class="code-container" data-rel="Shell"><figure class="iseeu highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/bash</span></span><br><span class="line"></span><br><span class="line">ipset create asan hash:net -exist # -exist 避免重复创建导致失败</span><br><span class="line">ipset flush asan # 清除旧地址</span><br><span class="line">[ -f &quot;in-aggregated.zone&quot; ] &amp;&amp; rm in-aggregated.zone # 删除旧文件</span><br><span class="line"></span><br><span class="line">wget .... # 下载 url 对应的文件</span><br><span class="line"></span><br><span class="line">if [ $? -eq 0 ] # 判断是否下载成功</span><br><span class="line">then</span><br><span class="line">echo &quot;********** Downloaded **************&quot;</span><br><span class="line">for address in `cat in-aggregatd.zone`</span><br><span class="line">do</span><br><span class="line">ipset add asan $address # 循环添加到 asan 集合中</span><br><span class="line">done</span><br><span class="line">else</span><br><span class="line">echo &quot;********** Failed to download **************&quot;</span><br><span class="line">fi</span><br><span class="line"></span><br><span class="line">iptables \</span><br><span class="line">--table filter \</span><br><span class="line">--append INPUT \</span><br><span class="line">--match set --match-set asan src \</span><br><span class="line">--jump DROP</span><br></pre></td></tr></table></figure></div><p>脚本编写完成，赋予文件可执行权限。</p><p>在执行之前先 ping 一下集合中的一个 IP 确认没有问题。</p><p>执行完脚本后，可以执行 <strong>ipset list asan | less</strong> 或结合 <strong>grep</strong> 查看是否添加成功。</p><p>重新 ping 之前测试用的 IP 发现 ping 不通，表示设置成功。</p><h4 id="持久化规则"><a class="markdownIt-Anchor" href="#持久化规则"></a> 持久化规则</h4><p>重启服务器后一般会自动清空 iptables 和 ipset 的设置。可以使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ipset save &gt; /etc/rules.ipset</span><br><span class="line">iptables-save &gt; /etc/rules.iptables</span><br></pre></td></tr></table></figure></div><p>对于 ipset 可以使用 <strong>ipset save &gt; /etc/rules.ipset</strong> 将规则保存到 rules.ipset 文件。</p><p>对于 iptables 可以使用 <strong>iptables-save &gt; /etc/rules.iptables</strong> 将规则保存到 rules.iptables 文件。</p><p>重启服务器，使用以下命令分别恢复设置</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ipset restore &lt; /etc/rules.ipset</span><br><span class="line">iptables-restore &lt; /etc/rules.iptables</span><br></pre></td></tr></table></figure></div><p>如果在恢复设置之前还添加了新的设置，不想在恢复时被覆盖掉，可以<strong>在 iptables-restore 后面加上 --noflush 或者 -n，在 ipset restore 后加上 -exist。</strong></p><blockquote><p>其它永久保存规则和集合的方法：deb 系提供了 iptables-persistent 包，rehl 系提供了iptables-services 包。</p></blockquote><p>iptables 在 Linux 服务器发展中有着很重要的地位。到了现代，即使已经出现了 nftables (iptables 的现代化替代)，还是有大量的服务器依旧使用 iptables 管理防火墙。</p><p>以下是一些补充资料的整理说明，简单地介绍为了简化 Linux 防火墙管理的一些工具。这些工具基本都是基于 iptables/nftables 实现的，本质上是对底层的 iptables/nftables 的抽象化实现，使得非专业用户也能很好地管理防火墙，满足日常需求。</p><h2 id="防火墙简化管理"><a class="markdownIt-Anchor" href="#防火墙简化管理"></a> 防火墙简化管理</h2><p><code>ufw</code>、<code>firewalld</code>、<code>iptables</code>和<code>nftables</code>都是Linux防火墙工具，它们可以独立使用，也有一些底层联系。</p><ul><li><strong>ufw</strong>：全称 Uncomplicated Firewall，是 Ubuntu 提供的一个 iptables 的接口，因为原生的 iptables 配置麻烦，学习成本较高。ufw 支持 IPv4 和 IPv6，最新版的 UFW 默认启用了 IPv6 配置。</li><li><strong>firewalld</strong>：Centos7 及以上的发行版自带了 firewalld 防火墙，firewalld 带有 iptables 防火墙。其原因是 iptables 的防火墙策略是交由内核层面的 netfilter 网络过滤器来处理的，而 firewalld 则是交由内核层面的 nftables 包过滤框架来处理。相较于 iptables 防火墙而言，firewalld 支持动态更新技术并加入了区域（zone）的概念。</li><li><strong>iptables</strong>：iptables 是最底层、最古老的防火墙系统，所有系统都会存在此防火墙，但一般而言使用其它上层管理工具的时候，只需保证该防火墙处于完全开放状态即可。</li><li><strong>nftables</strong>：nftables 是用于替换 iptables 的工具。</li></ul><blockquote><p>一些历史发展：iptables 是最早的防火墙工具，后来出现了 ufw 和 firewalld，这两个工具都是对 iptables 进行了封装，使得管理员在配置防火墙时可以通过执行相对简单，更容易理解的命令配置防火墙，而不是通过执行复杂难懂的 iptables 命令。最后，nftables 出现，作为 iptables 的替代品。</p></blockquote><h3 id="iptables-迁移到-nftables"><a class="markdownIt-Anchor" href="#iptables-迁移到-nftables"></a> iptables 迁移到 nftables</h3><p>可以按照以下步骤从 iptables 过渡到 nftables</p><ol><li><p><strong>停用 iptables</strong>：首先，需要停用 iptables 相关服务。</p><p>可以使用以下命令来停用 iptables、ip6tables</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl stop iptables</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">disable</span> iptables</span><br><span class="line"><span class="built_in">sudo</span> systemctl stop ip6tables</span><br><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">disable</span> ip6tables</span><br></pre></td></tr></table></figure></div><p>这些命令会停止 iptables 服务，并禁止它在系统启动时自动启动。</p></li><li><p><strong>设置 iptables</strong>：然后，选择将其配置为允许所有流量，这样它就不会干扰 nftables 的操作。</p><p>要让 iptables 允许所有流量，可以使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> iptables -P INPUT ACCEPT</span><br><span class="line"><span class="built_in">sudo</span> iptables -P FORWARD ACCEPT</span><br><span class="line"><span class="built_in">sudo</span> iptables -P OUTPUT ACCEPT</span><br><span class="line"><span class="built_in">sudo</span> iptables -F</span><br></pre></td></tr></table></figure></div><blockquote><p>这一步看起来似乎是多余的，但这实际上是一个谨慎的做法，旨在确保在迁移到 <code>nftables</code> 的过程中网络流量不会受到意外的中断或干扰。</p></blockquote></li><li><p><strong>安装并启用 nftables</strong>：最后，安装并启用 nftables。</p><p>可以使用以下命令来安装 nftables</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install nftables</span><br></pre></td></tr></table></figure></div><p>使用以下命令来启用 nftables 服务</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> nftables</span><br><span class="line"><span class="built_in">sudo</span> systemctl start nftables</span><br></pre></td></tr></table></figure></div><p>这些命令会启动nftables服务，并设置它在系统启动时自动启动。</p></li><li><p><strong>验证 nftables 规则</strong></p><p>查看当前的规则集来验证 <code>nftables</code> 的配置是否按预期工作。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nft list ruleset</span><br></pre></td></tr></table></figure></div></li><li><p><strong>规则集转换</strong></p><p>redhat 官方推出以下参考的资料，用于将 iptabls 的规则集转换为 nftables 的规则集。</p><p><a class="link"   href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/getting-started-with-nftables_configuring-and-managing-networking" >配置和管理网络——nftables 入门 | Red Hat<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/8/html/securing_networks/getting-started-with-nftables_securing-networks" >安全网络——nftables 入门 | Red Hat<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li></ol><p>注意：这些步骤通常都需要root权限才能执行。在执行这些步骤时，请确保已经备份了系统和数据，以防万一。</p><h3 id="firewalld-使用"><a class="markdownIt-Anchor" href="#firewalld-使用"></a> firewalld 使用</h3><blockquote><p><code>firewalld</code>支持<code>nftables</code>后端。</p></blockquote><p><code>firewalld</code> 是一个动态管理防火墙规则的守护进程，使用 <code>zones</code> 和 <code>services</code> 的概念来简化防火墙管理。它提供了更直观的方式来组织规则和管理网络流量的访问权限。以下是 <code>firewalld</code> 的一些基本用法：</p><h4 id="安装和启动-firewalld"><a class="markdownIt-Anchor" href="#安装和启动-firewalld"></a> 安装和启动 firewalld</h4><p>在大多数基于 RPM 的发行版（如 Fedora、CentOS、RHEL）上，<code>firewalld</code> 默认已安装。如果没有，可以使用包管理器进行安装：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> yum install firewalld    <span class="comment"># CentOS/RHEL</span></span><br><span class="line"><span class="built_in">sudo</span> dnf install firewalld    <span class="comment"># Fedora</span></span><br><span class="line"><span class="built_in">sudo</span> apt install firewalld    <span class="comment"># Ubuntu/Debian</span></span><br></pre></td></tr></table></figure></div><h4 id="基本管理"><a class="markdownIt-Anchor" href="#基本管理"></a> 基本管理</h4><p><strong>启动和停止 firewalld</strong>：</p><ul><li><p>启动</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl start firewalld</span><br></pre></td></tr></table></figure></div></li><li><p>停止</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl stop firewalld</span><br></pre></td></tr></table></figure></div></li><li><p>使 firewalld 服务在系统启动时自动启动</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> firewalld</span><br></pre></td></tr></table></figure></div></li><li><p>禁用自动启动</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">disable</span> firewalld</span><br></pre></td></tr></table></figure></div></li></ul><p><strong>重载 firewalld</strong>：不中断当前连接的情况下，应用更改。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --reload</span><br></pre></td></tr></table></figure></div><p><strong>查看 firewalld 状态</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --state</span><br></pre></td></tr></table></figure></div><h4 id="区域管理"><a class="markdownIt-Anchor" href="#区域管理"></a> 区域管理</h4><p><code>firewalld</code> 使用 zones 来定义网络的安全级别。每个 zone 都有一套规则，用于决定哪些流量可以进入。默认 zone 通常是 <code>public</code>。</p><ul><li><p>查看所有可用的 zones：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --get-zones</span><br></pre></td></tr></table></figure></div><ul><li>这个命令用于列出 <code>firewalld</code> 所知的所有可用防火墙区域。</li><li>这些区域包括预定义的区域和用户自定义的区域。</li><li>输出只是区域的名称列表，不包括每个区域的具体规则或配置详情。</li><li>这个命令非常有用，当你想要快速查看所有可用的防火墙区域时，比如在创建新规则或者修改现有规则时确定要使用哪个区域。</li></ul></li><li><p>详细查看所有的 zones 和配置：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --list-all-zones</span><br></pre></td></tr></table></figure></div><ul><li>这个命令用于详细列出所有防火墙区域及其当前配置，包括每个区域的规则、服务、接口、源地址等。</li><li>输出会显示每个区域的完整配置，这对于审查当前防火墙的配置和确定各个区域的作用非常有帮助。</li><li>因为输出可能会很长（取决于有多少区域以及它们的配置如何），所以通常在需要详细了解防火墙设置时使用这个命令。</li></ul></li><li><p>查看默认的 zones：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --get-default-zone</span><br></pre></td></tr></table></figure></div><ul><li>这个命令用于查询当前的默认防火墙区域。</li><li>默认区域是新加入网络接口或源未被指定为其他区域的情况下所使用的区域。</li><li>它是系统上所有未明确分配到其他区域的网络流量的默认处理区域。</li></ul></li><li><p>查看当前接口所属的 zone：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --get-active-zones</span><br></pre></td></tr></table></figure></div><ul><li>这个命令用于列出当前所有活跃的防火墙区域及其关联的网络接口和源地址。</li><li>活跃区域是指当前已经有网络接口或源地址被分配的区域。这表示这些区域正在被用于管理进出这些接口或源地址的网络流量。</li><li>这个命令提供了一个快照，展示了哪些区域是配置中的，以及它们是如何关联到特定的网络接口或源地址的。</li></ul></li><li><p>设置默认的 zones：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">firewall-cmd --set-default-zone=[zonename]</span><br></pre></td></tr></table></figure></div></li></ul><p><strong>修改区域配置</strong>：临时（直到下次重启或重载）或永久（持久化存储）修改防火墙规则</p><ul><li><p>临时添加服务：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=&lt;zone&gt; --add-service=&lt;service&gt;</span><br></pre></td></tr></table></figure></div></li><li><p>永久添加服务：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --permanent --zone=&lt;zone&gt; --add-service=&lt;service&gt;</span><br></pre></td></tr></table></figure></div><p>还可以使用以下命令对当前修改过的规则即时保存为永久配置，不过是截止到这条命令执行的时间点之前，后续添加新的规则不会受影响。</p> <div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> fiewall-cmd --runtime-to-permanent</span><br></pre></td></tr></table></figure></div><p>该命令可以在确定当前的运行时配置满足需求后，快速将其保存为永久配置，避免了手动重新添加规则的麻烦。</p><p>对于任何新的规则更改，如果希望它们即刻生效并持久化，应该继续使用 <code>--permanent</code> 参数并在需要时使用 <code>--reload</code> 命令来应用更改。</p></li></ul><h4 id="规则管理"><a class="markdownIt-Anchor" href="#规则管理"></a> 规则管理</h4><p><strong>允许/禁止服务</strong>：启用或禁用特定的服务。</p><ul><li><p>允许服务：<code>sudo firewall-cmd --zone=&lt;zone&gt; --add-service=&lt;service&gt;</code></p></li><li><p>禁止服务：<code>sudo firewall-cmd --zone=&lt;zone&gt; --remove-service=&lt;service&gt;</code></p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --add-service=http</span><br><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --remove-service=http</span><br></pre></td></tr></table></figure></div><p>添加 <code>--permanent</code> 参数以使更改永久。</p></li></ul><p><strong>允许/禁止端口</strong>：开放或关闭特定的端口。</p><ul><li><p>允许端口：<code>sudo firewall-cmd --zone=&lt;zone&gt; --add-port=&lt;port&gt;/&lt;protocol&gt;</code></p></li><li><p>禁止端口：<code>sudo firewall-cmd --zone=&lt;zone&gt; --remove-port=&lt;port&gt;/&lt;protocol&gt;</code></p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --add-port=8080/tcp</span><br><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --remove-port=8080/tcp</span><br></pre></td></tr></table></figure></div><p>同样，添加 <code>--permanent</code> 参数以使更改永久。</p></li></ul><p><strong>查看配置</strong></p><ol><li><p><strong>列出区域中的服务和端口</strong>：查看区域中当前允许的服务和端口。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=&lt;zone&gt; --list-services</span><br><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=&lt;zone&gt; --list-ports</span><br></pre></td></tr></table></figure></div></li><li><p><strong>列出所有配置</strong>：查看特定区域或所有区域的详细配置。</p><p>列出特定区域的所有配置：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=&lt;zone&gt; --list-all</span><br></pre></td></tr></table></figure></div><p>列出所有区域的配置：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --list-all-zones</span><br></pre></td></tr></table></figure></div></li></ol><p><strong>高级功能</strong></p><p>直接规则：直接操作 iptables/nftables 规则。</p><ul><li><p>添加直接规则：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport 80 -j ACCEPT</span><br></pre></td></tr></table></figure></div></li><li><p>删除直接规则：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport 80 -j ACCEPT</span><br></pre></td></tr></table></figure></div></li></ul><h4 id="重新加载配置"><a class="markdownIt-Anchor" href="#重新加载配置"></a> 重新加载配置</h4><p>更改 <code>firewalld</code> 的配置后，尤其是对于使用<code>--permanent</code>参数的配置，需要显式地重新加载规则，以便更改生效：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --reload</span><br></pre></td></tr></table></figure></div><p>使用 <code>--list-all</code> 参数定期检查区域配置，确保规则按预期工作。</p><h4 id="查看和添加服务"><a class="markdownIt-Anchor" href="#查看和添加服务"></a> 查看和添加服务</h4><p><code>firewalld</code> 使用服务（services）来简化管理。服务是预定义的端口和协议的组合，这使得开放对特定类型网络服务的访问变得简单。每个服务都有一个与之关联的配置文件，定义了所需的端口和协议。</p><p>查看所有预定义的服务(这里不指定<code>zone</code>会使用默认的设置，如<code>public</code>)：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --get-services</span><br></pre></td></tr></table></figure></div><p>查看特定服务的详细信息：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --info-service=http</span><br></pre></td></tr></table></figure></div><p>添加自定义服务：</p><ol><li><p>创建一个 XML 文件定义服务。例如，为自定义应用<code>myapp</code>创建<code>/etc/firewalld/services/myapp.xml</code>：</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">service</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">short</span>&gt;</span>MyApp<span class="tag">&lt;/<span class="name">short</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">description</span>&gt;</span>This is my custom application.<span class="tag">&lt;/<span class="name">description</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">port</span> <span class="attr">protocol</span>=<span class="string">&quot;tcp&quot;</span> <span class="attr">port</span>=<span class="string">&quot;8080&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">service</span>&gt;</span></span><br></pre></td></tr></table></figure></div></li><li><p>重新加载<code>firewalld</code>以识别新服务：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --reload</span><br></pre></td></tr></table></figure></div></li><li><p>将新服务添加到防火墙规则中：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --add-service=myapp</span><br></pre></td></tr></table></figure></div><p>添加<code>--permanent</code>参数以使更改永久</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --permanent --zone=public --add-service=myapp</span><br></pre></td></tr></table></figure></div><p>当使用<code>--permanent</code>参数时，规则会被保存在配置文件中，即使<code>firewalld</code>服务重启也会保留这些规则。不使用<code>--permanent</code>参数的更改仅在当前会话中有效，重启<code>firewalld</code>服务后会丢失。</p></li></ol><h4 id="富规则rich-rules"><a class="markdownIt-Anchor" href="#富规则rich-rules"></a> 富规则（Rich Rules）</h4><p><code>firewalld</code> 的富规则（Rich Rules）提供了更复杂的规则定义方式，允许创建包含源地址、端口、协议等多种条件的细致规则。</p><ul><li><p>添加富规则</p><p>例如，拒绝来自特定 IP 地址的所有访问：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --zone=public --add-rich-rule=<span class="string">&#x27;rule family=&quot;ipv4&quot; source address=&quot;192.168.1.100&quot; drop&#x27;</span> --permanent</span><br></pre></td></tr></table></figure></div></li></ul><h4 id="恐慌模式"><a class="markdownIt-Anchor" href="#恐慌模式"></a> 恐慌模式</h4><p><code>firewalld</code> 提供了一个非常有用的功能，即“恐慌模式”（panic mode），它允许管理员在遇到网络安全威胁时，快速地切断所有外部网络通信，有效防止潜在的攻击。这个功能对于紧急情况下保护系统免受进一步损害非常有帮助。</p><ol><li><p><strong>启用恐慌模式</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --panic-on</span><br></pre></td></tr></table></figure></div><p>启用恐慌模式会立即阻断所有进出的网络流量，这相当于一个紧急的“断网”开关。</p></li><li><p><strong>禁用恐慌模式</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --panic-off</span><br></pre></td></tr></table></figure></div><p>当安全威胁已经被处理，或者确定网络可以安全恢复时，可以使用此命令禁用恐慌模式，恢复正常的网络通信。</p></li><li><p><strong>查询恐慌模式状态</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --query-panic</span><br></pre></td></tr></table></figure></div><p>这个命令可以让你查询当前防火墙是否处于恐慌模式。这对于确认防火墙的当前状态非常有用。</p></li></ol><p><strong>注意事项</strong></p><ul><li><strong>恐慌模式的使用场景</strong>：只有在确信系统正遭受攻击，且需要立即切断所有网络连接以保护系统和数据安全时，才应该启用恐慌模式。这是一个极端措施，会影响到所有网络服务和通信。</li><li><strong>恢复网络通信</strong>：在禁用恐慌模式后，系统的网络通信会恢复正常。但是，根据攻击的性质和系统的配置，可能需要进一步的检查和修复工作，以确保系统的安全性。</li><li><strong>规划应急响应</strong>：将恐慌模式作为应急响应计划的一部分，确保相关人员了解如何正确使用这一功能，并了解启用和禁用恐慌模式后的后续步骤。</li></ul><p>恐慌模式是 <code>firewalld</code> 提供的一个强大工具，能够在遭受攻击时提供即时的保护措施。然而，它应该谨慎使用，并伴随着全面的安全策略和事后分析。</p><h4 id="锁闭机制"><a class="markdownIt-Anchor" href="#锁闭机制"></a> 锁闭机制</h4><p><code>firewalld</code> 提供了一个锁闭机制（lockdown mode），这是一种安全特性，用于限制本地程序比如 KVM 虚拟机组件或一些代理服务组件对防火墙配置的修改。当启用锁闭模式时，通常只有 root 用户才能修改防火墙规则。</p><ol><li><p><strong>查询防火墙的锁闭状态</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --query-lockdown</span><br></pre></td></tr></table></figure></div><p>这个命令会返回当前防火墙的锁闭状态，如果启用了锁闭模式，它会返回 <code>yes</code>，否则返回 <code>no</code>。</p></li><li><p><strong>启用锁闭模式</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --lockdown-on</span><br></pre></td></tr></table></figure></div><p>执行这个命令后，防火墙将进入锁闭模式，限制未授权的防火墙配置更改。</p></li><li><p><strong>禁用锁闭模式</strong>：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> firewall-cmd --lockdown-off</span><br></pre></td></tr></table></figure></div><p>使用这个命令可以禁用锁闭模式，恢复到允许更改防火墙配置的状态。</p></li></ol><p><strong>注意事项</strong></p><ul><li><strong>使用场景</strong>：锁闭模式特别适用于生产环境，或者任何对安全性要求较高的环境，它可以帮助确保防火墙配置不会被未授权的更改所破坏。</li><li><strong>权限</strong>：这些操作需要 root 用户权限，这意味着只有系统管理员和有适当权限的用户可以启用或禁用锁闭模式。</li><li><strong>影响</strong>：启用锁闭模式可能会影响那些需要修改防火墙规则的合法程序和服务，因此在启用之前需要仔细考虑。</li></ul><p><code>firewalld</code> 的锁闭机制提供了一个额外的安全层，帮助防止未授权的防火墙配置更改。管理员可以根据需要启用或禁用锁闭模式，以适应不同的安全需求和操作环境。在启用锁闭模式时，确保考虑到对合法服务和应用程序的潜在影响。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><p><a class="link"   href="https://www.bilibili.com/video/BV1Jz4y1u7Lz/" >iptables核心运作原理和数据包过滤方法 | 技术蛋老师<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.nftables.org/wiki-nftables/index.php/Main_Page" >nftables wiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://farkasity.gitbooks.io/nftables-howto-zh/content/" >Nftables HOWTO 中文翻译<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/getting-started-with-nftables_configuring-and-managing-networking" >配置和管理网络——nftables 入门 | Red Hat<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://access.redhat.com/documentation/zh-cn/red_hat_enterprise_linux/8/html/securing_networks/getting-started-with-nftables_securing-networks" >安全网络——nftables 入门 | Red Hat<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://firewalld.org/documentation/" >Documentation | firewalld.org<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhuanlan.zhihu.com/p/141473859" >FirewallD 防火墙使用概述 | Linux 中国<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;记录关于 Linux 防火墙的一些简单使用和说明</summary>
    
    
    
    <category term="linux" scheme="https://www.entropy-tree.top/categories/linux/"/>
    
    
    <category term="linux" scheme="https://www.entropy-tree.top/tags/linux/"/>
    
    <category term="firewall" scheme="https://www.entropy-tree.top/tags/firewall/"/>
    
    <category term="iptables" scheme="https://www.entropy-tree.top/tags/iptables/"/>
    
  </entry>
  
  <entry>
    <title>Monorepo 规范实践</title>
    <link href="https://www.entropy-tree.top/2024/02/20/monorepo-setup-guide/"/>
    <id>https://www.entropy-tree.top/2024/02/20/monorepo-setup-guide/</id>
    <published>2024-02-20T09:31:06.000Z</published>
    <updated>2024-03-11T04:03:54.581Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要记录关于 Monorepo 的项目规范实践，借助 pnpm 包管理器和 NodeJs 环境等，通过各种配置实现对项目的自动化或半自动化规范。<span id="more"></span></p><p>更新日志 2024-03-11：</p><ul><li><p><strong>新增</strong>：无</p></li><li><p><strong>改进</strong>：husky 新版本<code>V9</code>使用方式变更，查看<a class="link"   href="https://github.com/typicode/husky/releases/tag/v9.0.1" >changelog<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li><li><p><strong>修复</strong>：commitlint 在新版 husky 下的使用，参考<a class="link"   href="https://commitlint.js.org/guides/local-setup" >Guide: Local setup | commitlint.js.org <i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li></ul><blockquote><p>可以在当前页全文搜索<code>新版 husky </code>关键词查看博客变更内容</p></blockquote><h2 id="基础工具"><a class="markdownIt-Anchor" href="#基础工具"></a> 基础工具</h2><h3 id="nodejs"><a class="markdownIt-Anchor" href="#nodejs"></a> NodeJs</h3><p>安装可参考<a class="link"   href="https://www.runoob.com/nodejs/nodejs-install-setup.html" >菜鸟教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="pnpm"><a class="markdownIt-Anchor" href="#pnpm"></a> pnpm</h3><p>安装参考<a class="link"   href="https://pnpm.io/installation" >官网教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h2 id="项目初始化"><a class="markdownIt-Anchor" href="#项目初始化"></a> 项目初始化</h2><p>创建 package.json 和 tsconfig.json 文件，这两个文件不能为空，可以先单独写一个<code>&#123;&#125;</code>。</p><h3 id="手动创建"><a class="markdownIt-Anchor" href="#手动创建"></a> 手动创建</h3><p>package.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@monorepo-template/root&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;private&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Monorepo Template Setup&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;license&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MIT&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>tsconfig.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><h3 id="命令创建"><a class="markdownIt-Anchor" href="#命令创建"></a> 命令创建</h3><p>package.json</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm init</span><br></pre></td></tr></table></figure></div><p>tsconfig.json(需要提前安装好 typescript 相关的包)</p><div class="code-container" data-rel="Ada"><figure class="iseeu highlight ada"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tsc <span class="comment">--init</span></span><br></pre></td></tr></table></figure></div><h3 id="项目结构"><a class="markdownIt-Anchor" href="#项目结构"></a> 项目结构</h3><p>参考如下</p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">/my-monorepo-project</span><br><span class="line">  ├── /apps</span><br><span class="line">  │   ├── /web-app       # 前端应用</span><br><span class="line">  │   └── /mobile-app    # 移动应用</span><br><span class="line">  │</span><br><span class="line">  ├── /packages</span><br><span class="line">  │   ├── /ui-components # 通用 UI 组件库</span><br><span class="line">  │   ├── /utils         # 通用工具库</span><br><span class="line">  │   └── /api-client    # API 客户端库</span><br><span class="line">  │</span><br><span class="line">  ├── /services</span><br><span class="line">  │   ├── /user-service  # 用户服务</span><br><span class="line">  │   └── /product-service # 产品服务</span><br><span class="line">  │</span><br><span class="line">  ├── /libs              # 可能包含更底层的共享代码或第三方库的封装</span><br><span class="line">  │</span><br><span class="line">  ├── /scripts           # 构建脚本、部署脚本等</span><br><span class="line">  │</span><br><span class="line">  ├── /docs              # 项目文档</span><br><span class="line">  │</span><br><span class="line">  ├── /configs           # 通用配置文件，如 ESLint、Prettier 配置</span><br><span class="line">  │</span><br><span class="line">  ├── package.json       # Node.js 项目的依赖和脚本（如果使用）</span><br><span class="line">  ├── lerna.json         # 如果使用 Lerna 管理多包</span><br><span class="line">  ├── yarn.lock          # 如果使用 Yarn 作为包管理器</span><br><span class="line">  └── .gitignore         # Git 忽略文件配置</span><br></pre></td></tr></table></figure></div><blockquote><p>这里的 Lerna + Yarn 可以用 <a class="link"   href="https://pnpm.io/zh/workspaces" >pnpm workspaces<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 替代，下面会介绍。</p></blockquote><h2 id="项目规范化"><a class="markdownIt-Anchor" href="#项目规范化"></a> 项目规范化</h2><h3 id="pnpm-workspace"><a class="markdownIt-Anchor" href="#pnpm-workspace"></a> pnpm workspace</h3><p>在项目根目录下创建 pnpm-workspace.yaml</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">packages:</span> <span class="comment"># 这里包含的是需要通过pnpm管理的项目文件夹</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;apps/*&#x27;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&#x27;packages/*&#x27;</span></span><br></pre></td></tr></table></figure></div><h3 id="typescript"><a class="markdownIt-Anchor" href="#typescript"></a> typescript</h3><p>目前大部分项目会使用 typescript 对 javascript 进行增强，以下是在 Monorepo 中配置 typescript 的主要步骤</p><h4 id="1安装依赖"><a class="markdownIt-Anchor" href="#1安装依赖"></a> 1.安装依赖</h4><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D typescript ts-node @types/node</span><br></pre></td></tr></table></figure></div><p><code>-w</code>表示安装到项目根目录下，<code>-D</code>表示作为开发依赖安装。</p><p>注意：全程需要使用 pnpm，不能混合使用 npm、yarn 等。</p><h4 id="2配置"><a class="markdownIt-Anchor" href="#2配置"></a> 2.配置</h4><p>在项目根目录下创建 tsconfig.xxx.json 的文件，例如 tsconfig.option.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;composite&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;incremental&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">// 这几项配置与Typescript的Project References，tsc的--build模式有关，</span></span><br><span class="line">        <span class="comment">// 具体你可以查阅typescript文档，包括但不限于：</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#composite</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#incremental</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/docs/handbook/project-references.html</span></span><br><span class="line"></span><br><span class="line">        <span class="attr">&quot;declaration&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;declarationMap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;emitDeclarationOnly&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;noEmitOnError&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;skipLibCheck&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">// 这几项配置（当然还有其他相关配置）与生成js文件、d.ts文件，sourcemap文件有关，</span></span><br><span class="line">        <span class="comment">// 具体你可以查阅typescript文档，包括但不限于：</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#declaration</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#emitDeclarationOnly</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#noEmitOnError</span></span><br><span class="line"></span><br><span class="line">        <span class="attr">&quot;esModuleInterop&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;allowSyntheticDefaultImports&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">// 这几项配置与CommonJS和ESModule之间的互操作性有关，</span></span><br><span class="line">        <span class="comment">// 具体你可以查阅typescript文档，包括但不限于：</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#esModuleInterop</span></span><br><span class="line">        <span class="comment">// 我们也将会在后续的视频中演示依赖于此类配置的pkg，因此这里就不展开了</span></span><br><span class="line"></span><br><span class="line">        <span class="attr">&quot;resolveJsonModule&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;strict&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">        <span class="comment">//  这几项配置的作用比较显而易见，具体可查阅文档：</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#resolveJsonModule</span></span><br><span class="line">        <span class="comment">// https://www.typescriptlang.org/tsconfig#strict</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>配置前面创建好的 tsconfig.json 继承 tsconfig.option.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./tsconfig.option.json&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><h4 id="3多项目配置"><a class="markdownIt-Anchor" href="#3多项目配置"></a> 3.多项目配置</h4><p>以 packages 目录为例，这里包括 ui 和 utils 两个项目。</p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">packages</span><br><span class="line">├── ui</span><br><span class="line">│  ├── src</span><br><span class="line">│  ├── package.json</span><br><span class="line">│  └── tsconfig.json</span><br><span class="line">└── utils</span><br><span class="line">   ├── src</span><br><span class="line">   ├── package.json</span><br><span class="line">   └── tsconfig.json</span><br></pre></td></tr></table></figure></div><p>ui 文件夹下的配置</p><p>package.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@monorepo-template/ui&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;private&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;local ui package&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;license&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MIT&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./src/index.ts&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>tsconfig.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../tsconfig.option.json&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>utils 文件夹下的配置</p><p>package.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@monorepo-template/utils&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;private&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;local utils package&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;license&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MIT&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./src/index.ts&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>tsconfig.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../tsconfig.option.json&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>依次把需要通过 pnpm 管理的项目按照上面的方式配置。</p><h4 id="4安装使用本地包"><a class="markdownIt-Anchor" href="#4安装使用本地包"></a> 4.安装使用本地包</h4><p>在 utils/src 中创建 index.ts 如下</p><div class="code-container" data-rel="Ts"><figure class="iseeu highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">add</span>(<span class="params"><span class="attr">x</span>: <span class="built_in">number</span>, <span class="attr">y</span>: <span class="built_in">number</span></span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> x + y;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>然后在项目根目录的 apps 文件夹下的 web-app 项目中安装这个 utils 包</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> apps/web-app</span><br><span class="line">pnpm add @monorepo-template/utils</span><br></pre></td></tr></table></figure></div><p>在 web-app/src 下创建 index.ts 如下</p><div class="code-container" data-rel="Ts"><figure class="iseeu highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; add &#125; <span class="keyword">from</span> <span class="string">&#x27;@monorepo-template/utils&#x27;</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">main</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;sum: &#x27;</span>, <span class="title function_">add</span>(<span class="number">1</span>, <span class="number">2</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">main</span>();</span><br></pre></td></tr></table></figure></div><p>在终端使用 ts-node 执行该文件，需要先在全局安装 ts-node</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ts-node ./src/index.ts</span><br></pre></td></tr></table></figure></div><p>如果没有在全局安装 ts-node，可以在该项目的 package.json 中添加配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;debug&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ts-node ./src/index.ts&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>然后在终端执行</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run debug</span><br></pre></td></tr></table></figure></div><h3 id="代码质量和格式检查"><a class="markdownIt-Anchor" href="#代码质量和格式检查"></a> 代码质量和格式检查</h3><h4 id="1eslint-代码质量检查"><a class="markdownIt-Anchor" href="#1eslint-代码质量检查"></a> 1.Eslint 代码质量检查</h4><p>安装依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin</span><br></pre></td></tr></table></figure></div><p>在项目根目录下创建 .eslintrc 和 .eslintignore</p><p>.eslintrc</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">// 该配置项主要用于指示此.eslintrc文件是Eslint在项目内使用的根级别文件，并且 ESLint 不应在该目录之外搜索配置文件</span></span><br><span class="line">    <span class="attr">&quot;root&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 默认情况下，Eslint使用其内置的 Espree 解析器，该解析器与标准 JavaScript 运行时和版本兼容，而我们需要将ts代码解析为eslint兼容的AST，所以此处我们使用 @typescript-eslint/parser。</span></span><br><span class="line">    <span class="attr">&quot;parser&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@typescript-eslint/parser&quot;</span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 该配置项告诉eslint我们拓展了哪些指定的配置集，其中</span></span><br><span class="line">    <span class="comment">// eslint:recommended ：该配置集是 ESLint 内置的“推荐”，它打开一组小的、合理的规则，用于检查众所周知的最佳实践</span></span><br><span class="line">    <span class="comment">// @typescript-eslint/recommended：该配置集是typescript-eslint的推荐，它与eslint:recommended相似，但它启用了特定于ts的规则</span></span><br><span class="line">    <span class="comment">// @typescript-eslint/eslint-recommended ：该配置集禁用 eslint:recommended 配置集中已经由 typeScript 处理的规则，防止eslint和typescript之间的冲突。</span></span><br><span class="line">    <span class="comment">// prettier（即eslint-config-prettier）关闭所有可能干扰 Prettier 规则的 ESLint 规则，确保将其放在最后，这样它有机会覆盖其他配置集</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">&quot;eslint:recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;plugin:@typescript-eslint/recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;plugin:@typescript-eslint/eslint-recommended&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;prettier&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 该配置项指示要加载的插件，这里</span></span><br><span class="line">    <span class="comment">// @typescript-eslint 插件使得我们能够在我们的存储库中使用typescript-eslint包定义的规则集。</span></span><br><span class="line">    <span class="comment">// prettier插件（即eslint-plugin-prettier）将 Prettier 规则转换为 ESLint 规则</span></span><br><span class="line">    <span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;@typescript-eslint&quot;</span><span class="punctuation">,</span> <span class="string">&quot;prettier&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;prettier/prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;error&quot;</span><span class="punctuation">,</span> <span class="comment">// 打开prettier插件提供的规则，该插件从 ESLint 内运行 Prettier</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 关闭这两个 ESLint 核心规则，这两个规则和prettier插件一起使用会出现问题，具体可参阅</span></span><br><span class="line">        <span class="comment">// https://github.com/prettier/eslint-plugin-prettier/blob/master/README.md#arrow-body-style-and-prefer-arrow-callback-issue</span></span><br><span class="line">        <span class="attr">&quot;arrow-body-style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;prefer-arrow-callback&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;@typescript-eslint/no-explicit-any&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>.eslintignore</p><div class="code-container" data-rel="Cos"><figure class="iseeu highlight cos"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">node_modules/</span><br><span class="line"></span><br><span class="line">pnpm-<span class="keyword">lock</span>.yaml</span><br><span class="line"></span><br><span class="line">*.md</span><br></pre></td></tr></table></figure></div><p>在项目根目录下的 package.json 中配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="comment">//&quot;__eslit__comment__&quot;: &quot;查阅eslint文档 https://eslint.org/docs/latest/use/command-line-interface 了解cli工具的options&quot;,</span></span><br><span class="line">        <span class="attr">&quot;lint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;eslint ./ --ext .ts,.js,.json --max-warnings=0&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>在前面的 index.ts 中编写不符合 eslint 规则集的代码</p><div class="code-container" data-rel="Ts"><figure class="iseeu highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//...</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">test</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;test&quot;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//...</span></span><br></pre></td></tr></table></figure></div><p>执行命令检查代码</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run lint</span><br></pre></td></tr></table></figure></div><h4 id="2prettier-统一代码格式"><a class="markdownIt-Anchor" href="#2prettier-统一代码格式"></a> 2.Prettier 统一代码格式</h4><p>安装依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D prettier</span><br></pre></td></tr></table></figure></div><p>在项目根目录下创建 .prettierrc 和 .prettierignore</p><p>.prettierrc</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">//&quot;__comment__&quot;: &quot;查阅链接https://prettier.io/docs/en/options了解prettier各项配置&quot;,</span></span><br><span class="line">  <span class="attr">&quot;semi&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;singleQuote&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tabWidth&quot;</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;printWidth&quot;</span><span class="punctuation">:</span> <span class="number">120</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>.prettierignore</p><div class="code-container" data-rel="Cos"><figure class="iseeu highlight cos"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">node_modules/</span><br><span class="line"></span><br><span class="line">pnpm-<span class="keyword">lock</span>.yaml</span><br></pre></td></tr></table></figure></div><p>在项目根目录下的 package.json 中配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="attr">&quot;format&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prettier --config .prettierrc &#x27;.&#x27; --write&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>编写测试代码，执行命令格式化代码</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run format</span><br></pre></td></tr></table></figure></div><p>上面的配置方式仍需要手动格式化代码，不过实际编码过程中往往是持续、自动地根据对代码进行格式化。在对于 vscode、vim 等有 prettier 插件支持的编辑器中很容易实现，不过对于记事本这些无插件支持的编辑器可以考虑官方推荐的另一种方式 <a class="link"   href="https://www.npmjs.com/package/onchange" >onchange<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><p>对于 VsCode 编辑器，安装以下两个插件即可</p><style>.ttsjkeuunrmr{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/02/20/monorepo-setup-guide/image-20240220153025970.png"                      class="ttsjkeuunrmr" alt="image-20240220153025970"                ><style>.dkoqfcaffsbb{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/02/20/monorepo-setup-guide/image-20240220153115691.png"                      class="dkoqfcaffsbb" alt="image-20240220153115691"                ><p>以下配置对于有 prettier 插件支持的编辑器来说是可选的。</p><p>安装 onchange 依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D onchange</span><br></pre></td></tr></table></figure></div><p>在项目根目录下的 package.json 中配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="comment">//&quot;__prettier__comment&quot;: &quot;查阅prettier文档 https://prettier.io/docs/en/cli 了解cli工具的options, 查询链接https://prettier.io/docs/en/watching-files, https://www.npmjs.com/package/onchange 获取onchange文档&quot;,</span></span><br><span class="line">        <span class="attr">&quot;format&quot;</span><span class="punctuation">:</span> <span class="string">&quot;prettier --config .prettierrc &#x27;.&#x27; --write&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;format-watch&quot;</span><span class="punctuation">:</span> <span class="string">&quot;onchange -d 1000 &#x27;**/*&#x27; -- prettier --config .prettierrc --write &#123;&#123;changed&#125;&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>在终端运行该脚本，它就会持续检测文件变化情况并根据配置格式化代码。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run format-watch</span><br></pre></td></tr></table></figure></div><h4 id="eslint-与-prettier-适配"><a class="markdownIt-Anchor" href="#eslint-与-prettier-适配"></a> Eslint 与 Prettier 适配</h4><p>Eslint 中也包含了一部分格式化规则，但这些规则往往用不到，并且会与 Prettier 冲突。以下是通过一系列配置将 Prettier 的规则转换为 Eslint 的规则。</p><h5 id="eslint-config-prettier"><a class="markdownIt-Anchor" href="#eslint-config-prettier"></a> eslint-config-prettier</h5><p>该配置用于关闭所有可能干扰 Prettier 规则的 Eslint 规则，使 Prettier 规则可以覆盖 Eslint 规则。</p><h5 id="eslint-plugin-prettier"><a class="markdownIt-Anchor" href="#eslint-plugin-prettier"></a> eslint-plugin-prettier</h5><p>该配置用于将 Prettier 规则转换为 Eslint 规则</p><h5 id="安装依赖"><a class="markdownIt-Anchor" href="#安装依赖"></a> 安装依赖</h5><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D eslint-config-prettier eslint-plugin-prettier</span><br></pre></td></tr></table></figure></div><h5 id="配置"><a class="markdownIt-Anchor" href="#配置"></a> 配置</h5><p>在 .eslintrc 中增加配置(在前面的配置中已经添加，这里单独说明一下)</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="string">&quot;prettier&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;plugins&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;@typescript-eslint&quot;</span><span class="punctuation">,</span><span class="string">&quot;prettier&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;prettier/prettier&quot;</span><span class="punctuation">:</span> <span class="string">&quot;error&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;arrow-body-style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;prefer-arrow-callback&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;@typescript-eslint/no-explicit-any&quot;</span><span class="punctuation">:</span> <span class="string">&quot;off&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>至此 Eslint 与 Prettier 适配完成。</p><h4 id="3husky-创建管理-git-hook"><a class="markdownIt-Anchor" href="#3husky-创建管理-git-hook"></a> 3.Husky 创建管理 git hook</h4><blockquote><p>git hook 是由 git 提供的在满足特定条件下自动执行脚本的功能。</p></blockquote><p>安装依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D husky</span><br></pre></td></tr></table></figure></div><p>启用 git hook，该命令会在项目根目录下生成 .husky 文件夹</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky install</span><br></pre></td></tr></table></figure></div><p>新版 husky 使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky init</span><br></pre></td></tr></table></figure></div><p>在项目根目录下的 package.json 中配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="comment">// 自动在git clone时自动通过husky启用git hook</span></span><br><span class="line">        <span class="attr">&quot;prepare&quot;</span><span class="punctuation">:</span> <span class="string">&quot;husky install&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><blockquote><p>新版 husky 将上面的<code>husky install</code>简化为<code>husky</code></p></blockquote><p>第一个 git hook 是在提交 commit 之前执行 eslint 工具对代码进行质量和格式检查，即执行 package.json 中的 lint 脚本。</p><p>创建该 git hook，该命令会在 .husky 文件夹下生成 pre-commit 脚本。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky add .husky/pre-commit <span class="string">&quot;pnpm run lint&quot;</span></span><br></pre></td></tr></table></figure></div><p>新版 husky 使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;pnpm run lint&quot;</span> &gt;&gt; .husky/pre-commit</span><br></pre></td></tr></table></figure></div><p>如果在提交 commit 时检测到代码不符合 eslint 规则，会终止本次提交。在执行<code>pnpm run format-watch</code>格式化代码之后就能正常提交。</p><blockquote><p>不过这种方式还是会对所有文件进行检查，后面会介绍一种增量格式化的工具。</p></blockquote><h4 id="4lint-staged-增量格式化"><a class="markdownIt-Anchor" href="#4lint-staged-增量格式化"></a> 4.Lint-staged 增量格式化</h4><blockquote><p>lint-staged 是用于辅助 lint 的增强工具。</p></blockquote><p>安装依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D lint-staged</span><br></pre></td></tr></table></figure></div><p>在项目根目录下创建 .lintstagedrc.js (支持多种格式，具体参考官网文档)</p><div class="code-container" data-rel="Js"><figure class="iseeu highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; <span class="title class_">ESLint</span> &#125; = <span class="built_in">require</span>(<span class="string">&#x27;eslint&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">removeIgnoredFiles</span> = <span class="keyword">async</span> (<span class="params">files</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> eslint = <span class="keyword">new</span> <span class="title class_">ESLint</span>();</span><br><span class="line">    <span class="keyword">const</span> ignoredFiles = <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(files.<span class="title function_">map</span>(<span class="function">(<span class="params">file</span>) =&gt;</span> eslint.<span class="title function_">isPathIgnored</span>(file)));</span><br><span class="line">    <span class="keyword">const</span> filteredFiles = files.<span class="title function_">filter</span>(<span class="function">(<span class="params">_, i</span>) =&gt;</span> !ignoredFiles[i]);</span><br><span class="line">    <span class="keyword">return</span> filteredFiles.<span class="title function_">join</span>(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line">    <span class="string">&#x27;*&#x27;</span>: <span class="title function_">async</span> (files) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> filesToLint = <span class="keyword">await</span> <span class="title function_">removeIgnoredFiles</span>(files);</span><br><span class="line">        <span class="keyword">return</span> [<span class="string">`eslint <span class="subst">$&#123;filesToLint&#125;</span> --max-warnings=0`</span>];</span><br><span class="line">    &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></div><p>这段脚本的作用是对所有被 lint-staged 检测到的文件，过滤掉被忽略的文件，然后对这些文件执行 lint 脚本。</p><p>接下来需要手动更改 pre-commit 脚本如下</p><div class="code-container" data-rel="Sh"><figure class="iseeu highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env sh</span></span><br><span class="line">. <span class="string">&quot;<span class="subst">$(dirname -- <span class="string">&quot;<span class="variable">$0</span>&quot;</span>)</span>/_/husky.sh&quot;</span></span><br><span class="line"></span><br><span class="line">npx lint-staged</span><br></pre></td></tr></table></figure></div><p>新版 husky 只需要修改脚本如下</p><div class="code-container" data-rel="Sh"><figure class="iseeu highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx lint-staged</span><br></pre></td></tr></table></figure></div><p>使用命令<code>echo &quot;npx lint-staged&quot; &gt; ./husky/pre-commit</code>即可</p><p>之后提交 commit 时就只会对发生更改的文件进行检查。</p><h4 id="5commitlint-commitizen-规范提交"><a class="markdownIt-Anchor" href="#5commitlint-commitizen-规范提交"></a> 5.Commitlint + Commitizen 规范提交</h4><blockquote><p>commitlint 用于校验 commit message，commitizen 用于交互式生成 commit message，这两个组合使用对于 commit 规范的统一非常有帮助。</p></blockquote><h5 id="commitlint"><a class="markdownIt-Anchor" href="#commitlint"></a> Commitlint</h5><p>安装 commitlint 的依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D @commitlint/cli @commitlint/config-conventional </span><br></pre></td></tr></table></figure></div><p>在项目根目录下创建 .commitlintrc.json</p> <div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;@commitlint/config-conventional&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;rules&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;scope-empty&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="number">2</span><span class="punctuation">,</span> <span class="string">&quot;never&quot;</span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>这里的<code>extends</code>是扩展 commitlint 官方的配置，<code>scope-empty</code>表示的是提交 commit 时，scope 范围不能为空。</p><p>创建 commit-msg 的git hook</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx husky add .husky/commit-msg <span class="string">&#x27;npx --no -- commitlint --edit &quot;$1&quot;&#x27;</span></span><br></pre></td></tr></table></figure></div><p>新版 husky 使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;npx --no -- commitlint --edit \$1&quot;</span> &gt; .husky/commit-msg</span><br></pre></td></tr></table></figure></div><p>该脚本的作用是在提交或修改 commit message 时进行校验，以确保项目拥有统一规范的 commit messgae。</p><h5 id="commitizen"><a class="markdownIt-Anchor" href="#commitizen"></a> Commitizen</h5><p>安装 commitizen 依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm add -w -D commitizen cz-coventional-changelog </span><br></pre></td></tr></table></figure></div><p>在项目根目录下创建 .czrc 配置文件</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cz-conventional-changelog&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p><code>cz-conventional-changelog</code>是 commitizen 的 conventional-changelog 适配器，该适配器会以 AngularJS 的 commit messgae 规范逐步引导完成 commit message 的创建。</p><p>在项目根目录下的 package.json 中配置</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="attr">&quot;cz&quot;</span><span class="punctuation">:</span> <span class="string">&quot;cz&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>执行以下命令生成 AugularJS 规范的 commit message</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm run cz</span><br></pre></td></tr></table></figure></div><h4 id="vscode-的配置"><a class="markdownIt-Anchor" href="#vscode-的配置"></a> VsCode 的配置</h4><p>VsCode 编辑器自身也提供了一定的配置项。</p><p>在项目跟目录下创建 .vscode 文件夹，在文件夹内创建 extensions.json 和 settings.json。</p><p>extensions.json 用于推荐安装的插件，这里推荐 eslint 和 prettier 的插件。</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;recommendations&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;esbenp.prettier-vscode&quot;</span><span class="punctuation">,</span> <span class="string">&quot;dbaeumer.vscode-eslint&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>settings.json 是 VsCode 的项目内配置，下面是一份参考配置。</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;typescript.tsdk&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node_modules/typescript/lib&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.formatOnSave&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.codeActionsOnSave&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;source.organizeImports&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;source.fixAll.eslint&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.formatOnSaveMode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;file&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.formatOnType&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;editor.formatOnPaste&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;[typescript]&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;editor.formatOnSave&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;editor.defaultFormatter&quot;</span><span class="punctuation">:</span> <span class="string">&quot;esbenp.prettier-vscode&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;editor.codeActionsOnSave&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;source.organizeImports&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;source.fixAll.eslint&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;editor.formatOnSaveMode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;file&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;editor.formatOnType&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;editor.formatOnPaste&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>至此基础的项目工程化配置完成。</p><h3 id="项目依赖安装方式"><a class="markdownIt-Anchor" href="#项目依赖安装方式"></a> 项目依赖安装方式</h3><p>Monorepo 中有两种项目，一种是 Monorepo 自身这个总体的项目，一种是由 Monorepo 管理的具体的模块项目。</p><p>前面操作的都是在总体项目下完成的，而具体的项目有两种依赖安装方式。</p><p>一种方式是进入到对应的目录下，执行不带<code>-w</code>参数的 pnpm 命令安装依赖。</p><p>另一种方式是在总体项目根目录下的 package.json 中添加脚本</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="attr">&quot;project-name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;pnpm --filter @monorepo-template/project-name&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p><code>project-name</code>为具体项目对应的名称。</p><p>然后可以在总体项目根目录下通过以下命令安装依赖</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm project-name add xxx</span><br></pre></td></tr></table></figure></div><p>如果遇到 pnpm 的 missing peer 警告信息，可以在总体项目根目录下创建 .npmrc，添加以下内容</p><div class="code-container" data-rel="Ini"><figure class="iseeu highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">auto-install-peers</span>=<span class="literal">true</span></span><br><span class="line"><span class="attr">strict-peer-dependencies</span>=<span class="literal">false</span></span><br></pre></td></tr></table></figure></div><p>具体可以参考<a class="link"   href="https://pnpm.io/zh/npmrc#auto-install-peers" >官方文档<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><h3 id="project-references-和-tsc-build"><a class="markdownIt-Anchor" href="#project-references-和-tsc-build"></a> Project References 和 tsc --build</h3><p>project references 也是由 typescript 提供的功能，用于具体指出多项目之间的依赖关系，从而实现多项目的管理。</p><p>实际生产环境中不会使用 ts-node 运行项目，而是把项目编译打包成 js 产物，使用 NodeJS 运行时直接运行 js 启动服务。</p><p>这里假设有一个 project-name 项目，依赖一个 package-name 本地包。</p><p>修改 project-name 项目的 package.json 文件</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;scripts&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;debug&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ts-node ./src/app.ts&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;clean&quot;</span><span class="punctuation">:</span> <span class="string">&quot;rm -rf ./dist *.tsbuildinfo&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;compile&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tsc --build&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;build&quot;</span><span class="punctuation">:</span> <span class="string">&quot;pnpm run clean; pnpm run compile&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;start&quot;</span><span class="punctuation">:</span> <span class="string">&quot;node ./dist/app.js&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>然后修改 project-name 项目的 tsconfig.json 文件</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../tsconfig.option.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;declarationMap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;emitDeclarationOnly&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;rootDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./src&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;outDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>在总体项目根目录下执行命令编译打包项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm project-name run build</span><br></pre></td></tr></table></figure></div><p>使用以下命令运行项目</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm project-name run start</span><br></pre></td></tr></table></figure></div><p>但是目前还不能直接通过该命令运行，需要进一步配置。</p><p>多项目编译打包需要修改 package-name 本地包的 package.json，修改之前的 main 的配置<code>./src/index.ts</code>为编译后的产物<code>./dist/index.js</code></p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;main&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist/index.js&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>同样地配置 tsconfig.json</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;extends&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../tsconfig.option.json&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;declarationMap&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;emitDeclarationOnly&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;rootDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./src&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;outDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./dist&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>回到 project-name 项目的 tsconfig.json 文件，添加 references 配置，表明了当前项目的本地依赖包。</p><div class="code-container" data-rel="Json"><figure class="iseeu highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="attr">&quot;references&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="comment">//path具体到tsconfig.json所在的目录</span></span><br><span class="line">            <span class="attr">&quot;path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;../../packages/package-name&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;compilerOptions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="comment">//可选配置，不进行增量编译，不生成d.ts文件，也不会生成.tsbuildinfo文件</span></span><br><span class="line">        <span class="attr">&quot;incremental&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;composite&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;declaration&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div><p>在编译 project-name 项目时，tsc 会自动编译 package-name 本地包的源码。</p><p>现在就能通过<code>pnpm project-name run build</code>一步编译项目和项目依赖的本地包。</p><p>Project References 用于明确依赖关系，检测依赖是否正确编译或是否是最新的编译，如果没有，将会自动对被依赖的包执行<code>tsc --build</code>进行编译更新。</p><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><h3 id="视频资料"><a class="markdownIt-Anchor" href="#视频资料"></a> 视频资料</h3><p><a class="link"   href="https://www.bilibili.com/video/BV1hp4y1L7Mr/" >基于pnpm workspace，超清楚简单的monorepo项目创建与基础演示<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.bilibili.com/video/BV1a8411i77L/" >nodejs项目工程化 eslint prettier husky lint-staged commitlint commitizen<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.bilibili.com/video/BV1ku4y1r7pQ/" >monorepo中使用project refrences，基于fastify的NodeJS Web服务<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="源代码资料"><a class="markdownIt-Anchor" href="#源代码资料"></a> 源代码资料</h3><p><a class="link"   href="https://github.com/pnpm/pnpm" >pnpm<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/eslint/eslint" >eslint<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/prettier/prettier" >prettier<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/typicode/husky" >husky<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/lint-staged/lint-staged" >lint-staged<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/conventional-changelog/commitlint" >commitlint<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/commitizen-tools/commitizen" >commitizen<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/dmokel/startup-monorepo-fastify-phaser-colyseus-threejs" >startup-monorepo-fastify-phaser-colyseus-threejs<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文主要记录关于 Monorepo 的项目规范实践，借助 pnpm 包管理器和 NodeJs 环境等，通过各种配置实现对项目的自动化或半自动化规范。</summary>
    
    
    
    
    <category term="monorepo" scheme="https://www.entropy-tree.top/tags/monorepo/"/>
    
    <category term="nodejs" scheme="https://www.entropy-tree.top/tags/nodejs/"/>
    
    <category term="pnpm" scheme="https://www.entropy-tree.top/tags/pnpm/"/>
    
    <category term="typescript" scheme="https://www.entropy-tree.top/tags/typescript/"/>
    
  </entry>
  
  <entry>
    <title>webflux-project</title>
    <link href="https://www.entropy-tree.top/2024/02/09/webflux-project/"/>
    <id>https://www.entropy-tree.top/2024/02/09/webflux-project/</id>
    <published>2024-02-09T08:25:56.000Z</published>
    <updated>2024-02-09T08:25:56.082Z</updated>
    
    <content type="html"><![CDATA[<p>记录首次基于 Spring Webflux 响应式框架的后端项目开发过程中涉及到的一些东西。<span id="more"></span></p><p>webflux 框架是一个异步非阻塞的响应式框架，旨在提高吞吐量和并发量。webflux 框架的使用过程中涉及到很多函数式编程与响应式编程。下面是一些学习使用过程中的整理。</p><h3 id="对象模型-po-bo-dto-vo-do"><a class="markdownIt-Anchor" href="#对象模型-po-bo-dto-vo-do"></a> 对象模型 PO BO DTO VO (DO)</h3><h4 id="基本概念"><a class="markdownIt-Anchor" href="#基本概念"></a> 基本概念</h4><ul><li><strong>PO (Persistent Object) 持久对象</strong>：直接映射到数据库表的一个对象。通常与数据库表结构一一对应，用于ORM（对象关系映射）框架。</li><li><strong>BO (Business Object) 业务对象</strong>：包含业务逻辑的对象，一般位于业务层。BO通常从DAO（数据访问对象）获取数据，进行业务逻辑处理，并返回结果给前端。</li><li><strong>DTO (Data Transfer Object) 数据传输对象</strong>：用于服务层和外部的通信，或是不同服务间的数据传输。DTO主要用于远程调用时减少单个方法调用的参数数量，或是封装方法调用的返回结果。</li><li><strong>VO (Value Object) 值对象</strong>：主要用于表示展示层和客户端的数据对象。VO通常是根据界面的需求来创建的，包含界面显示所需要的数据。</li><li><strong>DO (Domain Object) 领域对象</strong>：在领域驱动设计（DDD）中，领域对象包含了业务逻辑，它代表了业务领域内的一个实体或概念，这时候等同于<strong>BO</strong>。在另外的开发规范中，DO (Data Object) 作为数据对象，对象属性与数据库字段形成映射关系，这时候等同于<strong>PO</strong>。</li></ul><h4 id="数据来源"><a class="markdownIt-Anchor" href="#数据来源"></a> 数据来源</h4><p><strong>VO</strong></p><ul><li><strong>数据来源</strong>：通常直接对应于前端视图展示的数据需求，可能来自于单个PO的转换，或者是多个PO、DTO的组合与转换的结果。</li><li><strong>简单场景案例</strong>：在一个CRUD应用中，创建用户的表单提交结果可能直接映射为一个<code>UserVO</code>，该VO包含用户的姓名、邮箱等信息，然后直接转换为PO进行数据库操作。</li></ul><p><strong>DTO</strong></p><ul><li><strong>数据来源</strong>：DTO通常用于服务层之间的数据传输，它的数据可能来源于数据库的多表查询结果，或者是BO的转换和封装。</li><li><strong>简单场景案例</strong>：在微服务架构中，<code>OrderService</code>调用<code>UserService</code>获取用户信息时，<code>UserService</code>可能会返回一个<code>UserDTO</code>，包含用户的基本信息，这个DTO可能是直接从数据库查询得到的，也可能是一个或多个PO转换而来。</li></ul><p><strong>BO</strong></p><ul><li><strong>数据来源</strong>：BO主要在业务逻辑层使用，它的数据来源可能是数据库查询得到的PO，或者是经过业务逻辑处理的DTO。</li><li><strong>简单场景案例</strong>：在处理订单支付逻辑时，<code>PaymentBO</code>可能包含订单详情、用户信息等多个方面的数据，这些数据可能通过查询数据库得到的PO转换而来，然后<code>PaymentBO</code>会封装支付处理的具体逻辑。</li></ul><p><strong>PO</strong></p><ul><li><strong>数据来源</strong>：PO就是直接对应数据库中的每一张数据表。</li><li><strong>简单场景案例</strong>：在数据库中存在<code>user</code>、<code>order</code>、<code>record</code>等数据表，根据每一张表的字段定义对应的PO。</li></ul><blockquote><h2 id="复杂场景案例"><a class="markdownIt-Anchor" href="#复杂场景案例"></a> 复杂场景案例</h2><p>一个典型的复杂场景案例，展示了一个电商系统中订单处理的流程，涉及用户下单、订单处理、支付、以及最终的订单状态更新和通知用户。</p><h3 id="场景描述"><a class="markdownIt-Anchor" href="#场景描述"></a> 场景描述</h3><p>假设有一个电商平台，用户可以浏览商品、添加商品到购物车、下单和支付。系统需要处理订单创建、库存检查、支付处理、订单状态更新和通知用户等一系列操作。</p><h3 id="涉及的模型和层次"><a class="markdownIt-Anchor" href="#涉及的模型和层次"></a> 涉及的模型和层次</h3><ul><li><strong>PO（持久对象）</strong>：<code>Order</code>、<code>OrderItem</code>、<code>Product</code>、<code>User</code>等，直接映射数据库中的订单表、订单项表、产品表和用户表。</li><li><strong>DTO（数据传输对象）</strong>：<code>OrderCreationDTO</code>、<code>PaymentInfoDTO</code>等，用于在服务层之间传递订单创建和支付信息。</li><li><strong>BO（业务对象）</strong>：<code>OrderBO</code>，封装订单处理的业务逻辑，如验证库存、计算价格等。</li><li><strong>VO（值对象）</strong>：<code>OrderSummaryVO</code>、<code>PaymentResultVO</code>等，用于向用户展示订单摘要信息和支付结果。</li></ul><h3 id="数据流转"><a class="markdownIt-Anchor" href="#数据流转"></a> 数据流转</h3><ol><li><strong>用户下单</strong>：<ul><li>用户在前端页面提交订单，前端发送包含订单详情（如商品ID、数量等）的<code>OrderCreationDTO</code>到后端。</li><li>Controller层接收<code>OrderCreationDTO</code>，调用Service层处理订单。</li></ul></li><li><strong>订单处理</strong>：<ul><li>Service层将<code>OrderCreationDTO</code>转换或直接使用来创建<code>OrderBO</code>。</li><li><code>OrderBO</code>负责业务逻辑处理，如验证库存、计算总价等，并使用PO操作数据库创建订单和订单项。</li><li>如果涉及到跨服务调用（如库存服务），可能会使用其他的<code>DTO</code>进行数据传输。</li></ul></li><li><strong>支付处理</strong>：<ul><li>用户选择支付方式并提交支付信息，后端接收支付信息（如<code>PaymentInfoDTO</code>）并处理支付。</li><li>支付服务可能是一个独立的服务，Service层将支付信息封装在<code>DTO</code>中调用支付服务。</li><li>支付完成后，更新订单状态，并将结果封装在<code>PaymentResultVO</code>中返回给前端。</li></ul></li><li><strong>订单完成和通知</strong>：<ul><li>订单支付成功后，Service层更新订单状态（使用PO操作数据库）。</li><li>系统通知用户订单已完成，可能通过发送<code>OrderSummaryVO</code>到前端展示订单摘要，或发送邮件/短信通知（内容可能基于<code>VO</code>生成）。</li></ul></li></ol></blockquote><h4 id="在-springboot-三层架构中的使用"><a class="markdownIt-Anchor" href="#在-springboot-三层架构中的使用"></a> 在 SpringBoot 三层架构中的使用</h4><h5 id="controller层"><a class="markdownIt-Anchor" href="#controller层"></a> Controller层</h5><ul><li><strong>VO（值对象）</strong>：主要用于Controller层与前端的数据交互。VO专门针对视图（View）的需求设计，封装了用户界面展示所需的数据。Controller层可能会接收前端传来的VO，也可能将数据封装成VO返回给前端。</li></ul><h5 id="service层"><a class="markdownIt-Anchor" href="#service层"></a> Service层</h5><ul><li><strong>DTO（数据传输对象）</strong>：Service层广泛使用DTO来传输跨层次、跨服务的数据。DTO用于封装从Controller传到Service层的数据，或者从Service层传到其他服务的数据。</li><li><strong>BO（业务对象）</strong>：BO包含业务逻辑和业务状态信息，Service层使用BO来执行具体的业务操作。BO可以看作是Service层的核心，封装了业务规则和算法。</li></ul><h5 id="dao或repository层"><a class="markdownIt-Anchor" href="#dao或repository层"></a> DAO（或Repository）层</h5><ul><li><strong>PO（持久对象）</strong>：DAO层使用PO与数据库表直接映射。ORM（对象关系映射）框架如Hibernate、JPA等通常操作PO，将数据库行记录映射成PO，或将PO持久化到数据库中。PO仅仅反映了数据库的结构，而没有复杂的业务逻辑。</li></ul><h5 id="数据流转-2"><a class="markdownIt-Anchor" href="#数据流转-2"></a> 数据流转</h5><ol><li><strong>前端到后端</strong>：前端发送请求，Controller层接收封装成VO的数据 -&gt; Service层将VO转换成DTO或直接使用DTO接收数据，进行业务处理，可能会涉及到BO -&gt; DAO层使用PO与数据库交互。</li><li><strong>后端到前端</strong>：DAO层查询数据库，将结果封装成PO -&gt; Service层处理业务逻辑，可能会转换PO为BO或DTO进行业务逻辑的处理 -&gt; Controller层将DTO或BO转换为VO，返回给前端。</li></ol><style>.vuqqxwhctell{zoom: 80%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/02/09/webflux-project/274001.png"                      class="vuqqxwhctell" alt="图片"                ><h3 id="外键关联约束下-po-的映射行为"><a class="markdownIt-Anchor" href="#外键关联约束下-po-的映射行为"></a> 外键关联约束下 PO 的映射行为</h3><h4 id="1-基本类型"><a class="markdownIt-Anchor" href="#1-基本类型"></a> 1. 基本类型</h4><ul><li><strong>定义</strong>：使用基本数据类型或其包装类直接在PO中表示外键字段。</li><li><strong>适用场景</strong>：当你只需要知道关联表的ID，而不需要访问关联表其他字段的详细信息时。</li><li><strong>建议</strong>：这种方式简单且高效，适用于关联关系较为简单的场景。</li></ul><h4 id="2-变量级组合"><a class="markdownIt-Anchor" href="#2-变量级组合"></a> 2. 变量级组合</h4><ul><li><strong>定义</strong>：在PO中通过对象变量的形式包含关联表的PO，以此来表示外键关系。</li><li><strong>适用场景</strong>：需要访问或操作关联表的详细信息时，如展示用户信息时同时需要展示用户的订单信息。</li><li><strong>建议</strong>：采用变量级组合可以增强模型的表达力，适用于需要在业务逻辑中处理关联数据的场景。使用ORM框架时，这种关系可以通过注解（如JPA的<code>@ManyToOne</code>、<code>@OneToMany</code>等）来实现。</li></ul><h4 id="3-类级组合bo"><a class="markdownIt-Anchor" href="#3-类级组合bo"></a> 3. 类级组合(BO)</h4><ul><li><strong>定义</strong>：通过创建新的类来封装多个表的组合数据，这些类不直接对应单一的数据库表，而是根据业务需求组合多个PO的数据(BO的来源之一)。</li><li><strong>适用场景</strong>：跨表查询结果的封装，或需要根据业务逻辑组合多个表数据的场景。</li><li><strong>建议</strong>：类级组合适用于复杂的业务逻辑处理，以及需要将多个表的数据作为一个整体进行处理的情况。这种方法可以提高代码的可读性和维护性，但需要注意避免过度使用，以免引入不必要的复杂性。</li></ul><h4 id="4-数据传输对象dto"><a class="markdownIt-Anchor" href="#4-数据传输对象dto"></a> 4. 数据传输对象(DTO)</h4><ul><li><strong>定义</strong>：特别地，可以创建DTO来封装从数据库查询得到的数据，特别是多表查询的结果，DTO不直接映射到数据库的表结构，而是根据展示或传输的需要定制。</li><li><strong>适用场景</strong>：在跨服务传输数据，或者在层与层之间传输经过处理的数据时。</li><li><strong>建议</strong>：当PO与视图或服务间的数据需求不一一对应时，使用DTO可以提供更大的灵活性和清晰的分层。</li></ul><h3 id="各种-er-关系下的对象模型设计"><a class="markdownIt-Anchor" href="#各种-er-关系下的对象模型设计"></a> 各种 ER 关系下的对象模型设计</h3><blockquote><p>使用变量级别的组合描述实体关系，简要地说明一个参考的对象模型设计方案。</p></blockquote><p>一个简单的图书管理系统，其中包括<code>Author</code>（作者）、<code>Book</code>（书籍）、<code>Review</code>（书评）和<code>BookDetail</code>（书籍详情）四个实体。</p><h4 id="实体关系"><a class="markdownIt-Anchor" href="#实体关系"></a> 实体关系</h4><ul><li><strong>多对多关系</strong>：一本书可以由多个作者共同编写，一个作者也可以写多本书（<code>Book</code> &lt;-&gt; <code>Author</code>）。</li><li><strong>一对多关系</strong>：一本书可以有多条书评，但每条书评只针对一本书（<code>Book</code> -&gt; <code>Review</code>）。</li><li><strong>一对一关系</strong>：为了方便展示一对一的关系，可以引入一个新的实体：书籍详情（BookDetail），假设每本书都有一个与之对应的详细信息页（<code>Book</code> -&gt; <code>BookDetail</code>）。</li></ul><h4 id="po-的设计"><a class="markdownIt-Anchor" href="#po-的设计"></a> PO 的设计</h4><p>对应数据表中的各个字段，分析实体关系进行设计。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Author</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Book&gt; books; <span class="comment">// 与Book的多对多关系</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Book</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String title;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Author&gt; authors; <span class="comment">// 与Author的多对多关系</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Review&gt; reviews; <span class="comment">// 与Review的一对多关系</span></span><br><span class="line">    <span class="keyword">private</span> BookDetail bookDetail; <span class="comment">// 与BookDetail的一对一关系</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Review</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String content;</span><br><span class="line">    <span class="keyword">private</span> Book book; <span class="comment">// 指向Book的外键，表示这条书评属于哪本书</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BookDetail</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String isbn;</span><br><span class="line">    <span class="keyword">private</span> String publisher;</span><br><span class="line">    <span class="keyword">private</span> Book book; <span class="comment">// 指向Book的引用，一对一关系</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h4 id="dto-的设计"><a class="markdownIt-Anchor" href="#dto-的设计"></a> DTO 的设计</h4><p>封装多表查询的数据结构，用于服务层和展示层的数据传输。（根据多表查询的结果、前端的请求数据结构等设计，这里是多表查询<code>Book</code>和<code>BookDetail</code>的示例，用来展示书籍列表，其中包括书名、作者名字列表和ISBN号。)</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BookListDTO</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String title;</span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; authorsNames; <span class="comment">// 书的所有作者的名字</span></span><br><span class="line">    <span class="keyword">private</span> String isbn; <span class="comment">// 书籍详情中的ISBN号</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h4 id="vo的设计"><a class="markdownIt-Anchor" href="#vo的设计"></a> VO的设计</h4><p>VO用于表示层，封装最终用户界面所需的数据。（根据前端展示要求设计，例如展示书籍的详细信息，包括书名、ISBN号、出版社、作者列表和书评列表，可以是由若干个 PO、DTO 单独或者组合转换而来。）</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BookDetailVO</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String title;</span><br><span class="line">    <span class="keyword">private</span> String isbn;</span><br><span class="line">    <span class="keyword">private</span> String publisher;</span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; authors;</span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; reviews; <span class="comment">// 用户对书籍的评论</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 构造器、Getter和Setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h4 id="bo的设计"><a class="markdownIt-Anchor" href="#bo的设计"></a> BO的设计</h4><p>BO封装业务逻辑，基于PO进行操作，可能会涉及多个PO之间的逻辑关系。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BookManagementBO</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Book book;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Author&gt; authors;</span><br><span class="line">    <span class="keyword">private</span> BookDetail bookDetail;</span><br><span class="line">    <span class="keyword">private</span> List&lt;Review&gt; reviews;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">BookManagementBO</span><span class="params">(Book book)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.book = book;</span><br><span class="line">        <span class="comment">// 假设这里填充了authors、bookDetail、reviews等信息</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">publishBook</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 实现发布书籍的业务逻辑，可能包括保存书籍信息、保存作者信息等</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addReview</span><span class="params">(Review review)</span> &#123;</span><br><span class="line">        <span class="comment">// 添加书评到书籍</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 更多业务逻辑方法</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>BO 的设计有些特殊，它主要负责处理业务逻辑，并可以直接访问DAO（数据访问对象）层。但是它的业务逻辑操作的类型又不同于在服务层的业务逻辑操作：</p><h5 id="服务层service-layer"><a class="markdownIt-Anchor" href="#服务层service-layer"></a> 服务层（Service Layer）</h5><p>服务层主要负责协调应用程序中不同部分的业务逻辑。它为表示层提供了一组可用的操作（API），这些操作代表了应用程序可以执行的业务操作。服务层的职责包括：</p><ul><li><strong>事务管理</strong>：确保业务操作的事务性，处理应用程序中的事务边界。</li><li><strong>应用逻辑</strong>：实现应用级别的业务逻辑，如用户权限验证、工作流控制等。</li><li><strong>协调数据访问</strong>：调用下层的数据访问对象（DAO）来访问和修改数据。</li><li><strong>聚合业务操作</strong>：组合和协调多个BO的操作，以执行复杂的业务逻辑。</li></ul><h5 id="业务对象bo"><a class="markdownIt-Anchor" href="#业务对象bo"></a> 业务对象（BO）</h5><p>业务对象通常更接近于特定的业务领域模型，封装了与之相关的数据以及对数据进行操作的业务逻辑。BO的职责包括：</p><ul><li><strong>封装业务规则</strong>：实现特定业务领域的规则和逻辑。</li><li><strong>状态管理</strong>：管理和维护业务对象的状态。</li><li><strong>数据验证</strong>：确保业务数据的完整性和有效性。</li><li><strong>数据转换</strong>：在业务模型和数据模型之间转换数据。</li></ul><h5 id="如何划分"><a class="markdownIt-Anchor" href="#如何划分"></a> 如何划分</h5><ul><li><strong>粒度和复杂性</strong>：如果业务操作涉及多个实体之间的复杂交互或需要执行跨实体的事务管理，通常这些操作应该放在服务层完成。服务层可以调用一个或多个BO来实现这些复杂的业务逻辑。</li><li><strong>领域逻辑</strong>：如果操作主要涉及单一实体内部的业务规则或逻辑，这些操作可以封装在相应的BO中。BO可以提供细粒度的业务方法，服务层可以利用这些方法来完成更高级别的业务操作。</li><li><strong>数据访问</strong>：尽管BO可以直接调用DAO来进行数据操作，但更常见的做法是在服务层中调用DAO，然后通过服务层来协调数据访问和业务逻辑。这样做有助于保持业务逻辑层的一致性和事务的完整性。</li></ul><h5 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h5><ul><li><strong>在服务层中操作</strong>：当业务逻辑需要跨多个领域模型或需要应用级别的逻辑处理时。例如针对上面的 DTO 的数据结构查询结果。</li><li><strong>在BO中操作</strong>：当业务逻辑主要针对单一领域模型且比较独立时。例如针对书籍的发布涉及到保存和查询两个连续操作，添加书评需要对书评的内容进行敏感词过滤等。</li></ul><h3 id="基础环境"><a class="markdownIt-Anchor" href="#基础环境"></a> 基础环境</h3><h4 id="项目依赖环境"><a class="markdownIt-Anchor" href="#项目依赖环境"></a> 项目依赖环境</h4><p>下面的 pom.xml 是项目涉及到的全部依赖</p><div class="code-container" data-rel="Xml"><figure class="iseeu highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">project</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://maven.apache.org/POM/4.0.0&quot;</span> <span class="attr">xmlns:xsi</span>=<span class="string">&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span></span></span><br><span class="line"><span class="tag">         <span class="attr">xsi:schemaLocation</span>=<span class="string">&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">modelVersion</span>&gt;</span>4.0.0<span class="tag">&lt;/<span class="name">modelVersion</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">parent</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-parent<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.2.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">relativePath</span>/&gt;</span> <span class="comment">&lt;!-- lookup parent from repository --&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">parent</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.entropy<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>webflux-demo<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.0.1-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">name</span>&gt;</span>gradems<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">properties</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">java.version</span>&gt;</span>17<span class="tag">&lt;/<span class="name">java.version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">properties</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-mongodb-reactive<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-r2dbc<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis-reactive<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-security<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.jsonwebtoken<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jjwt-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.12.5<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.jsonwebtoken<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jjwt-impl<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.12.5<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.jsonwebtoken<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jjwt-jackson<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.12.5<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-webflux<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-docker-compose<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>runtime<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">optional</span>&gt;</span>true<span class="tag">&lt;/<span class="name">optional</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.postgresql<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>postgresql<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>runtime<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.postgresql<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>r2dbc-postgresql<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>runtime<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">optional</span>&gt;</span>true<span class="tag">&lt;/<span class="name">optional</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.projectreactor<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>reactor-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.security<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-security-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-actuator<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-maven-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="name">excludes</span>&gt;</span></span><br><span class="line">                        <span class="tag">&lt;<span class="name">exclude</span>&gt;</span></span><br><span class="line">                            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">                            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">                        <span class="tag">&lt;/<span class="name">exclude</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;/<span class="name">excludes</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">project</span>&gt;</span></span><br></pre></td></tr></table></figure></div><h4 id="数据库环境搭建"><a class="markdownIt-Anchor" href="#数据库环境搭建"></a> 数据库环境搭建</h4><p>使用以下 docker-compose.yaml 配置文件即可快速搭建 postgresql、mongodb、redis 三个数据库</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.8&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">postgres:</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">gradms-postgres</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&#x27;postgres:latest&#x27;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;POSTGRES_DB=gradems&#x27;</span><span class="comment"># postgresql 数据库名称</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;POSTGRES_PASSWORD=gradems&#x27;</span><span class="comment"># postgresql 数据库密码</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;POSTGRES_USER=admin&#x27;</span><span class="comment"># postgresql 数据库用户名</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;5432:5432&#x27;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;postgres_data:/var/lib/postgresql/data&#x27;</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">gradms-redis</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&#x27;redis:latest&#x27;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;6379:6379&#x27;</span></span><br><span class="line">  <span class="attr">mongodb:</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">gradms-mongodb</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">&#x27;mongo:latest&#x27;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;27017:27017&#x27;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;mongodb_data:/data/db&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">postgres_data:</span></span><br><span class="line">  <span class="attr">mongodb_data:</span></span><br></pre></td></tr></table></figure></div><p>运行命令<code>docker-compose up -d</code>启动。</p><p>至此，基本的项目依赖环境和数据库环境准备完成。</p><h3 id="webflux-控制层"><a class="markdownIt-Anchor" href="#webflux-控制层"></a> Webflux 控制层</h3><p>Webflux 的控制层有两种不同的编写风格，一种是传统控制器风格，一种是函数式端点风格。</p><h4 id="传统控制器风格"><a class="markdownIt-Anchor" href="#传统控制器风格"></a> 传统控制器风格</h4><p>只需要在传统的控制器上将返回值设置为<code>Mono</code>或<code>Flux</code>两个特殊的类型即可。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyController</span> &#123;</span><br><span class="line">    <span class="meta">@GetMapping(&quot;/hello&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;String&gt; <span class="title function_">hello</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Mono.just(<span class="string">&quot;Hello, World!&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>在 Spring Webflux 中，<code>Mono</code>和<code>Flux</code>是响应式编程中的两种基本类型，它们来自于 Reactor 库：</p><ul><li><strong>Mono</strong>：代表单个值或空值的异步序列。它用于处理最多一个数据项的场景。例如，一个查询单个对象的数据库操作可以返回<code>Mono</code>。</li><li><strong>Flux</strong>：代表多个值的异步序列。它用于处理0到N个数据项的场景。例如，一个查询多个对象的数据库操作可以返回<code>Flux</code>。</li></ul><blockquote><p>控制层返回值的改动也会影响到服务层，数据访问层的代码。</p></blockquote><h4 id="函数式端点风格"><a class="markdownIt-Anchor" href="#函数式端点风格"></a> 函数式端点风格</h4><p>首先创建一个处理器类(handler)，需要返回固定的<code>Mono&lt;ServerResponse&gt;</code>类型。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyHandler</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">hello</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> ServerResponse.ok().body(Mono.just(<span class="string">&quot;Hello, WebFlux!&quot;</span>), String.class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>然后是创建路由配置类，用于定义路由节点。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RouterConfig</span> &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RouterFunction&lt;ServerResponse&gt; <span class="title function_">route</span><span class="params">(MyHandler handler)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> RouterFunctions</span><br><span class="line">            .route(RequestPredicates.GET(<span class="string">&quot;/hello&quot;</span>), handler::hello);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>此时传统的控制器被分成了处理器和路由配置两部分。</p><h4 id="统一响应结构"><a class="markdownIt-Anchor" href="#统一响应结构"></a> 统一响应结构</h4><p>在前后端分离项目，前后端之间的数据交互通常遵循统一的固定格式。</p><p>一个典型的固定格式包含以下几个关键部分：</p><ol><li><strong>状态码（Code）</strong>：表示请求处理的结果（如成功、错误的详细信息）。这通常是一个整数值，例如200表示成功，400表示客户端错误，500表示服务器错误。</li><li><strong>消息（Message）</strong>：提供关于请求处理结果的简短描述，尤其是在发生错误时。这有助于前端开发人员理解发生了什么问题，甚至可能向用户显示这些消息。</li><li><strong>数据（Data）</strong>：实际的响应数据，可以是任何结构，如对象、数组或简单值。这是请求的主要内容，包含了前端需要的所有数据。</li></ol><p>下面通过自定义 API 响应类来设置响应体格式</p><p>创建一个 API 响应类</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ApiResponse</span>&lt;T&gt; &#123;</span><br><span class="line">    <span class="keyword">private</span> Integer code;</span><br><span class="line">    <span class="keyword">private</span> String message;</span><br><span class="line">    <span class="keyword">private</span> T data; <span class="comment">// 使用泛型代替Object提高类型安全</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ApiResponse</span><span class="params">(Integer code, String message, T data)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.code = code;</span><br><span class="line">        <span class="built_in">this</span>.message = message;</span><br><span class="line">        <span class="built_in">this</span>.data = data;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 省略getters和setters...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>在处理器中使用自定义的 API 响应类封装数据</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyHandler</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">hello</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">        Mono&lt;ApiResponse&gt; resp = Mono.just(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>(<span class="number">200</span>, <span class="string">&quot;OK&quot;</span>, <span class="string">&quot;Hello, WebFlux!&quot;</span>););</span><br><span class="line">        <span class="keyword">return</span> ServerResponse.ok().body(resp, ApiResponse.class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyHandler</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">hello</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">        Mono&lt;ApiResponse&gt; resp = Mono.fromSupplier(() -&gt; <span class="keyword">new</span> <span class="title class_">ApiResponse</span>(<span class="number">200</span>, <span class="string">&quot;OK&quot;</span>, <span class="string">&quot;Hello, WebFlux!&quot;</span>));</span><br><span class="line">        <span class="keyword">return</span> ServerResponse.ok().body(resp, ApiResponse.class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>这里<code>Mono.just</code>和<code>Mono.fromSupplier</code>的区别是：</p><p><code>just</code>是立即创建并包装一个已经存在的值。当你需要包装一个已知的非异步值或非阻塞操作的结果时适合使用<code>just</code>。</p><p><code>fromSupplier</code>允许你在响应式流中延迟执行并动态地生成单个值。当你的操作是异步的或者你想要延迟执行某个操作直到订阅发生时适合使用<code>fromSupplier</code>。</p><blockquote><p>相关资料推荐：<a class="link"   href="https://zhuanlan.zhihu.com/p/334809573" >一文搞懂什么是RESTful API<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></blockquote><h4 id="crud-操作"><a class="markdownIt-Anchor" href="#crud-操作"></a> CRUD 操作</h4><p>Webflux 中进行 CRUD 操作的代码也和传统的 web 框架不太一样，这里大概介绍一下 Webflux 控制层中增删改查操作涉及的常用方法。</p><h5 id="路径参数与请求体的提取"><a class="markdownIt-Anchor" href="#路径参数与请求体的提取"></a> 路径参数与请求体的提取</h5><p>路径参数是 RESTful 风格中的概念，将 URL 中指定的值直接提取出来，不同于传统的查询参数，路径参数只包括了值本身，而“键”是在路由配置中定义的。</p><p>增删改和条件查询(单个结果)通常都需要一个路径参数指定全局唯一资源</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 增加(POST)</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">createItem</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">    Mono&lt;Item&gt; itemMono = request.bodyToMono(Item.class);</span><br><span class="line">    <span class="keyword">return</span> itemMono.flatMap(item -&gt; service.createItem(item))</span><br><span class="line">                   .flatMap(item -&gt; ServerResponse.ok().bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">200</span>, <span class="string">&quot;Item Created Successfully&quot;</span>, item)));</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 删除(DELETE)</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">deleteItem</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> request.pathVariable(<span class="string">&quot;id&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> service.deleteItem(id)</span><br><span class="line">                  .flatMap(item -&gt; ServerResponse.ok().bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">200</span>, <span class="string">&quot;Item Deleted Successfully&quot;</span>, item)));</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 更新(PUT)</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">updateItem</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> request.pathVariable(<span class="string">&quot;id&quot;</span>);</span><br><span class="line">    Mono&lt;Item&gt; itemMono = request.bodyToMono(Item.class);</span><br><span class="line">    <span class="keyword">return</span> itemMono.flatMap(item -&gt; service.updateItem(id, item))</span><br><span class="line">                   .flatMap(item -&gt; ServerResponse.ok().bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">200</span>, <span class="string">&quot;Item Updated Successfully&quot;</span>, item)));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>这里的<code>bodyToMono</code>方法用于将请求体转换为相应类型的<code>Mono</code>，主要是针对增加和更新操作，而<code>pathVariable</code>方法用于提取路径变量，主要是针对查询和删除操作。如果使用传统的查询参数可以使用<code>ServerRequest.queryParam</code>的方法获取。</p><h5 id="小结"><a class="markdownIt-Anchor" href="#小结"></a> 小结</h5><p>在 WebFlux 的函数式端点风格中，<code>ServerRequest</code>提供了多种方法来获取 HTTP 请求中的不同类型的数据：</p><ol><li><strong>获取响应体数据</strong>： 使用<code>request.bodyToMono(Class&lt;T&gt;)</code>来获取请求体中的数据，适用于 POST 和 PUT 请求。</li><li><strong>获取路径参数</strong>： 使用<code>request.pathVariable(&quot;name&quot;)</code>来获取 URL 路径中的变量，适用于任何类型的 HTTP 请求。</li><li><strong>获取查询参数</strong>： 使用<code>request.queryParam(&quot;name&quot;)</code>来获取 URL 查询参数，适用于 GET 请求。</li></ol><blockquote><h2 id="关于-flatmap"><a class="markdownIt-Anchor" href="#关于-flatmap"></a> 关于 flatMap</h2><p><code>flatMap</code>是响应式编程中的一个重要操作符，它用于处理流（如<code>Mono</code>或<code>Flux</code>中的元素），并将每个元素转换成另一个流，最后将这些流“扁平化”为一个流。在Reactor库中，这个操作符常用于处理异步操作的结果，尤其是当这些操作本身返回<code>Mono</code>或<code>Flux</code>时。</p><h3 id="具体是什么"><a class="markdownIt-Anchor" href="#具体是什么"></a> 具体是什么？</h3><p><code>flatMap</code>操作符接收一个函数作为参数。这个函数对源流中的每个元素进行操作，返回一个新的<code>Publisher</code>（<code>Mono</code>或<code>Flux</code>）。然后，<code>flatMap</code>将这些返回的<code>Publisher</code>合并成一个单一的<code>Publisher</code>。这使得<code>flatMap</code>非常适合处理嵌套的异步操作。</p><h3 id="应该在哪里使用它"><a class="markdownIt-Anchor" href="#应该在哪里使用它"></a> 应该在哪里使用它？</h3><ol><li><strong>链式异步调用</strong>：当你需要执行一系列的异步操作，并且后一个操作依赖于前一个操作的结果时。例如，从数据库查询一个对象，然后根据该对象的信息去查询另一个服务。</li><li><strong>转换和合并流</strong>：当你需要将流中的元素转换为另一种类型的流，并且希望最终结果是一个单一流时。</li><li><strong>处理嵌套的<code>Mono</code>或<code>Flux</code></strong>：当操作返回嵌套的<code>Mono&lt;Mono&lt;T&gt;&gt;</code>或<code>Flux&lt;Flux&lt;T&gt;&gt;</code>时，使用<code>flatMap</code>可以将其“扁平化”为<code>Mono&lt;T&gt;</code>或<code>Flux&lt;T&gt;</code>。</li></ol><h3 id="什么情况下使用它"><a class="markdownIt-Anchor" href="#什么情况下使用它"></a> 什么情况下使用它？</h3><ul><li>当你想要异步地转换数据，并且每个转换本身返回一个<code>Mono</code>或<code>Flux</code>时。</li><li>当你需要处理一个操作的结果，这个操作异步地返回另一个可观察的流时。</li><li>当你希望并发执行异步操作，并且需要合并它们的结果时。<code>flatMap</code>允许并发执行，但如果需要控制并发度，你可能需要使用其变体如<code>flatMapSequential</code>（保持原始序列顺序）或<code>flatMap</code>带有并发限制的重载方法。</li></ul></blockquote><h3 id="webflux-数据访问层"><a class="markdownIt-Anchor" href="#webflux-数据访问层"></a> Webflux 数据访问层</h3><h4 id="整合r2dbc"><a class="markdownIt-Anchor" href="#整合r2dbc"></a> 整合R2DBC</h4><h5 id="连接配置"><a class="markdownIt-Anchor" href="#连接配置"></a> 连接配置</h5><p>配置 application.yaml 文件，重点关注 url、username、password 这里的配置。</p><ul><li><strong>url</strong>：使用 r2dbc 协议连接 postgresql 数据库，默认本地端口 5432，数据库名称为 gradems。</li><li><strong>username</strong>：数据库用户名。在前面的 docker-compose.yaml 配置文件中已经指定。</li><li><strong>password</strong>：数据库密码。在前面的 docker-compose.yaml 配置文件中已经指定。</li></ul><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">r2dbc:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">r2dbc:postgresql://localhost:5432/gradems</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">admin</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">gradems</span></span><br><span class="line">    <span class="attr">pool:</span></span><br><span class="line">      <span class="attr">initial-size:</span> <span class="number">5</span></span><br><span class="line">      <span class="attr">max-size:</span> <span class="number">20</span></span><br></pre></td></tr></table></figure></div><h5 id="代码实现方式"><a class="markdownIt-Anchor" href="#代码实现方式"></a> 代码实现方式</h5><p>在使用 WebFlux 框架时，结合 R2DBC 进行反应式数据库操作，主要有以下几种实现方式：</p><ol><li><strong>Repository方式</strong>：利用Spring Data R2DBC提供的Repository支持，可以通过定义接口来自动实现反应式CRUD操作。这种方式极大简化了数据访问层的代码，开发者只需定义接口并选择性地添加查询方法，Spring Data R2DBC会自动实现这些接口。</li><li><strong>DatabaseClient API</strong>：Spring Data R2DBC提供了一个高级的<code>DatabaseClient</code> API，适用于更灵活的数据库访问和查询构建。它支持声明式的查询操作，允许开发者以链式调用的方式构建查询并处理结果。这种方式提供了比Repository更灵活的数据访问能力，适合于复杂查询和自定义操作。</li><li><strong>R2DBC EntityTemplate</strong>：<code>R2dbcEntityTemplate</code>提供了一个基于模板的方法来执行数据库操作，包括插入、更新、查询和删除等CRUD操作。它为操作单一实体或批量实体提供了便捷的API，并且可以与R2DBC的反应式特性很好地结合使用。</li><li><strong>自定义R2DBC使用</strong>：除了上述封装好的高级API外，开发者还可以直接使用R2DBC的底层API来执行数据库操作。这种方式需要开发者手动管理数据库连接和查询执行，提供了最大的灵活性和控制力，适用于需要高度定制化数据库操作的场景。</li><li><strong>R2DBC QueryDSL</strong>：对于需要构建类型安全的复杂查询的应用，可以考虑使用QueryDSL与R2DBC结合的方式。这种方法允许开发者以DSL（Domain-Specific Language）的方式构建查询，提高了代码的可读性和维护性。</li></ol><p>这里主要介绍最常用的 Repository 方式和 DatabaseClient API 方式，基本满足大部分应用场景。</p><p>首先创建以下实体类<code>Person</code></p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Table(&quot;people&quot;)</span> <span class="comment">// 如果数据表名和类名匹配，则该注解可省略，否则需要指定映射数据表名</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Person</span> &#123;</span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="meta">@Column(&quot;name&quot;)</span> <span class="comment">// 与@Table类似的映射作用，如果名称相同可以省略</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Getters and setters...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p><code>@Id</code>注解告诉Spring Data R2DBC框架哪个属性用作数据库表中的主键列。Spring Data R2DBC 依赖于该注解来识别实体的唯一标识符（ID），从而执行诸如查找、更新或删除特定实体的操作。如果缺少<code>@Id</code>注解，Spring Data R2DBC 可能无法正确执行这些操作，因为它无法确定哪个字段是实体的主键。</p><h6 id="使用repository-方式"><a class="markdownIt-Anchor" href="#使用repository-方式"></a> <strong>使用Repository 方式</strong></h6><p>定义<code>PersonRepository</code>接口，<code>ReactiveCrudRepository</code>具有方法名解析的功能，即可以通过方法名解析自动实现该方法的功能，方法名需要遵循特定的命名规则。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">PersonRepository</span> <span class="keyword">extends</span> <span class="title class_">ReactiveCrudRepository</span>&lt;Person, Long&gt; &#123;</span><br><span class="line">    Mono&lt;Person&gt; <span class="title function_">findByName</span><span class="params">(String name)</span>; <span class="comment">// 查询单个结果</span></span><br><span class="line">    Flux&lt;Person&gt; <span class="title function_">findByAge</span><span class="params">(<span class="type">int</span> age)</span>; <span class="comment">// 查询多个结果</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>CRUD 操作示例 <strong>(在服务层实现)</strong></p><blockquote><p>这里涉及的方法是<code>ReactiveCrudRepository</code>自带的接口实现。</p></blockquote><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 增加</span></span><br><span class="line"><span class="type">Person</span> <span class="variable">person</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Person</span>();</span><br><span class="line">person.setName(<span class="string">&quot;John Doe&quot;</span>);</span><br><span class="line">person.setAge(<span class="number">25</span>);</span><br><span class="line">Mono&lt;Person&gt; savedPerson = personRepository.save(person);</span><br><span class="line"><span class="comment">// 删除</span></span><br><span class="line">Mono&lt;Void&gt; deletePerson = personRepository.deleteById(<span class="number">1L</span>);</span><br><span class="line"><span class="comment">// 局部更新</span></span><br><span class="line">Mono&lt;Person&gt; updatedPerson = personRepository.findById(<span class="number">1L</span>)</span><br><span class="line">    .map(p -&gt; &#123;</span><br><span class="line">        p.setName(<span class="string">&quot;Jane Doe&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> p;</span><br><span class="line">    &#125;)</span><br><span class="line">    .flatMap(personRepository::save);</span><br><span class="line"><span class="comment">// 更新</span></span><br><span class="line"><span class="comment">// 更新实际上是通过 save 方法实现的，如果对象存在则更新，否则插入</span></span><br><span class="line"><span class="type">Person</span> <span class="variable">person</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Person</span>();</span><br><span class="line">person.setName(<span class="string">&quot;John Doe&quot;</span>);</span><br><span class="line">person.setAge(<span class="number">30</span>);</span><br><span class="line">Mono&lt;Person&gt; savedPerson = personRepository.save(person);</span><br><span class="line"><span class="comment">// 查询</span></span><br><span class="line"><span class="comment">// 查询单个结果</span></span><br><span class="line">Mono&lt;Person&gt; person = personRepository.findById(<span class="number">1L</span>);</span><br><span class="line"><span class="comment">// 查询多个结果</span></span><br><span class="line">Flux&lt;Person&gt; people = personRepository.findAll();</span><br></pre></td></tr></table></figure></div><h6 id="使用databaseclient-api"><a class="markdownIt-Anchor" href="#使用databaseclient-api"></a> <strong>使用DatabaseClient API</strong></h6><p>当使用Spring Data R2DBC的<code>DatabaseClient</code>进行数据库操作时，你会遇到一系列的链式调用方法，这些方法为构建和执行数据库命令提供了灵活的接口。下面是这些方法的简要说明：</p><p><code>.sql(String sql)</code></p><ul><li><strong>含义</strong>: 该方法用于指定要执行的SQL命令。</li><li><strong>用法</strong>: <code>.sql(&quot;SELECT * FROM my_table WHERE id = :id&quot;)</code>，其中字符串为要执行的SQL命令。</li></ul><p><code>.bind(String name, Object value)</code></p><ul><li><strong>含义</strong>: 用于绑定SQL命令中的参数。这是一个防止SQL注入的安全做法，比直接在SQL命令字符串中拼接参数更安全。</li><li><strong>用法</strong>: <code>.bind(&quot;id&quot;, 1)</code>，这会将SQL命令中的<code>:id</code>参数替换为<code>1</code>。</li></ul><p><code>.bind(index, value)</code></p><ul><li><strong>含义</strong>: 除了按名称绑定参数外，也可以按位置绑定参数。这在使用没有命名参数的数据库或驱动时特别有用。</li><li><strong>用法</strong>: <code>.bind(0, &quot;value&quot;)</code>，这会将SQL命令中的第一个参数（基于0的索引）绑定为指定的值。</li></ul><p><code>.bindNull(String name, Class&lt;?&gt; type)</code></p><ul><li><strong>含义</strong>: 用于绑定SQL命令中的参数为<code>NULL</code>值，同时指定该<code>NULL</code>值的类型。</li><li><strong>用法</strong>: <code>.bindNull(&quot;parameterName&quot;, String.class)</code>，这会将SQL命令中的<code>:parameterName</code>参数绑定为<code>NULL</code>，类型为<code>String</code>。</li></ul><p><code>.then()</code></p><ul><li><p><strong>含义</strong>: 在Reactor中，<code>.then()</code>方法用于忽略前面步骤的结果，并在上一个操作完成后继续执行。在<code>DatabaseClient</code>的上下文中，<code>.then()</code>通常用于执行完数据库操作（如插入、更新、删除）后，不需要处理任何返回结果，只需知道操作已成功完成。</p></li><li><p><strong>用法</strong>: <code>.then()</code>用于链式调用的最后，返回一个<code>Mono&lt;Void&gt;</code>，表示操作完成的信号。</p><blockquote><p>使用<code>.then()</code>方法后就不能使用<code>.fetch()</code>方法，因为一个不需要返回实际结果，一个需要返回结果。</p></blockquote></li></ul><p><code>.map(Function&lt;Row, T&gt; mappingFunction)</code></p><ul><li><p><strong>含义</strong>: 该方法用于将结果集中的每一行（<code>Row</code>）映射（或转换）为另一种类型（<code>T</code>）。这是通过提供一个函数来完成的，该函数接受一个<code>Row</code>对象并返回一个新类型的对象。</p></li><li><p><strong>用法</strong>: <code>.map(row -&gt; new MyObject(row.get(&quot;column_name&quot;, String.class)))</code>，其中<code>MyObject</code>是自定义类，<code>column_name</code>是结果集中的列名。<strong><code>.map</code>方法在查询操作中几乎是一定会使用到的方法。</strong></p><blockquote><p><code>.map()</code>在这里是对结果集中的每一项进行转换的操作，它是基于结果流<code>Flux</code>或单个结果<code>Mono</code>的Reactor操作符。当需要对查询结果进行转换时，<code>.map()</code>方法通常与<code>.all()</code>, <code>.one()</code>, 或<code>.first()</code>一起使用。</p></blockquote></li></ul><p><code>.one()</code></p><ul><li><p><strong>含义</strong>: 用于从反应式数据库操作中获取恰好一个结果的<code>Mono&lt;T&gt;</code>。如果查询结果为空，则返回<code>Mono.empty()</code>；如果查询结果有多于一条记录，通常会抛出异常。这适用于你期望查询返回单一结果的场景。</p></li><li><p><strong>用法</strong>: 在执行数据库查询操作时使用，适合于返回单条记录的查询。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Mono&lt;Person&gt; person = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        row.get(<span class="string">&quot;id&quot;</span>, Long.class), </span><br><span class="line">        row.get(<span class="string">&quot;name&quot;</span>, String.class), </span><br><span class="line">        row.get(<span class="string">&quot;age&quot;</span>, Integer.class)))</span><br><span class="line">    .one();</span><br></pre></td></tr></table></figure></div></li></ul><p><code>.first()</code></p><ul><li><p><strong>含义</strong>: 与<code>.one()</code>类似，但当存在多条记录时，它会返回结果集中的第一条记录包装在<code>Mono&lt;T&gt;</code>中。如果查询结果为空，则返回<code>Mono.empty()</code>。这适用于你只关心结果集的第一条记录，而不在乎是否还有其他记录的场景。</p></li><li><p><strong>用法</strong>: 在执行数据库查询操作时使用，当你只需要查询结果中的第一条记录。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Mono&lt;Person&gt; person = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person&quot;</span>)</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        row.get(<span class="string">&quot;id&quot;</span>, Long.class), </span><br><span class="line">        row.get(<span class="string">&quot;name&quot;</span>, String.class), </span><br><span class="line">        row.get(<span class="string">&quot;age&quot;</span>, Integer.class)))</span><br><span class="line">    .first();</span><br></pre></td></tr></table></figure></div></li></ul><p><code>.all()</code></p><ul><li><p><strong>含义</strong>: 用于获取操作结果中的所有记录，返回一个包含所有结果的<code>Flux&lt;T&gt;</code>。这适用于你需要处理查询返回的所有记录的场景。</p></li><li><p><strong>用法</strong>: 在执行数据库查询操作时使用，特别是当你期望获取多条记录作为响应。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Flux&lt;Person&gt; people = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person&quot;</span>)</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        row.get(<span class="string">&quot;id&quot;</span>, Long.class), </span><br><span class="line">        row.get(<span class="string">&quot;name&quot;</span>, String.class), </span><br><span class="line">        row.get(<span class="string">&quot;age&quot;</span>, Integer.class)))</span><br><span class="line">    .all();</span><br></pre></td></tr></table></figure></div></li></ul><p><code>.fetch()</code></p><ul><li><p><strong>含义</strong>: 该方法用于执行前面定义的SQL命令，并开始处理返回的结果。</p></li><li><p><strong>用法</strong>: 通常跟在<code>.sql()</code>和参数绑定方法（如<code>.bind()</code>）之后使用。</p><blockquote><p><code>.fetch()</code>方法用于指定如何处理SQL执行的结果，它返回一个<code>FetchSpec</code>，允许你使用<code>.rowsUpdated()</code>来获取受影响的行数(只对于增加、更新、删除等影响到实际数据的操作)。</p><p><code>fetch</code>同样可以结合<code>one</code>、<code>first</code>、<code>all</code>使用，但是因为<code>fetch</code>返回的是<code>FetchSpec&lt;Map&lt;String, Object&gt;&gt;</code>类型，需要手动强制转换，对于需要查询返回大量字段的操作不是很推荐，通常查询只返回单个字段或少量字段的可以使用。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">Mono&lt;Person&gt; person = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .fetch()</span><br><span class="line">    .one()</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        (Long) row.get(<span class="string">&quot;id&quot;</span>), </span><br><span class="line">        (String) row.get(<span class="string">&quot;name&quot;</span>), </span><br><span class="line">        (Integer) row.get(<span class="string">&quot;age&quot;</span>)));</span><br><span class="line"></span><br><span class="line">Mono&lt;Person&gt; person = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .fetch()</span><br><span class="line">    .first()</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        (Long) row.get(<span class="string">&quot;id&quot;</span>), </span><br><span class="line">        (String) row.get(<span class="string">&quot;name&quot;</span>), </span><br><span class="line">        (Integer) row.get(<span class="string">&quot;age&quot;</span>)));</span><br><span class="line"></span><br><span class="line">Flux&lt;Person&gt; person = databaseClient</span><br><span class="line">    .sql(<span class="string">&quot;SELECT * FROM person&quot;</span>)</span><br><span class="line">    .fetch()</span><br><span class="line">    .all()</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(</span><br><span class="line">        (Long) row.get(<span class="string">&quot;id&quot;</span>), </span><br><span class="line">        (String) row.get(<span class="string">&quot;name&quot;</span>), </span><br><span class="line">        (Integer) row.get(<span class="string">&quot;age&quot;</span>)));</span><br></pre></td></tr></table></figure></div><h3 id="比较两个版本"><a class="markdownIt-Anchor" href="#比较两个版本"></a> 比较两个版本</h3><ul><li><strong><code>fetch</code>版本</strong>：遵循了更常见的Spring Data R2DBC使用模式，即先执行<code>.fetch().all()</code>获取查询结果，然后通过<code>.map()</code>转换。</li><li><strong><code>map</code>版本</strong>：利用了Spring Data R2DBC提供的一个更直接的映射策略，允许在定义SQL之后立即应用映射函数。这个方法的优势在于代码更简洁，但可能在某些情况下减少了对查询结果处理流程的可见性。</li></ul></blockquote></li></ul><p><code>.rowsUpdated()</code></p><ul><li><p><strong>含义</strong>: 在执行更新、插入或删除操作后，<code>.rowsUpdated()</code>用于返回受这些操作影响的行数。</p></li><li><p><strong>用法</strong>: 用于确认操作影响了多少行数据。这个方法只会在增删改等影响实际数据的操作中使用。</p><blockquote><p><code>.fetch().rowsUpdated()</code>组合使用返回受影响的行数。</p></blockquote></li></ul><p>CRUD 操作示例 <strong>(在数据访问层实现)</strong></p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 增加</span></span><br><span class="line"><span class="type">Person</span> <span class="variable">person</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Person</span>();</span><br><span class="line">person.setName(<span class="string">&quot;John Doe&quot;</span>);</span><br><span class="line">person.setAge(<span class="number">25</span>);</span><br><span class="line">Mono&lt;Person&gt; savedPerson = databaseClient.sql(<span class="string">&quot;INSERT INTO person (name, age) VALUES (:name, :age)&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;name&quot;</span>, person.getName())</span><br><span class="line">    .bind(<span class="string">&quot;age&quot;</span>, person.getAge())</span><br><span class="line">    .fetch()</span><br><span class="line">    .rowsUpdated();</span><br><span class="line"><span class="comment">// 删除</span></span><br><span class="line">Mono&lt;Long&gt; deleteResult = databaseClient.sql(<span class="string">&quot;DELETE FROM person WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .fetch()</span><br><span class="line">    .rowsUpdated();</span><br><span class="line"><span class="comment">// 更新</span></span><br><span class="line">Mono&lt;Long&gt; updateResult = databaseClient.sql(<span class="string">&quot;UPDATE person SET name = :name WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;Jane Doe&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .fetch()</span><br><span class="line">    .rowsUpdated();</span><br><span class="line"><span class="comment">// 查询</span></span><br><span class="line"><span class="comment">// 查询单个结果</span></span><br><span class="line">Mono&lt;Person&gt; person = databaseClient.sql(<span class="string">&quot;SELECT * FROM person WHERE id = :id&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;id&quot;</span>, <span class="number">1</span>)</span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="comment">/* initialize properties from row */</span>))</span><br><span class="line">    .one();</span><br><span class="line"><span class="comment">// 查询多个结果</span></span><br><span class="line">Flux&lt;Person&gt; people = databaseClient.sql(<span class="string">&quot;SELECT * FROM person&quot;</span>)    </span><br><span class="line">    .map(row -&gt; <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="comment">/* initialize properties from row */</span>))</span><br><span class="line">    .all();</span><br></pre></td></tr></table></figure></div><p>批量增删改操作示例</p><blockquote><p>对于批量操作，更推荐的做法是使用请求体（Request Body）来传递数据，特别是使用<code>POST</code>或<code>PUT</code>方法时。这样可以无限制地传递大量数据，同时保持URL的简洁和安全性。</p></blockquote><p>普通版本</p><ul><li><p>批量增加操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Flux&lt;Person&gt; persons = Flux.just(<span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;John Doe&quot;</span>, <span class="number">25</span>), <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;Jane Doe&quot;</span>, <span class="number">30</span>));</span><br><span class="line">Mono&lt;Long&gt; insertResults = persons.flatMap(person -&gt;</span><br><span class="line">    databaseClient.sql(<span class="string">&quot;INSERT INTO person (name, age) VALUES (:name, :age)&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;name&quot;</span>, person.getName())</span><br><span class="line">        .bind(<span class="string">&quot;age&quot;</span>, person.getAge())</span><br><span class="line">        .fetch()</span><br><span class="line">        .rowsUpdated()</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum); <span class="comment">// 计数所有操作的总影响行数</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line">List&lt;Person&gt; persons = Arrays.asList(<span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;John Doe&quot;</span>, <span class="number">25</span>), <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;Jane Doe&quot;</span>, <span class="number">30</span>));</span><br><span class="line">Mono&lt;Long&gt; insertResults = Flux.fromIterable(persons)</span><br><span class="line">    .flatMap(person -&gt;</span><br><span class="line">        databaseClient.sql(<span class="string">&quot;INSERT INTO person (name, age) VALUES (:name, :age)&quot;</span>)</span><br><span class="line">            .bind(<span class="string">&quot;name&quot;</span>, person.getName())</span><br><span class="line">            .bind(<span class="string">&quot;age&quot;</span>, person.getAge())</span><br><span class="line">            .fetch()</span><br><span class="line">            .rowsUpdated()</span><br><span class="line">    ).reduce(<span class="number">0L</span>, Long::sum);</span><br></pre></td></tr></table></figure></div><blockquote><p><code>count</code>和<code>reduce</code>都是统计总数的方法，不过<code>count</code>统计的是流中元素的数量，是操作的总数，而<code>reduce</code>统计的是实际操作成功的总数。</p></blockquote></li><li><p>批量删除操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">Flux&lt;Long&gt; idsToDelete = Flux.just(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>); <span class="comment">// 假设这是要删除的ID列表</span></span><br><span class="line">Mono&lt;Long&gt; deleteResults = idsToDelete.collectList().flatMap(ids -&gt;</span><br><span class="line">    databaseClient.sql(<span class="string">&quot;DELETE FROM person WHERE id IN (:ids)&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">        .fetch()</span><br><span class="line">        .rowsUpdated()</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line">List&lt;Long&gt; idsToDelete = Arrays.asList(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>);</span><br><span class="line">Mono&lt;Long&gt; deleteResults = Flux.fromIterable(idsToDelete)</span><br><span class="line">    .collectList()</span><br><span class="line">    .flatMap(ids -&gt;</span><br><span class="line">        databaseClient.sql(<span class="string">&quot;DELETE FROM person WHERE id IN (:ids)&quot;</span>)</span><br><span class="line">            .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">            .fetch()</span><br><span class="line">            .rowsUpdated()</span><br><span class="line">    );</span><br></pre></td></tr></table></figure></div></li><li><p>批量更新操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 假设要将所有指定ID的人的年龄更新为30</span></span><br><span class="line">Flux&lt;Long&gt; idsToUpdate = Flux.just(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>);</span><br><span class="line">Mono&lt;Long&gt; updateResults = idsToUpdate.flatMap(ids -&gt;</span><br><span class="line">    databaseClient.sql(<span class="string">&quot;UPDATE person SET age = :age WHERE id = (:ids)&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;age&quot;</span>, <span class="number">30</span>)</span><br><span class="line">        .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">        .fetch()</span><br><span class="line">        .rowsUpdated()</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line">List&lt;Long&gt; idsToUpdate = Arrays.asList(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>);</span><br><span class="line">Mono&lt;Long&gt; updateResults = Flux.fromIterable(idsToUpdate)</span><br><span class="line">    .flatMap(ids -&gt;</span><br><span class="line">        databaseClient.sql(<span class="string">&quot;UPDATE person SET age = :age WHERE id = (:ids)&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;age&quot;</span>, <span class="number">30</span>)</span><br><span class="line">        .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">        .fetch()</span><br><span class="line">        .rowsUpdated()</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum);</span><br></pre></td></tr></table></figure></div></li></ul><p><code>Flux.defer</code>版本</p><ul><li><p>批量增加操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">List&lt;Person&gt; persons = Arrays.asList(<span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;John Doe&quot;</span>, <span class="number">25</span>), <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;Jane Doe&quot;</span>, <span class="number">30</span>));</span><br><span class="line">Mono&lt;Long&gt; insertResults = Flux.defer(() -&gt; </span><br><span class="line">    Flux.fromIterable(persons)</span><br><span class="line">        .flatMap(person -&gt;</span><br><span class="line">            databaseClient.sql(<span class="string">&quot;INSERT INTO person (name, age) VALUES (:name, :age)&quot;</span>)</span><br><span class="line">                .bind(<span class="string">&quot;name&quot;</span>, person.getName())</span><br><span class="line">                .bind(<span class="string">&quot;age&quot;</span>, person.getAge())</span><br><span class="line">                .fetch()</span><br><span class="line">                .rowsUpdated()</span><br><span class="line">        )</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum);</span><br></pre></td></tr></table></figure></div></li><li><p>批量删除操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">List&lt;Long&gt; idsToDelete = Arrays.asList(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>);</span><br><span class="line">Mono&lt;Long&gt; deleteResults = Flux.defer(() -&gt; </span><br><span class="line">    Flux.fromIterable(idsToDelete)</span><br><span class="line">        .collectList()</span><br><span class="line">        .flatMap(ids -&gt;</span><br><span class="line">            databaseClient.sql(<span class="string">&quot;DELETE FROM person WHERE id IN (:ids)&quot;</span>)</span><br><span class="line">                .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">                .fetch()</span><br><span class="line">                .rowsUpdated()</span><br><span class="line">        )</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum);</span><br></pre></td></tr></table></figure></div></li><li><p>批量修改操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">List&lt;Long&gt; idsToUpdate = Arrays.asList(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>);</span><br><span class="line">Mono&lt;Long&gt; updateResults = Flux.defer(() -&gt; </span><br><span class="line">Flux.fromIterable(idsToUpdate)</span><br><span class="line">    .flatMap(ids -&gt;</span><br><span class="line">        databaseClient.sql(<span class="string">&quot;UPDATE person SET age = :age WHERE id = (:ids)&quot;</span>)</span><br><span class="line">    .bind(<span class="string">&quot;age&quot;</span>, <span class="number">30</span>)</span><br><span class="line">        .bind(<span class="string">&quot;ids&quot;</span>, ids)</span><br><span class="line">        .fetch()</span><br><span class="line">        .rowsUpdated()</span><br><span class="line">).reduce(<span class="number">0L</span>, Long::sum);</span><br></pre></td></tr></table></figure></div></li></ul><blockquote><p>使用<code>Flux.defer</code>适合于某些特定的场景，主要是当你想确保每次订阅时都执行最新的操作逻辑，特别是在处理动态数据或需要确保操作使用最新状态的场景下。<code>Flux.defer</code>通过延迟操作的创建直到订阅发生，可以确保数据的实时性和操作的最新性。然而，并不是所有场景都适合使用<code>Flux.defer</code>。下面是一些考虑因素：</p><h3 id="适合使用fluxdefer的场景"><a class="markdownIt-Anchor" href="#适合使用fluxdefer的场景"></a> 适合使用<code>Flux.defer</code>的场景</h3><ul><li><strong>动态数据源</strong>：当数据源是动态变化的，比如依赖于时间或外部系统状态，使用<code>Flux.defer</code>可以确保每次订阅都获取最新数据。</li><li><strong>避免不必要的计算</strong>：如果有一些计算成本较高的操作，你只想在实际需要数据时才执行，<code>Flux.defer</code>可以帮助避免不必要的计算。</li><li><strong>条件性操作</strong>：当操作需要基于每次订阅时的特定条件来执行，使用<code>Flux.defer</code>可以确保操作符合当前的条件。</li></ul><h3 id="不适合使用fluxdefer的场景"><a class="markdownIt-Anchor" href="#不适合使用fluxdefer的场景"></a> 不适合使用<code>Flux.defer</code>的场景</h3><ul><li><strong>静态数据或一次性计算</strong>：对于不会改变的数据或只需要计算一次的操作，使用<code>Flux.defer</code>可能没有必要，直接使用<code>Flux.just</code>或<code>Mono.just</code>等操作可能更合适。</li><li><strong>共享操作结果</strong>：如果你想让所有订阅者共享同一个操作结果，而不是为每个订阅者重新执行操作，那么<code>Flux.defer</code>不是一个好选择。在这种情况下，可以考虑使用<code>.cache()</code>操作符。</li><li><strong>需要即时执行的操作</strong>：由于<code>Flux.defer</code>会延迟操作直到订阅发生，如果你需要在定义流的同时立即执行某些操作（例如，预加载数据），<code>Flux.defer</code>可能不适合。</li></ul></blockquote><h5 id="关于响应式-defer-的理解"><a class="markdownIt-Anchor" href="#关于响应式-defer-的理解"></a> 关于响应式 <code>defer</code> 的理解</h5><ol><li><strong>对于动态或实时数据</strong>：如果操作涉及获取可能随时间变化的数据，或者其结果依赖于当前的系统状态（如数据库中的数据、系统时间或外部服务的状态），那么使用<code>defer</code>是合适的。这确保了每次订阅都能获取到最新的数据，避免了由于早期执行而可能导致的数据过时问题。</li><li><strong>避免抢注问题</strong>：在用户注册的场景中，使用<code>defer</code>可以确保在订阅（也就是实际执行注册逻辑）的时刻检查用户名是否已存在。这有助于减少因多个用户几乎同时注册相同用户名而导致的冲突。</li><li><strong>非一成不变的数据</strong>：对于不是一成不变的数据，特别是那些可能受到外部状态影响的操作，使用<code>defer</code>来延迟操作的创建和执行，直到订阅发生，是一个很好的实践。这样做可以确保操作的执行反映了最新的状态，提高了程序的健壮性和数据的准确性。</li><li><strong>有副作用的操作</strong>：如果操作具有副作用（如修改数据库、调用外部服务等），使用<code>Mono.defer</code>确保副作用操作在每次订阅时执行，而不是在声明阶段就执行，这有助于更好地控制副作用的发生时机。</li></ol><p><strong>通俗理解</strong></p><ul><li>如果结果可能随着时间或状态的变化而变化，或者操作具有副作用，那么使用<code>defer</code>。</li><li>对于静态的、确定不变的数据或结果，直接使用<code>Mono.just</code>或<code>Flux.fromIterable</code>等其他不涉及延迟执行的操作符即可。</li></ul><p>不过，如果完全采用响应式编程，大部分情况下，都不需要显式地使用<code>defer</code>。</p><p><code>defer</code>在响应式编程中通常在以下几种情况下需要显式使用：</p><ol><li><p><strong>确保操作延迟执行</strong>：当你需要确保某个操作（如数据库查询、外部服务调用等）确实在每次订阅时执行，而不是在声明流时就执行。这对于包含时间相关的操作或依赖于每次请求可能不同的上下文信息的操作尤其重要。</p></li><li><p><strong>使用动态或实时参数</strong>：当操作的参数在声明流的时间点可能还不确定，或者参数值可能随时间变化（例如，依赖于用户的实时输入或其他变化的系统状态）时，<code>defer</code>可以确保参数是在实际执行操作时才被评估和使用。</p></li><li><p><strong>避免副作用的预先执行</strong>：如果某个操作具有副作用（比如修改全局状态或外部资源），使用<code>defer</code>可以防止这些副作用在不适当的时间（如流声明时）就发生，确保副作用操作在订阅时才执行。</p></li><li><p><strong>封装非响应式操作</strong>：当需要将非响应式操作（比如传统的同步方法调用）转换为响应式流时，<code>defer</code>可以用来封装这些操作，确保它们在响应式上下文中以非阻塞的方式延迟执行。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ReactiveService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 假设这是一个非响应式的同步操作</span></span><br><span class="line">    String <span class="title function_">nonReactiveOperation</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 模拟一些耗时操作，比如数据库查询或远程服务调用</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Thread.sleep(<span class="number">1000</span>); <span class="comment">// 模拟耗时</span></span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            Thread.currentThread().interrupt();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;非响应式操作的结果&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 使用Mono.defer封装非响应式操作</span></span><br><span class="line">    Mono&lt;String&gt; <span class="title function_">wrapNonReactiveOperationWithDefer</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Mono.defer(() -&gt; &#123;</span><br><span class="line">            <span class="comment">// 调用非响应式的同步操作，并将其结果封装为Mono</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">result</span> <span class="operator">=</span> nonReactiveOperation();</span><br><span class="line">            <span class="keyword">return</span> Mono.just(result);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ol><h4 id="整合-reactive-mongodb-reactive-redis"><a class="markdownIt-Anchor" href="#整合-reactive-mongodb-reactive-redis"></a> 整合 Reactive MongoDB、Reactive Redis</h4><h5 id="文档存储"><a class="markdownIt-Anchor" href="#文档存储"></a> 文档存储</h5><p>文档存储是一种非关系型数据库（NoSQL）存储方式，它以文档的形式存储和管理数据。文档存储数据库将数据存储为文档，这些文档可以是JSON、XML或其他类似的格式。文档内部可以包含嵌套的数据结构，如列表或字典，从而允许存储复杂的数据结构。文档存储数据库的例子包括 MongoDB、CouchDB 和 Amazon DynamoDB 等。</p><p>文档存储与传统的关系型数据库存储（如MySQL、PostgreSQL等）相比，主要有以下区别：</p><ol><li><strong>数据模型</strong>：关系型数据库基于表格模型，数据以行和列的形式存储，且每个表格通常需要预定义的模式（schema）。文档存储数据库则以更灵活的文档格式存储数据，每个文档可以有不同的结构，不需要预定义模式。</li><li><strong>灵活性</strong>：文档存储数据库因其无模式（schema-less）特性而提供更高的灵活性，便于存储结构化或半结构化数据，同时也更容易适应数据结构的变化。</li><li><strong>查询语言</strong>：关系型数据库使用标准的SQL作为查询语言，而文档存储数据库使用基于文档的查询语言，这些语言通常更适合于处理文档型数据。</li><li><strong>扩展性</strong>：文档存储数据库通常更容易水平扩展，支持分布式数据存储，而关系型数据库的扩展通常更复杂，尤其是在处理大规模数据时。</li><li><strong>一致性和事务处理</strong>：传统的关系型数据库通常提供强一致性和复杂的事务处理特性。相比之下，许多文档存储数据库采用最终一致性模型，并可能提供较为有限的事务处理能力，虽然一些文档存储数据库（如最新版本的MongoDB）已开始支持更复杂的事务处理。</li><li><strong>用例</strong>：文档存储数据库适合于需要存储大量非结构化或半结构化数据的应用场景，如内容管理系统、电商平台等。而关系型数据库则适合于需要复杂查询和事务处理的传统企业级应用。</li></ol><h5 id="键值对存储"><a class="markdownIt-Anchor" href="#键值对存储"></a> 键值对存储</h5><p>键值对存储是一种简单的数据存储模型，它通过键（key）来访问和存储数据值（value）。这种模型类似于字典或哈希表，其中每个键唯一对应一个值。键值对存储系统通常用于缓存、会话存储、简单的数据记录等场景。与文档存储和关系型数据库相比，键值对存储提供了高速的查找和存储能力，但在数据结构的复杂性和查询能力上相对有限。键值对存储数据库的典型例子就是 Redis。</p><p>以下是键值对存储方式的一些主要特点和与传统数据库存储的区别：</p><ol><li><strong>数据模型简单</strong>：键值对存储的数据模型极其简单，每个键对应一个值，值可以是字符串、数字或者更复杂的数据结构（取决于具体的键值对存储系统）。不同于关系型数据库的行和列或文档存储的JSON文档，键值对存储不关心值的内部结构。</li><li><strong>高性能</strong>：由于数据模型的简单，键值对存储能够提供非常高的读写性能。这使得键值对存储非常适合用作应用程序的缓存层，可以快速响应数据读取请求。</li><li><strong>易于扩展</strong>：键值对存储通常支持水平扩展，可以通过添加更多节点来增加数据库的容量和吞吐量，而不需要复杂的数据迁移或重构。</li><li><strong>灵活性</strong>：虽然键值对存储的数据模型相对简单，但其值的存储格式可以非常灵活。一些键值对存储系统允许存储复杂的数据结构作为值，如列表、集合或哈希表等。</li><li><strong>有限的查询能力</strong>：与关系型数据库或文档存储相比，键值对存储的查询能力较为有限。通常，只能通过键直接访问数据，缺乏对值的复杂查询能力，如基于值的内容进行搜索或过滤。</li><li><strong>适用场景</strong>：键值对存储特别适合于需要快速读写访问的场景，如缓存、会话存储（session storage）、实时计数器等。它不适合需要复杂查询或数据关联分析的应用。</li></ol><blockquote><p>关于 MongoDB 和 Redis 的详细介绍参考<a class="link"   href="https://www.runoob.com/mongodb/mongodb-tutorial.html" >MongoDB 教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 和 <a class="link"   href="https://www.runoob.com/redis/redis-tutorial.html" >Redis 教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p></blockquote><h5 id="连接配置-2"><a class="markdownIt-Anchor" href="#连接配置-2"></a> 连接配置</h5><p>配置 application.yaml 文件，参考如下</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="comment"># ... r2dbc 的配置内容</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">mongodb:</span></span><br><span class="line">      <span class="attr">uri:</span> <span class="string">mongodb://localhost:27017/your_database</span> <span class="comment"># MongoDB连接URI</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="string">localhost</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">6379</span></span><br><span class="line">      <span class="attr">reactive:</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">8</span> <span class="comment"># 根据需要调整连接池大小</span></span><br></pre></td></tr></table></figure></div><p>这里的<code>your_database</code>是自定义的数据库名称，它会在保存数据时自动创建，不需要手动创建。</p><p>特别的，对于 Redis，由于其本身的技术特性和数据模型，导致其无法通过简单的声明式配置实现，通常还需要专门的编程式配置，即配置类。</p><p>下面提供了两种配置类：</p><blockquote><p>注意：部分基础配置已经在前面的 application.yaml 中配置完成，下面的配置都是高度自定义的配置。</p></blockquote><p>使用<code>Jackson2JsonRedisSerializer</code>配置</p><p>当使用<code>Jackson2JsonRedisSerializer</code>时，你需要为每种类型提供一个序列化器实例。这意味着如果你有多种类型的数据需要存储，你可能需要为每种类型配置不同的<code>ReactiveRedisTemplate</code>实例。不过，通常情况下，一个应用中使用单一类型的情况较多。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JacksonRedisConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; ReactiveRedisTemplate&lt;String, T&gt; <span class="title function_">reactiveRedisTemplateWithJackson</span><span class="params">(ReactiveRedisConnectionFactory connectionFactory, Class&lt;T&gt; type)</span> &#123;</span><br><span class="line">        Jackson2JsonRedisSerializer&lt;T&gt; valueSerializer = <span class="keyword">new</span> <span class="title class_">Jackson2JsonRedisSerializer</span>&lt;&gt;(type);</span><br><span class="line">        <span class="type">StringRedisSerializer</span> <span class="variable">keySerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringRedisSerializer</span>();</span><br><span class="line">        </span><br><span class="line">        RedisSerializationContext.RedisSerializationContextBuilder&lt;String, T&gt; builder =</span><br><span class="line">                RedisSerializationContext.newSerializationContext(keySerializer);</span><br><span class="line">        RedisSerializationContext&lt;String, T&gt; context = builder.value(valueSerializer).build();</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ReactiveRedisTemplate</span>&lt;&gt;(connectionFactory, context);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>使用<code>GenericJackson2JsonRedisSerializer</code>配置</p><p><code>GenericJackson2JsonRedisSerializer</code>的一个优点是它可以在序列化时包含类型信息，从而支持多种类型的数据而无需为每种类型提供单独的序列化器实例。这使得它在处理多种数据类型的场景下更为方便。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GenericJacksonRedisConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> ReactiveRedisTemplate&lt;String, Object&gt; <span class="title function_">reactiveRedisTemplateWithGenericJackson</span><span class="params">(ReactiveRedisConnectionFactory connectionFactory)</span> &#123;</span><br><span class="line">        <span class="comment">// 创建一个定制化的ObjectMapper实例</span></span><br><span class="line">        <span class="type">ObjectMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</span><br><span class="line">        mapper.registerModule(<span class="keyword">new</span> <span class="title class_">JavaTimeModule</span>()); <span class="comment">// 注册JavaTimeModule</span></span><br><span class="line">        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); <span class="comment">// 确保Java 8日期/时间类型以ISO格式序列化</span></span><br><span class="line">        mapper.findAndRegisterModules(); <span class="comment">// 自动注册所有可用的模块（包括JavaTimeModule）</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 使用定制化的ObjectMapper实例创建GenericJackson2JsonRedisSerializer</span></span><br><span class="line">        <span class="type">GenericJackson2JsonRedisSerializer</span> <span class="variable">valueSerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">GenericJackson2JsonRedisSerializer</span>(mapper);</span><br><span class="line">        <span class="comment">// 上面的配置是为了支持LocalDate、LocalTime、LocalDateTime的序列化</span></span><br><span class="line">        <span class="comment">// GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();</span></span><br><span class="line">        <span class="type">StringRedisSerializer</span> <span class="variable">keySerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringRedisSerializer</span>();</span><br><span class="line">        </span><br><span class="line">        RedisSerializationContext.RedisSerializationContextBuilder&lt;String, Object&gt; builder =</span><br><span class="line">                RedisSerializationContext.newSerializationContext(keySerializer);</span><br><span class="line">        RedisSerializationContext&lt;String, Object&gt; context = builder.value(valueSerializer).build();</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ReactiveRedisTemplate</span>&lt;&gt;(connectionFactory, context);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>对比</p><ul><li>如果你的应用主要处理单一类型的数据或者你需要对序列化过程有更细致的控制，<code>Jackson2JsonRedisSerializer</code>可能是一个好的选择。</li><li>如果你需要在Redis中存储多种类型的数据，并希望简化序列化和反序列化的处理，那么<code>GenericJackson2JsonRedisSerializer</code>可能更适合你的需求。</li></ul><p>通常<code>GenericJackson2JsonRedisSerializer</code>通用性更强，但是由于其类型自动处理的功能导致在某些情况下性能不如<code>Jackson2JsonRedisSerializer</code>高，而且生成的 json 数据包含了类型信息，不如<code>Jackson2JsonRedisSerializer</code>简洁，不方便开发者进行调试。此外，初学者学习推荐先从<code>Jackson2JsonRedisSerializer</code>开始再到<code>GenericJackson2JsonRedisSerializer</code>。</p><blockquote><h2 id="不同的配置方式"><a class="markdownIt-Anchor" href="#不同的配置方式"></a> 不同的配置方式</h2><p>在Spring Boot应用中，通过<code>application.properties</code>或<code>application.yml</code>文件进行配置属于<strong>声明式配置</strong>。这种方式允许开发者以简洁明了的形式指定应用配置，而无需编写额外的代码。Spring Boot的自动配置特性会根据这些属性以及应用的依赖关系自动设置合理的默认配置。</p><h3 id="声明式配置"><a class="markdownIt-Anchor" href="#声明式配置"></a> 声明式配置</h3><p><strong>声明式配置</strong>通常用于配置数据库连接、应用参数、日志级别等。例如，配置MongoDB和PostgreSQL的连接信息：</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># MongoDB</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">mongodb:</span></span><br><span class="line">      <span class="attr">uri:</span> <span class="string">mongodb://username:password@localhost:27017/databaseName</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># PostgreSQL</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:postgresql://localhost:5432/databaseName</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">username</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">password</span></span><br></pre></td></tr></table></figure></div><h3 id="编程式配置"><a class="markdownIt-Anchor" href="#编程式配置"></a> 编程式配置</h3><p><strong>编程式配置</strong>涉及创建配置类（通常带有<code>@Configuration</code>注解的类），在这些类中，你可以使用Java代码定义Bean和配置应用程序的行为。这种方式提供了更高的灵活性和控制力，适用于需要根据复杂逻辑或条件动态决定配置的场景。</p><p>例如，创建Redis的配置类：</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> LettuceConnectionFactory <span class="title function_">redisConnectionFactory</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">LettuceConnectionFactory</span>(<span class="keyword">new</span> <span class="title class_">RedisStandaloneConfiguration</span>(<span class="string">&quot;localhost&quot;</span>, <span class="number">6379</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h3 id="其他配置方式"><a class="markdownIt-Anchor" href="#其他配置方式"></a> 其他配置方式</h3><p>除了声明式配置和编程式配置，Spring Boot还支持通过<strong>环境变量</strong>、<strong>命令行参数</strong>、<strong>外部配置文件</strong>等多种方式来覆盖或指定配置。这提供了极大的灵活性，使得同一个应用可以在不同环境下使用不同的配置。</p><h3 id="使用配置类配置mongodb和postgresql"><a class="markdownIt-Anchor" href="#使用配置类配置mongodb和postgresql"></a> 使用配置类配置MongoDB和PostgreSQL</h3><p>理论上，你也可以使用配置类去配置MongoDB和PostgreSQL，就像配置Redis那样。然而，考虑到Spring Boot提供了对这些数据库的广泛自动配置支持，通常没有必要手动创建这些配置类。通过声明式配置，你可以更加简洁地利用Spring Boot的自动配置特性，减少样板代码，同时也能够更容易地管理和修改配置。在大多数Spring Boot应用中，采用声明式配置来利用Spring Boot的自动配置功能通常更为简便和高效。这样做不仅减少了配置的复杂性，也使得配置更加集中和易于管理。只有在需要高度自定义配置或Spring Boot的自动配置不能满足需求时，才考虑使用编程式配置。</p></blockquote><h5 id="代码实现方式-2"><a class="markdownIt-Anchor" href="#代码实现方式-2"></a> 代码实现方式</h5><p>MongoDB 也有类似于 R2DBC 的 <code>ReactiveMongoRepository</code>(继承了<code>ReactiveCrudRepository</code>接口)，提供了类似的数据访问方式：自带的方法实现、方法名解析、自定义方法(<code>ReactiveMongoTemplate</code>实现)等。</p><p>Redis 则没有响应式接口支持，需要自定义实现，主要是通过<code>ReactiveRedisTemplate</code>实现数据库操作。</p><p><strong>实体类注解</strong></p><p>关于实体类，MongoDB 提供了一些相关的注解</p><ul><li><p><strong>@Document</strong>: 用于指定实体类对应的MongoDB集合（collection）。可以通过<code>collection</code>属性指定集合的名称，如果不指定，默认使用类的名称（首字母小写）。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Document(collection = &quot;departments&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Department</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>@Id</strong>: 用于标记实体类中的属性作为文档的ID。MongoDB文档总是有一个<code>_id</code>字段作为唯一标识符，使用此注解可以映射类的属性到这个字段。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Id</span></span><br><span class="line"><span class="keyword">private</span> String id;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>@Field</strong>: 用于指定实体类属性映射到文档中的字段。可以通过它自定义字段的名称。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Field(&quot;dept_name&quot;)</span></span><br><span class="line"><span class="keyword">private</span> String departmentName;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>@Indexed</strong>: 用于标记一个字段应该被索引。这对于提高查询性能非常有用，特别是对于经常作为查询条件的字段。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Indexed(unique = true)</span></span><br><span class="line"><span class="keyword">private</span> String departmentCode;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>@Transient</strong>: 标记为<code>@Transient</code>的字段不会被持久化到MongoDB中。这对于临时状态或计算得出的属性很有用。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transient</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> memberCount;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>@CompoundIndex</strong>: 用于类级别，指定复合索引。这可以用来在多个字段上创建索引，以优化特定类型的查询。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Document</span></span><br><span class="line"><span class="meta">@CompoundIndex(def = &quot;&#123;&#x27;departmentCode&#x27;: 1, &#x27;departmentName&#x27;: -1&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Department</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ul><p>Redis 关于实体类的注解没有那么多，主要是以下几个，部分注解是通用的。</p><ul><li><strong>@RedisHash</strong>: 用于类，标记该类的对象将被存储在Redis的哈希结构中。<code>@RedisHash</code>注解可以接受一个可选的参数，用于指定在Redis中存储时使用的哈希的名称。</li><li><strong>@Id</strong>: 用于标记实体的id字段，这个字段的值将被用作Redis哈希结构中的key。</li><li><strong>@Indexed</strong>: 可以用于字段上，表示该字段将被索引，适用于进行查询操作。</li><li><strong>@TimeToLive</strong>: 可以用于字段上，指定一个实体或其部分的过期时间（生存时间）。</li></ul><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RedisHash(&quot;people&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Person</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> String id;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Indexed</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TimeToLive</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">Long</span> <span class="variable">expiration</span> <span class="operator">=</span> <span class="number">86400L</span>; <span class="comment">// 生存时间设置为24小时（86400秒）</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 省略构造函数、Getter和Setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p><strong>注意：使用 Redis 缓存的数据都需要实现可序列化接口<code>Serializable</code>。</strong></p><p><strong>CRUD 操作示例</strong></p><p>由于 Reactive MongoDB 的<code>ReactiveMongoRepository</code>是继承<code>ReactiveCrudRepository</code>接口实现的，这里只介绍自定义方法实现，即通过<code>ReactiveMongoTemplate</code>实现。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 增加</span></span><br><span class="line"><span class="comment">// 单个对象</span></span><br><span class="line"><span class="type">Person</span> <span class="variable">person</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;John Doe&quot;</span>, <span class="number">30</span>);</span><br><span class="line">Mono&lt;Person&gt; insertMono = reactiveMongoTemplate.insert(person);</span><br><span class="line"><span class="comment">// 多个对象</span></span><br><span class="line">List&lt;Person&gt; persons = Arrays.asList(<span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;John Doe&quot;</span>, <span class="number">30</span>), <span class="keyword">new</span> <span class="title class_">Person</span>(<span class="string">&quot;Jane Doe&quot;</span>, <span class="number">25</span>));</span><br><span class="line">Flux&lt;Person&gt; insertFlux = reactiveMongoTemplate.insertAll(persons);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 删除</span></span><br><span class="line"><span class="comment">// 单个对象</span></span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;name&quot;</span>).is(<span class="string">&quot;John Doe&quot;</span>));</span><br><span class="line">Mono&lt;DeleteResult&gt; deleteMono = reactiveMongoTemplate.remove(query, Person.class);</span><br><span class="line"><span class="comment">// 多个对象</span></span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;age&quot;</span>).lt(<span class="number">25</span>));</span><br><span class="line">Mono&lt;DeleteResult&gt; deleteMono = reactiveMongoTemplate.remove(query, Person.class);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 更新</span></span><br><span class="line"><span class="comment">// 单个对象</span></span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;name&quot;</span>).is(<span class="string">&quot;John Doe&quot;</span>));</span><br><span class="line"><span class="type">Update</span> <span class="variable">update</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Update</span>().set(<span class="string">&quot;age&quot;</span>, <span class="number">31</span>);</span><br><span class="line">Mono&lt;UpdateResult&gt; updateMono = reactiveMongoTemplate.updateFirst(query, update, Person.class);</span><br><span class="line"><span class="comment">// 多个对象</span></span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;age&quot;</span>).lt(<span class="number">30</span>));</span><br><span class="line"><span class="type">Update</span> <span class="variable">update</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Update</span>().set(<span class="string">&quot;status&quot;</span>, <span class="string">&quot;active&quot;</span>);</span><br><span class="line">Mono&lt;UpdateResult&gt; updateMono = reactiveMongoTemplate.updateMulti(query, update, Person.class);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 查询</span></span><br><span class="line"><span class="comment">// 单个对象</span></span><br><span class="line">Mono&lt;Person&gt; personMono = reactiveMongoTemplate.findById(<span class="string">&quot;someId&quot;</span>, Person.class);</span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;name&quot;</span>).is(<span class="string">&quot;John Doe&quot;</span>)); <span class="comment">// 或者使用查询对象 Query</span></span><br><span class="line">Mono&lt;Person&gt; personMono = reactiveMongoTemplate.findOne(query, Person.class);</span><br><span class="line"><span class="comment">// 多个对象</span></span><br><span class="line"><span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;age&quot;</span>).gt(<span class="number">20</span>));</span><br><span class="line">Flux&lt;Person&gt; personsFlux = reactiveMongoTemplate.find(query, Person.class);</span><br></pre></td></tr></table></figure></div><blockquote><p>上述示例中的<code>UpdateResult</code>和<code>DeleteResult</code>是来自于<code>com.mongodb.reactivestreams.client</code>包，它们提供了操作的结果信息，比如影响的文档数量等。</p></blockquote><p>Redis 也有类似的 CRUD 操作，但是需要考虑更多的模式和策略。</p><p>下面先给出 CRUD 操作的示例</p><p><strong>单对象 CRUD 操作</strong></p><ol><li><p>创建或更新（单个对象）</p><p>使用<code>ReactiveRedisTemplate</code>的<code>opsForValue().set()</code>方法可以存储单个对象。如果键已存在，它会被更新。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Mono&lt;Boolean&gt; <span class="title function_">savePerson</span><span class="params">(Person person)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;person:&quot;</span> + person.getId();</span><br><span class="line">    <span class="keyword">return</span> reactiveRedisTemplate.opsForValue().set(key, person);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p>查询（单个对象）</p><p>使用<code>opsForValue().get()</code>方法根据键查询单个对象。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Mono&lt;Person&gt; <span class="title function_">findPerson</span><span class="params">(String id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;person:&quot;</span> + id;</span><br><span class="line">    <span class="keyword">return</span> reactiveRedisTemplate.opsForValue().get(key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p>删除（单个对象）</p><p>使用<code>delete()</code>方法根据键删除单个对象。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Mono&lt;Boolean&gt; <span class="title function_">deletePerson</span><span class="params">(String id)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;person:&quot;</span> + id;</span><br><span class="line">    <span class="keyword">return</span> reactiveRedisTemplate.opsForValue().delete(key);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ol><p><strong>多对象 CRUD 操作</strong></p><ol><li><p>创建或更新（多个对象）</p><p>Redis 并不直接支持一次性创建或更新多个对象的操作。你需要对每个对象进行遍历并分别调用设置方法。这可以通过<code>Flux</code>来实现。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Flux&lt;Boolean&gt; <span class="title function_">savePersons</span><span class="params">(List&lt;Person&gt; persons)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Flux.fromIterable(persons)</span><br><span class="line">        .flatMap(person -&gt; savePerson(person)); <span class="comment">// savePerson 就是前面单对象操作中的实现方法</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p>查询（多个对象）</p><p>查询多个对象通常涉及到根据多个键查询。在Redis中，可以使用<code>Flux</code>来遍历键并查询。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Flux&lt;Person&gt; <span class="title function_">findPersons</span><span class="params">(List&lt;String&gt; ids)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Flux.fromIterable(ids)</span><br><span class="line">        .flatMap(id -&gt; findPerson(id));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p>删除（多个对象）</p><p>删除多个对象也需要对每个键进行遍历并删除。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Flux&lt;Boolean&gt; <span class="title function_">deletePersons</span><span class="params">(List&lt;String&gt; ids)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Flux.fromIterable(ids)</span><br><span class="line">        .flatMap(id -&gt; deletePerson(id));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ol><p><strong>实现策略</strong></p><p>存储多对象数据</p><ol><li><strong>每个对象存储一份数据</strong>：这种方式适合于需要频繁访问或更新单个对象的场景。通过使用唯一键为每个对象存储一份数据，可以快速地访问和更新单个对象而不影响其他对象。</li><li><strong>存储对象集合</strong>：当需要一次性加载或处理多个对象时，可以将这些对象作为一个集合存储。这种方式适用于数据一致性要求不高或同时操作整个集合的场景。例如，可以将对象序列化为JSON数组存储在单个键下。</li></ol><p>Redis中的 CRUD 和缓存失效策略</p><ul><li><strong>创建和更新（C和U）</strong>：在Redis中创建和更新操作通常是通过设置键值对来完成的。如果数据发生变化，直接更新对应的键值对即可。对于缓存场景，如果是更新操作，通常会直接更新缓存中的数据，或者删除缓存中的旧数据让其在下次访问时重新加载。</li><li><strong>读取（R）</strong>：读取操作是Redis中最常见的用例之一，特别是作为缓存时。由于Redis的读取性能非常高，它非常适合用作频繁读取的数据的缓存。</li><li><strong>删除（D）</strong>：在数据变更或不再需要时，可以通过删除操作移除Redis中的数据。对于缓存，当底层数据发生变化导致缓存数据过时时，常见的做法是直接删除缓存中的数据，避免提供过时的信息。</li></ul><p>缓存失效策略</p><ul><li><strong>主动失效</strong>：应用程序在更新底层数据时主动删除或更新缓存中的数据。这种策略可以确保缓存数据的一致性，但需要应用程序显式地管理缓存的失效。</li><li><strong>被动失效</strong>：利用Redis的<code>EXPIRE</code>命令为缓存数据设置生存时间（TTL），让数据在指定时间后自动过期。这种策略适用于那些即使数据稍微过时也不会造成大问题的场景。</li></ul><blockquote><h2 id="更新缓存-or-清空缓存"><a class="markdownIt-Anchor" href="#更新缓存-or-清空缓存"></a> 更新缓存 or 清空缓存?</h2><h3 id="更新缓存"><a class="markdownIt-Anchor" href="#更新缓存"></a> 更新缓存</h3><p><strong>适用场景</strong>：</p><ul><li>数据频繁读取且更新成本相对较低的情况。</li><li>需要保证数据一致性，且能够容易地计算出更新后的值。</li><li>应用场景对数据实时性要求较高，必须确保缓存中的数据尽可能反映最新状态。</li></ul><p><strong>优点</strong>：</p><ul><li>减少对后端数据源的查询压力，提高数据访问速度。</li><li>保持缓存数据的实时性和一致性。</li></ul><p><strong>缺点</strong>：</p><ul><li>实现复杂度较高，尤其是当更新逻辑复杂或与原始数据生成逻辑不一致时。</li><li>在高并发场景下，更新缓存可能引入竞态条件，需要额外的同步机制。</li></ul><h3 id="清空缓存"><a class="markdownIt-Anchor" href="#清空缓存"></a> 清空缓存</h3><p><strong>适用场景</strong>：</p><ul><li>数据更新频率低，或者更新操作对性能影响较大的场景。</li><li>数据一致性要求不是非常严格，可以接受短时间内缓存数据与数据库数据的不一致。</li><li>更新操作涉及复杂逻辑，直接重新生成缓存内容比更新现有缓存更简单或更高效。</li></ul><p><strong>优点</strong>：</p><ul><li>实现简单，只需删除缓存项即可，无需处理复杂的更新逻辑。</li><li>避免了因缓存数据更新不当而导致的数据不一致问题。</li></ul><p><strong>缺点</strong>：</p><ul><li>频繁清空缓存可能导致对后端数据源的访问增加，影响性能。</li><li>用户可能会遇到缓存失效后的延迟增加，尤其是对于计算或获取成本较高的数据。</li></ul><h3 id="总结-2"><a class="markdownIt-Anchor" href="#总结-2"></a> 总结</h3><ul><li>如果能够容易且准确地更新缓存以反映最新数据，而且这样做对性能影响不大，则<strong>更新缓存</strong>可能是更好的选择。</li><li>如果更新缓存的逻辑复杂，或者保持数据最新性的成本较高，清空（删除）缓存然后在下次访问时重新生成可能是更简单、更有效的方法。</li></ul></blockquote><p><strong>封装到数据访问层</strong></p><p>MongoDB 可以将自定义的接口和主接口组合起来</p><p>首先创建自定义的 repository 接口，声明自定义方法</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">CustomPersonRepository</span> &#123;</span><br><span class="line">    Flux&lt;Person&gt; <span class="title function_">findCustomQuery</span><span class="params">(String criteria)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>然后创建自定义接口的实现类，注入<code>ReactiveMongoTemplate</code>来执行实际的数据库操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CustomPersonRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">CustomPersonRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveMongoTemplate mongoTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">CustomPersonRepositoryImpl</span><span class="params">(ReactiveMongoTemplate mongoTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.mongoTemplate = mongoTemplate;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Flux&lt;Person&gt; <span class="title function_">findCustomQuery</span><span class="params">(String criteria)</span> &#123;</span><br><span class="line">        <span class="type">Query</span> <span class="variable">query</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Query</span>(Criteria.where(<span class="string">&quot;someField&quot;</span>).is(criteria));</span><br><span class="line">        <span class="keyword">return</span> mongoTemplate.find(query, Person.class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>最后组合自定义 repository 接口到主 repository 接口，让主 repository 接口扩展自定义的接口</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">PersonRepository</span> <span class="keyword">extends</span> <span class="title class_">ReactiveMongoRepository</span>&lt;Person, String&gt;, CustomPersonRepository &#123;</span><br><span class="line">    <span class="comment">// 自带的CRUD方法和查询方法名解析方法</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>Redis 直接定义接口和实现类即可</p><p>创建接口</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ReactivePersonRepository</span> &#123;</span><br><span class="line">    Mono&lt;Person&gt; <span class="title function_">save</span><span class="params">(Person person)</span>;</span><br><span class="line">    Mono&lt;Person&gt; <span class="title function_">findById</span><span class="params">(String id)</span>;</span><br><span class="line">    Flux&lt;Person&gt; <span class="title function_">findAll</span><span class="params">()</span>;</span><br><span class="line">    Mono&lt;Void&gt; <span class="title function_">deleteById</span><span class="params">(String id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>创建实现类，注入<code>ReactiveRedisTemplate</code>和<code>ReactiveValueOperations</code>(实际上就是对<code>ReactiveRedisTemplate.opsForValue()</code>的引用，简化了代码)</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ReactivePersonRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">ReactivePersonRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveRedisTemplate&lt;String, Person&gt; redisTemplate;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveValueOperations&lt;String, Person&gt; reactiveValueOps;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ReactivePersonRepositoryImpl</span><span class="params">(ReactiveRedisTemplate&lt;String, Person&gt; redisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.redisTemplate = redisTemplate;</span><br><span class="line">        <span class="built_in">this</span>.reactiveValueOps = redisTemplate.opsForValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Person&gt; <span class="title function_">save</span><span class="params">(Person person)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.set(person.getId(), person).thenReturn(person);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Person&gt; <span class="title function_">findById</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.get(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Flux&lt;Person&gt; <span class="title function_">findAll</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> redisTemplate.execute(connection -&gt; </span><br><span class="line">            connection.keyCommands().scan(ScanOptions.scanOptions().match(<span class="string">&quot;*&quot;</span>).build()))</span><br><span class="line">            .flatMap(cursor -&gt; Flux.fromIterable(cursor)</span><br><span class="line">            .flatMap(key -&gt; reactiveValueOps.get(<span class="keyword">new</span> <span class="title class_">String</span>(key)).cast(T.class)));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">deleteById</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.delete(id).then();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>更通用的泛型写法</p><p>接口</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ReactiveGenericRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 保存一个对象到Redis。</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key Redis键</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> value 要保存的对象</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt; 对象的类型</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 保存后的对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; Mono&lt;T&gt; <span class="title function_">save</span><span class="params">(String key, T value)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据键查找一个对象。</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key Redis键</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt; 对象的预期类型</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 找到的对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; Mono&lt;T&gt; <span class="title function_">findById</span><span class="params">(String key)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查找所有对象。</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> clazz 对象的类类型，用于类型转换</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt; 对象的预期类型</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 找到的所有对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    &lt;T&gt; Flux&lt;T&gt; <span class="title function_">findAll</span><span class="params">(Class&lt;T&gt; clazz)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据键删除一个对象。</span></span><br><span class="line"><span class="comment">     * </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key Redis键</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 删除操作的结果</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    Mono&lt;Boolean&gt; <span class="title function_">deleteById</span><span class="params">(String key)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>实现类</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ReactiveGenericRepositoryImpl</span> <span class="keyword">implements</span> <span class="title class_">ReactiveGenericRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveRedisTemplate&lt;String, Object&gt; redisTemplate;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveValueOperations&lt;String, Object&gt; reactiveValueOps;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ReactiveGenericRepositoryImpl</span><span class="params">(ReactiveRedisTemplate&lt;String, Object&gt; redisTemplate)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.redisTemplate = redisTemplate;</span><br><span class="line">        <span class="built_in">this</span>.reactiveValueOps = redisTemplate.opsForValue();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; Mono&lt;T&gt; <span class="title function_">save</span><span class="params">(String key, T value)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.set(key, value).thenReturn(value);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; Mono&lt;T&gt; <span class="title function_">findById</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.get(key);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; Flux&lt;T&gt; <span class="title function_">findAll</span><span class="params">(Class&lt;T&gt; clazz)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> redisTemplate.scan(ScanOptions.scanOptions().match(<span class="string">&quot;*&quot;</span>).build())</span><br><span class="line">            .flatMap(key -&gt; redisTemplate.opsForValue().get(key));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Boolean&gt; <span class="title function_">deleteById</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> reactiveValueOps.delete(key);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><blockquote><h3 id="关于泛型的使用"><a class="markdownIt-Anchor" href="#关于泛型的使用"></a> 关于泛型的使用</h3><p>泛型可以在类、接口或方法上使用，具体取决于设计的需求：</p><ol><li><p><strong>类或接口上的泛型</strong>：当你希望定义一个类或接口时就确定其操作的数据类型，可以在类或接口上声明泛型参数。这种方式适用于整个类或接口中所有方法操作的数据类型是一致的情况。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Repository</span>&lt;T&gt; &#123;</span><br><span class="line">    Mono&lt;T&gt; <span class="title function_">save</span><span class="params">(T entity)</span>;</span><br><span class="line">    Mono&lt;T&gt; <span class="title function_">findById</span><span class="params">(String id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>方法上的泛型</strong>：当你希望在具体的方法调用时确定数据类型，可以在方法上声明泛型参数。这种方式提供了更高的灵活性，允许在同一个类或接口中支持不同类型的操作。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">GenericRepository</span> &#123;</span><br><span class="line">    &lt;T&gt; Mono&lt;T&gt; <span class="title function_">save</span><span class="params">(String key, T value)</span>;</span><br><span class="line">    &lt;T&gt; Mono&lt;T&gt; <span class="title function_">findById</span><span class="params">(String key, Class&lt;T&gt; clazz)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ol></blockquote><h3 id="webflux-服务层"><a class="markdownIt-Anchor" href="#webflux-服务层"></a> Webflux 服务层</h3><h4 id="多数据库联合操作"><a class="markdownIt-Anchor" href="#多数据库联合操作"></a> 多数据库联合操作</h4><h5 id="策略"><a class="markdownIt-Anchor" href="#策略"></a> 策略</h5><p>在实际业务场景中，联合多种数据库操作需要考虑诸多的策略：</p><p><strong>1.查询策略</strong></p><ul><li><strong>缓存优先</strong>：对于读操作，通常采用缓存优先的策略。首先查询Redis缓存，如果缓存命中，则直接返回缓存中的数据；如果缓存未命中，则查询MongoDB或PostgreSQL，并将查询结果缓存到Redis中，以便后续请求能够直接从缓存中获取数据。</li></ul><p><strong>2.数据存储和一致性</strong></p><ul><li><p><strong>分离存储</strong>：MongoDB和PostgreSQL应该处理各自业务范围内的数据，避免存储重复数据。例如，MongoDB可以用于存储文档型或半结构化数据，而PostgreSQL用于存储关系数据和进行复杂查询。</p></li><li><p><strong>数据一致性</strong>：在进行增删改操作时，直接对MongoDB或PostgreSQL进行操作。操作完成后，根据业务需求和数据一致性要求决定是更新Redis缓存还是清空缓存。</p></li></ul><p><strong>3.缓存失效策略</strong></p><ul><li><p><strong>缓存更新</strong>：如果数据变更不频繁，或者实时性要求高，可以在数据变更后更新Redis缓存。这可以减少缓存不一致的窗口期，但需要额外的开销来维护缓存数据的更新。</p></li><li><p><strong>缓存清空</strong>：对于高变更频率的数据，或者当缓存更新逻辑较为复杂时，可以选择在数据变更后清空（删除）相关的Redis缓存。这简化了缓存管理，但可能导致更频繁的数据库查询。</p></li></ul><p><strong>4.事务和一致性模型</strong></p><ul><li><p><strong>事务管理</strong>：在涉及到多个数据操作的业务逻辑中，考虑使用事务（对于支持事务的数据库，如PostgreSQL）来保证操作的原子性和一致性。</p></li><li><p><strong>最终一致性</strong>：在分布式系统中，完全的一致性很难实现，可以采用最终一致性模型，通过合理的缓存策略和数据同步机制，确保数据在一定时间内达到一致状态。</p></li></ul><p><strong>5.数据备份和恢复</strong></p><ul><li><strong>备份策略</strong>：定期备份数据是保证数据安全的重要措施。MongoDB和PostgreSQL都提供了备份和恢复的机制，应该根据业务需求制定备份计划并定期执行。</li><li><strong>灾难恢复</strong>：设计灾难恢复计划，确保在数据丢失或系统故障的情况下能够快速恢复服务。</li></ul><p><strong>6.性能优化</strong></p><ul><li><strong>索引优化</strong>：对于MongoDB和PostgreSQL，合理使用索引可以显著提高查询效率。需要定期评估索引的使用情况和性能影响，并根据实际查询模式进行优化。</li><li><strong>缓存策略</strong>：对于Redis，合理设计缓存策略，包括选择合适的数据过期时间、使用合适的数据结构等，可以提高缓存效率和命中率。</li></ul><p><strong>7.安全性考虑</strong></p><ul><li><strong>访问控制</strong>：确保数据库的访问控制得当，只有授权的用户和服务才能访问敏感数据。</li><li><strong>数据加密</strong>：对敏感数据进行加密，包括在传输过程中的加密（如使用SSL/TLS）和存储时的加密。</li></ul><p><strong>8.监控和警报</strong></p><ul><li><strong>性能监控</strong>：监控数据库的性能指标，如响应时间、查询负载等，可以帮助及时发现和解决性能问题。</li><li><strong>系统警报</strong>：设置警报机制，当出现潜在的问题，如访问异常、资源使用率过高等时，能够及时通知到相关人员。</li></ul><p><strong>9.微服务架构下的数据管理</strong></p><ul><li><strong>服务间数据一致性</strong>：在微服务架构中，不同服务可能会操作不同的数据库实例或类型。设计合理的服务间通信和数据同步机制，以保证跨服务的数据一致性。</li><li><strong>API网关和服务聚合</strong>：考虑使用API网关来聚合来自不同微服务的数据请求和响应，提供统一的数据访问入口。</li></ul><blockquote><h3 id="命令查询责任分离-cqrs"><a class="markdownIt-Anchor" href="#命令查询责任分离-cqrs"></a> 命令查询责任分离 CQRS</h3><p>命令查询责任分离（Command Query Responsibility Segregation，CQRS）是一种软件架构模式，它将应用的读操作（查询）和写操作（命令）明确分离开来。CQRS的核心思想是对读和写使用不同的模型，以优化性能、可伸缩性和安全性。这种模式并不一定要求使用两个物理不同的数据库，但在某些实现中，确实会使用不同的数据存储来分别处理读和写操作，以此来充分利用每种存储的优势。</p><h3 id="cqrs的关键概念"><a class="markdownIt-Anchor" href="#cqrs的关键概念"></a> CQRS的关键概念</h3><ul><li><strong>命令（Command）</strong>：表示对系统的写操作（如创建、更新、删除），这些操作可以改变系统的状态但不返回任何值。</li><li><strong>查询（Query）</strong>：表示对系统的读操作，这些操作返回系统的当前状态，但不改变状态。</li></ul><h3 id="cqrs的优势"><a class="markdownIt-Anchor" href="#cqrs的优势"></a> CQRS的优势</h3><ul><li><strong>性能优化</strong>：通过分离读写操作，可以针对查询和命令操作分别优化数据模型和存储机制，从而提高应用的性能。</li><li><strong>可伸缩性</strong>：可以独立地扩展读操作和写操作的处理能力，适应不同的负载需求。</li><li><strong>安全性与复杂性管理</strong>：简化复杂的业务逻辑，因为读模型可以专门针对查询进行优化，而写模型则专注于业务规则和数据一致性。</li><li><strong>灵活的技术选择</strong>：可以针对读和写操作选择最适合的技术和数据库。</li></ul><h3 id="cqrs与数据一致性"><a class="markdownIt-Anchor" href="#cqrs与数据一致性"></a> CQRS与数据一致性</h3><p>在使用CQRS时，确实可能会涉及到不同的数据存储系统，这在某些情况下可能引入数据一致性的挑战。例如，当一个命令操作更新了写模型的数据后，读模型的数据可能需要一些时间才能反映这些更改，这导致了最终一致性的情况而不是即时一致性。</p><p>为了管理这种一致性问题，通常会采用以下策略之一：</p><ul><li><strong>即时同步</strong>：在命令操作后立即更新读模型。这种方式尽可能保持读写模型的同步，但可能会影响写操作的性能。</li><li><strong>异步更新</strong>：通过事件驱动的方式，当写模型更新后异步更新读模型。这种方式可以提高写操作的性能，但读模型可能会暂时与写模型不一致，实现最终一致性。</li></ul><p><strong>CQRS最适合那些读写负载差异大、对数据一致性要求较为宽松的场景。在需要强一致性或读写模式没有显著差异的应用中，CQRS可能带来过多的复杂性而不是收益。</strong></p><h4 id="cqrs适用场景读写负载差异大对数据一致性要求宽松"><a class="markdownIt-Anchor" href="#cqrs适用场景读写负载差异大对数据一致性要求宽松"></a> CQRS适用场景：读写负载差异大，对数据一致性要求宽松</h4><ul><li><strong>社交网络的Feed流</strong>：用户的动态更新（写操作）可能不是非常频繁，但是用户浏览Feed流（读操作）非常频繁。此外，用户通常可以接受Feed流不是实时更新的，即如果某个好友的动态延迟几秒钟出现在Feed中通常是可接受的。在这种情况下，CQRS可以用来优化Feed流的读取性能，通过将写操作和Feed流的生成/查询逻辑分离来实现。</li><li><strong>电商平台的商品推荐系统</strong>：商品的购买和评价（写操作）相对于商品推荐的阅读（读操作）来说较少。推荐系统可以基于用户的历史行为和偏好生成，且用户通常可以接受推荐信息不是实时更新的。使用CQRS可以将推荐生成（写操作）和推荐展示（读操作）分离，优化用户的阅读体验。</li></ul><h4 id="非cqrs适用场景强一致性需求或读写模式没有显著差异"><a class="markdownIt-Anchor" href="#非cqrs适用场景强一致性需求或读写模式没有显著差异"></a> 非CQRS适用场景：强一致性需求或读写模式没有显著差异</h4><ul><li><strong>银行系统</strong>：在处理金融交易（如转账）时，系统必须保证数据的强一致性，以确保账户余额的准确性。在这种场景下，读写操作对数据一致性的要求非常高，采用CQRS可能会引入不必要的复杂性和风险。</li><li><strong>实时协作工具</strong>：如在线文档编辑器，允许多个用户同时编辑同一个文档。这类应用需要实时同步不同用户的操作，以保持所有用户视图的一致性。由于读写操作紧密相关且对实时性要求很高，CQRS可能不是最佳选择。</li></ul><h4 id="针对-cqrs-逻辑读写分离而非物理读写分离的案例"><a class="markdownIt-Anchor" href="#针对-cqrs-逻辑读写分离而非物理读写分离的案例"></a> 针对 CQRS 逻辑读写分离而非物理读写分离的案例</h4><p>由于实践过程中，简化的设计通常是按物理数据库来划分读写职责，但这可能会造成“ CQRS 就是将两个数据库分成一个主要用于读取的数据库(读取的数据仍需要通过另一个数据库同步写入)和一个只用于写入数据库”的误解（这是在十分理想化的情况下）。</p><p>设计一个更复杂的场景，其中PostgreSQL和MongoDB都承担读写操作，但各自侧重于不同类型的读写负载。这样的设计可以展示CQRS并非简单地物理划分读写操作到不同的数据库，而是根据操作的性质和数据模型的优化需求来逻辑分配任务。</p><h3 id="场景设定"><a class="markdownIt-Anchor" href="#场景设定"></a> 场景设定</h3><p>假设我们正在开发一个电商平台，其中包含产品管理和用户行为分析两大功能。</p><h3 id="使用postgresql和mongodb的cqrs实现"><a class="markdownIt-Anchor" href="#使用postgresql和mongodb的cqrs实现"></a> 使用PostgreSQL和MongoDB的CQRS实现</h3><ul><li><strong>PostgreSQL</strong>：<ul><li><strong>写操作</strong>：负责产品的创建、更新和删除。这些操作需要事务支持和一致性保证，适合关系数据库的特点。</li><li><strong>复杂读操作</strong>：负责执行复杂的SQL查询，如生成报表、聚合查询等。这些操作利用了PostgreSQL强大的SQL支持和优化器。</li></ul></li><li><strong>MongoDB</strong>：<ul><li><strong>写操作</strong>：负责记录用户行为数据，如页面浏览、点击事件等。这些操作对写入速度有很高要求，并且数据结构可能经常变化，适合文档数据库的特点。</li><li><strong>简单读操作</strong>：负责提供快速的查询响应，如用户行为数据的即时查询。MongoDB的灵活性和索引优化可以提供高效的查询性能。</li></ul></li></ul><h3 id="数据同步和一致性"><a class="markdownIt-Anchor" href="#数据同步和一致性"></a> 数据同步和一致性</h3><p>在这个例子中，PostgreSQL和MongoDB都承担读写操作，但针对不同的业务场景。为了保持数据一致性：</p><ul><li><strong>数据同步</strong>：可以使用事件驱动机制同步必要的数据变更。例如，当一个新产品在PostgreSQL中被创建或更新后，相关的信息可以异步同步到MongoDB中的用户行为分析模型。</li><li><strong>CQRS视角</strong>：从CQRS的视角看，我们在逻辑上区分了命令（写操作）和查询（读操作），并根据不同操作的特点选择了最合适的数据存储。即便每种数据库都执行读写操作，但这些操作服务于不同的业务需求和数据模型。</li></ul><p>除了命令查询责任分离 CQRS 根据读写职责划分来设计多个数据库行为以外，另一种架构则是按照业务边界划分来设计多个数据库行为。</p><p>这种架构下，每一个数据库之间不需要考虑数据同步和一致性问题，因为它们负责的是从业务逻辑上分离的数据，任何一个数据库的业务操作都不会影响另一个数据库的业务操作，就像两个可以独立运行的模块。这种架构设计比较接近于微服务架构的理念：在微服务架构中，应用被分解成一系列较小、松耦合的服务，每个服务围绕着特定的业务功能构建，并可以独立部署、扩展和维护。每个服务通常管理自己的数据库（或数据存储），以保持业务边界的清晰和数据的封装性。</p></blockquote><h5 id="代码实现"><a class="markdownIt-Anchor" href="#代码实现"></a> 代码实现</h5><blockquote><p>使用PostgreSQL存储核心业务数据，MongoDB存储文档或日志类型的数据，而Redis作为缓存层来提高数据读取的性能。</p></blockquote><p>一个博客系统，其中PostgreSQL用于存储文章信息，MongoDB用于存储用户评论，Redis用于缓存热门文章。</p><p>有以下已经提前准备好的 repository 接口，假设里面的方法都已经实现</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 假设的PostgreSQL文章Repository</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">ArticleRepository</span> &#123;</span><br><span class="line">    Mono&lt;Article&gt; <span class="title function_">saveArticle</span><span class="params">(Article article)</span>;</span><br><span class="line">    Mono&lt;Article&gt; <span class="title function_">findArticleById</span><span class="params">(String id)</span>;</span><br><span class="line">    Mono&lt;Void&gt; <span class="title function_">deleteArticleById</span><span class="params">(String id)</span>;</span><br><span class="line">    Flux&lt;Article&gt; <span class="title function_">findAllArticles</span><span class="params">()</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 假设的MongoDB评论Repository</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">CommentRepository</span> &#123;</span><br><span class="line">    Mono&lt;Comment&gt; <span class="title function_">saveComment</span><span class="params">(Comment comment)</span>;</span><br><span class="line">    Flux&lt;Comment&gt; <span class="title function_">findCommentsByArticleId</span><span class="params">(String articleId)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 假设的Redis缓存操作</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">CacheService</span> &#123;</span><br><span class="line">    &lt;T&gt; Mono&lt;T&gt; <span class="title function_">findInCache</span><span class="params">(String key, Class&lt;T&gt; type)</span>;</span><br><span class="line">    &lt;T&gt; Mono&lt;Boolean&gt; <span class="title function_">updateCache</span><span class="params">(String key, T data)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>然后基于这些方法编写服务层，协调不同数据库的操作</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BlogService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> ArticleRepository articleRepository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> CommentRepository commentRepository;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> CacheService cacheService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Article&gt; <span class="title function_">createOrUpdateArticle</span><span class="params">(Article article)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> articleRepository.saveArticle(article)</span><br><span class="line">                .flatMap(savedArticle -&gt; cacheService.updateCache(<span class="string">&quot;article_&quot;</span> + savedArticle.getId(), savedArticle)</span><br><span class="line">                        .thenReturn(savedArticle));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Article&gt; <span class="title function_">getArticleById</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> cacheService.findInCache(<span class="string">&quot;article_&quot;</span> + id, Article.class)</span><br><span class="line">                .switchIfEmpty(articleRepository.findArticleById(id)</span><br><span class="line">                        .flatMap(article -&gt; cacheService.updateCache(<span class="string">&quot;article_&quot;</span> + article.getId(), article)</span><br><span class="line">                                .thenReturn(article)));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Flux&lt;Comment&gt; <span class="title function_">getCommentsByArticleId</span><span class="params">(String articleId)</span> &#123;</span><br><span class="line">        <span class="comment">// 直接从MongoDB获取评论，因为评论不适合缓存</span></span><br><span class="line">        <span class="keyword">return</span> commentRepository.findCommentsByArticleId(articleId);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">deleteArticleById</span><span class="params">(String id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> articleRepository.deleteArticleById(id)</span><br><span class="line">                .then(cacheService.updateCache(<span class="string">&quot;article_&quot;</span> + id, <span class="literal">null</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>逻辑说明</p><ul><li><strong>文章操作（PostgreSQL + Redis）</strong>：文章的创建、更新、查询和删除操作主要依赖PostgreSQL。Redis用作文章信息的缓存，以加速热门文章的读取性能。</li><li><strong>评论操作（MongoDB）</strong>：评论数据由于其量可能很大并且更新频繁，因此存储在MongoDB中，并且通常不被缓存。</li><li><strong>缓存逻辑（Redis）</strong>：在文章创建或更新后更新缓存，在文章查询时先尝试从缓存获取，缓存未命中则从数据库加载并更新缓存。</li></ul><h3 id="webflux-安全层"><a class="markdownIt-Anchor" href="#webflux-安全层"></a> Webflux 安全层</h3><h4 id="整合spring-security-jwt"><a class="markdownIt-Anchor" href="#整合spring-security-jwt"></a> 整合Spring Security、JWT</h4><p>在引入 Spring Security 之前，建议先实现 JWT 的生成和验证逻辑，这样方便后面整合 Spring Security 框架来进行认证和授权。</p><h5 id="jwt-工具类"><a class="markdownIt-Anchor" href="#jwt-工具类"></a> JWT 工具类</h5><p>创建一个 JWT 工具类来处理 JWT 的创建、解析和验证</p><blockquote><p>一个更完善的版本还应该包括Token的有效期、处理异常、以及更安全的密钥管理等方面的考虑</p></blockquote><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtUtil</span> &#123;</span><br><span class="line">    <span class="comment">// 生产环境中应从配置文件或环境变量中安全地获取密钥</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">Key</span> <span class="variable">key</span> <span class="operator">=</span> Keys.secretKeyFor(SignatureAlgorithm.HS256); <span class="comment">// 使用HS256算法生成密钥</span></span><br><span class="line">    <span class="keyword">private</span> <span class="type">long</span> <span class="variable">expiration</span> <span class="operator">=</span> <span class="number">3600000</span>; <span class="comment">// Token有效期，这里示例为1小时（单位毫秒）</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">generateToken</span><span class="params">(String username, List&lt;String&gt; roles)</span> &#123;</span><br><span class="line">        <span class="comment">// Date now = new Date();</span></span><br><span class="line">        <span class="comment">// Date expiryDate = new Date(now.getTime() + expiration); // 设置Token过期时间</span></span><br><span class="line">        <span class="comment">// 如果不希望使用Date，可以使用以下方法实现</span></span><br><span class="line">        <span class="type">Instant</span> <span class="variable">now</span> <span class="operator">=</span> Instant.now();</span><br><span class="line">        <span class="type">Instant</span> <span class="variable">expiryDate</span> <span class="operator">=</span> now.plus(<span class="number">1</span>, ChronoUnit.HOURS); <span class="comment">// 设置Token过期时间为1小时后</span></span><br><span class="line">        <span class="keyword">return</span> Jwts.builder()</span><br><span class="line">            .setSubject(username)</span><br><span class="line">            .setIssuedAt(Date.from(now))</span><br><span class="line">            .setExpiration(Date.from(expiryDate))</span><br><span class="line">            .claim(<span class="string">&quot;roles&quot;</span>, roles) <span class="comment">// 在这里添加角色信息，方便Spring Security认证</span></span><br><span class="line">            .signWith(key) <span class="comment">// 使用生成的密钥签名</span></span><br><span class="line">            .compact();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Claims <span class="title function_">validateToken</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Jwts.parser()</span><br><span class="line">                .setSigningKey(key) <span class="comment">// 使用密钥验证</span></span><br><span class="line">                .build()</span><br><span class="line">                .parseClaimsJws(token)</span><br><span class="line">                .getBody();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">getUsernameFromToken</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> validateToken(token);</span><br><span class="line">        <span class="keyword">return</span> claims.getSubject();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> List&lt;String&gt; <span class="title function_">getRolesFromToken</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> validateToken(token);</span><br><span class="line">        <span class="keyword">return</span> claims.get(<span class="string">&quot;roles&quot;</span>, List.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isTokenExpired</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> validateToken(token);</span><br><span class="line">        <span class="comment">// return claims.getExpiration().before(new Date());</span></span><br><span class="line">        <span class="keyword">return</span> claims.getExpiration().toInstant().isBefore(Instant.now());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>使用<code>Keys.secretKeyFor</code>生成密钥</p><ul><li><strong>生产环境</strong>：在生产环境中使用<code>Keys.secretKeyFor(SignatureAlgorithm.HS256)</code>来生成密钥是推荐的做法，因为它能为所使用的签名算法生成足够强度的密钥。为了安全起见，密钥不应该硬编码在代码中，而是应该通过安全的方式（如环境变量、配置服务等）提供，并且保证其安全性（不被泄露）。</li><li><strong>测试环境</strong>：在测试环境中，使用<code>Keys.secretKeyFor(SignatureAlgorithm.HS256)</code>同样适用，它可以帮助确保测试环境尽可能地模拟生产环境的配置和安全性。然而，考虑到测试环境的便利性和调试需求，可能会选择使用更简单的密钥管理方式，但应确保测试用的密钥不会用于生产环境。</li></ul><p>下面提供一个简单的测试类用于测试该工具类的功能</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">JwtUtilTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">JwtUtil</span> <span class="variable">jwtUtil</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JwtUtil</span>();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">testGenerateAndValidateToken</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> <span class="string">&quot;testUser&quot;</span>;</span><br><span class="line">        List&lt;String&gt; roles = Arrays.asList(<span class="string">&quot;ROLE_USER&quot;</span>, <span class="string">&quot;ROLE_ADMIN&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Generate token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtil.generateToken(username, roles);</span><br><span class="line">        assertNotNull(token);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Validate token and extract claims</span></span><br><span class="line">        <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> jwtUtil.validateToken(token);</span><br><span class="line">        assertNotNull(claims);</span><br><span class="line">        assertEquals(username, claims.getSubject());</span><br><span class="line">        assertTrue(claims.get(<span class="string">&quot;roles&quot;</span>) <span class="keyword">instanceof</span> List);</span><br><span class="line">        assertEquals(roles, claims.get(<span class="string">&quot;roles&quot;</span>, List.class));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Check if token is not expired</span></span><br><span class="line">        assertFalse(jwtUtil.isTokenExpired(token));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">testTokenExpiration</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="comment">// Assuming we have a very short expiry time for testing</span></span><br><span class="line">        jwtUtil.setExpiration(<span class="number">10</span>); <span class="comment">// Set token expiration to 10 milliseconds for the test</span></span><br><span class="line"></span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> <span class="string">&quot;testUser&quot;</span>;</span><br><span class="line">        List&lt;String&gt; roles = Arrays.asList(<span class="string">&quot;ROLE_USER&quot;</span>, <span class="string">&quot;ROLE_ADMIN&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Generate token</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtil.generateToken(username, roles);</span><br><span class="line">        Thread.sleep(<span class="number">20</span>); <span class="comment">// Wait longer than the token expiration time</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// Verify the token is expired</span></span><br><span class="line">        assertTrue(jwtUtil.isTokenExpired(token));</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">testToken</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> <span class="string">&quot;user123&quot;</span>;</span><br><span class="line">        List&lt;String&gt; roles = Arrays.asList(<span class="string">&quot;ROLE_USER&quot;</span>, <span class="string">&quot;ROLE_ADMIN&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Generate token with user information</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> jwtUtil.generateToken(username, roles);</span><br><span class="line">        System.out.println(<span class="string">&quot;Generated Token: &quot;</span> + token);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Assume token is being sent to and received from a client in a real application</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// Validate token and extract user information</span></span><br><span class="line">        <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> jwtUtil.validateToken(token);</span><br><span class="line">        <span class="type">String</span> <span class="variable">extractedUsername</span> <span class="operator">=</span> claims.getSubject();</span><br><span class="line">        List&lt;String&gt; extractedRoles = claims.get(<span class="string">&quot;roles&quot;</span>, List.class);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;Username from Token: &quot;</span> + extractedUsername);</span><br><span class="line">        System.out.println(<span class="string">&quot;Roles from Token: &quot;</span> + extractedRoles);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>在这个测试类中，<code>testGenerateAndValidateToken</code>测试用例生成一个Token，并验证Token是否正确包含了用户名和角色信息。<code>testTokenExpiration</code>测试用例则是为了测试Token过期逻辑是否正常。这里为了方便测试过期逻辑，在<code>JwtUtil</code>类中定义了<code>setExpiration</code>方法来动态设置Token的过期时间，这主要用于测试目的，实际生产环境中不应该使用此方法。<code>testToken</code>测试用例是用于展示一个简单的Token各个阶段的信息内容。</p><p><strong>JWT Token</strong></p><p>JWT Token可以包含多种标准的声明（Claim），以下是一些常见的标准声明：</p><ul><li><strong>iss (Issuer)</strong>：Token的发行者。</li><li><strong>sub (Subject)</strong>：Token的主题，通常用来存储用户的唯一标识。</li><li><strong>aud (Audience)</strong>：Token的接收方。</li><li><strong>exp (Expiration Time)</strong>：Token的过期时间，通常是一个时间戳，表示Token在此时间之后不再有效。</li><li><strong>nbf (Not Before)</strong>：Token的生效时间，表示在此时间之前，Token不可用。</li><li><strong>iat (Issued At)</strong>：Token的发行时间。</li><li><strong>jti (JWT ID)</strong>：Token的唯一标识符。</li></ul><p>除了这些标准声明外，还可以在 Token 中添加自定义声明来存储特定于应用的信息，如用户角色、权限等。使用<code>Claims</code>对象，可以通过<code>get</code>方法以键值对的形式访问这些自定义声明，例如</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">role</span> <span class="operator">=</span> claims.get(<span class="string">&quot;role&quot;</span>, String.class); <span class="comment">// 假设我们在Token中添加了一个名为&quot;role&quot;的自定义声明</span></span><br></pre></td></tr></table></figure></div><blockquote><h3 id="案例"><a class="markdownIt-Anchor" href="#案例"></a> 案例</h3><p>假设有一个简单的Web应用，用于文章的阅读和评论，用户需要登录才能评论。在用户登录时，系统会生成一个JWT Token，其中包含如下标准声明：</p><ul><li><strong>iss (Issuer)</strong>：<code>&quot;MyArticleApp&quot;</code>。这表明Token是由&quot;MyArticleApp&quot;应用发行的。</li><li><strong>sub (Subject)</strong>：<code>&quot;1234567890&quot;</code>。这是用户的唯一标识，通常可以是用户ID、用户名或其他唯一标识用户的信息。在这个例子中，假设它是用户ID。</li><li><strong>aud (Audience)</strong>：<code>&quot;MyArticleAppUsers&quot;</code>。这指示了Token的预期接收者，确保Token只被指定的受众使用。</li><li><strong>exp (Expiration Time)</strong>：<code>1627499999</code>。这是一个UNIX时间戳，表示Token的过期时间。假设这代表Token将在未来某个时间点过期。</li><li><strong>nbf (Not Before)</strong>：<code>1627480000</code>。这同样是一个UNIX时间戳，表示Token在这个时间之前不应被接受。</li><li><strong>iat (Issued At)</strong>：<code>1627486400</code>。这表示Token的发行时间。</li><li><strong>jti (JWT ID)</strong>：<code>&quot;a87ff679a2f3e71d9181a67b7542122c&quot;</code>。这是Token的唯一标识符，用于防止重放攻击。</li></ul><h3 id="如何使用这些信息安全地识别用户身份"><a class="markdownIt-Anchor" href="#如何使用这些信息安全地识别用户身份"></a> 如何使用这些信息安全地识别用户身份？</h3><ol><li><strong>验证Token的签名</strong>：首先，确保Token的签名是有效的，这通过使用发行Token时用到的相同密钥来验证。这一步是必需的，以确保Token未被篡改。</li><li><strong>检查Token的发行者（iss）和受众（aud）</strong>：确认Token的<code>iss</code>和<code>aud</code>声明与你的应用期望的值匹配。这有助于确保Token是为你的应用发行的，且仅供指定的受众使用。</li><li><strong>验证Token的有效期</strong>：检查当前时间是否在Token的<code>nbf</code>（Not Before）和<code>exp</code>（Expiration Time）声明指定的时间范围内。如果不在这个范围内，Token应被认为是无效的。</li><li><strong>获取和验证用户身份</strong>：一旦Token通过了上述验证，你可以信任Token中的<code>sub</code>（Subject）声明。在这个例子中，<code>sub</code>是用户ID<code>&quot;1234567890&quot;</code>。你可以使用这个ID从数据库或其他存储系统中检索用户的详细信息，并在需要时进行进一步的授权检查。</li></ol></blockquote><p>在基于Spring Security和Spring WebFlux的应用中使用JWT进行认证时，<code>JwtAuthenticationManager</code>、<code>JwtSecurityContextRepository</code>和<code>JwtAuthenticationFilter</code>可以协作以支持认证流程。这三个组件各自承担不同的职责，以下是它们如何协作的简化示例及解释：</p><h5 id="jwtauthenticationfilter"><a class="markdownIt-Anchor" href="#jwtauthenticationfilter"></a> JwtAuthenticationFilter</h5><p>这个过滤器负责拦截进入的HTTP请求，从请求中提取JWT Token，并尝试对其进行验证。</p><ul><li><p><strong>职责</strong>：从HTTP请求中提取JWT Token，并创建一个未经认证的<code>Authentication</code>对象（如<code>UsernamePasswordAuthenticationToken</code>），然后将其提交给<code>AuthenticationManager</code>进行认证。</p></li><li><p><strong>用途</strong>：负责拦截进入的请求，从中提取JWT Token，并尝试进行认证。它是连接前端请求和后端安全逻辑的桥梁。</p></li><li><p><strong>适用场景</strong>：几乎所有需要JWT认证的场景。</p></li><li><p><strong>实现</strong>：这个过滤器通常继承自<code>WebFilter</code>接口（对于WebFlux应用）。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationFilter</span> <span class="keyword">implements</span> <span class="title class_">WebFilter</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> JwtAuthenticationManager authenticationManager;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">JwtAuthenticationFilter</span><span class="params">(JwtAuthenticationManager authenticationManager)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.authenticationManager = authenticationManager;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, WebFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">request</span> <span class="operator">=</span> exchange.getRequest();</span><br><span class="line">        <span class="comment">// 提取JWT Token的逻辑</span></span><br><span class="line">        <span class="comment">// 简单示例中，可以不实现具体逻辑，或根据需要添加</span></span><br><span class="line">        <span class="keyword">return</span> chain.filter(exchange);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>参考示例</strong></p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationFilter</span> <span class="keyword">implements</span> <span class="title class_">WebFilter</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> JwtAuthenticationManager authenticationManager;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">JwtAuthenticationFilter</span><span class="params">(JwtAuthenticationManager authenticationManager)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.authenticationManager = authenticationManager;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, WebFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="comment">// 提取请求中的Authorization头部</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">authHeader</span> <span class="operator">=</span> exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);</span><br><span class="line">        <span class="keyword">if</span> (authHeader != <span class="literal">null</span> &amp;&amp; authHeader.startsWith(<span class="string">&quot;Bearer &quot;</span>)) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">authToken</span> <span class="operator">=</span> authHeader.substring(<span class="number">7</span>);</span><br><span class="line">            <span class="comment">// 创建未认证的Authentication Token</span></span><br><span class="line">            <span class="type">Authentication</span> <span class="variable">authRequest</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(authToken, authToken);</span><br><span class="line">            <span class="comment">// 使用JwtAuthenticationManager进行认证</span></span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">this</span>.authenticationManager.authenticate(authRequest)</span><br><span class="line">                .flatMap(auth -&gt; chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))</span><br><span class="line">                .onErrorResume(e -&gt; chain.filter(exchange)); <span class="comment">// 处理认证失败</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 对于没有Bearer Token的请求直接放行</span></span><br><span class="line">        <span class="keyword">return</span> chain.filter(exchange);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>如果在后面实现了<code>JwtSecurityContextRepository</code>，则上面的代码可简化为以下代码</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationFilter</span> <span class="keyword">implements</span> <span class="title class_">WebFilter</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> JwtSecurityContextRepository jwtSecurityContextRepository;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">JwtAuthenticationFilter</span><span class="params">(JwtSecurityContextRepository jwtSecurityContextRepository)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.jwtSecurityContextRepository = jwtSecurityContextRepository;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, WebFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> jwtSecurityContextRepository.load(exchange)</span><br><span class="line">                .flatMap(context -&gt; chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(context))))</span><br><span class="line">                .switchIfEmpty(chain.filter(exchange));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p><code>JwtAuthenticationFilter</code>在请求到达时提取JWT Token，并使用<code>JwtAuthenticationManager</code>进行认证。这个过程实际上在<code>JwtSecurityContextRepository</code>的<code>load</code>方法中已经体现。</p></li></ul><h5 id="jwtauthenticationmanager"><a class="markdownIt-Anchor" href="#jwtauthenticationmanager"></a> JwtAuthenticationManager</h5><p>这是一个自定义的<code>AuthenticationManager</code>，负责处理由<code>JwtAuthenticationFilter</code>提交的未经认证的<code>Authentication</code>对象。</p><ul><li><p><strong>职责</strong>：验证<code>Authentication</code>对象中的JWT Token的有效性，如果Token有效，则创建一个已认证的<code>Authentication</code>对象，并设置用户的权限。</p></li><li><p><strong>用途</strong>：对通过<code>JwtAuthenticationFilter</code>提取出的Token进行验证，确定用户的身份并授权。在WebFlux中，它通常是<code>ReactiveAuthenticationManager</code>的实现，在Spring MVC中，则是<code>AuthenticationManager</code>的实现。</p></li><li><p><strong>适用场景</strong>：需要验证用户Token的场景，实现自定义的认证逻辑。</p></li><li><p><strong>实现</strong>：可以通过实现<code>ReactiveAuthenticationManager</code>接口来自定义认证逻辑。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationManager</span> <span class="keyword">implements</span> <span class="title class_">ReactiveAuthenticationManager</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Authentication&gt; <span class="title function_">authenticate</span><span class="params">(Authentication authentication)</span> &#123;</span><br><span class="line">        <span class="comment">// 在这里实现JWT验证逻辑</span></span><br><span class="line">        <span class="comment">// 简单示例中，可以直接返回已认证的Authentication，或根据需要添加逻辑</span></span><br><span class="line">        <span class="keyword">return</span> Mono.just(authentication);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>参考示例</strong></p><p>在<code>JwtAuthenticationManager</code>验证Token并创建已认证的<code>Authentication</code>对象时，需要从Token中解析用户的角色或权限，并将它们转换为<code>GrantedAuthority</code>对象，以便Spring Security进行授权判断。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationManager</span> <span class="keyword">implements</span> <span class="title class_">ReactiveAuthenticationManager</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> JwtUtil jwtUtil;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">JwtAuthenticationManager</span><span class="params">(JwtUtil jwtUtil)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.jwtUtil = jwtUtil;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Authentication&gt; <span class="title function_">authenticate</span><span class="params">(Authentication authentication)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">authToken</span> <span class="operator">=</span> authentication.getCredentials().toString();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> jwtUtil.getUsernameFromToken(authToken);</span><br><span class="line">            <span class="keyword">if</span> (!jwtUtil.isTokenExpired(authToken)) &#123;</span><br><span class="line">                List&lt;String&gt; roles = jwtUtil.getRolesFromToken(authToken);</span><br><span class="line">                List&lt;SimpleGrantedAuthority&gt; authorities =</span><br><span class="line">                        roles</span><br><span class="line">                                .stream()</span><br><span class="line">                                .map(role -&gt; <span class="keyword">new</span> <span class="title class_">SimpleGrantedAuthority</span>(<span class="string">&quot;ROLE_&quot;</span> + role.toUpperCase()))</span><br><span class="line">                                .collect(Collectors.toList());</span><br><span class="line">                <span class="type">Authentication</span> <span class="variable">auth</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(username, authToken, authorities);</span><br><span class="line">                <span class="keyword">return</span> Mono.just(auth);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty();</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ul><h5 id="jwtsecuritycontextrepository"><a class="markdownIt-Anchor" href="#jwtsecuritycontextrepository"></a> JwtSecurityContextRepository</h5><p>这个组件用于在请求处理过程中加载和保存<code>SecurityContext</code>。对于基于JWT的无状态认证，它通常不会保存<code>SecurityContext</code>，但会在每个请求中基于JWT Token加载<code>SecurityContext</code>。</p><ul><li><p><strong>职责</strong>：基于每个请求中的JWT Token动态构建<code>SecurityContext</code>。</p></li><li><p><strong>用途</strong>：负责在每个请求中重建<code>SecurityContext</code>，根据JWT Token中的信息设置用户的认证状态。由于JWT认证是无状态的，它通常不用于保存状态，而是用于在请求之间重建状态。</p></li><li><p><strong>适用场景</strong>：适用于所有基于JWT进行认证的场景，尤其是在需要将JWT Token信息转化为Spring Security认识的认证信息时。</p></li><li><p><strong>实现</strong>：实现<code>ServerSecurityContextRepository</code>接口，通常在<code>load</code>方法中处理Token的解析和验证。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtSecurityContextRepository</span> <span class="keyword">implements</span> <span class="title class_">ServerSecurityContextRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">save</span><span class="params">(ServerWebExchange exchange, SecurityContext context)</span> &#123;</span><br><span class="line">        <span class="comment">// JWT是无状态的，不需要实现保存</span></span><br><span class="line">        <span class="keyword">return</span> Mono.error(<span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>(<span class="string">&quot;Save method not supported&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;SecurityContext&gt; <span class="title function_">load</span><span class="params">(ServerWebExchange exchange)</span> &#123;</span><br><span class="line">        <span class="comment">// 在这里实现JWT解析和安全上下文的加载</span></span><br><span class="line">        <span class="comment">// 简单示例中，可以直接返回空Mono，或根据需要添加逻辑</span></span><br><span class="line">        <span class="keyword">return</span> Mono.empty();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li><li><p><strong>参考示例</strong></p><p>在Spring Security中，<code>ServerSecurityContextRepository</code>用于在每个请求上加载或创建<code>SecurityContext</code>，这是处理安全上下文的一部分。默认行为是，在请求开始时尝试加载安全上下文，以便在处理请求时可以使用当前安全状态。也就是说默认<code>load</code>方法总是会在每次请求的时候被执行，因此对于不需要认证的路径（如使用<code>permitAll()</code>配置的路径），如果没有有效的认证Token，方法应该能够快速返回，而不影响请求的处理。否则就会造成“你需要先认证才能登录，而你又需要先登录才能认证”的局面。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtSecurityContextRepository</span> <span class="keyword">implements</span> <span class="title class_">ServerSecurityContextRepository</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ReactiveAuthenticationManager authenticationManager;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">JwtSecurityContextRepository</span><span class="params">(ReactiveAuthenticationManager authenticationManager)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.authenticationManager = authenticationManager;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">save</span><span class="params">(ServerWebExchange exchange, SecurityContext context)</span> &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>(<span class="string">&quot;Save method is not supported&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;SecurityContext&gt; <span class="title function_">load</span><span class="params">(ServerWebExchange exchange)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">path</span> <span class="operator">=</span> exchange.getRequest().getURI().getPath();</span><br><span class="line">        <span class="comment">// 对于不需要认证的接口，直接返回Mono.empty()</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;/login&quot;</span>.equals(path) || <span class="string">&quot;/register&quot;</span>.equals(path)) &#123;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> extractToken(exchange.getRequest());</span><br><span class="line">        <span class="comment">// 如果Token为空，也直接返回Mono.empty()</span></span><br><span class="line">        <span class="keyword">if</span> (token == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">Authentication</span> <span class="variable">auth</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UsernamePasswordAuthenticationToken</span>(token, token);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.jwtAuthenticationManager.authenticate(auth).map(SecurityContextImpl::<span class="keyword">new</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String <span class="title function_">extractToken</span><span class="params">(ServerHttpRequest request)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">bearerToken</span> <span class="operator">=</span> request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);</span><br><span class="line">        <span class="keyword">if</span> (StringUtils.hasText(bearerToken) &amp;&amp; bearerToken.startsWith(<span class="string">&quot;Bearer &quot;</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> bearerToken.substring(<span class="number">7</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></li></ul><p><strong>协作示例</strong></p><p>当一个请求到达应用时：</p><ol><li>**<code>JwtAuthenticationFilter</code>**拦截请求，从请求头中提取JWT Token，并构造一个未经认证的<code>Authentication</code>对象，然后提交给<code>JwtAuthenticationManager</code>。</li><li>**<code>JwtAuthenticationManager</code>**接收到未经认证的<code>Authentication</code>对象，验证JWT Token的有效性。如果验证通过，它将创建一个已认证的<code>Authentication</code>对象，包含用户的权限等信息。</li><li>在整个请求处理流程中，**<code>JwtSecurityContextRepository</code>**负责根据<code>JwtAuthenticationManager</code>验证后的<code>Authentication</code>对象构建<code>SecurityContext</code>，并使其在当前请求上下文中可用。</li><li>根据<code>SecurityContext</code>中的认证信息，Spring Security框架执行后续的授权判断。</li></ol><p>这三个组件的协作实现了一个完整的基于JWT的认证和授权流程，使得应用能够处理无状态的HTTP请求。</p><blockquote><p>以上三个核心组件虽然覆盖了JWT认证流程的主要部分：提取Token、验证Token、以及根据Token重建安全上下文。不过，根据具体的应用需求和安全要求，可能还会涉及其他重要的组件或配置。以下是一些可能涉及到的额外组件或方法：</p><h3 id="1-authenticationentrypoint"><a class="markdownIt-Anchor" href="#1-authenticationentrypoint"></a> 1. AuthenticationEntryPoint</h3><p>用于处理认证过程中的异常，如Token无效或过期时的情况。<code>AuthenticationEntryPoint</code>负责在认证失败时返回适当的响应，比如一个401 Unauthorized状态码。</p><p>简单示例</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtAuthenticationEntryPoint</span> <span class="keyword">implements</span> <span class="title class_">ServerAuthenticationEntryPoint</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">commence</span><span class="params">(ServerWebExchange exchange, AuthenticationException e)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Mono.fromRunnable(() -&gt; &#123;</span><br><span class="line">            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);</span><br><span class="line">            <span class="comment">// 你可以添加更多的响应设置，如设置响应体</span></span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h3 id="2-accessdeniedhandler"><a class="markdownIt-Anchor" href="#2-accessdeniedhandler"></a> 2. AccessDeniedHandler</h3><p>当用户尝试访问他们没有权限访问的资源时，<code>AccessDeniedHandler</code>负责处理这种授权失败的情况。它可以用于返回一个403 Forbidden响应或重定向到一个错误页面。</p><h3 id="3-corsconfigurationsource"><a class="markdownIt-Anchor" href="#3-corsconfigurationsource"></a> 3. CorsConfigurationSource</h3><p>在前后端分离的应用中，跨源资源共享（CORS）配置变得非常重要。<code>CorsConfigurationSource</code>用于定义CORS策略，允许或拒绝来自不同源的请求。</p><h3 id="4-tokenrefreshmechanism"><a class="markdownIt-Anchor" href="#4-tokenrefreshmechanism"></a> 4. TokenRefreshMechanism</h3><p>虽然不是Spring Security的标准部分，但在许多基于Token的认证系统中，实现Token刷新机制是一个常见需求。这涉及到提供一个机制，允许客户端在当前Token快要过期时，通过一个有效的刷新Token来获取一个新的访问Token。</p><h3 id="5-userdetails和userdetailsservice"><a class="markdownIt-Anchor" href="#5-userdetails和userdetailsservice"></a> 5. UserDetails和UserDetailsService</h3><p><code>UserDetailsService</code>用于在认证过程中加载用户特定的数据。它通常与<code>UserDetails</code>接口一起使用，后者表示一个用户的认证信息。在JWT认证过程中，一旦Token被验证，可以通过<code>UserDetailsService</code>加载用户的详细信息，并构建一个<code>UserDetails</code>对象，进一步用于构建<code>Authentication</code>对象。</p><h3 id="6-reactive-counterparts-for-webflux"><a class="markdownIt-Anchor" href="#6-reactive-counterparts-for-webflux"></a> 6. Reactive counterparts for WebFlux</h3><p>对于使用Spring WebFlux的应用，上述组件（如<code>AuthenticationEntryPoint</code>、<code>AccessDeniedHandler</code>等）有相应的反应式版本或等效方法，以支持响应式编程模型。</p></blockquote><p><strong>其他重要的方法</strong></p><p><strong>Token的生成和响应</strong>：在用户登录成功后，需要一个服务或控制器方法来生成JWT Token，并将其返回给客户端。这通常发生在用户认证成功后的登录接口中。<strong>这里的功能需要在三层架构中实现，下文中会提到三层架构的适配。</strong></p><h5 id="spring-security-配置"><a class="markdownIt-Anchor" href="#spring-security-配置"></a> Spring Security 配置</h5><p>接下来，配置 Spring Security 以集成 JWT 认证。这包括定义一个自定义的认证管理器和安全过滤器链，用于解析请求中的 JWT Token 并进行认证</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableWebFluxSecurity</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SecurityConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 定义JwtUtil Bean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> JwtUtil <span class="title function_">jwtUtil</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JwtUtil</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 定义JwtAuthenticationManager Bean</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> JwtAuthenticationManager <span class="title function_">jwtAuthenticationManager</span><span class="params">(JwtUtil jwtUtil)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JwtAuthenticationManager</span>(jwtUtil);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 定义JwtSecurityContextRepository Bean，依赖JwtAuthenticationManager</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> JwtSecurityContextRepository <span class="title function_">jwtSecurityContextRepository</span><span class="params">(JwtAuthenticationManager jwtAuthenticationManager)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JwtSecurityContextRepository</span>(jwtAuthenticationManager);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 定义JwtAuthenticationFilter Bean，依赖JwtSecurityContextRepository</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> JwtAuthenticationFilter <span class="title function_">jwtAuthenticationFilter</span><span class="params">(JwtSecurityContextRepository jwtSecurityContextRepository)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JwtAuthenticationFilter</span>(jwtSecurityContextRepository);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> SecurityWebFilterChain <span class="title function_">securityWebFilterChain</span><span class="params">(ServerHttpSecurity http,</span></span><br><span class="line"><span class="params">                                                         JwtAuthenticationManager jwtAuthenticationManager,</span></span><br><span class="line"><span class="params">                                                         JwtSecurityContextRepository jwtSecurityContextRepository)</span> &#123;</span><br><span class="line">        http</span><br><span class="line">                .csrf(ServerHttpSecurity.CsrfSpec::disable) <span class="comment">// 禁用CSRF保护，对于REST API是常见的做法</span></span><br><span class="line">                .authenticationManager(jwtAuthenticationManager) <span class="comment">// 设置自定义的认证管理器</span></span><br><span class="line">                .securityContextRepository(jwtSecurityContextRepository) <span class="comment">// 设置自定义的安全上下文仓库</span></span><br><span class="line">                .authorizeExchange(exchanges -&gt; exchanges</span><br><span class="line">                        .pathMatchers(<span class="string">&quot;/api/public/**&quot;</span>).permitAll() <span class="comment">// 公开访问的路径</span></span><br><span class="line">                        .anyExchange().authenticated() <span class="comment">// 其他所有路径都需要认证</span></span><br><span class="line">                )</span><br><span class="line">                .addFilterAt(jwtAuthenticationFilter(jwtSecurityContextRepository), SecurityWebFiltersOrder.AUTHENTICATION); <span class="comment">// 添加自定义JWT认证过滤器</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> http.build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h4 id="三层架构的适配"><a class="markdownIt-Anchor" href="#三层架构的适配"></a> 三层架构的适配</h4><p>设置好基于JWT的认证机制并且配置了Spring Security之后，下一步是确保三层架构（表示层、业务逻辑层、数据访问层）能够与这些安全配置无缝协作。以下是参考的适配流程</p><h5 id="大致流程"><a class="markdownIt-Anchor" href="#大致流程"></a> 大致流程</h5><p><strong>1.定义安全模型</strong></p><p>首先，定义应用中的安全模型，包括用户角色和权限模型。这可能涉及到在数据库中创建角色和权限表，并确定哪些API端点对应哪些角色或权限。</p><p><strong>2.调整用户模型和数据访问层</strong></p><ul><li>确保用户模型（通常是<code>User</code>实体）包含与安全相关的属性，如密码、角色列表等。</li><li>更新用户的数据访问层（例如，用户的Repository），以支持查找用户的安全凭证（用户名和密码）、角色和权限。</li></ul><p><strong>3.实现UserDetailsService</strong></p><ul><li>实现<code>UserDetailsService</code>接口，提供一种从数据库加载用户详情（包括权限）的方式。这是Spring Security调用来获取用户信息并进行认证的服务。</li><li>在<code>loadUserByUsername</code>方法中，根据用户名查找用户，构建并返回一个<code>UserDetails</code>对象，这个对象应包含用户名、密码和权限信息。</li></ul><p><strong>4.配置方法级别的安全性</strong></p><ul><li>使用<code>@PreAuthorize</code>、<code>@PostAuthorize</code>、<code>@Secured</code>等注解来指定方法级别的安全要求。例如，可以在服务层的方法上使用这些注解来限定只有特定角色的用户才能调用某个方法。</li></ul><p><strong>5.保护API端点</strong></p><ul><li>根据的安全模型调整<code>SecurityWebFilterChain</code>中的<code>.authorizeExchange()</code>部分，指定哪些API端点是公开的，哪些需要认证，以及它们所需的权限或角色。</li><li>例如，可以使用<code>.pathMatchers(&quot;/api/admin/**&quot;).hasRole(&quot;ADMIN&quot;)</code>来保护以<code>/api/admin/</code>开头的所有端点，使其只能由拥有<code>ADMIN</code>角色的用户访问。</li></ul><p><strong>6.处理认证和授权失败</strong></p><ul><li>自定义认证失败和授权失败的处理逻辑，比如返回特定的HTTP状态码或错误信息给客户端。这可以通过自定义<code>AuthenticationEntryPoint</code>和<code>AccessDeniedHandler</code>来实现。</li></ul><p><strong>7.测试</strong></p><ul><li>对整个安全配置进行测试，包括但不限于：<ul><li>测试未认证的请求是否被正确拒绝。</li><li>测试具有不同角色的用户是否只能访问他们有权限的API端点。</li><li>测试用户认证流程，包括使用JWT进行认证。</li></ul></li></ul><p>上面的流程仅供参考，实际简单场景可能不需要考虑那么多繁琐的步骤。</p><h5 id="具体实现"><a class="markdownIt-Anchor" href="#具体实现"></a> 具体实现</h5><h6 id="数据库"><a class="markdownIt-Anchor" href="#数据库"></a> 数据库</h6><p>首先创建好数据库中对应的数据表和初始测试数据，主键采用<code>serial</code>伪类型自动递增，参考如下</p><div class="code-container" data-rel="Postgresql"><figure class="iseeu highlight postgresql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 用户</span></span><br><span class="line"><span class="keyword">create</span> <span class="keyword">table</span> users</span><br><span class="line">(</span><br><span class="line">    id       <span class="type">serial</span> <span class="keyword">primary key</span>,</span><br><span class="line">    username <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">unique</span> <span class="keyword">not</span> <span class="keyword">null</span>,</span><br><span class="line">    <span class="keyword">password</span> <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">not</span> <span class="keyword">null</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> users (username, <span class="keyword">password</span>) <span class="keyword">values</span> (<span class="string">&#x27;admin&#x27;</span>, <span class="string">&#x27;admin&#x27;</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> users (username, <span class="keyword">password</span>) <span class="keyword">values</span> (<span class="string">&#x27;user&#x27;</span>, <span class="string">&#x27;user&#x27;</span>);</span><br><span class="line"><span class="comment">-- 角色</span></span><br><span class="line"><span class="keyword">create</span> <span class="keyword">table</span> roles</span><br><span class="line">(</span><br><span class="line">    id   <span class="type">serial</span> <span class="keyword">primary key</span>,</span><br><span class="line">    <span class="type">name</span> <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">not</span> <span class="keyword">null</span></span><br><span class="line">);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> roles (<span class="type">name</span>) <span class="keyword">values</span> (<span class="string">&#x27;admin&#x27;</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> roles (<span class="type">name</span>) <span class="keyword">values</span> (<span class="string">&#x27;user&#x27;</span>);</span><br><span class="line"><span class="comment">-- 用户角色</span></span><br><span class="line"><span class="keyword">create</span> <span class="keyword">table</span> user_role</span><br><span class="line">(</span><br><span class="line">    user_id <span class="type">int</span> <span class="keyword">not</span> <span class="keyword">null</span>,</span><br><span class="line">    role_id <span class="type">int</span> <span class="keyword">not</span> <span class="keyword">null</span>,</span><br><span class="line">    <span class="keyword">primary key</span> (user_id, role_id),</span><br><span class="line">    <span class="keyword">constraint</span> fk_user_id <span class="keyword">foreign key</span> (user_id) <span class="keyword">references</span> users (id),</span><br><span class="line">    <span class="keyword">constraint</span> fk_role_id <span class="keyword">foreign key</span> (role_id) <span class="keyword">references</span> roles (id)</span><br><span class="line">);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> user_role (user_id, role_id) <span class="keyword">values</span> (<span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> user_role (user_id, role_id) <span class="keyword">values</span> (<span class="number">2</span>, <span class="number">2</span>);</span><br></pre></td></tr></table></figure></div><blockquote><p>不推荐直接将密码明文存储在数据库中，这里是为了方便测试，通常应该将明文加密后再存储到数据库中。</p></blockquote><h6 id="实体类"><a class="markdownIt-Anchor" href="#实体类"></a> 实体类</h6><p>然后创建好对应的实体类</p><p>users 数据表对应实体类 User</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@Table(&quot;users&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> Integer id;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>roles 数据表对应实体类 Role</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@Table(&quot;roles&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Role</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> Integer id;</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>user_role 数据表对应实体类 UserRole</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserRole</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> Integer userId;</span><br><span class="line">    <span class="keyword">private</span> Integer roleId;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h6 id="数据访问层"><a class="markdownIt-Anchor" href="#数据访问层"></a> 数据访问层</h6><p>这里假设已经提供了<code>databaseClient</code></p><ul><li>登录的逻辑被拆分为两次查询，第一次查询是验证该用户是否存在，第二次查询是获取用户所有的角色列表。密码验证的逻辑在服务层实现。</li><li>注册的逻辑比较复杂，注册提供的数据有用户名、密码和角色名称列表。首先是根据用户名查询<code>users</code>表判断该用户名是否已经注册，如果该用户名未被注册，则新增数据并返回新增数据的<code>id</code>字段（在 Postgresql 中通过<code>returning</code>语法实现新增时返回新增数据的<code>id</code>字段），然后是根据角色名称列表查询<code>roles</code>表得到角色id列表，最后将唯一的用户id和角色id列表批量增加到<code>user_role</code>表中。</li></ul><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;User&gt; <span class="title function_">findByUsername</span><span class="params">(String username)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> databaseClient.sql(<span class="string">&quot;select * from users where username = :username&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;username&quot;</span>, username)</span><br><span class="line">        .map((row, rowMetadata) -&gt; <span class="keyword">new</span> <span class="title class_">User</span>(</span><br><span class="line">            row.get(<span class="string">&quot;id&quot;</span>, Integer.class), <span class="comment">// 这里的id是自增的，所以不需要传入</span></span><br><span class="line">            row.get(<span class="string">&quot;username&quot;</span>, String.class),</span><br><span class="line">            row.get(<span class="string">&quot;password&quot;</span>, String.class)))</span><br><span class="line">        .one();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;UserRoleDTO&gt; <span class="title function_">getUserRole</span><span class="params">(String username)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> databaseClient.sql(<span class="string">&quot;select username, name &quot;</span> +</span><br><span class="line">                              <span class="string">&quot;from users u &quot;</span> +</span><br><span class="line">                              <span class="string">&quot;join user_role ur on u.id = ur.user_id &quot;</span> +</span><br><span class="line">                              <span class="string">&quot;join roles r on ur.role_id = r.id &quot;</span> +</span><br><span class="line">                              <span class="string">&quot;where username = :username&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;username&quot;</span>, username)</span><br><span class="line">        .map((row, rowMetadata) -&gt; Tuples.of(</span><br><span class="line">            row.get(<span class="string">&quot;username&quot;</span>, String.class),</span><br><span class="line">            row.get(<span class="string">&quot;name&quot;</span>, String.class)))</span><br><span class="line">        .all()</span><br><span class="line">        .collectMultimap(tuple -&gt; tuple.getT1(), tuple -&gt; tuple.getT2())</span><br><span class="line">        .flatMap(map -&gt; &#123;</span><br><span class="line">            <span class="comment">// 从map中取出username和对应的roles列表</span></span><br><span class="line">            <span class="keyword">return</span> Mono.justOrEmpty(map.entrySet().stream()</span><br><span class="line">                                    .map(entry -&gt; <span class="keyword">new</span> <span class="title class_">UserRoleDTO</span>(entry.getKey(), <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(entry.getValue())))</span><br><span class="line">                                    .findFirst());</span><br><span class="line">        &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;Integer&gt; <span class="title function_">insertUser</span><span class="params">(User user)</span> &#123;</span><br><span class="line">    <span class="comment">// 返回插入的id，这里的密码应该是加密后的</span></span><br><span class="line">    <span class="keyword">return</span> databaseClient.sql(<span class="string">&quot;insert into users (username, password) values (:username, :password) returning id&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;username&quot;</span>, user.getUsername())</span><br><span class="line">        .bind(<span class="string">&quot;password&quot;</span>, user.getPassword())</span><br><span class="line">        .map((row, rowMetadata) -&gt; row.get(<span class="string">&quot;id&quot;</span>, Integer.class))</span><br><span class="line">        .first();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;Long&gt; <span class="title function_">insertUserRole</span><span class="params">(Integer userId, UserRoleDTO userRoleDTO)</span> &#123;</span><br><span class="line">    <span class="comment">// 根据角色名查询对应的id，然后插入到user_role表中</span></span><br><span class="line">    <span class="keyword">return</span> databaseClient.sql(<span class="string">&quot;select id from roles where name in (:names)&quot;</span>)</span><br><span class="line">        .bind(<span class="string">&quot;names&quot;</span>, userRoleDTO.getRoles())</span><br><span class="line">        .fetch()</span><br><span class="line">        .all()</span><br><span class="line">        .flatMap(row -&gt;</span><br><span class="line">                 databaseClient.sql(<span class="string">&quot;insert into user_role (user_id, role_id) values (:user_id, :role_id)&quot;</span>)</span><br><span class="line">                 .bind(<span class="string">&quot;user_id&quot;</span>, userId)</span><br><span class="line">                 .bind(<span class="string">&quot;role_id&quot;</span>, (Integer) row.get(<span class="string">&quot;id&quot;</span>))</span><br><span class="line">                 .fetch()</span><br><span class="line">                 .rowsUpdated()</span><br><span class="line">                ).reduce(<span class="number">0L</span>, Long::sum);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>这里提供了一个<code>UserRoleDTO</code>，对应前面的 Token 生成逻辑，传入<code>username</code>和<code>roles</code>用于 Token 的生成</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserRoleDTO</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; roles;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h6 id="服务层"><a class="markdownIt-Anchor" href="#服务层"></a> 服务层</h6><ul><li><p><strong><code>login</code></strong>：先根据<code>username</code>参数查询用户，然后比较<code>password</code>（实际的密码比较应该使用专门的类处理，下文会补充），如果通过了验证，则使用<code>UserDTO</code>的数据生成 Token，并将 Token 传输到控制层。</p></li><li><p><strong><code>register</code></strong>：先用<code>UserRoleDTO</code>的<code>username</code>查询用户是否已注册，如果没有注册过则将<code>username</code>和<code>password</code>封装成一个<code>User</code>用于新增用户，然后用新增用户返回的<code>userId</code>和<code>UserRoleDTO</code>一并传给新增用户角色方法，用户角色列表数据新增完成后生成 Token 并传输到控制层。</p></li></ul><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;String&gt; <span class="title function_">login</span><span class="params">(String username, String password)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> userRepo.findByUsername(username)</span><br><span class="line">        .flatMap(user -&gt; &#123;</span><br><span class="line">            <span class="keyword">if</span> (user.getPassword().equals(password)) &#123;</span><br><span class="line">                <span class="keyword">return</span> userRepo.getUserRole(username)</span><br><span class="line">                    .flatMap(userRole -&gt; Mono.fromSupplier(() -&gt;</span><br><span class="line">                                                           jwtUtil.generateToken(username, userRole.getRoles()))</span><br><span class="line">                            );</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty();</span><br><span class="line">        &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;String&gt; <span class="title function_">register</span><span class="params">(UserRoleDTO userRoleDTO, String password)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> userRepo.findByUsername(userRoleDTO.getUsername())</span><br><span class="line">        .flatMap(user -&gt; Mono.just(<span class="string">&quot;User already exists&quot;</span>))</span><br><span class="line">        .switchIfEmpty(Mono.defer(() -&gt; &#123;</span><br><span class="line">            <span class="comment">// id为null，由数据库自动生成</span></span><br><span class="line">            <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>(<span class="literal">null</span>, userRoleDTO.getUsername(), password);</span><br><span class="line">            <span class="keyword">return</span> userRepo.insertUser(user)</span><br><span class="line">                .flatMap(userId -&gt; userRepo.insertUserRole(userId, userRoleDTO)</span><br><span class="line">                         .thenReturn(jwtUtil.generateToken(userRoleDTO.getUsername(), userRoleDTO.getRoles())));</span><br><span class="line">        &#125;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h6 id="控制层"><a class="markdownIt-Anchor" href="#控制层"></a> 控制层</h6><p>handler 处理器中将 Token 存入响应头，或者也可以存入封装好的响应体中</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">login</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> request.bodyToMono(LoginRequest.class)</span><br><span class="line">        .flatMap(login -&gt;</span><br><span class="line">                 userService.login(login.getUsername(), login.getPassword())</span><br><span class="line">                )</span><br><span class="line">        .flatMap(token -&gt;</span><br><span class="line">                 ServerResponse.ok()</span><br><span class="line">                 .header(HttpHeaders.AUTHORIZATION, <span class="string">&quot;Bearer &quot;</span> + token) <span class="comment">// 将生成的Token放入响应头</span></span><br><span class="line">                 .bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">200</span>, token))</span><br><span class="line">                )</span><br><span class="line">        .switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> Mono&lt;ServerResponse&gt; <span class="title function_">register</span><span class="params">(ServerRequest request)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> request.bodyToMono(RegisterRequest.class)</span><br><span class="line">        .flatMap(register -&gt; &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> register.getUsername();</span><br><span class="line">            <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> register.getPassword();</span><br><span class="line">            List&lt;String&gt; roles = register.getRoles();</span><br><span class="line">            <span class="type">UserRoleDTO</span> <span class="variable">userRoleDTO</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UserRoleDTO</span>(username, roles);</span><br><span class="line">            <span class="keyword">return</span> userService.register(userRoleDTO, password)</span><br><span class="line">                .flatMap(token -&gt; &#123;</span><br><span class="line">                    <span class="keyword">if</span> (<span class="string">&quot;User already exists&quot;</span>.equals(token)) &#123;</span><br><span class="line">                        <span class="keyword">return</span> ServerResponse.status(HttpStatus.CONFLICT)</span><br><span class="line">                            .bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">409</span>, token));</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="keyword">return</span> ServerResponse.ok()</span><br><span class="line">                        .header(HttpHeaders.AUTHORIZATION, <span class="string">&quot;Bearer &quot;</span> + token) <span class="comment">// 将生成的Token放入响应头</span></span><br><span class="line">                        .bodyValue(<span class="keyword">new</span> <span class="title class_">ApiResponse</span>&lt;&gt;(<span class="number">200</span>, token));</span><br><span class="line">                &#125;</span><br><span class="line">                        );</span><br><span class="line">        &#125;)</span><br><span class="line">        .switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>在这里需要接受前端不同的请求数据，为了方便处理自定义了<code>LoginRequst</code>和<code>RegisterRequest</code>，内容如下</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// LoginRequest.java</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoginRequest</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// RegisterRequest.java</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RegisterRequest</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; roles;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>RouterConfig 路由配置中定义请求方式和 URL</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> RouterFunction&lt;ServerResponse&gt; <span class="title function_">userRoutes</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> RouterFunctions</span><br><span class="line">        .route(RequestPredicates.POST(<span class="string">&quot;/login&quot;</span>), userHandler::login)</span><br><span class="line">        .andRoute(RequestPredicates.POST(<span class="string">&quot;/register&quot;</span>), userHandler::register);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h6 id="安全层"><a class="markdownIt-Anchor" href="#安全层"></a> 安全层</h6><p>只需要添加对应路径的角色权限即可，例如<code>/api/admin/</code>前缀的所有路径需要<code>ROLE_ADMIN</code>权限。这个权限的加载已经在前面的<code>JwtAuthenticationManager</code>中实现。</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> SecurityWebFilterChain <span class="title function_">securityWebFilterChain</span><span class="params">(ServerHttpSecurity http)</span> &#123;</span><br><span class="line">    http</span><br><span class="line">        .csrf(ServerHttpSecurity.CsrfSpec::disable)</span><br><span class="line">        .formLogin(ServerHttpSecurity.FormLoginSpec::disable)</span><br><span class="line">        .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)</span><br><span class="line">        .authenticationManager(jwtAuthenticationManager())</span><br><span class="line">        .securityContextRepository(jwtSecurityContextRepository())</span><br><span class="line">        .authorizeExchange(exchanges -&gt; exchanges</span><br><span class="line">                           .pathMatchers(<span class="string">&quot;/login&quot;</span>, <span class="string">&quot;/register&quot;</span>).permitAll()</span><br><span class="line">                           .pathMatchers(<span class="string">&quot;/api/admin/**&quot;</span>).hasRole(<span class="string">&quot;ADMIN&quot;</span>)</span><br><span class="line">                           .anyExchange().authenticated()</span><br><span class="line">                          )</span><br><span class="line">        .addFilterAt(jwtAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION);</span><br><span class="line">    <span class="keyword">return</span> http.build();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h4 id="passwordencoder-密码加密"><a class="markdownIt-Anchor" href="#passwordencoder-密码加密"></a> PasswordEncoder 密码加密</h4><p>前面直接比较明文密码（<code>user.getPassword().equals(password)</code>）并不是一个安全的做法。为了增强安全性，应该使用密码哈希值进行存储和验证。Spring Security提供了<code>PasswordEncoder</code>接口来帮助实现密码的加密和验证。</p><h5 id="引入passwordencoder"><a class="markdownIt-Anchor" href="#引入passwordencoder"></a> 引入<code>PasswordEncoder</code></h5><p>首先，需要在 Spring 配置中定义一个<code>PasswordEncoder</code>的Bean。自Spring Security 5起，推荐使用<code>BCryptPasswordEncoder</code>，它是一种基于bcrypt强哈希方法的密码编码器</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> PasswordEncoder <span class="title function_">passwordEncoder</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">BCryptPasswordEncoder</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h5 id="更新用户注册逻辑"><a class="markdownIt-Anchor" href="#更新用户注册逻辑"></a> 更新用户注册逻辑</h5><p>在用户注册或创建密码时，使用<code>PasswordEncoder</code>对密码进行加密，并存储加密后的密码</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;String&gt; <span class="title function_">register</span><span class="params">(UserRoleDTO userRoleDTO, String rawPassword)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> userRepo.findByUsername(userRoleDTO.getUsername())</span><br><span class="line">        .flatMap(user -&gt; Mono.just(<span class="string">&quot;User already exists&quot;</span>))</span><br><span class="line">        .switchIfEmpty(Mono.defer(() -&gt; &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> passwordEncoder.encode(rawPassword); <span class="comment">// 使用encode方法加密密码</span></span><br><span class="line">            <span class="comment">// id为null，由数据库自动生成</span></span><br><span class="line">            <span class="comment">// 创建User对象，设置加密后的密码，然后保存User对象</span></span><br><span class="line">            <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>(<span class="literal">null</span>, userRoleDTO.getUsername(), password);</span><br><span class="line">            <span class="keyword">return</span> userRepo.insertUser(user)</span><br><span class="line">                .flatMap(userId -&gt; userRepo.insertUserRole(userId, userRoleDTO)</span><br><span class="line">                         .thenReturn(jwtUtil.generateToken(userRoleDTO.getUsername(), userRoleDTO.getRoles())));</span><br><span class="line">        &#125;));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><h5 id="更新登录验证逻辑"><a class="markdownIt-Anchor" href="#更新登录验证逻辑"></a> 更新登录验证逻辑</h5><p>在登录逻辑中，使用<code>PasswordEncoder</code>的<code>matches</code>方法来验证提交的密码与存储的加密密码是否匹配</p><div class="code-container" data-rel="Java"><figure class="iseeu highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> PasswordEncoder passwordEncoder;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Mono&lt;String&gt; <span class="title function_">login</span><span class="params">(String username, String rawPassword)</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> userRepo.findByUsername(username)</span><br><span class="line">        .flatMap(user -&gt; &#123;</span><br><span class="line">            <span class="keyword">if</span> (passwordEncoder.matches(rawPassword, user.getPassword())) &#123; <span class="comment">// 使用matches方法验证密码</span></span><br><span class="line">                <span class="keyword">return</span> userRepo.getUserRole(username)</span><br><span class="line">                    .flatMap(userRole -&gt; Mono.fromSupplier(() -&gt;</span><br><span class="line">                                                           jwtUtil.generateToken(username, userRole.getRoles()))</span><br><span class="line">                            );</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> Mono.empty(); <span class="comment">// 密码不匹配时返回空</span></span><br><span class="line">        &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div><p>通过使用<code>PasswordEncoder</code>，可以增强系统的安全性，防止密码泄露时直接暴露用户的明文密码。同时，这也是符合安全最佳实践的做法。</p><h3 id="总结-3"><a class="markdownIt-Anchor" href="#总结-3"></a> 总结</h3><p>关于 Spring Webflux 还有很多其它可以实践的内容，例如整合<code>OAuth2</code>进行第三方服务鉴权认证、使用响应式 Web 客户端<code>WebClient</code>调用外部 HTTP 服务等。</p><p>以上就是基于 Spring Webflux 响应式框架后端项目开发过程中涉及到的主要内容。其实关于 Spring Webflux 的应用并没有传统的 Spring Web 那么广泛，而且使用 Spring Webflux 需要有一定的函数式编程与响应式编程的熟练度，对于习惯了传统 Spring Web 框架的开发者来说是具有一定的难度，但是它提供了一种不同于传统同步阻塞模型的异步非阻塞模型的视角来看待问题。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;记录首次基于 Spring Webflux 响应式框架的后端项目开发过程中涉及到的一些东西。</summary>
    
    
    
    
    <category term="Spring Webflux" scheme="https://www.entropy-tree.top/tags/Spring-Webflux/"/>
    
  </entry>
  
  <entry>
    <title>简单配置 Linux 上的 NVIDIA</title>
    <link href="https://www.entropy-tree.top/2024/01/25/simple-nvidia-setup-for-linux/"/>
    <id>https://www.entropy-tree.top/2024/01/25/simple-nvidia-setup-for-linux/</id>
    <published>2024-01-25T10:35:33.000Z</published>
    <updated>2024-03-06T09:32:19.177Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要介绍在 Linux 上简单地配置 NVIDIA 用于特定应用程序(例如游戏)的图形加速，而非全局性的设置。<span id="more"></span></p><p>以 <a class="link"   href="https://github.com/HMCL-dev/HMCL" >HMCL<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a> 为例，只需安装一些基础包，进行简单的配置即可让 Minecraft 在 NVIDIA 显卡下工作。</p><h3 id="基础安装"><a class="markdownIt-Anchor" href="#基础安装"></a> 基础安装</h3><blockquote><p>这里仅列举需要安装的包，大部分包可能已经被安装了。</p></blockquote><p><code>nvidia nvidia-settings lib32-nvidia-utils</code>：这三个是必须安装的包，是 NVIDIA 的基础驱动。</p><p><code>nvidia-prime</code>：可选安装，是用于指定应用程序使用 NVIDIA 显卡加速。本质上是通过设置环境变量实现。</p><blockquote><p>以下是用于监控 NVIDIA 工作情况的一些选项。</p></blockquote><p>命令行工具</p><ol><li><p><strong>nvidia-smi（NVIDIA System Management Interface）：</strong></p><ul><li><code>nvidia-smi</code> 是 NVIDIA 提供的一个命令行工具，它能够显示 NVIDIA GPU 的详细状态，包括温度、使用率、显存使用情况等。</li><li>使用方法：在终端中输入 <code>nvidia-smi</code>。你也可以使用 <code>watch -n 1 nvidia-smi</code> 命令来每秒刷新显示状态。</li></ul></li><li><p><strong>nvtop：</strong></p><ul><li><p><code>nvtop</code> 是一个类似于 <code>htop</code> 的工具，但专门用于 NVIDIA GPU。它提供了一个基于文本的用户界面，显示 GPU 的使用率、显存使用情况、正在运行的进程等信息。</p></li><li><p>安装方法：在许多 Linux 发行版中，<code>nvtop</code> 可以通过包管理器安装，如 <code>sudo apt install nvtop</code>（在基于 Debian 的系统上）或 <code>sudo pacman -S nvtop</code>（在 Arch Linux 上）。</p></li></ul></li></ol><p>图形化工具</p><ol><li><p><strong>NVIDIA X Server Settings：</strong></p><ul><li>如果你使用的是 NVIDIA 官方驱动，那么 NVIDIA X Server Settings 工具应该已经包含在驱动安装包中。</li><li>这个图形界面工具提供了关于显卡状态的信息，包括温度、显存使用量等。</li></ul></li><li><p><strong>Conky：</strong></p><ul><li>Conky 是一个轻量级的系统监控工具，它可以配置为显示 NVIDIA GPU 的状态。</li><li>Conky 需要一些配置工作才能显示 GPU 信息，但它提供了很高的可定制性。</li></ul></li></ol><h3 id="使用-prime-run-命令控制"><a class="markdownIt-Anchor" href="#使用-prime-run-命令控制"></a> 使用 prime-run 命令控制</h3><p>如果选择安装<code>nvidia-prime</code>包，可以直接执行如下格式的命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">prime-run program</span><br></pre></td></tr></table></figure></div><p>在一些启动应用程序的命令之前添加<code>prime-run</code>命令即可使其工作在NVIDIA 显卡下。</p><h3 id="设置环境变量控制"><a class="markdownIt-Anchor" href="#设置环境变量控制"></a> 设置环境变量控制</h3><p>如果选择手动设置环境，大部分时候只需要关注三个环境变量，更多时候可能只需要关注两个环境变量。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">__NV_PRIME_RENDER_OFFLOAD=1<span class="comment"># 启用 PRIME Render Offload，允许应用程序使用 NVIDIA GPU</span></span><br><span class="line">__GLX_VENDOR_LIBRARY_NAME=<span class="string">&quot;nvidia&quot;</span><span class="comment"># 指定使用 NVIDIA 的 GLX 实现，确保 OpenGL 调用通过 NVIDIA GPU</span></span><br><span class="line">__VK_LAYER_NV_optimus=<span class="string">&quot;NVIDIA_only&quot;</span><span class="comment"># 专门用于 Vulkan 应用程序强制使用 NVIDIA GPU</span></span><br></pre></td></tr></table></figure></div><p>对于大部分不使用<code>vulkan</code>图形 API 的应用程序来说，不需要关心<code>__VK_LAYER_NV_optimus</code>这个环境变量。</p><p>以该方式运行一个应用程序，手动执行命令如下</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">__NV_PRIME_LIBRARY_RENDER_OFFLOAD=1 __CLX_VENDOR_LIBRARY_NAME=nvidia program</span><br></pre></td></tr></table></figure></div><p>如果觉得该方式比较繁琐，也可自行编写脚本并进行软链接，用于自定义需要使用 NVIDIA 的应用程序</p><p>参考脚本如下</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置环境变量以使用NVIDIA显卡</span></span><br><span class="line"><span class="built_in">export</span> __NV_PRIME_RENDER_OFFLOAD=1</span><br><span class="line"><span class="built_in">export</span> __GLX_VENDOR_LIBRARY_NAME=nvidia</span><br><span class="line"></span><br><span class="line"><span class="comment"># 启动你的应用程序</span></span><br><span class="line"><span class="built_in">exec</span> program</span><br></pre></td></tr></table></figure></div><h3 id="在-hmcl-中配置-nvidia-加速游戏"><a class="markdownIt-Anchor" href="#在-hmcl-中配置-nvidia-加速游戏"></a> 在 HMCL 中配置 NVIDIA 加速游戏</h3><blockquote><p>HMCL 本身是一个游戏启动器，这里需要加速的是游戏，而非启动器本身。</p></blockquote><p>先启动 HMCL，进入“设置”页面，选择“修改高级设置”</p><style>.pfyoaxpujsve{zoom: 50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/01/25/simple-nvidia-setup-for-linux/image-20240125175909038.png"                      class="pfyoaxpujsve" alt="image-20240125175909038"                ><p>如果选择使用<code>prime-run</code>的方式，可以在“自定义命令”模块的“包装命令”中填入<code>prime-run</code></p><style>.owoldvgwlkgp{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/01/25/simple-nvidia-setup-for-linux/image-20240125182631549.png"                      class="owoldvgwlkgp" alt="image-20240125182631549"                ><p>如果选择设置环境变量的方式，可以在“Java 虚拟机设置”模块的”环境变量“中填入<code>__NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia</code></p><style>.onmcmvgmlsmb{zoom:50%;}</style><img                       lazyload                     src="/images/loading.svg"                     data-src="/2024/01/25/simple-nvidia-setup-for-linux/image-20240125182927742.png"                      class="onmcmvgmlsmb" alt="image-20240125182927742"                ><p>接下来就可以运行游戏，在 NVIDIA 显卡下工作，可以通过前面提到的 NVIDIA 的监控工具查看 NVIDIA 显卡是否在工作。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://archlinuxstudio.github.io/ArchLinuxTutorial/#/rookie/graphic_driver" >显卡驱动 | archlinuxstudio<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文主要介绍在 Linux 上简单地配置 NVIDIA 用于特定应用程序(例如游戏)的图形加速，而非全局性的设置。</summary>
    
    
    
    
    <category term="nvidia" scheme="https://www.entropy-tree.top/tags/nvidia/"/>
    
    <category term="hmcl" scheme="https://www.entropy-tree.top/tags/hmcl/"/>
    
  </entry>
  
  <entry>
    <title>现代密码学——DES</title>
    <link href="https://www.entropy-tree.top/2023/11/18/cryptology-des/"/>
    <id>https://www.entropy-tree.top/2023/11/18/cryptology-des/</id>
    <published>2023-11-18T14:48:03.000Z</published>
    <updated>2023-11-23T12:13:20.073Z</updated>
    
    <content type="html"><![CDATA[<p>本文整理了关于现代密码学入门之作——DES（数据加密标准）的一些相关资料。<span id="more"></span></p><h3 id="des的工作原理"><a class="markdownIt-Anchor" href="#des的工作原理"></a> DES的工作原理</h3><p>DES（数据加密标准）是一种对称密钥加密算法，它的工作原理如下：</p><ol><li><strong>DES的总体结构</strong>：DES使用 Feistel 网络结构，总共有 16 轮迭代。每轮迭代都包含了置换、代换和异或等操作。</li><li><strong>初始置换（IP）和逆初始置换（IP-1）</strong>：在DES加密的开始，64位的明文会经过一个初始置换IP，这个置换是固定的。然后在16轮迭代结束后，会进行逆初始置换IP-1，得到最终的密文。</li><li><strong>F函数</strong>：F函数是DES中最重要的部分，它包括扩展置换、S盒代换和P盒置换三个步骤。扩展置换将32位的输入扩展到48位，然后与子密钥进行异或操作。S盒代换将48位的输入转换为32位的输出，这个过程包含了非线性变换。最后，P盒置换是一个简单的线性置换。</li><li><strong>密钥调度算法</strong>：DES使用了一个56位的密钥，但是在每轮迭代中，都会生成一个48位的子密钥。这个过程被称为密钥调度。密钥调度包括了置换选择1（PC-1）、循环左移和置换选择2（PC-2）三个步骤。</li></ol><blockquote><p>DES算法中置换表的使用顺序如下：</p><ol><li><strong>PC-1置换</strong>：在密钥调度算法中，首先使用PC-1置换来选择56位的密钥材料。</li><li><strong>PC-2置换</strong>：在密钥调度算法中，每轮迭代都会使用PC-2置换来生成48位的子密钥。</li><li><strong>初始置换（IP）</strong>：在加密和解密过程的开始，使用初始置换来重新排列64位的输入。</li><li><strong>扩展置换（E）</strong>：在每轮迭代中，使用扩展置换将32位的输入扩展到48位。</li><li><strong>S盒</strong>：在每轮迭代中，使用S盒将48位的输入转换为32位的输出。</li><li><strong>P盒置换（P）</strong>：在每轮迭代中，使用P盒置换来重新排列32位的输出。</li><li><strong>逆初始置换（IP-1）</strong>：在加密和解密过程的结束，使用逆初始置换来得到最终的64位的输出。</li></ol></blockquote><h3 id="des中的数学基础"><a class="markdownIt-Anchor" href="#des中的数学基础"></a> DES中的数学基础</h3><p>DES（数据加密标准）的数学基础主要包括以下几个部分：</p><ol><li><strong>置换和代换的数学原理</strong>：在DES中，置换和代换是两种基本的操作。置换是将输入的位重新排列，而代换是将输入的位进行非线性变换。这两种操作的组合可以提供混淆和扩散，这是现代密码学的两个基本原则。</li><li><strong>S盒的设计和数学性质</strong>：S盒（Substitution-box）是DES中的核心部分，它提供了非线性变换。S盒的设计基于一些复杂的数学性质，例如在给定输入差分的情况下，输出差分的概率分布应该尽可能均匀。这可以防止差分密码分析等攻击。</li><li><strong>轮函数和Feistel网络的数学性质</strong>：DES使用了Feistel网络结构，这是一种特殊的轮函数结构。在Feistel网络中，每一轮的输出可以表示为输入和子密钥的某种函数。这种结构的一个重要性质是，解密过程和加密过程几乎相同，只是子密钥的顺序相反。</li></ol><h3 id="des的安全性分析"><a class="markdownIt-Anchor" href="#des的安全性分析"></a> DES的安全性分析</h3><p>DES（数据加密标准）的安全性分析主要包括以下几个部分：</p><ol><li><strong>差分密码分析</strong>：差分密码分析是一种针对对称密钥加密算法的攻击方法，它基于输入和输出之间的差分分布。在DES中，由于S盒的设计，差分密码分析的效率并不高。</li><li><strong>线性密码分析</strong>：线性密码分析是另一种针对对称密钥加密算法的攻击方法，它基于输入、输出和密钥之间的线性关系。在DES中，由于P盒和S盒的设计，线性密码分析的效率也并不高。</li><li><strong>对DES的常见攻击</strong>：除了差分密码分析和线性密码分析，还有一些其他的攻击方法可以用于攻击DES，例如穷举攻击（暴力破解）。由于DES的密钥长度只有56位，所以穷举攻击是可行的。这也是为什么现在很少使用单DES，而更倾向于使用三重DES或AES。</li></ol><h3 id="des的基本实现"><a class="markdownIt-Anchor" href="#des的基本实现"></a> DES的基本实现</h3><h4 id="des加密算法的伪代码"><a class="markdownIt-Anchor" href="#des加密算法的伪代码"></a> DES加密算法的伪代码</h4><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function DES_Encrypt(plaintext, key):</span><br><span class="line">    // 初始置换</span><br><span class="line">    plaintext = initial_permutation(plaintext)</span><br><span class="line">    </span><br><span class="line">    // 密钥调度</span><br><span class="line">    subkeys = key_schedule(key)</span><br><span class="line">    </span><br><span class="line">    // 16轮迭代</span><br><span class="line">    for i from 1 to 16:</span><br><span class="line">        left, right = split(plaintext)</span><br><span class="line">        temp = right</span><br><span class="line">        right = left xor f_function(right, subkeys[i])</span><br><span class="line">        left = temp</span><br><span class="line">        plaintext = join(left, right)</span><br><span class="line">    </span><br><span class="line">    // 逆初始置换</span><br><span class="line">    ciphertext = inverse_initial_permutation(plaintext)</span><br><span class="line">    </span><br><span class="line">    return ciphertext</span><br></pre></td></tr></table></figure></div><h4 id="des解密算法的伪代码"><a class="markdownIt-Anchor" href="#des解密算法的伪代码"></a> DES解密算法的伪代码</h4><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function DES_Decrypt(ciphertext, key):</span><br><span class="line">    // 初始置换</span><br><span class="line">    ciphertext = initial_permutation(ciphertext)</span><br><span class="line">    </span><br><span class="line">    // 密钥调度</span><br><span class="line">    subkeys = key_schedule(key)</span><br><span class="line">    </span><br><span class="line">    // 16轮迭代（注意子密钥的顺序相反）</span><br><span class="line">    for i from 16 down to 1:</span><br><span class="line">        left, right = split(ciphertext)</span><br><span class="line">        temp = right</span><br><span class="line">        right = left xor f_function(right, subkeys[i])</span><br><span class="line">        left = temp</span><br><span class="line">        ciphertext = join(left, right)</span><br><span class="line">    </span><br><span class="line">    // 逆初始置换</span><br><span class="line">    plaintext = inverse_initial_permutation(ciphertext)</span><br><span class="line">    </span><br><span class="line">    return plaintext</span><br></pre></td></tr></table></figure></div><h4 id="编程代码实现"><a class="markdownIt-Anchor" href="#编程代码实现"></a> 编程代码实现</h4><p>这里主要考虑在 ECB 工作模式下实现 DES 加解密算法。</p><p>Pyhton的版本可以直接使用<code>pycryptodome</code>第三方库快速实现DES加解密功能，这里的代码执行的结果也可用于验证下面 C++ 实现 DES 加解密的正确性。</p><blockquote><p>在使用 C++ 实现 DES 的过程中可能会出现加解密可以正常工作，但是中间生成的密文并不符合标准 DES 算法生成的密文的情况，这实际上只是满足了对称加解密的特点，并没有标准 DES 算法的安全性。因此，一般不推荐任何人去使用个人实现的加解密算法，而应该选择那些经过全世界验证的公开的加解密算法库。</p></blockquote><div class="code-container" data-rel="Python"><figure class="iseeu highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> Crypto.Cipher <span class="keyword">import</span> DES</span><br><span class="line"><span class="comment"># from Crypto.Util.Padding import pad, unpad</span></span><br><span class="line"><span class="keyword">from</span> binascii <span class="keyword">import</span> hexlify, unhexlify</span><br><span class="line"></span><br><span class="line">key = unhexlify(<span class="string">&#x27;0123456789ABCDEF&#x27;</span>)</span><br><span class="line">plaintext = unhexlify(<span class="string">&#x27;0123456789ABCDEF&#x27;</span>)</span><br><span class="line"></span><br><span class="line">cipher = DES.new(key, DES.MODE_ECB)</span><br><span class="line"><span class="comment"># ciphertext = cipher.encrypt(pad(plaintext, DES.block_size))</span></span><br><span class="line">ciphertext = cipher.encrypt(plaintext)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;密文: &quot;</span>, hexlify(ciphertext).decode())</span><br><span class="line"></span><br><span class="line">cipher = DES.new(key, DES.MODE_ECB)</span><br><span class="line"><span class="comment"># decrypted = unpad(cipher.decrypt(ciphertext), DES.block_size)</span></span><br><span class="line">decrypted = cipher.decrypt(ciphertext)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;明文: &quot;</span>, hexlify(decrypted).decode())</span><br></pre></td></tr></table></figure></div><p>C++版本的实现较为复杂，可以参考以下开源库</p><ul><li><p><a href="https://github.com/openssl/openssl"><strong>openssl</strong></a>: C语言实现，功能相当强大和全面，代码量比较庞大，不太适合初学者学习</p></li><li><p><a href="https://github.com/weidai11/cryptopp"><strong>cryptopp</strong></a>: C++实现，代码庞大且复杂</p></li><li><p><a href="https://github.com/libtom/libtomcrypt"><strong>libtomcrypt</strong></a>: C语言实现，适合初学密码学或期望对密码学有进一步了解的人。名气和使用广泛性不如 OpenSSL 和 Crypto++</p></li></ul><p>实现的思路如下，将代码分为以下几个文件逐步实现</p><ol><li><strong>des.h</strong>：这个头文件中定义了DES类和它的公有接口。这些接口包括构造函数（用于设置密钥），加密函数和解密函数。</li><li><strong>des.cpp</strong>：这个源文件中实现了DES类的公有接口。这包括构造函数（其中调用了密钥调度算法），加密函数和解密函数（其中调用了初始置换、F函数和逆初始置换）。</li><li><strong>utils.h</strong>：这个头文件中定义了一些工具函数，如置换函数和代换函数。</li><li><strong>utils.cpp</strong>：这个源文件中实现了工具函数。</li><li><strong>main.cpp</strong>：这个源文件中包含了主函数，用于测试DES类的功能。</li></ol><p>实现的过程参考如下，其中涉及的大量置换等统一封装到工具函数</p><ol><li><p>先考虑密钥调度算法实现，生成 16 轮迭代中轮函数<code>f_function</code>所需要的 16 把子密钥。</p><blockquote><p>密钥调度算法细节：</p><p>64 位主密钥 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mrow><mi>P</mi><mi>C</mi><mo>−</mo><mn>1</mn></mrow></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{PC-1}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.711em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.13889em;">P</span><span class="mord mathnormal mtight" style="margin-right:0.07153em;">C</span><span class="mbin mtight">−</span><span class="mord mtight">1</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 56 位密钥 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mtext>均分</mtext></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{均分}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord cjk_fallback mtight">均</span><span class="mord cjk_fallback mtight">分</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 两把 28 位密钥 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mtext>循环左移</mtext></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{循环左移}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord cjk_fallback mtight">循</span><span class="mord cjk_fallback mtight">环</span><span class="mord cjk_fallback mtight">左</span><span class="mord cjk_fallback mtight">移</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 两把 28 位密钥  <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mtext>合并</mtext></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{合并}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord cjk_fallback mtight">合</span><span class="mord cjk_fallback mtight">并</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 56 位密钥 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mrow><mi>P</mi><mi>C</mi><mo>−</mo><mn>2</mn></mrow></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{PC-2}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.711em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.13889em;">P</span><span class="mord mathnormal mtight" style="margin-right:0.07153em;">C</span><span class="mbin mtight">−</span><span class="mord mtight">2</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 48 位子密钥</p><p>重复上述过程 16 次，只有第一次使用 64 位的主密钥初始化，其余都使用上一次合并后的 56 位密钥初始化，对两把 28 位的密钥法分别进行循环左移操作时，按照固定设计的位移规则进行。</p><p>轮函数细节：</p><p>32 位的密文 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mtext>扩展置换</mtext></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{扩展置换}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord cjk_fallback mtight">扩</span><span class="mord cjk_fallback mtight">展</span><span class="mord cjk_fallback mtight">置</span><span class="mord cjk_fallback mtight">换</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 48 位的密文 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mtext>与子密钥异或</mtext></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{与子密钥异或}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord cjk_fallback mtight">与</span><span class="mord cjk_fallback mtight">子</span><span class="mord cjk_fallback mtight">密</span><span class="mord cjk_fallback mtight">钥</span><span class="mord cjk_fallback mtight">异</span><span class="mord cjk_fallback mtight">或</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 48 位的密文 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mrow><mi>S</mi><mtext>盒代换</mtext></mrow></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{S盒代换}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.05764em;">S</span><span class="mord cjk_fallback mtight">盒</span><span class="mord cjk_fallback mtight">代</span><span class="mord cjk_fallback mtight">换</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 32 位的密文 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo><mover><mo><mo>⟶</mo></mo><mrow><mi>P</mi><mtext>盒置换</mtext></mrow></mover></mo></mrow><annotation encoding="application/x-tex">\stackrel{P盒置换}\longrightarrow</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.3003310000000001em;vertical-align:-0.011em;"></span><span class="mrel"><span class="mop op-limits"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.2893310000000002em;"><span style="top:-3em;"><span class="pstrut" style="height:3em;"></span><span><span class="mop">⟶</span></span></span><span style="top:-3.7110000000000003em;margin-left:0em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight" style="margin-right:0.13889em;">P</span><span class="mord cjk_fallback mtight">盒</span><span class="mord cjk_fallback mtight">置</span><span class="mord cjk_fallback mtight">换</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.011em;"><span></span></span></span></span></span></span></span></span></span> 32 位的密文</p><p>S 盒的实现细节：</p><p>48 位的密文均分为 8 组，每组 6 位，取最高位和最低位组合成 2 位二进制，计算得到十进制的行号；取剩余的 4 位二进制计算得到列号，根据行、列号找到 S 盒中对应的数字记录到密文中。重复 8 次，每次从不同的 S 盒寻找对应值。</p><p>这样从 8 个 S 盒中取出 8 个数字，每个数字占 4 位二进制，组成 32 位的密文。</p></blockquote></li><li><p>再考虑加解密算法实现，按照初始置换、16 轮迭代、逆初始置换的先后顺序执行即可。</p><blockquote><p>迭代的细节：</p><p>在每一轮迭代中，先把初始置换得到的密文均分为左右 2 段密文，缓存右半部分用于后面进行左右交换。右半部分由左半部分密文与轮函数的返回值（即 48 位子密钥）进行异或运算得到，左半部分则接收之前缓存的右半部分原始值，最后将左、右部分合并得到本轮迭代的 64 位密文，这样的过程重复 16 次。</p><p>注意：</p><p>16 轮迭代意味着有 16 次左右交换，但有的算法只进行了 15 次交换，在最后一轮迭代时将右、左部分合并，相当于没有进行交换。</p><p>实际上，按正常 16 次迭代之后，已经交换了 16 次，但是逆初始置换本身就具有左右交换的性质，那么逆初始置换之后，总共交换了 17 次，这并不是标准的 DES 实现。所以可能会出现以下两种实现：</p><ul><li>在 16 轮迭代之后，再加一次左右交换用于抵消逆初始置换造成的影响</li><li>在 16 轮迭代的最后一轮不进行左右交换，第 16 次交换就由逆初始置换去完成</li></ul><p>这两种实现都是可行的，笔者的参考代码使用的是后者。</p><p>由于对称密码体系的特点，解密的算法与加密基本一致，只需要以相反于加密算法的顺序依次使用 16 把子密钥即可解密。</p></blockquote></li></ol><p>完整的 C++ 实现代码存放在<a class="link"   href="https://github.com/s-chance/CryptoStudy" >github 仓库<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>中。</p><h4 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h4><p>在DES加密算法中，涉及到位数变化的置换主要有以下几种：</p><ol><li>PC-1置换：将64位的密钥减少到56位。这个置换丢弃了原始密钥中的每个字节的最后一位，这些位通常用作奇偶校验位。</li><li>PC-2置换：将56位的密钥减少到48位，用于生成子密钥。这个置换丢弃了8位，使得每个子密钥都有一个不同的48位的部分。</li><li>扩展置换（E）：在每个轮次的开始，将右半部分（32位）扩展到48位，以便与子密钥进行异或操作。这个置换通过复制一些位来实现扩展。</li></ol><h3 id="des的变体和替代方案"><a class="markdownIt-Anchor" href="#des的变体和替代方案"></a> DES的变体和替代方案</h3><p>**三重DES（3DES）<strong>和</strong>高级加密标准（AES）**是DES的两个主要替代方案：</p><ol><li><strong>三重DES（3DES）</strong>：由于DES的密钥长度只有56位，所以它容易受到穷举攻击。为了解决这个问题，人们提出了三重DES。三重DES是对DES加密算法的一个扩展，它使用了三个56位的密钥，所以总的密钥长度为168位。三重DES的工作方式是先用第一个密钥进行DES加密，然后用第二个密钥进行DES解密，最后用第三个密钥进行DES加密。这样，即使有一部分密钥被破解，攻击者也无法得到完整的明文。</li><li><strong>高级加密标准（AES）</strong>：AES是一种更先进的对称密钥加密算法，它取代了DES成为了新的加密标准。AES使用了128位、192位或256位的密钥，所以它的安全性更高。与DES不同，AES不是基于Feistel网络，而是基于置换-代换网络（SPN）。AES的设计更加简单和高效，所以现在它被广泛应用在各种场合。</li></ol><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://www.bilibili.com/video/BV1qW4y1L7tN" >【计算机博物志】DES的生与死<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文整理了关于现代密码学入门之作——DES（数据加密标准）的一些相关资料。</summary>
    
    
    
    <category term="信息安全" scheme="https://www.entropy-tree.top/categories/%E4%BF%A1%E6%81%AF%E5%AE%89%E5%85%A8/"/>
    
    
    <category term="对称密钥算法" scheme="https://www.entropy-tree.top/tags/%E5%AF%B9%E7%A7%B0%E5%AF%86%E9%92%A5%E7%AE%97%E6%B3%95/"/>
    
    <category term="入门之作" scheme="https://www.entropy-tree.top/tags/%E5%85%A5%E9%97%A8%E4%B9%8B%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>docker持久化存储</title>
    <link href="https://www.entropy-tree.top/2023/10/07/docker-persistent-storage/"/>
    <id>https://www.entropy-tree.top/2023/10/07/docker-persistent-storage/</id>
    <published>2023-10-07T05:48:41.000Z</published>
    <updated>2024-07-10T07:35:07.337Z</updated>
    
    <content type="html"><![CDATA[<p>在使用 docker 提供的容器服务时，如果不小心删除了容器，那么默认情况下与容器关联的数据也会一并丢失，不过实际上 docker 提供了几种持久化存储的方式。<span id="more"></span></p><h3 id="docker-数据存储的方式"><a class="markdownIt-Anchor" href="#docker-数据存储的方式"></a> docker 数据存储的方式</h3><h4 id="问题"><a class="markdownIt-Anchor" href="#问题"></a> 问题</h4><p>默认情况下，所有的数据文件都是在容器内创建并存储在可写入的存储层中。这意味着一旦容器被删除，那么数据也会丢失，而且这种方式不利于数据的迁移和共享，性能也会大大降低。</p><h4 id="解决方案"><a class="markdownIt-Anchor" href="#解决方案"></a> 解决方案</h4><p>针对上述问题，docker 提供了两种方案实现持久化存储，分别是<strong>数据卷 volumes</strong> 和<strong>绑定挂载 bind mounts</strong>。</p><p>volumes 是由 docker 管理的，可以在多个容器之间共享，并且在容器删除之后 volume 中的数据也会保留下来。volumes 不需要关注宿主机的文件系统结构，可移植性强，易于管理和备份，但是在宿主机下通常无法直接修改 volumes 的内容，也不推荐直接在宿主机修改 volumes 的内容。</p><p>bind mounts 允许你将宿主机的文件或目录挂载到容器中。与 volumes 不同，bind mounts 依赖于宿主机的文件系统结构，这也表示它的可移植性不如 volumes，但它可能是最简单易用的方式，因为它可以将你在宿主机上对 bind mounts 挂载点下的文件修改操作立即反映到容器内部。</p><h4 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h4><p>对于不想持久化存储的数据，可以考虑使用 tmpfs mounts。tmpfs mounts 允许你在容器内存中创建一个临时文件系统，只需要重启或关闭容器就会删除数据。</p><p>tmpfs mounts 的使用参考<a class="link"   href="https://docs.docker.com/storage/tmpfs/" >docker官方文档<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h3 id="docker-持久化存储实践"><a class="markdownIt-Anchor" href="#docker-持久化存储实践"></a> docker 持久化存储实践</h3><h4 id="使用-volumes"><a class="markdownIt-Anchor" href="#使用-volumes"></a> 使用 volumes</h4><p>创建一个名为 <code>vol</code> 的空的 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume create vol</span><br></pre></td></tr></table></figure></div><p>查看已有的 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume <span class="built_in">ls</span></span><br></pre></td></tr></table></figure></div><p>可以通过 <code>-v</code> 或 <code>--mount</code> 参数指定 volume 数据卷挂载到容器的指定目录，在容器中所有针对该目录的写入操作都会保存到宿主机的 volume 中。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">--name=nginxtest \</span><br><span class="line">--mount <span class="built_in">type</span>=volume,<span class="built_in">source</span>=nginx-vol,target=/usr/share/nginx/html \</span><br><span class="line">nginx:latest</span><br></pre></td></tr></table></figure></div><p>或使用以下写法</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">--name=nginxtest \</span><br><span class="line">-v nginx-vol:/usr/share/nginx/html \</span><br><span class="line">nginx:latest</span><br></pre></td></tr></table></figure></div><p>在 docker-compose.yml 中</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">frontend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">node:lts</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">myapp:/home/node/app</span></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">myapp:</span></span><br></pre></td></tr></table></figure></div><p>或使用以下写法</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">frontend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">node:lts</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">type:</span> <span class="string">volume</span></span><br><span class="line">    <span class="attr">source:</span> <span class="string">myapp</span></span><br><span class="line">    <span class="attr">target:</span> <span class="string">/home/node/app</span></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">myapp:</span></span><br></pre></td></tr></table></figure></div><p>执行 <code>docker-compose up</code> 即可。</p><p>如果创建时没有指定 volume 的名称，则会创建匿名卷。</p><p>为了防止用户忘记将关键数据挂载到宿主机目录，通常会在 Dockerfile 中定义命名卷，指定了容器内特定的路径应该被外部卷挂载。</p><p>在运行容器时，如果指定了容器内路径但未提供卷名，Docker 会自动创建匿名卷。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -v /path/in/container my_image</span><br></pre></td></tr></table></figure></div><p>未指定卷名，会创建匿名卷。</p><p>删除一个名为<code>vol</code>的 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume <span class="built_in">rm</span> vol</span><br></pre></td></tr></table></figure></div><p>批量删除 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker volume prune <span class="comment"># 只删除未使用的匿名卷</span></span><br><span class="line">docker volume prune -a <span class="comment"># 删除所有未使用的卷</span></span><br></pre></td></tr></table></figure></div><p>查看容器关联的 volume 可使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker inspect [container_id] | grep Mounts -A 50</span><br></pre></td></tr></table></figure></div><h4 id="使用bind-mounts"><a class="markdownIt-Anchor" href="#使用bind-mounts"></a> 使用bind mounts</h4><p>bind mounts 的使用和 volumes 类似，也是通过 <code>-v</code> 和 <code>--mount</code> 参数将宿主机文件挂载到容器中</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">--name=nginxtest \</span><br><span class="line">--mount <span class="built_in">type</span>=<span class="built_in">bind</span>,<span class="built_in">source</span>=/usr/local/web,target=/usr/share/nginx/html \</span><br><span class="line">nginx:latest</span><br></pre></td></tr></table></figure></div><p>或使用以下写法</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">--name=nginxtest \</span><br><span class="line">-v /usr/local/web:/usr/share/nginx/html \</span><br><span class="line">nginx:latest</span><br></pre></td></tr></table></figure></div><p>在 docker-compose.yml 中</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">frontend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">node:lts</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./static:/opt/app/static</span></span><br></pre></td></tr></table></figure></div><p>或使用以下写法</p><div class="code-container" data-rel="Yaml"><figure class="iseeu highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">frontend:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">node:lts</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">type:</span> <span class="string">bind</span></span><br><span class="line">        <span class="attr">source:</span> <span class="string">./static</span></span><br><span class="line">        <span class="attr">target:</span> <span class="string">/opt/app/static</span></span><br></pre></td></tr></table></figure></div><p>其它的删除修改等操作可直接在宿主机下进行，但是删除宿主机挂载点下的文件后可能会导致容器因为配置文件或其他文件丢失而出错。</p><h4 id="存储方式的转换"><a class="markdownIt-Anchor" href="#存储方式的转换"></a> 存储方式的转换</h4><h5 id="从-volumes-转换到-bind-mounts"><a class="markdownIt-Anchor" href="#从-volumes-转换到-bind-mounts"></a> 从 volumes 转换到 bind mounts</h5><p>在宿主机上创建新的目录，作为 bind mount 的源</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> /path/on/host/bind/mount</span><br></pre></td></tr></table></figure></div><p>使用 <code>docker cp</code> 命令将卷中的数据复制到宿主机目录</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">cp</span> my_container:/path/in/container /path/on/host/bind_mount</span><br></pre></td></tr></table></figure></div><p>在 <code>docker cp</code> 不可用的情况下，可以通过临时容器复制数据</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> \ </span><br><span class="line">--volumes-from my_container \</span><br><span class="line">-v /path/on/host:/backup \</span><br><span class="line">  ubuntu</span><br><span class="line">  <span class="built_in">cp</span> -ar /path/in/container /backup</span><br></pre></td></tr></table></figure></div><p>在新的容器使用 bind mount</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -v /path/on/host/bind_mount:/path/in/container my_image</span><br></pre></td></tr></table></figure></div><h5 id="从-bind-mounts-转换到-volumes"><a class="markdownIt-Anchor" href="#从-bind-mounts-转换到-volumes"></a> 从 bind mounts 转换到 volumes</h5><p>在宿主机上创建一个新的 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume create my_new_volume</span><br></pre></td></tr></table></figure></div><p>挂载新创建的 volume 和宿主机上的 bind mount 目录到容器内的不同路径上</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> -it \</span><br><span class="line">  -v my_new_volume:/path/in/container/new \</span><br><span class="line">  -v /path/on/host/bind_mount:/path/in/container/old \</span><br><span class="line">  ubuntu /bin/bash</span><br></pre></td></tr></table></figure></div><p>在容器内执行复制操作，将数据从 bind mount 的路径复制到新创建的 Docker 卷的路径</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> -ar /path/in/container/old/. /path/in/container/new/</span><br></pre></td></tr></table></figure></div><p>在新的容器使用 volume</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -v my_new_volume:/path/in/container my_image</span><br></pre></td></tr></table></figure></div><h3 id="使用场景"><a class="markdownIt-Anchor" href="#使用场景"></a> 使用场景</h3><p>volumes 和 bind mounts 都是一种宿主机与容器之间的共享文件。但是它们的使用场景有所不同。以下是一些对比：</p><p>volumes</p><ul><li>易于备份和迁移(可移植性高)</li><li>通过docker完全管理，数据安全性更好</li><li>易于数据共享</li><li>可以借助驱动实现远程挂载</li></ul><p>bind mounts</p><ul><li>具有更好的性能</li><li>使用起来更加简单</li><li>在宿主机下可以很方便地访问修改文件</li></ul><p>关于 volumes 和 bind mounts 在挂载行为上的差异。空的 volumes 在挂载时，会自动将容器内指定目录的数据复制过来。而空的 bind mounts 在挂载时则会覆盖容器内的配置(原本的内容会被隐藏)。</p><p>因此使用 volume 更倾向于数据备份，使用 bind mounts 更倾向于文件修改。</p><h3 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h3><p>docker 的持久化存储方式有 volumes 数据卷存储和 bind mounts 绑定挂载存储。</p><p>volumes 比较适合数据库持久化存储等需要易于备份和迁移的场景，而 bind mounts 比较适合修改文件并即时反映到容器的开发环境场景等。</p><p>在简单的场景下，这两种存储方式在使用上并没有什么明显差异。不过，如果要开发新的 docker 应用程序，则应该优先考虑使用 volumes。</p><p>持久化存储方式解决了容器的数据长期存储问题。不过容器存在一定的性能损耗，能否作为生产环境也是个问题。当然现在有很流行的 Kubernetes 项目，平常用户所购买的 vps、云服务器等都借助了虚拟化技术，性能问题或许已经不是最主要的问题了。</p><p>从个人学习的角度上来看，docker 也非常具有学习价值，并且可以通过 docker 来快速搭建各种环境，例如通过 docker 安装 ubuntu 来学习基础的 linux 命令，通过 docker 搭建各种 web 服务等。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://docs.docker.com/storage/" >storage | docker docs<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><h4 id="相关推荐"><a class="markdownIt-Anchor" href="#相关推荐"></a> 相关推荐</h4><p><a class="link"   href="https://podman.io/" >另一个容器化工具 podman<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在使用 docker 提供的容器服务时，如果不小心删除了容器，那么默认情况下与容器关联的数据也会一并丢失，不过实际上 docker 提供了几种持久化存储的方式。</summary>
    
    
    
    <category term="运维" scheme="https://www.entropy-tree.top/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="docker" scheme="https://www.entropy-tree.top/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>Git中的特殊文件</title>
    <link href="https://www.entropy-tree.top/2023/09/18/special-git-files/"/>
    <id>https://www.entropy-tree.top/2023/09/18/special-git-files/</id>
    <published>2023-09-18T12:56:45.000Z</published>
    <updated>2023-09-18T16:10:44.132Z</updated>
    
    <content type="html"><![CDATA[<p>对于经常使用Git来进行版本控制的用户来说，接触最多的可能就是那几个核心的命令了。不过这篇文章的主要目的并不是研究命令，而是了解一些可能比较冷门的知识。<span id="more"></span></p><h3 id="特殊文件"><a class="markdownIt-Anchor" href="#特殊文件"></a> 特殊文件</h3><p>在Git中，有一些特殊的文件可以提交到仓库，这些文件对于项目的管理和开发非常有用。以下是一些主要的Git特殊文件:</p><ul><li><p><strong>.gitignore 文件</strong>：用于指定哪些文件和目录应该被Git忽略，不进行版本控制。通常在这个文件中列出一些<strong>临时文件、日志文件、编译产生的文件、敏感信息文件</strong>等，避免将它们包含到代码仓库中。</p></li><li><p><strong>.gitattributes 文件</strong>：用于配置Git的文件属性，根据不同的文件类型，指定不同的行为。这些属性可以决定文件的换行符、文件编码，以及是否进行二进制合并等。这在<strong>处理跨平台开发时的换行符问题</strong>非常有用。不过如果项目团队是在同一个操作系统下开发，那么这个文件可能就没有什么必要。</p></li><li><p><strong>.gitmodules 文件</strong>：用于管理Git仓库中的子模块。子模块是指一个独立的Git仓库，可以作为父项目的一部分。</p></li><li><p><strong>.gitkeep 文件</strong>：用于保持Git的目录结构，即使目录中没有实际文件也可以提交到仓库中。在创建一个空的文件夹时，如果想要保持这个文件夹在Git中存在，可以添加一个名为<code>.gitkeep</code>的空文件。</p></li><li><p><strong>.gitmessage 文件</strong>：用于自定义Git提交时的默认信息。当使用git commit命令进行提交时，如果没有提供自定义的提交信息，Git就会使用<code>.gitmessage</code>文件中的内容作为默认提交信息。</p></li></ul><p>以上这些特殊文件其实在特定的应用场景下使用得会很频繁，只是大部分普通用户只需要一些基本的核心命令即可满足日常需求。这些特殊文件在项目的管理和开发中起到了重要的作用，尤其可以让团队更好的组织和管理项目。</p><h3 id="提交规范"><a class="markdownIt-Anchor" href="#提交规范"></a> 提交规范</h3><p>Git提交规范对于代码的可维护性和版本管理非常重要。常见的提交规范如下：</p><ul><li><p>提交信息格式：</p><p>每个Git提交信息都应该包含一个清晰简洁的标题和一个更详细的描述。推荐的提交信息格式如下：</p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</span><br><span class="line">&lt;BLANK LINE&gt;</span><br><span class="line">&lt;body&gt;</span><br><span class="line">&lt;BLANK LINE&gt;</span><br><span class="line">&lt;footer&gt;</span><br></pre></td></tr></table></figure></div><p>其中，<code>&lt;type&gt;</code>代表提交的类型，<code>&lt;scope&gt;</code>代表影响范围，<code>&lt;subject&gt;</code>是提交信息的简短描述，<code>&lt;body&gt;</code>是详细描述，<code>&lt;footer&gt;</code>是一些元数据，比如关联的issue、PR等。</p></li><li><p>提交类型：</p><p>每个类型值都表示了不同的含义，类型值必须是以下的其中一个：</p><ul><li><code>feat</code>：提交新功能</li><li><code>fix</code>：修复了bug</li><li><code>docs</code>：只修改了文档</li><li><code>style</code>：调整代码格式，未修改代码逻辑（比如修改空格、格式化等不影响代码的正常运行）</li><li><code>refactor</code>：代码重构，不修复bug也不增加新功能</li><li><code>perf</code>：性能优化，提高性能而不影响正常功能的代码修改</li><li><code>test</code>：增加或修改代码测试功能</li><li><code>chore</code>：对构建流程或辅助工具和依赖库（如文档生成）的更改</li></ul></li><li><p>代码回滚：</p><p>代码回滚比较特殊，若本次提交是为了恢复到之前的某个提交，那么提交信息必须是以<code>revert:</code>开头，后面跟上那个提交的完整标题。然后在信息正文中写上<code>This reverts commit &lt;hash&gt;</code>，其中的<code>&lt;hash&gt;</code>是要还原的那个提交的SHA值。</p></li><li><p>影响范围：</p><p>范围不是固定值，它可以是你提交代码实际影响到的任何内容。唯一需要注意的是它必须足够简短。当影响范围较大时也可以使用<code>*</code>来表示。</p></li><li><p>标题：</p><p>标题是对变更的简明描述，使用祈使句，现在时态，首字母不要大写，结尾不要加句号。</p></li><li><p>正文：</p><p>正文是对标题的补充，但它不是必须的。和标题一样，它也要求使用祈使句和现在时态。正文应该包含更详细的信息，如代码修改的目的，与修改前的代码对比等。</p></li><li><p>页脚：</p><p>任何<code>Breaking Changes</code>(破坏性变更，不向下兼容)都应该在页脚中进行说明，它经常也用来引用本次提交解决的Github Issue。<code>Breaking Changes</code>应该以<code>BREAKING CHANGE:</code>开头，然后紧跟一个空格或两个换行符。</p></li></ul><p>以下给出一份提交信息的具体示例：</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git commit -m <span class="string">&quot;feat(login): add login functionality</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">This commit adds a new login functionality to the login module.Now, users can login to the application using their email and password.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">Related issues: #123 #456&quot;</span></span><br></pre></td></tr></table></figure></div><p>看上去是这样，但实际上在终端这种特殊环境里，回车换行会导致命令直接被执行。实际写法可能是用<code>$'...\n\n...'</code>或者在每行结尾加上一个反斜杠<code>\</code>来实现换行和空行以满足固定的格式要求，那么后面就介绍直接使用<code>git commit</code>命令配合<code>.gitmessage</code>文件来规范提交。</p><h3 id="使用gitmessage规范提交模板"><a class="markdownIt-Anchor" href="#使用gitmessage规范提交模板"></a> 使用.gitmessage规范提交模板</h3><p>前面提到<code>.gitmessage</code>可以用于自定义Git提交时的默认信息，在使用<code>git commit</code>命令提交如果没有提供自定义的提交信息，Git就会使用<code>.gitmessge</code>文件中的内容作为默认提交信息。那么下面说明如何设置提交模板：</p><ol><li><p>在项目目录中创建一个名为<code>.gitmessage</code>的文件</p></li><li><p>在<code>.gitmessage</code>文件中写入以下参考的提交模板</p><div class="code-container" data-rel="Plaintext"><figure class="iseeu highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">类型(必填): feat、fix、docs、style、refactor、perf、test、chore</span><br><span class="line">影响范围(可选)：尽量简短</span><br><span class="line">标题(必填): 尽量简短</span><br><span class="line">正文：详细说明修改</span><br><span class="line">页脚：关联的Issue号</span><br><span class="line"></span><br><span class="line">特别注意</span><br><span class="line">代码回滚请以revert:开头，并在正文指定回滚提交的SHA值</span><br><span class="line">Breaking Changes请务必在页脚注明</span><br></pre></td></tr></table></figure></div><p>保存文件并退出</p></li><li><p>设置<code>.gitmessage</code>文件为默认的提交模板，在父目录中执行以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 局部设置，影响单个仓库的所有分支</span></span><br><span class="line">git config commit.template .gitmessage</span><br><span class="line"><span class="comment"># 全局设置，影响全部仓库</span></span><br><span class="line">git config --global commit.template .gitmessage</span><br></pre></td></tr></table></figure></div></li><li><p>之后，每次直接使用<code>git commit</code>命令将会自动显示该文件中的内容用于规范提交信息。效果如下</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230918233735676.png"                      alt="image-20230918233735676"                 ></p></li><li><p>如果要撤销模板设置可以使用以下命令</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 撤销局部设置</span></span><br><span class="line">git config --<span class="built_in">unset</span> commit.template</span><br><span class="line"><span class="comment"># 撤销全局设置</span></span><br><span class="line">git config --global --<span class="built_in">unset</span> commit.template</span><br></pre></td></tr></table></figure></div></li></ol><p><code>.gitmessage</code>相比直接在命令行里写提交信息主要有以下区别：</p><ul><li><code>.gitmessage</code>文件可以帮助你定义一个统一的提交信息模板，从而保证每次提交时的信息格式的一致性。这对于多人协作开发的项目来说非常有用，它可以帮助团队成员更好地理解每次提交的信息。</li><li>直接在命令行中写提交信息更加灵活，但是可能会导致提交信息的格式不一致，在多人协作项目中不利于提交信息的理解和共享。</li></ul><h3 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h3><p>在广大的用户群体中，这些特殊文件和提交规范可能都不是特别关注的对象，实际上很多用户本身都不是团队开发成员，可能仅仅只是使用过Git的一个命令就不再去深入了解其他的内容。但是在一些特殊的应用场景中，对于Git深度用户来说这可能是很基础的内容。不过在很多时候，还是有人可能并不会严格根据规范来执行，毕竟规范的目的都是为了更好的理解，而规范本身的设计也并不一定完全合理。总之，选择一种最妥善的表达方式，提交规范可以提供一个不错的参考。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://deepinout.com/git/git-questions/154_git_what_are_all_available_git_special_files_that_can_be_committed_to_a_repository.html" >Git 可以提交到仓库的所有特殊文件 | 极客笔记<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhuanlan.zhihu.com/p/67804026" >Git提交规范 | 知乎<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;对于经常使用Git来进行版本控制的用户来说，接触最多的可能就是那几个核心的命令了。不过这篇文章的主要目的并不是研究命令，而是了解一些可能比较冷门的知识。</summary>
    
    
    
    <category term="Git" scheme="https://www.entropy-tree.top/categories/Git/"/>
    
    
    <category term="不经意的发现" scheme="https://www.entropy-tree.top/tags/%E4%B8%8D%E7%BB%8F%E6%84%8F%E7%9A%84%E5%8F%91%E7%8E%B0/"/>
    
    <category term="记录" scheme="https://www.entropy-tree.top/tags/%E8%AE%B0%E5%BD%95/"/>
    
  </entry>
  
  <entry>
    <title>在linux上启用QEMU/KVM</title>
    <link href="https://www.entropy-tree.top/2023/09/05/qemu-kvm/"/>
    <id>https://www.entropy-tree.top/2023/09/05/qemu-kvm/</id>
    <published>2023-09-05T06:08:55.000Z</published>
    <updated>2023-09-05T16:23:24.669Z</updated>
    
    <content type="html"><![CDATA[<p>虚拟机的起源可追溯到1966年IBM的CP-40和CP-67虚拟机操作系统，虚拟化技术最初是为了解决大型系统中的资源利用率问题而开发的。<span id="more"></span>而发展到了现在，无论是在大型企业上还是在个人电脑上，都会以某种方式使用到虚拟机和虚拟化技术。</p><p>不过本文的目的是介绍linux平台上的虚拟机QEMU/KVM，学习QEMU/KVM的简单配置。</p><p>这里笔者的宿主系统是<code>Archlinux</code>、<code>x86_64</code>架构。</p><h3 id="qemukvm环境搭建"><a class="markdownIt-Anchor" href="#qemukvm环境搭建"></a> qemu/kvm环境搭建</h3><p>linux内核中有KVM内核模块，这使其在linux平台上拥有相对出色的性能。不过KVM能否使用还要看硬件是否支持。</p><p>执行以下命令查看系统架构是否为<strong>x86_64</strong></p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">uname</span> -m</span><br></pre></td></tr></table></figure></div><p>执行以下命令检测硬件虚拟化支持</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> lscpu</span><br></pre></td></tr></table></figure></div><p>确认硬件环境没有问题之后，还需要下载相关的工具包。KVM并不能单独使用，通常配合QEMU使用。</p><p>安装QEMU等工具包，命令如下</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S qemu-base virt-manager libvirt virt-viewer dnsmasq vde2 bridge-utils openbsd-netcat ebtables libguestfs</span><br><span class="line"><span class="comment"># 关于qemu还可以选择qemu-full包，支持更多的架构</span></span><br></pre></td></tr></table></figure></div><p>以上只是列举了所有可能用到的包，这些包并不需要全部安装，下面进一步说明</p><ul><li><p>qemu：开源的机器模拟器和虚拟机。安装这个包之后，其实已经可以用命令行启动虚拟机了，但是所有的操作都得通过命令行实现。</p></li><li><p>libvirt：用于控制KVM、QEMU等虚拟化引擎的一组软件的汇集。</p><ul><li><p>virt-manager：libvirt的图形化管理工具，是一个全功能的虚拟机管理程序。</p></li><li><p>virt-viewer：更轻量的libvirt的图形化工具，主要用于查看虚拟机的图形控制台，功能没有virt-manager全面。也就说虚拟机的创建还是在命令行完成。</p><blockquote><p>可以根据需求在virt-manager和virt-viewer中选择一个安装。virt-manager包含了virt-viewer的功能。</p></blockquote></li><li><p>dnsmasq：用于虚拟机的NAT/DHCP网络。</p></li><li><p>bridge-utils：用于虚拟机桥接网络。</p></li><li><p>openbsd-netcat：用于多虚拟机之间建立TCP/IP连接。</p></li><li><p>vde2：以太网虚拟化，用于模拟复杂网络环境。</p></li><li><p>ebtables：桥接防火墙，用于控制多虚拟机之间的流量。</p></li></ul></li><li><p>libguestfs：虚拟机磁盘镜像处理工具，用于查看和修改虚拟机镜像、转换镜像格式等。具体使用参考这篇<a class="link"   href="https://thiscute.world/posts/qemu-kvm-usage/" >文章<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p></li></ul><p>如果只是需要简单地使用qemu创建单一虚拟机，那么安装以下包即可。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> pacman -S qemu-base libvirt virt-manager dnsmasq bridge-utils</span><br></pre></td></tr></table></figure></div><h3 id="启用嵌套虚拟化可选"><a class="markdownIt-Anchor" href="#启用嵌套虚拟化可选"></a> 启用嵌套虚拟化(可选)</h3><p>嵌套虚拟化，即在虚拟机里嵌套虚拟机，这个可根据实际需求选择是否启用。相关命令如下</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启用嵌套虚拟化</span></span><br><span class="line"><span class="built_in">sudo</span> modprobe -r kvm_intel</span><br><span class="line"><span class="built_in">sudo</span> modprobe kvm_intel nested=1</span><br><span class="line"><span class="comment"># 验证嵌套虚拟化，输出类似Y或1的字符则表示成功</span></span><br><span class="line"><span class="built_in">cat</span> /sys/module/kvm_intel/parameters/nested</span><br></pre></td></tr></table></figure></div><h3 id="非root用户使用qemukvm虚拟化"><a class="markdownIt-Anchor" href="#非root用户使用qemukvm虚拟化"></a> 非root用户使用qemu/kvm虚拟化</h3><p>qemu/kvm默认需要root权限才能使用。为了方便使用，可以编辑配置文件，配置文件位于<code>/etc/libvirt/libvirtd.conf</code>(另外还有一个<code>libvirt.conf</code>文件，注意不要混淆)。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> vim /etc/libvirt/libvirtd.conf</span><br></pre></td></tr></table></figure></div><ol><li><p>找到<code>unix_sock_group = &quot;libvirt&quot;</code>，取消这一行的注释，允许<code>libvirt</code>组的用户使用unix套接字。</p></li><li><p>找到<code>unix_sock_rw_perms = &quot;0770&quot;</code>，取消这一行的注释，允许用户读写unix套接字。</p></li></ol><p>将当前普通用户添加到libvirt组，<code>$USER</code>会获取当前用户的名称。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> usermod -aG libvirt <span class="variable">$USER</span></span><br></pre></td></tr></table></figure></div><p>启动libvirtd服务</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> --now libvirtd</span><br></pre></td></tr></table></figure></div><p>启动virt-manager图形界面</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">virt-manager</span><br></pre></td></tr></table></figure></div><h3 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h3><h4 id="kvm和qemu的关系"><a class="markdownIt-Anchor" href="#kvm和qemu的关系"></a> kvm和qemu的关系</h4><ul><li>qemu本身是跨平台的，它并不依赖于kvm内核模块，可以自行选择单独工作或者是启用kvm模块辅助工作，kvm相当于qemu的性能优化插件。只是kvm是linux内核模块，因此qemu在linux上的体验可能是最好的。</li><li>kvm依赖于硬件是否支持虚拟化以及需要使用qemu这样的工具与其交互。</li><li>kvm是为x86架构量身定做的，启用了kvm的虚拟机，意味着qemu的跨平台虚拟化会受到限制，但是性能会得到明显提升。</li><li>不使用kvm虚拟化的qemu虚拟机能够凭借其完整的系统仿真实现最佳的跨平台虚拟化支持，但是系统开销会更高。</li></ul><h4 id="virtualbox-vmware-or-qemukvm"><a class="markdownIt-Anchor" href="#virtualbox-vmware-or-qemukvm"></a> virtualbox、vmware or qemu/kvm?</h4><ul><li>virtualbox和vmware已经能够满足大部分的非重度使用用户了，它们本身的使用易于上手，相关的学习资料也很多。</li><li>qemu/kvm主要是在linux平台上使用，目的是为了在linux上获得更高的性能和效率。在virt-manager的辅助下，使用也很方便。</li></ul><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://linux.cn/article-16153-1.html" >什么是虚拟机？| linux中国<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://cn.linux-console.net/?p=3102#gsc.tab=0" >如何在 Manjaro/Archlinux 上安装 KVM/QEMU<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://wiki.archlinuxcn.org/wiki/QEMU" >QEMU | archwiki<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://thiscute.world/posts/qemu-kvm-usage/" >QEMU/KVM 虚拟化环境的搭建与使用 | This Cute World<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://cn.linux-console.net/?p=12047#gsc.tab=0" >QEMU 与 KVM：您应该使用哪一个进行虚拟化？<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;虚拟机的起源可追溯到1966年IBM的CP-40和CP-67虚拟机操作系统，虚拟化技术最初是为了解决大型系统中的资源利用率问题而开发的。</summary>
    
    
    
    <category term="linux" scheme="https://www.entropy-tree.top/categories/linux/"/>
    
    
    <category term="qemu" scheme="https://www.entropy-tree.top/tags/qemu/"/>
    
    <category term="kvm" scheme="https://www.entropy-tree.top/tags/kvm/"/>
    
  </entry>
  
  <entry>
    <title>linux中的特殊权限与隐藏属性</title>
    <link href="https://www.entropy-tree.top/2023/08/10/linux-special-permissions/"/>
    <id>https://www.entropy-tree.top/2023/08/10/linux-special-permissions/</id>
    <published>2023-08-10T05:27:28.000Z</published>
    <updated>2023-08-11T03:23:29.702Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要记录一些关于linux系统中特殊权限与隐藏属性的内容<span id="more"></span></p><h3 id="前言"><a class="markdownIt-Anchor" href="#前言"></a> 前言</h3><p>在linux系统中，有文件属性和文件权限两个不同的概念。文件权限指的是用户对文件或目录的操作权限，包括读取、写入和执行。而文件属性则指的是文件的一些特征，例如是否可以被修改或移动。文件属性能够针对所有用户，包括root用户。</p><p>但除了以上三个常见的文件权限以外还有另外三个特殊文件权限，以及文件的隐藏属性。</p><h3 id="文件的特殊权限"><a class="markdownIt-Anchor" href="#文件的特殊权限"></a> 文件的特殊权限</h3><p>linux文件的三种特殊权限：suid、sgid、sticky。这些权限都可以通过<code>chmod</code>命令设置，可以使用对应的符号设置也可以使用对应的数字设置。suid和guid对应的符号都是s，ticky对应的符号是t，它们依次对应所有者、用户组、其他用户的权限。suid(u+s)表示所有者权限，其对应的数字是4；sgid(g+s)表示用户组权限，其对应的数字是2；sticky(o+t)表示其他用户权限，其对应的数字是1。通常情况下，使用<code>chmod</code>命令进行八进制数字语法设置权限时，特殊权限是位于第一个数字位，其余的三个数字位就是对应所有者、用户组、其他用户的读写执行权限。</p><h4 id="suid权限"><a class="markdownIt-Anchor" href="#suid权限"></a> suid权限</h4><p>SUID(Set User ID)：当设置了SUID位的可执行文件被运行时，任何用户在执行该文件时都会以文件所有者的身份执行，而不是以启动它的用户的身份执行。这意味着当可执行文件属主是root用户时，且该文件设置了suid权限，那么任何用户在执行这个文件时都会临时拥有root权限。<strong>这在普通用户执行需要root权限的命令时很有用。</strong></p><p>例如，普通用户正常情况下是无法在根目录直接创建文件。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810163953702.png"                      alt="image-20230810163953702"                 ></p><p>但是可以通过suid权限让普通用户可以在执行<code>touch</code>命令时，以root的身份执行。</p><p>使用以下命令为<code>touch</code>命令设置suid权限，并在普通用户下在根目录创建文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># root用户</span></span><br><span class="line"><span class="built_in">chmod</span> u+s /bin/touch</span><br><span class="line"><span class="built_in">ls</span> -l /bin/touch</span><br><span class="line"></span><br><span class="line"><span class="comment"># test用户</span></span><br><span class="line"><span class="built_in">cd</span> /</span><br><span class="line"><span class="built_in">touch</span> test.txt</span><br></pre></td></tr></table></figure></div><p>查看命令，可以看到在所有者的权限上出现了<strong>s</strong>的符号，代表了suid权限。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810165344173.png"                      alt="image-20230810165344173"                 ></p><p>之后普通用户就可以直接执行具有suid权限的可执行文件或命令，直接在根目录下创建文件。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810165701363.png"                      alt="image-20230810165701363"                 ></p><p>可通过以下命令查找所有具有suid权限的文件</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> find / -perm -4000 <span class="comment"># 查找所有具有suid权限的可执行文件</span></span><br></pre></td></tr></table></figure></div><h4 id="suid权限和sudo命令的比较"><a class="markdownIt-Anchor" href="#suid权限和sudo命令的比较"></a> suid权限和sudo命令的比较</h4><p>从“以root身份执行”的角度来看，二者具有同样的效果，但实际使用与体验上可能大不一样。</p><p>首先，suid通过<code>chmod</code>命令来设置的一种特殊文件权限(如果要获取root权限，则还必须要求这个可执行文件的所有者是root)，而<code>sudo</code>是一种可以在一定时间内以root身份进行操作的权限命令。</p><p>suid是针对可执行文件，只需要使用<code>chmod</code>命令设置即可，但是获取的权限取决于这个文件的所有者，而并不一定是root。<code>sudo</code>是针对特定的用户，需要在/etc/sudoers文件中进行配置，便于这些用户使用root权限进行操作。</p><p>由此可见，suid可以使得某一个具有该权限的可执行文件能被任何用户执行，但是也仅限于被赋予了suid权限的可执行文件，例如上面的示例中，笔者虽然给<code>touch</code>命令设置了suid权限，但普通用户根本无法正常地对这个文件进行写入以及删除的操作等，除非把其他一系列相关的可执行文件都设置suid权限，但这样容易导致安全问题(suid提权)。而<code>sudo</code>命令可以给受信任的用户在一定时间内以root的身份进行操作。</p><p>因此在日常中，<code>sudo</code>可能是相对安全且方便的选择，而suid权限的设置需要用户具有一定的专业知识，<strong>实际上在大多数linux发行版中已经预置了一些具有suid权限的可执行文件</strong>，例如<code>passwd</code>命令，方便用户安全地修改自己的密码。这些是经过了专业的设计，内置了安全措施，所以suid权限对于普通用户来说，可能是不怎么被关注的，即使它已经存在于一些日常使用的命令中。大多数普通用户主要了解基础的三个权限即可。</p><h4 id="sgid权限"><a class="markdownIt-Anchor" href="#sgid权限"></a> sgid权限</h4><p>SGID(Set Group ID)：当一个目录被设置了sgid权限时，任何用户在该目录下创建的新文件都会继承该目录的属组。这意味着，如果一个目录的属组是test，则在该目录下创建的新文件都会属于test组。sgid权限通常用于设置目录权限，对文件设置sgid权限是没有什么实际意义的。这对<strong>团队在linux服务器开发项目时，成员之间共享文件和目录</strong>时很有用，<strong>每个新添加的文件和目录都会具有与父目录相同的属组</strong>。</p><h4 id="sgid使用案例"><a class="markdownIt-Anchor" href="#sgid使用案例"></a> sgid使用案例</h4><p>使用以下命令为webapp目录设置sgid权限，并用不同的用户在该目录下创建文件和子目录。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 准备测试用户、组和目录</span></span><br><span class="line">useradd -m <span class="built_in">test</span></span><br><span class="line">useradd -m webappdev</span><br><span class="line">groupadd webappgroup</span><br><span class="line">usermod -aG webappgroup <span class="built_in">test</span></span><br><span class="line">usermod -aG webappgroup webappdev</span><br><span class="line"><span class="built_in">mkdir</span> /webapp</span><br><span class="line"><span class="built_in">chmod</span> 775 /webapp <span class="comment"># 允组内所有用户写入执行，这是多用户共享目录的必要权限</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置webapp目录的所有者和组</span></span><br><span class="line"><span class="built_in">chown</span> webappdev:webappgroup /webapp</span><br><span class="line"><span class="comment"># 设置webapp目录的sgid权限</span></span><br><span class="line"><span class="built_in">chmod</span> g+s /webapp</span><br><span class="line"><span class="comment"># 查看权限</span></span><br><span class="line"><span class="built_in">ls</span> -ld /webapp</span><br><span class="line"></span><br><span class="line"><span class="comment"># test用户添加文件和目录</span></span><br><span class="line">su - <span class="built_in">test</span></span><br><span class="line"><span class="built_in">touch</span> test_file</span><br><span class="line"><span class="built_in">mkdir</span> test_dir</span><br><span class="line"><span class="comment"># webappdev用户添加文件和目录</span></span><br><span class="line">su - webappdev</span><br><span class="line"><span class="built_in">touch</span> dev_file</span><br><span class="line"><span class="built_in">mkdir</span> dev_dir</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看文件和目录权限</span></span><br><span class="line"><span class="built_in">cd</span> /webapp</span><br><span class="line"><span class="built_in">ls</span> -l</span><br></pre></td></tr></table></figure></div><p>在没有设置sgid权限的情况下，不同用户创建的文件和目录属于各自的组</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810210143869.png"                      alt="image-20230810210143869"                 ></p><p>在设置sgid权限的情况下，不同用户创建的文件和目录统一为父目录的属组</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810210327163.png"                      alt="image-20230810210327163"                 ></p><p>sgid权限要求<strong>父目录组权限至少要有写入执行(g+wx)的权限</strong>，否则只能给目录所有者一个用户使用，那么也没有什么实际意义了。</p><p>但这样还不能够满足共享文件和目录的基本要求，因为<strong>用户可以直接删除其他用户的文件和目录</strong>，而sticky权限可以弥补这一个缺点。</p><h4 id="sticky权限"><a class="markdownIt-Anchor" href="#sticky权限"></a> sticky权限</h4><p>sticky(sticky bit)：当一个目录被设置了sticky权限时，即使其他用户对该目录具有写入权限，也只能删除自己拥有的文件，这意味着，如果一个目录具有sticky权限，那么任何用户都不能删除该目录中其他用户拥有的文件。sticky权限也是用于目录的权限设置，对文件设置sticky权限没有什么实际意义。这对<strong>共享文件和目录时，防止普通用户删除其他用户的文件</strong>很有用。</p><h4 id="sticky使用案例"><a class="markdownIt-Anchor" href="#sticky使用案例"></a> sticky使用案例</h4><p>使用以下命令，可以直接使用前面sgid案例的测试数据，为目录设置sticky权限，并用不同的用户去测试上传删除自己和其他用户的文件。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建共享目录</span></span><br><span class="line"><span class="built_in">mkdir</span> /webapp</span><br><span class="line"><span class="comment"># 设置sticky权限</span></span><br><span class="line"><span class="built_in">chmod</span> o+t /webapp</span><br><span class="line"><span class="built_in">ls</span> -ld /webapp</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置所有权，允许所有用户上传文件，但只允许root用户和文件对应的所有者删除对应的文件</span></span><br><span class="line"><span class="built_in">chown</span> root:webappgroup /webappgroup</span><br><span class="line"><span class="built_in">chmod</span> 3777 /webapp <span class="comment"># 这里如果使用777则会把前面设置的sticky权限覆盖掉</span></span><br><span class="line"><span class="built_in">ls</span> -ld /webapp</span><br><span class="line"></span><br><span class="line"><span class="comment"># test用户添加文件</span></span><br><span class="line">su - <span class="built_in">test</span></span><br><span class="line"><span class="built_in">cd</span> /webapp &amp;&amp; <span class="built_in">touch</span> test_file</span><br><span class="line"></span><br><span class="line"><span class="comment"># webappdev用户添加文件</span></span><br><span class="line">su - webappdev</span><br><span class="line"><span class="built_in">cd</span> /webapp &amp;&amp; <span class="built_in">touch</span> dev_file</span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试用户删除其他用户的文件</span></span><br><span class="line">su - <span class="built_in">test</span></span><br><span class="line"><span class="built_in">cd</span> /webapp &amp;&amp; <span class="built_in">rm</span> dev_file</span><br><span class="line">su - webappdev</span><br><span class="line"><span class="built_in">cd</span> /webapp &amp;&amp; <span class="built_in">rm</span> test_file</span><br></pre></td></tr></table></figure></div><p>设置了sticky权限之后，即使拥有父目录的所有权限，只要不是该文件的所有者，就无法删除文件，每个用户只能删除自己创建的文件。另外，<strong>root用户无视sticky权限的限制</strong>。</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230810223346767.png"                      alt="image-20230810223346767"                 ></p><p>linux系统中，创建、删除、重命名文件至少需要父目录的写入执行(wx)权限，因此sticky权限也要至少<strong>保证目录的所有者具有相应的权限</strong>，否则任何普通用户都无法在目录下创建自己的文件了，那么也没有什么实际意义了。如果考虑和sgid权限配合使用，则还要保证<strong>组具有相应的权限</strong>。</p><h3 id="文件的隐藏属性"><a class="markdownIt-Anchor" href="#文件的隐藏属性"></a> 文件的隐藏属性</h3><p>linux文件属性包括文件的许多特征，通常有文件类型、文件大小、文件所有者、文件所属组、文件最后修改时间等，而隐藏属性是属于文件的特殊性质。这些隐藏属性在linux中可以通过<code>chattr</code>命令进行设置，通过<code>lsattr</code>命令查看。</p><h4 id="chattr"><a class="markdownIt-Anchor" href="#chattr"></a> chattr</h4><p><code>chattr</code>是一个linux命令，用于设置文件的某些隐藏属性，以保护重要文件和文件夹免受意外删除或修改，而且是少有的能够直接限制root用户的命令。</p><p><code>chattr</code>的语法如下，</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">chattr [选项] [操作符] [属性] 文件...</span><br></pre></td></tr></table></figure></div><p>其中操作符有以下几个符号：</p><ul><li><code>+</code>：将指定的属性添加到现有属性</li><li><code>-</code>：从现有属性中删除指定的属性</li><li><code>=</code>：将指定的属性设置为唯一属性，覆盖原来的属性</li></ul><p>操作符后可接多个属性，下面是一些常见属性：</p><ul><li><code>a</code>：文件只能以追加模式写入。</li><li><code>A</code>：文件的访问时间(atime)记录不会被修改，可以减少磁盘IO。</li><li><code>e</code>：文件使用extents映射磁盘上的块，这个属性无法被<code>chattr</code>修改，与文件系统有关，例如ext4文件系统。</li><li><code>i</code>：文件不可变，即文件不能被删除或修改。</li></ul><p>更多说明内容可以使用<code>man chattr</code>查询。</p><h4 id="lsattr"><a class="markdownIt-Anchor" href="#lsattr"></a> lsattr</h4><p><code>lsattr</code>命令主要用于查看文件对应的隐藏属性。</p><p>例如</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsattr /etc/passwd</span><br></pre></td></tr></table></figure></div><h4 id="chattr锁定文件"><a class="markdownIt-Anchor" href="#chattr锁定文件"></a> chattr锁定文件</h4><p>前面提到<code>chattr</code>是少有的能够直接限制root用户的命令，但同时它也需要谨慎使用。</p><p>可以通过以下命令创建并锁定文件，即使root用户也无法直接删除和修改。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">touch</span> ~/immutable.txt</span><br><span class="line"><span class="built_in">sudo</span> chattr +i ~/immutable.txt</span><br><span class="line">lsattr ~/immutable.txt</span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> -rf ~/immutable.txt</span><br></pre></td></tr></table></figure></div><p>执行命令可以看到即使使用root权限也无法删除该文件</p><p><img                       lazyload                     src="/images/loading.svg"                     data-src="image-20230811010112011.png"                      alt="image-20230811010112011"                 ></p><p>要删除文件，只能使用<code>chattr</code>重新取消<code>i</code>属性。</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> chattr -i ~/immutable.txt</span><br><span class="line"></span><br><span class="line"><span class="built_in">rm</span> -rf ~/immutable.txt</span><br></pre></td></tr></table></figure></div><p><code>chattr</code>命令虽然可以阻止文件被意外删除或修改，但是它所针对的文件或目录对象，通常是一些内容不会去改动的系统配置文件、日志文件、备份文件等，而且这些文件和目录不应该对其他的文件和目录有依赖关系。例如，你对那些linux命令的可执行文件使用<code>chattr</code>锁定，那它们大概率会无法正常工作。</p><h3 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h3><p>以上，主要介绍了suid、sgid、sticky三个特殊权限和文件的隐藏属性。</p><p>说明了如何使用sgid权限创建一个能够自动继承父目录属组的共享目录，使用sticky权限来限制共享目录的多个用户对共享文件的删除权限，以及整合sgid和sticky权限来打造一个安全且便于协作的共享文件目录。</p><p>最后介绍了如何利用文件的隐藏属性来限制root用户及其他任何用户的误操作以保护重要文件。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://www.runoob.com/linux/linux-comm-chattr.html" >Linux chattr命令<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.cnblogs.com/poloyy/p/12586677.html" >Linux - 文件的三种时间之atime、ctime、mtime的区别和简单用法<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文主要记录一些关于linux系统中特殊权限与隐藏属性的内容</summary>
    
    
    
    <category term="linux" scheme="https://www.entropy-tree.top/categories/linux/"/>
    
    
    <category term="特殊文件属性" scheme="https://www.entropy-tree.top/tags/%E7%89%B9%E6%AE%8A%E6%96%87%E4%BB%B6%E5%B1%9E%E6%80%A7/"/>
    
    <category term="特殊文件权限" scheme="https://www.entropy-tree.top/tags/%E7%89%B9%E6%AE%8A%E6%96%87%E4%BB%B6%E6%9D%83%E9%99%90/"/>
    
  </entry>
  
  <entry>
    <title>一些现代化的linux命令</title>
    <link href="https://www.entropy-tree.top/2023/07/24/modern-linux-commands/"/>
    <id>https://www.entropy-tree.top/2023/07/24/modern-linux-commands/</id>
    <published>2023-07-24T13:15:50.000Z</published>
    <updated>2024-06-14T02:36:30.162Z</updated>
    
    <content type="html"><![CDATA[<p>本文主要介绍一些现代化的linux基础命令。对比传统的命令，它们在实现了原有功能的基础上或多或少有一些不同于传统命令的特性。<span id="more"></span></p><h3 id="文件目录"><a class="markdownIt-Anchor" href="#文件目录"></a> 文件目录</h3><h4 id="文件内容查看"><a class="markdownIt-Anchor" href="#文件内容查看"></a> 文件内容查看</h4><p><code>cat</code>命令的现代化实现<a href="https://github.com/sharkdp/bat"><code>bat</code></a>命令。<code>bat</code>命令在<code>cat</code>命令的基础上加入了<strong>行号显示、代码高亮和Git集成</strong>。</p><h4 id="文件查找"><a class="markdownIt-Anchor" href="#文件查找"></a> 文件查找</h4><p><code>find</code>命令的现代化实现<a href="https://github.com/sharkdp/fd"><code>fd</code></a>命令。<code>fd</code>命令并不是为了取代传统的<code>find</code>命令，<code>fd</code>命令主要是实现了<code>find</code>大多数常用的用法，并不具有<code>find</code>命令所有强大的功能，但是通常<code>fd</code>命令的速度要快于<code>find</code>命令。</p><p><code>fd</code>命令内部使用正则搜索，而<a href="https://github.com/junegunn/fzf"><code>fzf</code></a>项目提供了模糊搜索的功能，可以将<code>fd</code>和<code>fzf</code>组合使用。</p><h4 id="文本内容搜索"><a class="markdownIt-Anchor" href="#文本内容搜索"></a> 文本内容搜索</h4><p><code>grep</code>命令的现代化实现<code>ack</code>命令。<code>ack</code>命令的速度也通常快于<code>grep</code>命令，基于<code>ack</code>命令又派生了<a href="https://github.com/ggreer/the_silver_searcher"><code>ag</code></a>命令。<code>grep</code>命令和<code>ack</code>命令的搜索方式不同，<code>grep</code>只会在指定路径的所有文件上搜索内容，而<code>ack</code>则采取更加智能的搜索方式。<a href="https://github.com/BurntSushi/ripgrep"><code>ripgrep</code></a>的<code>rg</code>命令则结合了<code>grep</code>和<code>ack</code>，能够自动选择最佳搜索策略。目前从检索速度上来看，<code>ripgrep</code>的性能最强，其次是<code>silversearcher-ag</code>，最后是<code>grep</code>。具体可以参考<a class="link"   href="https://www.ubuntukylin.com/news/1740-cn.html" >巧用这几种文本检索工具，节省80%的时间<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><h4 id="目录内容列举"><a class="markdownIt-Anchor" href="#目录内容列举"></a> 目录内容列举</h4><p><code>ls</code>命令的现代化实现<a href="https://github.com/ogham/exa"><code>exa</code></a>命令。<code>exa</code>命令的默认设置可能看上去比<code>ls</code>命令更加美观，但是在编写脚本时，还是应该使用更加通用的<code>ls</code>命令，以避免不必要的麻烦。具体参考<a class="link"   href="https://linux.cn/article-13972-1.html" >exa：一个 ls 命令的现代替代品<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><blockquote><p><code>exa</code>项目作者由于没有精力维护项目，现在推荐使用社区分支<a href="https://github.com/eza-community/eza"><code>eza</code></a></p></blockquote><p>此外，还有类似的<a href="https://github.com/lsd-rs/lsd"><code>lsd</code></a>项目。<code>exa</code>、<code>eza</code>和<code>lsd</code>都是基于<code>Rust</code>实现。</p><h4 id="目录路径切换"><a class="markdownIt-Anchor" href="#目录路径切换"></a> 目录路径切换</h4><p><code>cd</code>命令的现代化实现<a href="https://github.com/ajeetdsouza/zoxide"><code>zoxide</code></a>命令。<code>zoxide</code>命令是一个更加智能的命令，受到了<a href="https://github.com/wting/autojump"><code>autojump</code></a>的启示，能够记录目录切换的历史并模糊匹配历史记录的路径。</p><h4 id="终端文件管理器"><a class="markdownIt-Anchor" href="#终端文件管理器"></a> 终端文件管理器</h4><p>对于前面目录内容列举的<code>cli</code>命令行界面(<em>Command Line Interface</em>)工具，其实还可以考虑使用<code>tui</code>终端用户界面(<em>Terminal User Interface</em>)工具。以下是几个基于<code>Rust</code>实现的推荐项目</p><ul><li><p><a href="https://github.com/Canop/broot"><code>broot</code></a>：<code>broot</code>是一个用<code>Rust</code>编写的终端文件管理器，基于非阻塞异步I/O。它集成了<code>ls</code>、<code>tree</code>、<code>find</code>、<code>grep</code>、<code>du</code>、<code>fzf</code>等工具的常用功能，在一个TUI界面里完成上述各种工作。</p></li><li><p><a href="https://github.com/kamiyaa/joshuto"><code>joshuto</code></a>：是一个类似<code>ranger</code>的终端文件管理器，速度更快，但是需要手动配置才能使用预览文本和图片的功能。</p></li><li><p><a href="https://github.com/sxyazi/yazi"><code>yazi</code></a>：<code>yazi</code>是一个用Rust编写的终端文件管理器，基于非阻塞异步I/O。它旨在提供一个高效、用户友好、可定制的文件管理体验。在大部分终端中支持预览文本和图片，资源占用会比<code>joshuto</code>更高。</p></li></ul><blockquote><p>关于终端文件管理器的选择可以参考这里的<a class="link"   href="https://github.com/kamiyaa/joshuto/discussions/454" >discussions<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>，根据实际需求选择。</p></blockquote><h3 id="系统管理"><a class="markdownIt-Anchor" href="#系统管理"></a> 系统管理</h3><h4 id="进程管理监控"><a class="markdownIt-Anchor" href="#进程管理监控"></a> 进程管理监控</h4><p><code>top</code>命令的现代化实现有很多，这里主要介绍<code>htop</code>命令，更多内容可以参考<a class="link"   href="https://zhuanlan.zhihu.com/p/51911977" >关于 top 工具的 6 个替代方案<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><p><code>htop</code>命令通常相对<code>top</code>命令更易于使用，可以很方便地对进程进行过滤和排序，在必要的时候杀死对应的进程。</p><p><code>ps</code>命令的现代化实现<a href="https://github.com/dalance/procs"><code>procs</code></a>命令。<code>procs</code>命令也是基于<code>Rust</code>实现。</p><h4 id="磁盘空间管理"><a class="markdownIt-Anchor" href="#磁盘空间管理"></a> 磁盘空间管理</h4><p><code>df</code>命令的现代化实现<a href="https://github.com/muesli/duf"><code>duf</code></a>命令。<code>duf</code>命令提供了一个所有挂载设备的概览。</p><h4 id="目录空间管理"><a class="markdownIt-Anchor" href="#目录空间管理"></a> 目录空间管理</h4><p><code>du</code>命令的现代化实现<a href="https://github.com/rofl0r/ncdu"><code>ncdu</code></a>命令。<code>ncdu</code>命令相比<code>du</code>命令更加直观易用，<code>du</code>命令实际上不是一个很直观的命令，通常<code>du</code>命令需要配合多个参数使用才能得到直观的信息。此外，<code>ncdu</code>命令提供的是一个可交互的界面，可以进行排序以及交互式地删除目录。</p><blockquote><p>截止2024年，<code>ncdu</code>的github仓库已经很久没有更新，可以考虑基于<code>Rust</code>实现的<a href="https://github.com/bootandy/dust"><code>dust</code></a>项目。但是<code>dust</code>目前没有类似于 <code>ncdu</code> 的交互式功能。另外一个值得关注的项目是使用<code>go</code>语言实现的<a href="https://github.com/dundee/gdu"><code>gdu</code></a>，提供和<code>ncdu</code>类似的界面和功能，并且由于使用并行处理,速度可能比<code>ncdu</code>更快。</p></blockquote><h3 id="网络管理"><a class="markdownIt-Anchor" href="#网络管理"></a> 网络管理</h3><h4 id="主机网络管理工具"><a class="markdownIt-Anchor" href="#主机网络管理工具"></a> 主机网络管理工具</h4><p><code>net-tools</code>工具的现代化替代<code>iproute2</code>工具。<code>net-tools</code>工具中有几个很知名的命令，例如<code>ifconfig</code>命令。</p><p>实际上，自2001年起，linux社区已经停止了对<code>net-tools</code>的维护，而<code>iproute2</code>旨在取代<code>net-tools</code>，并提供了一些新功能。一些linux发行版已经停止支持<code>net-tools</code>，只支持<code>iproute2</code>，例如<code>Archlinux</code>的livecd中就默认使用<code>iproute2</code>而不是<code>net-tools</code>。</p><p>虽然<code>net-tools</code>是一个比较古老的命令，但是<code>net-tools</code>目前仍在广泛使用，因此<code>net-tools</code>还是值得linux系统管理员学习的。</p><h4 id="网络请求工具"><a class="markdownIt-Anchor" href="#网络请求工具"></a> 网络请求工具</h4><p><code>curl</code>和<code>wget</code>的现代化实现<a href="https://github.com/httpie/cli"><code>httpie</code></a>工具。此外还有<a href="https://github.com/rs/curlie"><code>curlie</code></a>项目，<code>curlie</code>集成了<code>curl</code>的强大功能与<code>httpie</code>的易用性。</p><blockquote><p>与<code>curlie</code>类似的还有<a href="https://github.com/ducaale/xh"><code>xh</code></a>项目。</p><p><code>httpie</code>基于<code>Python</code>实现，<code>curlie</code>基于<code>Go</code>实现，<code>xh</code>基于<code>Rust</code>实现。</p></blockquote><h4 id="dns查询工具"><a class="markdownIt-Anchor" href="#dns查询工具"></a> DNS查询工具</h4><p><code>nslookup</code>工具的现代化实现<code>dig</code>工具，而<code>dig</code>工具又有其替代实现<a href="https://github.com/ogham/dog"><code>dog</code></a>项目。<code>dog</code>是一款简单易用的命令行工具，具有丰富多彩的输出，支持<code>DNS-over-TLS</code>和<code>DNS-over-HTTPS</code>协议。</p><h4 id="网络安全工具"><a class="markdownIt-Anchor" href="#网络安全工具"></a> 网络安全工具</h4><p><code>netcat</code>工具的现代化实现<code>ncat</code>和<code>socat</code>工具。<code>ncat</code>是<code>netcat</code>的现代版本，而<code>socat</code>是<code>netcat</code>的现代化N倍加强版，不仅支持<code>ncat</code>和<code>netcat</code>的所有功能，还支持更多的协议和连接方式等，比<code>ncat</code>更强大、更复杂。</p><h4 id="远程文件传输工具"><a class="markdownIt-Anchor" href="#远程文件传输工具"></a> 远程文件传输工具</h4><p><code>scp</code>命令的现代化替代<code>sftp</code>命令或<code>rsync</code>命令。<code>scp</code>命令属于OpenSSH项目的一部分，它被认为已经过时，不灵活且不容易修复。推荐使用<code>sftp</code>命令或<code>rsync</code>命令进行文件传输。但是，有一定的用户群体习惯使用<code>scp</code>命令，这里提供了一篇过渡参考<a class="link"   href="https://linux.cn/article-12575-1.html" >scp 用户的 rsync 迁移指南<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://zhuanlan.zhihu.com/p/401429004" >25 个 Linux 下最炫酷又强大的命令行神器<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://linux.cn/article-14172-1.html" >一些经典 Linux 命令的现代替代品 | Linux 中国<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://zhuanlan.zhihu.com/p/439233295" >10 款你不知道的 Linux 环境下的替代工具！<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://www.ruanyifeng.com/blog/2022/01/cli-alternative-tools.html" >命令行常用工具的替代品<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p><p><a class="link"   href="https://github.com/ibraheemdev/modern-unix" >modern-unix | github<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本文主要介绍一些现代化的linux基础命令。对比传统的命令，它们在实现了原有功能的基础上或多或少有一些不同于传统命令的特性。</summary>
    
    
    
    <category term="linux" scheme="https://www.entropy-tree.top/categories/linux/"/>
    
    
    <category term="modern" scheme="https://www.entropy-tree.top/tags/modern/"/>
    
    <category term="commands" scheme="https://www.entropy-tree.top/tags/commands/"/>
    
  </entry>
  
  <entry>
    <title>使用certbot自动化管理免费ssl证书</title>
    <link href="https://www.entropy-tree.top/2023/07/22/certbot-for-ssl/"/>
    <id>https://www.entropy-tree.top/2023/07/22/certbot-for-ssl/</id>
    <published>2023-07-22T03:08:28.000Z</published>
    <updated>2023-10-07T06:37:50.034Z</updated>
    
    <content type="html"><![CDATA[<p>ssl证书是用于验证身份和保护数据安全的一种数字证书。在缺少ssl证书的情况下，网络通信将不会被加密，所有数据将以明文传输，容易被拦截和窃听，这样的网站容易受到中间人攻击。<span id="more"></span>目前也推崇使用https代替传统的http，传统的http已经被证明存在了很多安全问题，且浏览器也会将此类传统http网站标记为不安全网站。但是ssl证书也并不都是免费的，一些企业级ssl证书价格非常昂贵，不是个人开发者所能够承担的，当然也没有必要去承担。在笔者之前文章中提到的证书网站如<a class="link"   href="https://zerossl.com/" >zerossl<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>中虽然有提供免费ssl证书但是目前有申请次数的限制(也正因为如此才有了这篇文章)。</p><p>以下，笔者将介绍使用<a class="link"   href="https://letsencrypt.org/zh-cn/" >Let’s Encrypt<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>的<a class="link"   href="https://certbot.eff.org/" >certbot<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>客户端申请免费的ssl证书。certbot可与apache、nginx等服务配合使用，本文以在<strong>Ubuntu20.04上为nginx服务配置ssl证书</strong>为例。</p><h3 id="lets-encrypt"><a class="markdownIt-Anchor" href="#lets-encrypt"></a> Let’s Encrypt</h3><p>Let’s Encrypt是一家全球性的证书颁发机构（CA），为世界各地的个人和团体提供获取、续期、管理SSL/TLS证书的服务。网站可以使用其证书来启用安全的HTTPS连接。Let’s Encrypt属于非营利组织，旨在推广HTTPS技术的应用，从而构建更加安全且尊重隐私的互联网环境。</p><h3 id="certbot"><a class="markdownIt-Anchor" href="#certbot"></a> certbot</h3><p>在早期，申请并配置Let’s Encrypt签发的安全证书比较麻烦，只能手动进行配置以及部署，需要一定的技术水平。直到后来推出了一些自动化工具才使得ssl证书的配置及部署变得非常方便。certbot就是其中的一款官方的客户端工具。</p><p>certbot是ISRG (Internet Security Research Group，互联网安全研究小组) 组织的发起者EFF(电子前哨基金会)为Let’s Encrypt项目所发布的一个官方的客户端，实现了完全自动化的获取、部署和更新安全证书。</p><h4 id="具体使用"><a class="markdownIt-Anchor" href="#具体使用"></a> 具体使用</h4><p>前提：Ubuntu20.04环境、使用nginx服务。</p><h5 id="1安装certbot客户端及插件"><a class="markdownIt-Anchor" href="#1安装certbot客户端及插件"></a> 1.安装certbot客户端及插件</h5><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt install certbot python3-certbot-nginx</span><br></pre></td></tr></table></figure></div><h5 id="2生成证书"><a class="markdownIt-Anchor" href="#2生成证书"></a> 2.生成证书</h5><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> certbot --nginx</span><br></pre></td></tr></table></figure></div><p>使用以上命令生成证书的过程中，会要求提供邮箱地址以及需要配置证书的域名信息，以及选择是否需要重定向等。生成的ssl证书位于**/etc/letsencrypt/live**目录下。</p><h5 id="3更新证书"><a class="markdownIt-Anchor" href="#3更新证书"></a> 3.更新证书</h5><p>在大数情况下，certbot在安装时会自动配置一个cron job或systemd定时器来定期检查并更新证书。这意味着大部分时间里开发者不需要手动更新证书，certbot会自动进行更新。自动更新功能可通过以下命令进行测试</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> certbot renew --dry-run</span><br></pre></td></tr></table></figure></div><p><code>--dry-run</code>参数表示测试，而非真正执行更新。如果显示<code>&quot;Congratulations, all renewals succeeded.&quot;</code>的信息，则表示自动更新功能测试成功。</p><p>当然也可以通过以下命令手动更新证书</p><div class="code-container" data-rel="Bash"><figure class="iseeu highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> certbot renew</span><br></pre></td></tr></table></figure></div><p>这将检查所有已安装的证书，并更新即将过期的证书。</p><h3 id="补充"><a class="markdownIt-Anchor" href="#补充"></a> 补充</h3><p>除了Let’s Encrypt组织及其提供的certbot客户端以外，还有类似的<a class="link"   href="https://www.buypass.com/" >buypass<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>组织以及更轻量的自动化shell脚本<a class="link"   href="https://github.com/acmesh-official/acme.sh" >acme.sh<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a>。</p><p>Let’s Encrypt和buypass所提供的证书有一些区别</p><ul><li>Let’s Encrypt的单次申请证书有效期为90天，而buypass的单次申请证书有效期则为180天。</li><li>Let’s Encrypt支持签发泛域名和多域名证书，buypass的证书不支持泛域名、支持多域名。</li></ul><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><p><a class="link"   href="https://zhuanlan.zhihu.com/p/25538921" >HTTPS 简介及使用官方工具 Certbot 配置 Let’s Encrypt SSL 安全证书详细教程<i class="fa-solid fa-arrow-up-right ml-[0.2em] font-light align-text-top text-[0.7em] link-icon"></i></a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;ssl证书是用于验证身份和保护数据安全的一种数字证书。在缺少ssl证书的情况下，网络通信将不会被加密，所有数据将以明文传输，容易被拦截和窃听，这样的网站容易受到中间人攻击。</summary>
    
    
    
    <category term="运维" scheme="https://www.entropy-tree.top/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="ssl" scheme="https://www.entropy-tree.top/tags/ssl/"/>
    
    <category term="certbot" scheme="https://www.entropy-tree.top/tags/certbot/"/>
    
  </entry>
  
</feed>
