<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Shallow Memory&#39;s Blog</title>
  
  <subtitle>生活明朗，万物可爱</subtitle>
  <link href="https://www.shallowrecall.top/atom.xml" rel="self"/>
  
  <link href="https://www.shallowrecall.top/"/>
  <updated>2026-10-27T18:22:13.996Z</updated>
  <id>https://www.shallowrecall.top/</id>
  
  <author>
    <name>Recall</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Hello World</title>
    <link href="https://www.shallowrecall.top/posts/4a17b156.html"/>
    <id>https://www.shallowrecall.top/posts/4a17b156.html</id>
    <published>2026-10-27T18:22:13.996Z</published>
    <updated>2026-10-27T18:22:13.996Z</updated>
    
    <content type="html"><![CDATA[<p>Welcome to <a href="https://hexo.io/">Hexo</a>! This is your very first post. Check <a href="https://hexo.io/docs/">documentation</a> for more info. If you get any problems when using Hexo, you can find the answer in <a href="https://hexo.io/docs/troubleshooting.html">troubleshooting</a> or you can ask me on <a href="https://github.com/hexojs/hexo/issues">GitHub</a>.</p><h2 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h2><h3 id="Create-a-new-post"><a href="#Create-a-new-post" class="headerlink" title="Create a new post"></a>Create a new post</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo new <span class="string">&quot;My New Post&quot;</span></span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/writing.html">Writing</a></p><h3 id="Run-server"><a href="#Run-server" class="headerlink" title="Run server"></a>Run server</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo server</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/server.html">Server</a></p><h3 id="Generate-static-files"><a href="#Generate-static-files" class="headerlink" title="Generate static files"></a>Generate static files</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo generate</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/generating.html">Generating</a></p><h3 id="Deploy-to-remote-sites"><a href="#Deploy-to-remote-sites" class="headerlink" title="Deploy to remote sites"></a>Deploy to remote sites</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ hexo deploy</span><br></pre></td></tr></table></figure><p>More info: <a href="https://hexo.io/docs/one-command-deployment.html">Deployment</a></p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;Welcome to &lt;a href=&quot;https://hexo.io/&quot;&gt;Hexo&lt;/a&gt;! This is your very first post. Check &lt;a href=&quot;https://hexo.io/docs/&quot;&gt;documentation&lt;/a&gt; for</summary>
      
    
    
    
    
  </entry>
  
  <entry>
    <title>Obsidian 发布到 Hexo 工作流</title>
    <link href="https://www.shallowrecall.top/posts/ffe21298.html"/>
    <id>https://www.shallowrecall.top/posts/ffe21298.html</id>
    <published>2026-10-27T18:22:13.996Z</published>
    <updated>2026-10-27T18:22:13.996Z</updated>
    
    <content type="html"><![CDATA[<h1 id="📝-Obsidian-发布到-Hexo-工作流"><a href="#📝-Obsidian-发布到-Hexo-工作流" class="headerlink" title="📝 Obsidian 发布到 Hexo 工作流"></a>📝 Obsidian 发布到 Hexo 工作流</h1><blockquote><p>✨ 目标很简单：平时继续在 Obsidian 写笔记，需要发布时，一键导出成 Hexo 能直接使用的文章。</p></blockquote><hr><h2 id="🧠-推荐方案"><a href="#🧠-推荐方案" class="headerlink" title="🧠 推荐方案"></a>🧠 推荐方案</h2><p>我建议你走这条路：</p><ol><li><strong>Obsidian 作为主写作端</strong></li><li><strong>Hexo 作为展示端</strong></li><li><strong>用 frontmatter 控制是否发布、slug、分包、封面图</strong></li><li><strong>用脚本把笔记转换到 Hexo 的 <code>source/_posts/&lt;分类包&gt;/</code></strong></li></ol><p>这条路的好处：</p><ul><li>日常写笔记不受影响</li><li>不需要在 Hexo 和 Obsidian 里重复维护两份内容</li><li>可以批量同步所有 <code>publish: true</code> 的笔记</li><li>可以先 dry-run 预览落盘位置，再真正发布</li></ul><hr><h2 id="🧩-目录建议"><a href="#🧩-目录建议" class="headerlink" title="🧩 目录建议"></a>🧩 目录建议</h2><p>你当前笔记库里已经新增了一个发布目录：</p><ul><li><code>hexo-publish/</code></li><li><code>hexo-publish/assets/covers/</code></li><li><code>hexo-publish/publish-note.mjs</code></li><li><code>hexo-publish/config.example.json</code></li></ul><p>当前有两份常用配置：</p><ul><li><code>hexo-publish/config.local.json</code>：导出到 vault 内的测试目录</li><li><code>hexo-publish/config.deploy.json</code>：导出到 <code>HEXO_ROOT/source/_posts</code></li></ul><p>配置里的路径支持占位符，换电脑时不用改脚本：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;vaultRoot&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;configDir&#125;/..&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;hexoRoot&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;env:HEXO_ROOT&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;hexoPostsDir&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$&#123;hexoRoot&#125;/source/_posts&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>常用占位符：</p><ul><li><code>$&#123;configDir&#125;</code>：当前配置文件所在目录</li><li><code>$&#123;cwd&#125;</code>：执行命令时所在目录</li><li><code>$&#123;home&#125;</code>：当前用户主目录</li><li><code>$&#123;env:HEXO_ROOT&#125;</code>：读取环境变量</li></ul><hr><h2 id="✍️-笔记怎么写"><a href="#✍️-笔记怎么写" class="headerlink" title="✍️ 笔记怎么写"></a>✍️ 笔记怎么写</h2><p>建议每篇准备发布的笔记都加上这些属性：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">Git</span> <span class="string">与</span> <span class="string">GitHub</span> <span class="string">SSH</span> <span class="string">指南</span></span><br><span class="line"><span class="attr">publish:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">slug:</span> <span class="string">git-github-ssh-guide</span></span><br><span class="line"><span class="attr">package:</span> <span class="string">技术笔记/开发工具</span></span><br><span class="line"><span class="attr">note_type:</span> <span class="string">工具类</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-06-24T10:00:00+08:00</span></span><br><span class="line"><span class="attr">updated:</span> <span class="number">2026-06-24T10:00:00+08:00</span></span><br><span class="line"><span class="attr">tags:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Git</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">GitHub</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">SSH</span></span><br><span class="line"><span class="attr">categories:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">开发工具</span></span><br><span class="line"><span class="attr">cover:</span> <span class="string">hexo-publish/assets/covers/cover-git-terminal.png</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">一篇够用的</span> <span class="string">Git、GitHub</span> <span class="string">和</span> <span class="string">SSH</span> <span class="string">上手笔记。</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>核心字段说明：</p><ul><li><code>publish</code>: 是否允许导出</li><li><code>slug</code>: Hexo 文章链接名</li><li><code>package</code>: 文章导出到 <code>_posts</code> 下的分包路径</li><li><code>note_type</code>: 笔记类型，用来保留工具、速查、配置、流程、复盘这套习惯</li><li><code>cover</code>: 文章封面图</li><li><code>description</code>: 摘要描述</li></ul><p>分包规则：</p><ul><li><code>package</code> 写了就优先使用，例如 <code>技术笔记/开发工具</code></li><li><code>categories</code> 第一项是 <code>编程学习</code>、<code>大学课程</code>、<code>技术笔记</code>、<code>英语学习</code> 时，直接按它作为目录</li><li><code>categories</code> 是 <code>开发工具</code>、<code>工作流</code>、<code>Markdown</code> 这类子分类时，自动挂到 <code>技术笔记</code> 下</li><li>没写 <code>package</code> 和 <code>categories</code> 时，会按 <code>note_type</code> 或标签推导，最后兜底到 <code>技术笔记</code></li></ul><hr><h2 id="🚀-怎么发布"><a href="#🚀-怎么发布" class="headerlink" title="🚀 怎么发布"></a>🚀 怎么发布</h2><p>在当前 vault 目录执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --note <span class="string">&quot;.\Git指南.md&quot;</span></span><br></pre></td></tr></table></figure><p>如果某次只想给单篇文章临时指定封面，可以加 <code>--cover</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --note <span class="string">&quot;.\Obsidian 发布到 Hexo 工作流.md&quot;</span> --cover <span class="string">&quot;hexo-publish/assets/covers/cover-obsidian-hexo-workflow.png&quot;</span></span><br></pre></td></tr></table></figure><p>先预览所有准备发布的笔记会落到哪里：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --all --dry-run</span><br></pre></td></tr></table></figure><p>批量同步所有 <code>publish: true</code> 的笔记：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --all</span><br></pre></td></tr></table></figure><p>也可以把 Hexo 仓库路径放到环境变量里，命令会更短：</p><figure class="highlight powershell"><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="variable">$env:HEXO_ROOT</span> = <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span></span><br><span class="line">node .\hexo<span class="literal">-publish</span>\<span class="built_in">publish-note</span>.mjs <span class="literal">--config</span> .\hexo<span class="literal">-publish</span>\config.deploy.json <span class="literal">--all</span> <span class="literal">--dry-run</span></span><br></pre></td></tr></table></figure><p>如果文件里没有 <code>publish: true</code>，脚本会跳过。确实要强制导出时再加 <code>--force</code>。</p><p>导出成功后，目标文章会出现在：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">你的 Hexo 项目/source/_posts/&lt;package&gt;/&lt;slug&gt;.md</span><br></pre></td></tr></table></figure><p>如果封面图是本地文件，脚本会复制到 Hexo 静态资源目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">你的 Hexo 项目/source/img/posts/&lt;slug&gt;/</span><br></pre></td></tr></table></figure><p>文章 frontmatter 会引用公开路径：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/img/posts/&lt;slug&gt;/&lt;image-file&gt;</span><br></pre></td></tr></table></figure><hr><h2 id="🔄-脚本会帮你做什么"><a href="#🔄-脚本会帮你做什么" class="headerlink" title="🔄 脚本会帮你做什么"></a>🔄 脚本会帮你做什么</h2><p>这个脚本当前会做几件事：</p><ul><li>读取 Obsidian frontmatter</li><li>把笔记导出为 Hexo frontmatter</li><li>自动生成或使用已有 <code>slug</code></li><li>按 <code>package / categories / note_type / tags</code> 推导分包目录</li><li>支持 <code>--all</code> 批量发布和 <code>--dry-run</code> 预览</li><li>把 <code>[[内部链接]]</code> 转成普通 Markdown 链接</li><li>把 <code>![[图片]]</code> 转成 Hexo 能识别的图片引用</li><li>复制封面图到 Hexo 静态资源目录</li><li>自动插入 <code>&lt;!-- more --&gt;</code> 摘要分隔符</li></ul><hr><h2 id="⚠️-需要你知道的限制"><a href="#⚠️-需要你知道的限制" class="headerlink" title="⚠️ 需要你知道的限制"></a>⚠️ 需要你知道的限制</h2><p>当前这版是轻量实用版，所以有几条边界：</p><ul><li>YAML 解析支持常见简单写法，复杂嵌套暂时不建议</li><li><code>[[笔记#标题]]</code> 会转成博客内链接，但前提是对应文章也有 slug</li><li>复杂 Obsidian embed 语法还没有全部兼容</li><li>当前脚本只处理 Markdown 正文和常见 frontmatter，不负责自动改写复杂插件语法</li><li>如果封面图写的是本地路径，脚本会复制到对应文章资源目录；如果写的是 <code>https://</code> 图片，会保持原链接</li></ul><hr><h2 id="🎨-封面图怎么用"><a href="#🎨-封面图怎么用" class="headerlink" title="🎨 封面图怎么用"></a>🎨 封面图怎么用</h2><p>我已经给你预留了一类图：</p><ul><li><code>covers/</code>：适合作为文章封面</li></ul><p>推荐用法：</p><ul><li>工具类文章用偏实景、桌面、终端、代码感封面</li><li>同一系列文章尽量固定 2 到 4 张图循环使用，博客会更整齐</li></ul><hr><h2 id="🧭-我建议你的长期用法"><a href="#🧭-我建议你的长期用法" class="headerlink" title="🧭 我建议你的长期用法"></a>🧭 我建议你的长期用法</h2><p>最稳的一套是：</p><ol><li>在 Obsidian 里写完整笔记</li><li>给准备公开的文章补 <code>publish / slug / package / note_type / cover</code></li><li>先执行 <code>--all --dry-run</code> 看分包是否正确</li><li>用 <code>--all</code> 同步到 Hexo</li><li>在 Hexo 本地预览，没问题再部署</li></ol><p>这样 Obsidian 是知识库，Hexo 是展示面，它们分工很清楚。</p><hr><h2 id="✅-常用命令"><a href="#✅-常用命令" class="headerlink" title="✅ 常用命令"></a>✅ 常用命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 预览所有 publish: true 笔记的导出位置</span></span><br><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --all --dry-run</span><br><span class="line"></span><br><span class="line"><span class="comment"># 批量发布到 Hexo _posts</span></span><br><span class="line">node .\hexo-publish\publish-note.mjs --config .\hexo-publish\config.deploy.json --hexo-root <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span> --all</span><br><span class="line"></span><br><span class="line"><span class="comment"># 到 Hexo 仓库预览</span></span><br><span class="line"><span class="built_in">cd</span> <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span></span><br><span class="line">npm run server</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成静态站点</span></span><br><span class="line">npm run build</span><br></pre></td></tr></table></figure><p>如果在 Hexo 仓库里执行 npm scripts，可以设置一次 vault 路径：</p><figure class="highlight powershell"><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">cd</span> <span class="string">&quot;&lt;你的 Hexo 仓库路径&gt;&quot;</span></span><br><span class="line"><span class="variable">$env:OBSIDIAN_VAULT</span> = <span class="string">&quot;&lt;你的 Obsidian vault 路径&gt;&quot;</span></span><br><span class="line">npm run publish:dry</span><br><span class="line">npm run publish:all</span><br></pre></td></tr></table></figure><p>如果不想每次设置环境变量，可以在 Hexo 仓库放一个本机专用配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;vaultRoot&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&lt;你的 Obsidian vault 路径&gt;&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>文件名使用 <code>hexo-publish.local.json</code>，它已经在 Hexo 仓库的 <code>.gitignore</code> 里，不会把本机路径提交出去。</p>]]></content>
    
    
    <summary type="html">在 Obsidian 写作，再自动导出到 Hexo 博客的轻量方案。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="博客" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%8D%9A%E5%AE%A2/"/>
    
    <category term="工作流" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%8D%9A%E5%AE%A2/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    
    
    <category term="Obsidian" scheme="https://www.shallowrecall.top/tags/Obsidian/"/>
    
    <category term="Butterfly" scheme="https://www.shallowrecall.top/tags/Butterfly/"/>
    
    <category term="Hexo" scheme="https://www.shallowrecall.top/tags/Hexo/"/>
    
    <category term="博客" scheme="https://www.shallowrecall.top/tags/%E5%8D%9A%E5%AE%A2/"/>
    
    <category term="工作流" scheme="https://www.shallowrecall.top/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    
  </entry>
  
  <entry>
    <title>J4125 全闪 All-in-One 折腾笔记:ESXi + iStoreOS + 飞牛 + Debian + Windows Server</title>
    <link href="https://www.shallowrecall.top/posts/j4125-esxi-all-in-one-notes.html"/>
    <id>https://www.shallowrecall.top/posts/j4125-esxi-all-in-one-notes.html</id>
    <published>2026-10-25T08:30:00.000Z</published>
    <updated>2026-10-25T08:30:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="🧭-J4125-全闪-All-in-One-折腾笔记-ESXi-iStoreOS-飞牛-Debian-Windows-Server"><a href="#🧭-J4125-全闪-All-in-One-折腾笔记-ESXi-iStoreOS-飞牛-Debian-Windows-Server" class="headerlink" title="🧭 J4125 全闪 All-in-One 折腾笔记:ESXi + iStoreOS + 飞牛 + Debian + Windows Server"></a>🧭 J4125 全闪 All-in-One 折腾笔记:ESXi + iStoreOS + 飞牛 + Debian + Windows Server</h1><span id="more"></span><blockquote><p>💡 前传:《ESXi 存储一夜消失:SATA 控制器直通引发的”血案”复盘》——建议先读,本文直接从事故修复后开始。</p></blockquote><h2 id="1-硬件与总架构"><a href="#1-硬件与总架构" class="headerlink" title="1. 硬件与总架构"></a>1. 硬件与总架构</h2><div class="table-container"><table><thead><tr><th>项目</th><th>配置</th><th>备注</th></tr></thead><tbody><tr><td>CPU</td><td>Intel J4125(4C4T)</td><td>Gemini Lake,VT-d 可用 ✅</td></tr><tr><td>内存</td><td>16 GB</td><td>拆机笔记本条</td></tr><tr><td>系统盘</td><td>512G SATA SSD</td><td>ESXi 8.0.2 + datastore1(剩 ~350G)</td></tr><tr><td>数据盘</td><td>希捷 2T(USB 3.0 硬盘盒)</td><td>保留原有 NTFS 数据,直通给 NAS VM</td></tr><tr><td>网口</td><td>4 × I226 2.5G</td><td>vmnic0 管理,vmnic1/2/3 直通</td></tr><tr><td>无线</td><td>Intel AX210(M.2→Mini-PCIe 转接)</td><td>直通给 OpenWrt 做 AP</td></tr></tbody></table></div><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">flowchart LR</span><br><span class="line">    subgraph ESXi[&quot;ESXi 8.0.2 @192.168.31.99&quot;]</span><br><span class="line">        VM1[&quot;① iStoreOS&lt;br/&gt;2C/1G · 软路由+AP&lt;br/&gt;直通: 3×I226 + AX210&quot;]</span><br><span class="line">        VM2[&quot;② 飞牛OS fnOS&lt;br/&gt;2C/5G · NAS&lt;br/&gt;直通: 希捷2T USB&quot;]</span><br><span class="line">        VM3[&quot;③ Debian 13&lt;br/&gt;1C/3G · Docker&quot;]</span><br><span class="line">        VM4[&quot;④ WinServer 2025&lt;br/&gt;2C/3G · RDP&quot;]</span><br><span class="line">    end</span><br><span class="line">    MODEM[光猫] -. WAN直通 .-&gt; VM1</span><br><span class="line">    VM1 -. LAN直通 .-&gt; SW[交换机/终端]</span><br><span class="line">    SW --- MGMT[小米路由器&lt;br/&gt;管理网段 192.168.31.0/24]</span><br><span class="line">    MGMT --- ESXi</span><br></pre></td></tr></table></figure><p><strong>网络设计原则:管理口与业务口分离。</strong> vmnic0(管理)+ 所有 VM 的 e1000 虚拟网卡走 vSwitch0 → 小米路由器网段(192.168.31.x);vmnic1/2/3 物理直通给 OpenWrt 做未来的 WAN/LAN。这样<strong>无论路由 VM 怎么折腾,ESXi 管理口永不失联</strong>——血泪教训。</p><h2 id="2-镜像归档体系-重装自由的基础"><a href="#2-镜像归档体系-重装自由的基础" class="headerlink" title="2. 镜像归档体系(重装自由的基础)"></a>2. 镜像归档体系(重装自由的基础)</h2><p>所有镜像统一归档,生成 <code>SHA256SUMS.txt</code> + <code>README.md</code>(版本、官方源、重装步骤)。核心纪律:</p><ul><li><strong>能下官方源绝不第三方</strong>;sysin 打包镜像仅 ESXi 本体(已装好的不再动)</li><li>大体积 ISO(WinServer 7.8G)只存 ESXi <code>datastore1/iso/</code>,本地不留</li><li>每次系统装好,顺手 <code>auto-backup.sh</code> 拉一份 state.tgz 到归档</li></ul><figure class="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"># ESXi 侧 iso 目录</span></span><br><span class="line"><span class="built_in">mkdir</span> -p /vmfs/volumes/datastore1/iso</span><br></pre></td></tr></table></figure><h2 id="3-虚拟机-①-iStoreOS-OpenWrt-软路由-WiFi-AP"><a href="#3-虚拟机-①-iStoreOS-OpenWrt-软路由-WiFi-AP" class="headerlink" title="3. 虚拟机 ①:iStoreOS(OpenWrt 软路由 + WiFi AP)"></a>3. 虚拟机 ①:iStoreOS(OpenWrt 软路由 + WiFi AP)</h2><h3 id="3-1-镜像转换-img-→-vmdk"><a href="#3-1-镜像转换-img-→-vmdk" class="headerlink" title="3.1 镜像转换(img → vmdk)"></a>3.1 镜像转换(img → vmdk)</h3><p>iStoreOS 只发 raw img,ESXi 不能直接用,需转 streamOptimized VMDK:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Windows 上(scoop install qemu)</span></span><br><span class="line">curl -L -o istoreos.img.gz <span class="string">&quot;https://fw.koolcenter.com/iStoreOS/x86_64/istoreos-24.10.8-2026073111-x86-64-squashfs-combined.img.gz&quot;</span></span><br><span class="line">gzip -dk istoreos.img.gz</span><br><span class="line">qemu-img convert -f raw -O vmdk -o subformat=streamOptimized istoreos.img istoreos.vmdk</span><br></pre></td></tr></table></figure><blockquote><p>选 iStoreOS 而非官方 OpenWrt 的理由:中文生态 + 应用商店,而且 <strong>LAN 默认 DHCP 客户端模式</strong>——官方 OpenWrt 首启会在局域网广播 192.168.1.x 的野 DHCP,怼上家里现有网段就是事故。</p></blockquote><h3 id="3-2-建盘-注册-VM"><a href="#3-2-建盘-注册-VM" class="headerlink" title="3.2 建盘 + 注册 VM"></a>3.2 建盘 + 注册 VM</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">scp istoreos.vmdk root@192.168.31.99:/vmfs/volumes/datastore1/OpenWrt/</span><br><span class="line">ssh root@192.168.31.99</span><br><span class="line"><span class="built_in">cd</span> /vmfs/volumes/datastore1/OpenWrt</span><br><span class="line">vmkfstools -i istoreos.vmdk -d thin OpenWrt.vmdk   <span class="comment"># 克隆为 ESXi 原生精简盘</span></span><br></pre></td></tr></table></figure><p>vmx 模板(踩坑后总结:<strong>BIOS 引导 + SATA 磁盘 + e1000 网卡</strong>,OpenWrt 全兼容):</p><figure class="highlight ini"><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="attr">displayName</span> = <span class="string">&quot;OpenWrt&quot;</span></span><br><span class="line"><span class="attr">config.version</span> = <span class="string">&quot;8&quot;</span></span><br><span class="line"><span class="attr">virtualHW.version</span> = <span class="string">&quot;21&quot;</span></span><br><span class="line"><span class="attr">guestOS</span> = <span class="string">&quot;other6xlinux-64&quot;</span></span><br><span class="line"><span class="attr">memSize</span> = <span class="string">&quot;1024&quot;</span></span><br><span class="line"><span class="attr">numvcpus</span> = <span class="string">&quot;2&quot;</span></span><br><span class="line"><span class="attr">firmware</span> = <span class="string">&quot;bios&quot;</span></span><br><span class="line"><span class="attr">sata0.present</span> = <span class="string">&quot;TRUE&quot;</span></span><br><span class="line">sata0:<span class="attr">0.present</span> = <span class="string">&quot;TRUE&quot;</span></span><br><span class="line">sata0:<span class="attr">0.fileName</span> = <span class="string">&quot;OpenWrt.vmdk&quot;</span></span><br><span class="line"><span class="attr">ethernet0.present</span> = <span class="string">&quot;TRUE&quot;</span></span><br><span class="line"><span class="attr">ethernet0.virtualDev</span> = <span class="string">&quot;e1000&quot;</span></span><br><span class="line"><span class="attr">ethernet0.networkName</span> = <span class="string">&quot;VM Network&quot;</span></span><br><span class="line"><span class="attr">ethernet0.addressType</span> = <span class="string">&quot;generated&quot;</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vim-cmd solo/registervm /vmfs/volumes/datastore1/OpenWrt/OpenWrt.vmx OpenWrt</span><br></pre></td></tr></table></figure><h3 id="3-3-找-IP-的三种姿势-没有-VMware-Tools-时"><a href="#3-3-找-IP-的三种姿势-没有-VMware-Tools-时" class="headerlink" title="3.3 找 IP 的三种姿势(没有 VMware Tools 时)"></a>3.3 找 IP 的三种姿势(没有 VMware Tools 时)</h3><ol><li><code>esxcli network vm port list --world-id &lt;wid&gt;</code> 拿到 VM 的 MAC</li><li>全网段并 ping 一轮后 <code>arp -a | grep &lt;MAC&gt;</code> 反查 IP</li><li>兜底:在 vSwitch0 上建临时 portgroup + vmk(如 192.168.100.2/24)直连 VM 的默认网段</li></ol><h3 id="3-4-初始配置-SSH"><a href="#3-4-初始配置-SSH" class="headerlink" title="3.4 初始配置(SSH)"></a>3.4 初始配置(SSH)</h3><p>iStoreOS 有个反直觉的坑:<strong>网页 LuCI 用默认密码 <code>password</code> 能进,SSH 用它却 Authentication failed</strong>(root 密码实际未设置,LuCI 有回退)。正确姿势:先在网页 <code>系统→管理权→路由器密码</code> 设置密码,SSH 才通。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 固定管理 IP(DHCP 拿到的地址固化)</span></span><br><span class="line">uci <span class="built_in">set</span> network.lan.proto=<span class="string">&#x27;static&#x27;</span></span><br><span class="line">uci <span class="built_in">set</span> network.lan.ipaddr=<span class="string">&#x27;192.168.31.50&#x27;</span></span><br><span class="line">uci <span class="built_in">set</span> network.lan.netmask=<span class="string">&#x27;255.255.255.0&#x27;</span></span><br><span class="line">uci <span class="built_in">set</span> network.lan.gateway=<span class="string">&#x27;192.168.31.1&#x27;</span></span><br><span class="line">uci add_list network.lan.dns=<span class="string">&#x27;192.168.31.1&#x27;</span>; uci add_list network.lan.dns=<span class="string">&#x27;223.5.5.5&#x27;</span></span><br><span class="line">uci commit network &amp;&amp; /etc/init.d/network restart</span><br></pre></td></tr></table></figure><p><strong>无线驱动预装</strong>(iStoreOS 24.10 已内置全套 Intel 固件,只缺内核模块):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">opkg update</span><br><span class="line">opkg install kmod-iwlwifi iw</span><br><span class="line"><span class="comment"># AP 守护系统已带 hostapd-openssl;若装 wpad-basic-mbedtls 会和它冲突,无需理会</span></span><br></pre></td></tr></table></figure><h3 id="3-5-AX210-直通排障实录"><a href="#3-5-AX210-直通排障实录" class="headerlink" title="3.5 AX210 直通排障实录"></a>3.5 AX210 直通排障实录</h3><p>AX210 走 M.2→Mini-PCIe 转接卡插入主板。<strong>现象:PCIe(WiFi)与 USB(蓝牙)双双无踪影</strong>,<code>lspci</code>/<code>vmkchdev -l</code>/<code>usbarbitrator</code> 日志三处确认为零。</p><p>诊断逻辑:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">WiFi(PCIE)无 &amp;&amp; 蓝牙(USB)无</span><br><span class="line">  → 不是驱动/直通配置问题(那两层都到不了)</span><br><span class="line">  → 卡在插槽里完全没被供电/响应</span><br><span class="line">  → 排查方向:①接触不良重插 ②插槽只焊了 USB 没走 PCIe(廉价板常见,原设计给 4G 模块)</span><br><span class="line">    ③转接卡与卡不兼容 ④需完全断电冷启动(PCIe 训练)</span><br></pre></td></tr></table></figure><blockquote><p>ESXi 侧知识:<strong>ESXi 没有任何 Wi-Fi 驱动,无线网卡永远不可能变成宿主机的 vmnic</strong>,唯一玩法是 PCI 直通给 VM。所以宿主机层面 lspci 能看到设备就算”成功”,剩下的交给 OpenWrt 的 iwlwifi。</p></blockquote><h3 id="3-6-开机自启"><a href="#3-6-开机自启" class="headerlink" title="3.6 开机自启"></a>3.6 开机自启</h3><figure class="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">vim-cmd hostsvc/autostartmanager/enable_autostart <span class="literal">true</span></span><br><span class="line">vim-cmd hostsvc/autostartmanager/update_autostartentry &lt;vmid&gt; powerOn 30 1 powerOff 30 systemDefault</span><br></pre></td></tr></table></figure><h2 id="4-虚拟机-②-飞牛OS-fnOS-NAS"><a href="#4-虚拟机-②-飞牛OS-fnOS-NAS" class="headerlink" title="4. 虚拟机 ②:飞牛OS fnOS(NAS)"></a>4. 虚拟机 ②:飞牛OS fnOS(NAS)</h2><ul><li>官方 ISO 直链藏在 <code>fnnas.com/download</code> 页面 JS 里(<code>iso.liveupdate.fnnas.com/.../fnos_Mainland-PE_x86_1.2.0800_2778.iso</code>)</li><li>⚠️ <strong>2026 年起 CDN 全面鉴权</strong>:未登录时 curl/PowerShell/.NET/真实浏览器直访一律 403(TLS 指纹 + 账号态双重校验),必须在官网登录账号后才能下载</li><li>VM:2C/5G/60G 精简盘,EFI + CD-ROM 挂 ISO,中文向导两分钟装完</li><li>2T 希捷走 <strong>USB 设备直通</strong>(见 §6)</li></ul><h2 id="5-虚拟机-③④-Debian-13-Windows-Server-2025"><a href="#5-虚拟机-③④-Debian-13-Windows-Server-2025" class="headerlink" title="5. 虚拟机 ③④:Debian 13 + Windows Server 2025"></a>5. 虚拟机 ③④:Debian 13 + Windows Server 2025</h2><p><strong>Debian</strong>(netinst,清华源):</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">guestOS</span> = <span class="string">&quot;debian12-64&quot;</span>      <span class="comment">; debian13 仍用此 guestOS 标识</span></span><br><span class="line"><span class="attr">bios.bootOrder</span> = <span class="string">&quot;cdrom,hdd&quot;</span></span><br><span class="line">ide1:<span class="attr">0.deviceType</span> = <span class="string">&quot;cdrom-image&quot;</span></span><br><span class="line">ide1:<span class="attr">0.fileName</span> = <span class="string">&quot;/vmfs/volumes/datastore1/iso/debian-13.7.0-amd64-netinst.iso&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Windows Server 2025 评估版</strong>直链获取(免填表):</p><figure class="highlight text"><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">https://go.microsoft.com/fwlink/?linkid=2345732   ← zh-CN ISO,7.84 GB</span><br><span class="line">跟随 302 → aka.ms → software-static.download.prss.microsoft.com/...SERVER_EVAL_x64FRE_zh-cn.iso</span><br></pre></td></tr></table></figure><p>评估版注意事项:<strong>10 天内联网激活评估许可</strong>(免费,不激活会自动关机),180 天到期可重装续命。</p><h3 id="ESXi-侧下载大文件的三个坑"><a href="#ESXi-侧下载大文件的三个坑" class="headerlink" title="ESXi 侧下载大文件的三个坑"></a>ESXi 侧下载大文件的三个坑</h3><ol><li><strong>ESXi 没配 DNS</strong> → <code>wget: bad address</code>;<code>esxcli network ip dns server add --server 223.5.5.5</code>(小米路由器对 ESXi 的 DNS 查询不响应,别用 .1)</li><li><strong>解析出 IPv6</strong> → ESXi 无公网 v6,握手挂起;本地解析 IPv4 写入 <code>/etc/hosts</code> 强制走 v4</li><li><strong>busybox wget 的精简 TLS 握不上 Akamai</strong> → <code>error getting response</code>;ESXi 又没有 curl → 最终方案:本机下载 + 内网 scp(千兆 ~80MB/s,7.8G 约 2 分钟)</li></ol><h2 id="6-USB-硬盘直通给-NAS-保留数据的关键操作"><a href="#6-USB-硬盘直通给-NAS-保留数据的关键操作" class="headerlink" title="6. USB 硬盘直通给 NAS(保留数据的关键操作)"></a>6. USB 硬盘直通给 NAS(保留数据的关键操作)</h2><p>ESXi 默认<strong>扣留 USB 存储设备</strong>(usbarbitrator 服务,留给 VM 直通),数据存储里看不到是正常现象:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 想把 USB 盘做成 ESXi 数据存储(不推荐,官方不支持 VMFS on USB):</span></span><br><span class="line">/etc/init.d/usbarbitrator stop &amp;&amp; chkconfig usbarbitrator off</span><br><span class="line">esxcli storage core adapter rescan --all   <span class="comment"># 出现 mpx.vmhbaXX 设备</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 想把 USB 盘直通给虚拟机(NAS 场景,推荐):保持 usbarbitrator 开启</span></span><br><span class="line"><span class="comment"># VM → 编辑设置 → 添加其他设备 → USB 设备 → 选希捷</span></span><br></pre></td></tr></table></figure><blockquote><p>热插拔教训:<strong>SATA/PCIe 设备不要带电插</strong>,USB 盘插入瞬间的 5V 冲击曾把这台小主机直接拉挂(机械盘启动电流 &gt;2A)。要加盘,先关机。</p></blockquote><h2 id="7-路由切换-爱快-→-OpenWrt-可回退"><a href="#7-路由切换-爱快-→-OpenWrt-可回退" class="headerlink" title="7. 路由切换:爱快 → OpenWrt(可回退)"></a>7. 路由切换:爱快 → OpenWrt(可回退)</h2><p>最后一步,物理动作 + 配置联动:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 光猫改桥接(找运营商),准备宽带 PPPoE 账号密码</span><br><span class="line">2. 断电接线:光猫 → 主机 WAN 口(vmnic1);vmnic2 → 交换机/AP</span><br><span class="line">3. OpenWrt 配 PPPoE 拨号 + DHCP + 防火墙;小米路由器改 AP 模式</span><br><span class="line">4. 观察 1 周 → 爱快导出 OVF 归档 → 删除</span><br></pre></td></tr></table></figure><p>回退方案:爱快 VM 与其 3 个网卡直通原封不动,网线插回爱快对应网口、光猫改回路由模式即可。</p><h2 id="8-避坑清单-按血泪程度排序"><a href="#8-避坑清单-按血泪程度排序" class="headerlink" title="8. 避坑清单(按血泪程度排序)"></a>8. 避坑清单(按血泪程度排序)</h2><div class="table-container"><table><thead><tr><th>#</th><th>坑</th><th>一句话解法</th></tr></thead><tbody><tr><td>1</td><td>系统盘控制器被开直通,存储全灭</td><td><code>vmkchdev -l</code> 查归属;直通前确认盘上没有 BOOTBANK/OSDATA</td></tr><tr><td>2</td><td>ESXi 永不支持 Wi-Fi 当物理网卡</td><td>只能 PCI 直通给 VM(iwlwifi)</td></tr><tr><td>3</td><td>fnOS ISO 下载全 403</td><td>官网登录后下载,任何脚本直下都会被指纹墙拦</td></tr><tr><td>4</td><td>OpenWrt 首启野 DHCP</td><td>用 iStoreOS(LAN 默认 DHCP 客户端)或首启先断网配好</td></tr><tr><td>5</td><td>ESXi 下载:DNS/IPv6/TLS 三连坑</td><td>配 223.5.5.5 + hosts 锁 v4 + 终极方案本机 scp</td></tr><tr><td>6</td><td>iStoreOS 网页密码≠SSH 密码</td><td>网页设完密码 SSH 才可用</td></tr><tr><td>7</td><td>USB 盘在数据存储里”消失”</td><td>usbarbitrator 机制,直通给 VM 就好</td></tr><tr><td>8</td><td>带电插 SATA/机械盘 → 整机掉电</td><td>加盘必关机</td></tr><tr><td>9</td><td>主板电池没电 → 时间回 2012</td><td>换 CR2032 + 配 NTP</td></tr></tbody></table></div><h2 id="9-资源占用快照-部署完-4-VM-后"><a href="#9-资源占用快照-部署完-4-VM-后" class="headerlink" title="9. 资源占用快照(部署完 4 VM 后)"></a>9. 资源占用快照(部署完 4 VM 后)</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">vCPU: 2+2+1+2 = 7(4C 轻度超配,负载型 VM 不同时满载无碍)</span><br><span class="line">内存: 1+5+3+3 = 12G(ESXi 自留 4G)</span><br><span class="line">磁盘: OpenWrt 2.4G + fnOS 60G + Debian 60G + WinServer 80G ≈ 200G(datastore1 剩 ~150G)</span><br></pre></td></tr></table></figure><hr><blockquote><p>本系列所有命令都在真机上验证过,排障时间线来自实际 vmkernel 日志。折腾快乐,数据无价,<strong>每天想一遍:今天的配置,重启后还能回来吗?</strong></p></blockquote>]]></content>
    
    
    <summary type="html">一台 J4125 小主机,从&quot;存储消失事故&quot;的废墟上重建 All-in-One:四台虚拟机(软路由/NAS/Docker/Windows)的完整部署流程、PCI 直通实战、AX210 无线排障、镜像归档体系,以及一路上踩过的所有坑。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="流程类" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E6%B5%81%E7%A8%8B%E7%B1%BB/"/>
    
    
    <category term="ESXi" scheme="https://www.shallowrecall.top/tags/ESXi/"/>
    
    <category term="Homelab" scheme="https://www.shallowrecall.top/tags/Homelab/"/>
    
    <category term="All-in-One" scheme="https://www.shallowrecall.top/tags/All-in-One/"/>
    
    <category term="OpenWrt" scheme="https://www.shallowrecall.top/tags/OpenWrt/"/>
    
    <category term="NAS" scheme="https://www.shallowrecall.top/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>ESXi 存储一夜消失:SATA 控制器直通引发的&quot;血案&quot;复盘</title>
    <link href="https://www.shallowrecall.top/posts/esxi-sata-passthrough-disaster-review.html"/>
    <id>https://www.shallowrecall.top/posts/esxi-sata-passthrough-disaster-review.html</id>
    <published>2026-10-25T08:00:00.000Z</published>
    <updated>2026-10-25T08:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="📝-ESXi-存储一夜消失-SATA-控制器直通引发的”血案”复盘"><a href="#📝-ESXi-存储一夜消失-SATA-控制器直通引发的”血案”复盘" class="headerlink" title="📝 ESXi 存储一夜消失:SATA 控制器直通引发的”血案”复盘"></a>📝 ESXi 存储一夜消失:SATA 控制器直通引发的”血案”复盘</h1><span id="more"></span><blockquote><p>💡 如果你的 ESXi 也出现”数据存储归零、虚拟机无效、但系统还能 SSH”——大概率不是数据丢了,先看本文第 3 节。</p></blockquote><h2 id="0-背景"><a href="#0-背景" class="headerlink" title="0. 背景"></a>0. 背景</h2><p>家里的 All-in-One 主机:</p><div class="table-container"><table><thead><tr><th>项目</th><th>配置</th></tr></thead><tbody><tr><td>平台</td><td>Intel J4125(Gemini Lake)小主机</td></tr><tr><td>内存</td><td>16 GB</td></tr><tr><td>系统盘</td><td>金百达 512G SATA SSD(ESXi 系统盘 + datastore1)</td></tr><tr><td>网卡</td><td>4 × Intel I226 2.5G</td></tr><tr><td>虚拟化</td><td>VMware ESXi 8.0.2(sysin.org 打包镜像)</td></tr><tr><td>虚拟机</td><td>爱快软路由(3 个物理网卡 PCI 直通)</td></tr></tbody></table></div><p>某次重启 ESXi 后,打开网页:</p><ul><li><strong>存储页面:数据存储计数 0</strong>——空空如也</li><li><strong>虚拟机页面</strong>:爱快 VM 显示「无效」,配置路径指向 <code>/vmfs/volumes/4f02eeb0-.../iKuai/iKuai.vmx</code></li></ul><blockquote><p>512G 的 SSD 就装在机器里,ESXi 却像一台”没插硬盘”的裸机一样运行着。<br>更诡异的是:系统还能开机、SSH 能登、密码和 IP 都还在——<strong>配置没丢,只是存储消失了</strong>。</p></blockquote><h2 id="1-诊断决策树"><a href="#1-诊断决策树" class="headerlink" title="1. 诊断决策树"></a>1. 诊断决策树</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">flowchart TD</span><br><span class="line">    A[数据存储全部消失] --&gt; B&#123;esxcli storage core device list&#125;</span><br><span class="line">    B --&gt;|空| C[所有磁盘设备不可见]</span><br><span class="line">    C --&gt; D&#123;adapter list / vmfs/devices/disks&#125;</span><br><span class="line">    D --&gt;|空| E[连 HBA 适配器都没有]</span><br><span class="line">    E --&gt; F&#123;lspci 看 SATA 控制器&#125;</span><br><span class="line">    F --&gt;|设备存在| G&#123;vmkchdev -l 看归属&#125;</span><br><span class="line">    G --&gt;|passthru| H[🎯 根因:SATA 控制器被 PCI 直通层抢占]</span><br><span class="line">    H --&gt; I[手动加载 vmw_ahci 驱动]</span><br><span class="line">    I --&gt; J[解除直通 → 磁盘/数据存储瞬间恢复]</span><br><span class="line">    J --&gt; K[修复持久化 → 重启验证]</span><br></pre></td></tr></table></figure><h3 id="1-1-第一层-不是”卷丢了”-是”盘丢了”"><a href="#1-1-第一层-不是”卷丢了”-是”盘丢了”" class="headerlink" title="1.1 第一层:不是”卷丢了”,是”盘丢了”"></a>1.1 第一层:不是”卷丢了”,是”盘丢了”</h3><p>SSH 登录(幸好 SSH 和密码配置还在),三条命令定位问题层级:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">esxcli storage filesystem list      <span class="comment"># 输出为空!连 BOOTBANK/OSDATA 都没有</span></span><br><span class="line">esxcli storage core device list     <span class="comment"># 为空!没有任何磁盘设备</span></span><br><span class="line">localcli storage core adapter list  <span class="comment"># 为空!连 HBA 适配器都没注册</span></span><br></pre></td></tr></table></figure><p><strong>关键判断</strong>:如果只是 VMFS 卷损坏/未挂载,至少能看到块设备(如 <code>t10.ATA_____xxx</code>)。连设备节点都没有,说明问题在<strong>存储驱动层之下</strong>——要么驱动没加载,要么控制器没交给 ESXi 内核。</p><h3 id="1-2-第二层-干扰项——VIB-签名告警"><a href="#1-2-第二层-干扰项——VIB-签名告警" class="headerlink" title="1.2 第二层:干扰项——VIB 签名告警"></a>1.2 第二层:干扰项——VIB 签名告警</h3><p><code>dmesg</code> 里翻到一条触目惊心的 ALERT:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">ALERT: Failed to verify signatures of the following vib(s):</span><br><span class="line">[atlantic bcm-mpi3 bmcal bnxtnet bnxtroce brcmfcoe clusterstore</span><br><span class="line"> cndi-igc cpu-microcode crx drivervm-gpu-base dwi2c ...]</span><br></pre></td></tr></table></figure><p>几乎所有 VIB 都”验签失败”?!第一反应:</p><ol><li>Secure Boot 开了,第三方打包镜像被拒?</li><li>启动分区数据损坏,签名校验不过?</li></ol><p>逐项排除:</p><figure class="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">dmesg | grep -i secureboot</span><br><span class="line"><span class="comment"># → &quot;UEFI Secure Boot is not enabled&quot; ✅ 排除 1</span></span><br></pre></td></tr></table></figure><p>再深挖发现 <code>/bootbank</code> 指向了 <code>/tmp/_bootbankxabv0jit</code>(内存中的降级空目录),<code>vdf -h</code> 里所有挂载点全是 Ramdisk——<strong>根分区、/etc、osdata、configstore 全部跑在内存里</strong>。</p><blockquote><p>⚠️ 此时很容易得出错误结论:”因为启动分区坏了 → VIB 验签失败 → 驱动不加载 → 看不到盘”。<br>因果其实是<strong>反的</strong>,这是本次排障最大的陷阱,下一节揭晓。</p></blockquote><h3 id="1-3-第三层-实锤——PCI-Passthrough-劫持了-SATA-控制器"><a href="#1-3-第三层-实锤——PCI-Passthrough-劫持了-SATA-控制器" class="headerlink" title="1.3 第三层:实锤——PCI Passthrough 劫持了 SATA 控制器"></a>1.3 第三层:实锤——PCI Passthrough 劫持了 SATA 控制器</h3><p>不纠结告警,直接看”谁在管这块 SATA 控制器”:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">lspci | grep -i sata</span><br><span class="line"><span class="comment"># 0000:00:12.0 SATA controller: Intel Celeron/Pentium Silver Processor SATA Controller</span></span><br><span class="line"></span><br><span class="line">vmkchdev -l | grep 00:12</span><br><span class="line"><span class="comment"># 0000:00:12.0 8086:31e3 8086:7270 passthru vmhba0</span></span><br><span class="line"><span class="comment">#                                            ^^^^^^^^ !!!</span></span><br></pre></td></tr></table></figure><p><strong><code>passthru</code>!</strong> 这块承载着系统盘的 SATA 控制器,在开机早期被 <strong>PCI 直通层(VM Passthrough)</strong> 从 ESXi 内核手里抢走了。</p><p>内核日志给出了精确时间线:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">03:09:28.869  PCI: 0000:00:12.0 named &#x27;vmhba0&#x27; (was &#x27;&#x27;)      ← 设备被枚举</span><br><span class="line">03:09:29.185  PCIPassthru: Attached to device 0000:00:12.0  ← 542ms 后被直通层抢占</span><br></pre></td></tr></table></figure><h3 id="1-4-完整因果链-倒推"><a href="#1-4-完整因果链-倒推" class="headerlink" title="1.4 完整因果链(倒推)"></a>1.4 完整因果链(倒推)</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">(人为)在网页界面给 00:12.0 SATA 控制器开启了&quot;直通&quot;</span><br><span class="line">        ↓ 提示&quot;需重启生效&quot;,重启</span><br><span class="line">开机早期:PCIPassthru 按配置抢占 SATA 控制器(早于驱动加载)</span><br><span class="line">        ↓</span><br><span class="line">系统盘所在的控制器从 ESXi 消失</span><br><span class="line">        ↓</span><br><span class="line">BOOTBANK1/2、OSDATA 分区无法挂载 → 全部降级为内存盘</span><br><span class="line">        ↓</span><br><span class="line">/etc、configstore、esx.conf 沦为&quot;接近默认&quot;的临时状态</span><br><span class="line">        ↓</span><br><span class="line">VIB 激活校验依赖 bootbank 中的描述文件 → 空目录 → &quot;全部验签失败&quot; ← 这只是【表象】</span><br><span class="line">        ↓</span><br><span class="line">数据存储 0、VM 无效、存储消失</span><br></pre></td></tr></table></figure><blockquote><p>破案补充:这台机器爱快 VM 一直用 3 个 I226 网卡直通,一切正常——因为<strong>那 3 个口上没有系统盘</strong>。某次把 SATA 控制器也加入了直通(可能是想给 NAS 虚拟机直通硬盘),重启后系统盘控制器直接消失,于是”存储一夜蒸发”。</p></blockquote><h2 id="2-修复"><a href="#2-修复" class="headerlink" title="2. 修复"></a>2. 修复</h2><h3 id="2-1-运行时恢复-不重启"><a href="#2-1-运行时恢复-不重启" class="headerlink" title="2.1 运行时恢复(不重启)"></a>2.1 运行时恢复(不重启)</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 手动加载 AHCI 驱动(模块文件一直在内存 tardisk 里,只是没被激活)</span></span><br><span class="line">vmkload_mod vmw_ahci</span><br><span class="line"><span class="comment"># Module vmw_ahci loaded successfully</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 解除 00:12.0 的直通,立即交还内核</span></span><br><span class="line">localcli hardware pci pcipassthru <span class="built_in">set</span> --device-id 0000:00:12.0 --<span class="built_in">enable</span> 0 --apply-now</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 重扫</span></span><br><span class="line">esxcli storage core adapter rescan --all</span><br></pre></td></tr></table></figure><p>解除直通的<strong>那一秒</strong>,所有分区瞬间回归:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">/vmfs/volumes/4f02eeb0-...  datastore1   VMFS-6  374467461120  353919565824  ← 数据回来了!</span><br><span class="line">/vmfs/volumes/4f02eeaf-...  OSDATA-...   VMFSOS  ...</span><br><span class="line">/vmfs/volumes/f030c6aa-...  BOOTBANK1    vfat   ...</span><br><span class="line">/vmfs/volumes/239cdbfd-...  BOOTBANK2    vfat   ...</span><br></pre></td></tr></table></figure><p>爱快虚拟机也从「无效」恢复为正常,<code>/bootbank</code> 符号链接指回真实分区。</p><blockquote><p>🐛 顺带踩了个工具坑:老牌命令 <code>vmkchdev -v 0000:00:12.0</code> 在 8.0.2 上有进制转换 bug——把 16 进制的槽位 <code>12</code> 当成 10 进制转换成了 <code>0c</code>,报 <code>Did not find PCI device at 0000:00:0c.0</code>。遇到这个错直接用上面的 <code>localcli hardware pci pcipassthru set</code> 即可。</p></blockquote><h3 id="2-2-持久化-否则重启复发"><a href="#2-2-持久化-否则重启复发" class="headerlink" title="2.2 持久化(否则重启复发)"></a>2.2 持久化(否则重启复发)</h3><p>运行时的修改此时只会写进<strong>内存里的临时 bootbank</strong>,重启即失。所以:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 把 bootbank 符号链接指回真实分区</span></span><br><span class="line"><span class="built_in">rm</span> /bootbank</span><br><span class="line"><span class="built_in">ln</span> -s /vmfs/volumes/BOOTBANK1 /bootbank</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 用官方脚本把当前配置(含&quot;解除直通&quot;)加密打包回写 BOOTBANK1</span></span><br><span class="line">/sbin/auto-backup.sh</span><br><span class="line"><span class="comment"># 输出: Saving current state in /bootbank</span></span><br><span class="line"><span class="comment">#       Using key ID a65e4282-... to encrypt</span></span><br></pre></td></tr></table></figure><p>ESXi 8 的持久化链路:<strong>hostd/configstore(内存)→ auto-backup.sh → state.tgz(AES 加密)→ BOOTBANK1 分区</strong>,下次开机由引导程序读回。</p><h3 id="2-3-重启验证"><a href="#2-3-重启验证" class="headerlink" title="2.3 重启验证"></a>2.3 重启验证</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">vmkchdev -l | grep 00:12</span><br><span class="line"><span class="comment"># 0000:00:12.0 8086:31e3 8086:7270 vmkernel vmhba0   ← 重启后仍是 vmkernel ✅</span></span><br><span class="line">esxcli storage filesystem list | grep datastore1      <span class="comment"># 自动挂载 ✅</span></span><br></pre></td></tr></table></figure><p>另外两处顺手修复:</p><ul><li><strong>系统时间停在 2012</strong>(主板纽扣电池没电的典型症状):配置 NTP <code>esxcli system ntp set -e true -s 203.107.6.88 -s 106.55.184.199</code></li><li><strong><code>/etc/vmware/passthru.map</code> 末尾</strong>有一条手工添加的 <code>8086 31e3 d3d0 fasle</code>(还拼错了 false)——这条只是直通重置方式的提示表,<strong>不是</strong>本次故障根因,但留着容易误导后人,建议清理。</li></ul><h2 id="3-教训与红线"><a href="#3-教训与红线" class="headerlink" title="3. 教训与红线"></a>3. 教训与红线</h2><ol><li><strong>系统盘所在控制器,永远不要开直通。</strong> 开直通前先 <code>vmkchdev -l</code> 确认设备上挂着什么。</li><li><strong>直通的代价是”重启后才爆”:</strong> 界面上点完开关一切正常,提示”需重启”,重启完才崩——间隔让人无法把两件事关联起来。</li><li><strong>“所有 VIB 验签失败” ≠ 镜像坏了。</strong> 它可能是 bootbank 不可达的<strong>结果</strong>。看到这条 ALERT 先查 <code>/bootbank</code> 指向哪、<code>vdf -h</code> 是不是全内存盘,再下结论。</li><li><strong>配置”还在”但 esx.conf 变干净</strong> 也是同款症状:密码/网络来自 state.tgz 的其他部分,而 esx.conf 被 configstore 同步降级成了默认值。</li><li>修完务必做<strong>持久化验证</strong>(重启一次),并留一份 <code>state.tgz</code> 备份到别处:</li></ol><figure class="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">/sbin/auto-backup.sh</span><br><span class="line">scp root@ESXi:/vmfs/volumes/BOOTBANK1/state.tgz ./esxi-state-backup.tgz</span><br></pre></td></tr></table></figure><h2 id="4-一图总结"><a href="#4-一图总结" class="headerlink" title="4. 一图总结"></a>4. 一图总结</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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><br><span class="line">│                    ESXi 8.0.2 主机                    │</span><br><span class="line">│                                                       │</span><br><span class="line">│  00:12.0 SATA 控制器 ──直通开启──▶ PCIPassthru 抢占    │</span><br><span class="line">│        │                                              │</span><br><span class="line">│        │ (系统盘/数据存储/BOOTBANK/OSDATA 全在这条线上)  │</span><br><span class="line">│        ▼                                              │</span><br><span class="line">│  vmw_ahci 无法绑定 → 无 HBA → 无磁盘 → 无数据存储       │</span><br><span class="line">│        ▼                                              │</span><br><span class="line">│  bootbank/osdata 落内存 → 配置降级 + VIB 验签告警(假象) │</span><br><span class="line">└─────────────────────────────────────────────────────┘</span><br><span class="line">修复:localcli hardware pci pcipassthru set --enable 0 --apply-now</span><br><span class="line">     + 修 /bootbank 软链 + auto-backup.sh 持久化</span><br></pre></td></tr></table></figure><hr><blockquote><p>排障工具速查:<code>lspci</code> / <code>vmkchdev -l</code>(设备归属)/ <code>vmkload_mod &lt;mod&gt;</code>(手动加载驱动)/ <code>esxcli storage core adapter rescan --all</code> / <code>vdf -h</code>(看内存盘)/ <code>dmesg</code>(时间线)。</p></blockquote>]]></content>
    
    
    <summary type="html">一台 J4125 小主机上的 ESXi 8.0.2,所有数据存储一夜之间凭空消失,虚拟机集体&quot;无效&quot;。完整复盘从现象到根因的诊断决策树、修复与持久化方案,以及那行差点让人误判的 VIB 签名告警。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="复盘类" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%A4%8D%E7%9B%98%E7%B1%BB/"/>
    
    
    <category term="ESXi" scheme="https://www.shallowrecall.top/tags/ESXi/"/>
    
    <category term="Homelab" scheme="https://www.shallowrecall.top/tags/Homelab/"/>
    
    <category term="存储" scheme="https://www.shallowrecall.top/tags/%E5%AD%98%E5%82%A8/"/>
    
    <category term="踩坑记录" scheme="https://www.shallowrecall.top/tags/%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/"/>
    
  </entry>
  
  <entry>
    <title>baoyu-infographic实战：把结构化资料变成可信的信息图</title>
    <link href="https://www.shallowrecall.top/posts/b38ca02a.html"/>
    <id>https://www.shallowrecall.top/posts/b38ca02a.html</id>
    <published>2026-10-15T13:00:00.000Z</published>
    <updated>2026-10-15T13:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p><a href="../宝玉Skills实战教程.md">返回总教程</a></p><p><code>baoyu-infographic</code> 的重点不是把文章“画得更热闹”，而是把事实按关系重新组织成可阅读的空间结构。它先生成内容分析和结构化材料，再选择布局与风格，最终调用图像后端输出信息图。</p><p>本文基于固定提交源码核对；案例是操作示例，非付费服务或账号实测，没有实际生成图片或调用服务。</p><span id="more"></span><h2 id="一、场景与边界"><a href="#一、场景与边界" class="headerlink" title="一、场景与边界"></a>一、场景与边界</h2><p>适合产品对比、时间线、分类表、知识总览、步骤指南、指标摘要和高密度信息大图。相比文章配图，它更强调一张图内部的完整结构；相比多页社交卡片，它不负责滑动阅读节奏。</p><p>技能有两个独立维度：布局处理信息关系，风格处理视觉表现。源码的主目录列出 21 种布局和 22 种风格。布局如 <code>binary-comparison</code>、<code>comparison-matrix</code>、<code>linear-progression</code>、<code>bento-grid</code>、<code>dense-modules</code>；风格如 <code>craft-handmade</code>、<code>technical-schematic</code>、<code>corporate-memphis</code>、<code>hand-drawn-edu</code>。末尾参考清单仍有“21 styles”的旧表述，应以当前主风格表为准，不把文档数量不一致扩展成新的能力承诺。</p><p>最重要的边界是保真：结构化内容不应新增信息，统计和引用应照原文保留，且必须先清除密钥、令牌等秘密。若输入是一篇需要大幅删减的长文，先单独得到用户确认的精简稿，再送进本技能，不能一边出图一边静默改写事实。</p><h2 id="二、前置依赖与按需安装"><a href="#二、前置依赖与按需安装" class="headerlink" title="二、前置依赖与按需安装"></a>二、前置依赖与按需安装</h2><p>需要支持 Skills 的 Agent、明确且可读取的资料，以及可用图像后端。对文档分析而言不必准备所有供应商；对出图而言，至少要有一种已经授权的原生或非原生图像能力。</p><p>经 README 核实的安装入口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-infographic --agent codex</span><br></pre></td></tr></table></figure><p>Claude Code 用户可将 <code>codex</code> 替换为 <code>claude-code</code>。</p><p>按需选择 <code>baoyu-infographic</code>；缺少后端再补 <a href="baoyu-image-gen.md">图片生成</a>。该入口未固定本文提交，安装最新版后仍需重新核对差异。固定源码的项目级 Codex 安装可复制完整目录到 <code>.agents/skills/baoyu-infographic/</code>，必须保留布局、风格和配置参考文件。</p><p>本教程不执行安装、配置密钥或付费调用。上传输入前要确认资料可以交给所选供应商处理；脱敏不仅是最终发布阶段的工作。</p><h2 id="三、输入输出"><a href="#三、输入输出" class="headerlink" title="三、输入输出"></a>三、输入输出</h2><p>输入建议由清晰标题、受众、事实条目和关系构成。例如对比两种备份策略，先给出恢复目标、保存频率、存储成本口径和适用场景，不能只给零散数字让 Agent 自己推断单位。</p><p>默认输出目录是 <code>infographic/&#123;topic-slug&#125;/</code>，文件包括源内容、<code>analysis.md</code>、<code>structured-content.md</code>、<code>prompts/infographic.md</code> 与 <code>infographic.png</code>。前两份中间材料很重要：如果最后的图出现错误，可以判断是内容结构阶段就错了，还是图像渲染阶段才写错。</p><p>比例支持横向 16:9、纵向 9:16、方形 1:1，也可指定自定义 W:H。比例不等于保证像素尺寸，具体还要看后端支持。输出是位图，不是每个模块可独立编辑的设计源文件。</p><h2 id="四、可复制的-Agent-请求"><a href="#四、可复制的-Agent-请求" class="headerlink" title="四、可复制的 Agent 请求"></a>四、可复制的 Agent 请求</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-infographic，将我提供的两种备份方案对照表制作成中文信息图。</span><br><span class="line">先核对输入中的单位、统计时间和术语，遇到缺失信息向我询问，不补造数据。</span><br><span class="line">保留每条事实的原文表述；移除任何令牌、密钥和敏感账号信息。</span><br><span class="line">先生成分析与 structured-content.md，推荐三种布局和风格组合。</span><br><span class="line">优先考虑二元对比或比较矩阵，横向 16:9，受众是技术团队。</span><br><span class="line">让我确认组合、比例、语言和图像后端后，再保存最终提示词并出图。</span><br><span class="line">完成时给出数据核对清单和文件路径，不发布、不自动改写原文。</span><br></pre></td></tr></table></figure><p>若要先做资料整理，明确说“本轮停在结构化材料，不调用图像后端”。这只是限制本轮执行范围，不是假造新的 CLI 参数。</p><h2 id="五、案例：两种备份策略的比较图"><a href="#五、案例：两种备份策略的比较图" class="headerlink" title="五、案例：两种备份策略的比较图"></a>五、案例：两种备份策略的比较图</h2><p>准备一份已经由资料负责人确认的表格，其中每一行是相同口径：备份频率、恢复流程、维护成本、适用数据类型。本例不填入虚构的恢复秒数；没有实测指标就保持定性描述。</p><p>第一步分析内容，判断它是多因素对比而不是时间线。把受众定为技术团队，学习目标是“读者能区分两种方案的取舍”，而不是“看完觉得某方案更先进”。</p><p>第二步生成 <code>structured-content.md</code>。为每个模块保留原始事实，分别记录关键概念、原文内容、视觉元素与标签。数值、单位、引文不随意换算或简写；如果太长，在这个阶段请用户先批准修改原文，而不是让绘图模型自由压缩。</p><p>第三步推荐布局。两种方案、少量差异可选 <code>binary-comparison</code>；多因素多方案可选 <code>comparison-matrix</code>；概览模块可选 <code>bento-grid</code>。源码对“信息图”“高密度信息大图”有关键词优先推荐，因此泛泛说“信息图”可能先得到便当格建议。明确说明比较关系并在确认时选择矩阵，通常比只强调风格更有帮助。</p><p>第四步确认组合。保守选择易读的技术或企业风格，不用装饰纹理抢占文字空间。默认 <code>bento-grid + craft-handmade</code> 只是推荐，不意味着自动跳过确认。</p><p>第五步读取布局定义、风格定义和基础提示词模板，写入完整提示词再生成。如果已有同名源文件、分析、提示词或图片，应先按时间戳备份，再创建新版本。</p><p>第六步逐项对照。不要只检查标题好不好看，要把原表每行与图中对应项逐字比对。若图像把“每天”写成“每周”，这是交付阻断问题。修正提示词并另存候选图，不在位图上程序覆盖文字。</p><h2 id="六、配置与参考图片"><a href="#六、配置与参考图片" class="headerlink" title="六、配置与参考图片"></a>六、配置与参考图片</h2><p>偏好查找顺序是项目 <code>.baoyu-skills/baoyu-infographic/EXTEND.md</code>、XDG 配置目录、用户 <code>$HOME/.baoyu-skills/baoyu-infographic/EXTEND.md</code>，首个命中生效。未找到时进入首次偏好询问。</p><p>常用字段有 <code>preferred_layout</code>、<code>preferred_style</code>、<code>preferred_aspect</code>、<code>language</code> 和 <code>preferred_image_backend</code>。例如经常做高密度中文卡片，可保存纵向比例与固定风格；但这些值只是后续推荐和确认的默认值，不会授权跳过确认。</p><p>参考图支持三种模式：<code>direct</code> 直接传给后端，<code>style</code> 抽取线条和表现特点，<code>palette</code> 抽取配色。直接参考需要后端支持；否则应把提取特征写进提示词并告知用户，并非声称完成了相同的图像参考过程。文件应复制到输出目录 <code>refs/</code>，在提示词里记录用途。</p><h2 id="七、验收与排错"><a href="#七、验收与排错" class="headerlink" title="七、验收与排错"></a>七、验收与排错</h2><ul><li>[ ] 每个数值、单位、引用和结论都能回溯输入。</li><li>[ ] 结构化材料没有新增事实或偷偷改变统计口径。</li><li>[ ] 图的布局反映真实关系，不把相关性画成因果。</li><li>[ ] 最终提示词在生成前保存，已有文件得到备份。</li><li>[ ] 中文可读，标题、模块标签和数据值没有错误。</li><li>[ ] Obsidian 与 Hexo 可访问图片附件，公开目录没有秘密。</li></ul><p><strong>信息密度过高</strong>：优先拆任务或让用户确认精简稿，不是无限缩小文字。<strong>图漂亮但数据错</strong>：先对照 <code>structured-content.md</code>，定位内容错误还是渲染错误。<strong>默认布局不合理</strong>：检查关键词快捷推荐，明确说明比较、循环或层级关系。<strong>没有图像后端</strong>：停止生成并选择后端，源码不允许用 HTML 或 SVG 冒充该技能的位图结果。<strong>博客阅读困难</strong>：在实际手机宽度预览，必要时拆成 <a href="baoyu-xhs-images.md">社交图片卡片</a>，不要只在桌面大屏验收。</p><h2 id="八、相关技能与源码"><a href="#八、相关技能与源码" class="headerlink" title="八、相关技能与源码"></a>八、相关技能与源码</h2><p><a href="baoyu-diagram.md">技术图解</a> 适合严格可编辑的关系图；<a href="baoyu-article-illustrator.md">文章配图</a> 负责文章不同位置的解释图；<a href="baoyu-compress-image.md">图片压缩</a> 用于通过验收后减小体积。</p><p>固定提交 <code>1567581c26ec29f4216c6e6835415bf30343b0e3</code>：</p><ul><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-infographic/SKILL.md">技能完整源码</a></li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-infographic/references/structured-content-template.md">结构化内容模板</a></li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/README.md">安装说明</a></li></ul>]]></content>
    
    
    <summary type="html">先保真整理资料，再选择布局与视觉风格，制作数据可追溯、文字可核验的单张信息图。</summary>
    
    
    
    
    <category term="AI" scheme="https://www.shallowrecall.top/tags/AI/"/>
    
    <category term="Agent Skills" scheme="https://www.shallowrecall.top/tags/Agent-Skills/"/>
    
    <category term="宝玉Skills" scheme="https://www.shallowrecall.top/tags/%E5%AE%9D%E7%8E%89Skills/"/>
    
  </entry>
  
  <entry>
    <title>baoyu-url-to-markdown实战：把网页保存为可核验的本地资料</title>
    <link href="https://www.shallowrecall.top/posts/9224e3e2.html"/>
    <id>https://www.shallowrecall.top/posts/9224e3e2.html</id>
    <published>2026-10-15T13:00:00.000Z</published>
    <updated>2026-10-15T13:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p><a href="../宝玉Skills实战教程.md">返回总教程</a></p><p>网页保存成功，不等于正文采集成功。<code>baoyu-url-to-markdown</code> 的价值是把浏览器中可访问的内容转成 Markdown，并让 Agent 检查结果，而不是只下载一份看似正常的网页外壳。</p><p>本文基于固定提交源码核对。下文案例是操作示例，不是账号、付费页面或在线抓取实测；撰写时未执行安装、登录或采集。</p><span id="more"></span><h2 id="一、适用场景与边界"><a href="#一、适用场景与边界" class="headerlink" title="一、适用场景与边界"></a>一、适用场景与边界</h2><p>它适合收集公开技术文章、产品文档、Hacker News 讨论，以及自己有权访问的网页资料。底层使用随技能分发的 <code>baoyu-fetch</code>，经 Chrome DevTools Protocol 控制浏览器，按网站选择适配器。普通网页走 Defuddle 提取，必要时使用 Readability；X、YouTube、Hacker News 有专门逻辑。</p><p>这不是“所有网址都能完整下载”的承诺。视频页面可能只有描述而没有字幕；登录页可能以 HTTP 成功状态返回；懒加载文章可能只抓到前半段。原页面的互动组件、复杂表格、动态附件也不一定能完整搬到 Markdown。</p><p>遇到登录、验证码或订阅内容，应由用户在可见浏览器中完成正常授权，再采集已获准访问的内容。等待交互不是绕过访问控制，更不能用来获取无权查看的付费正文。网页里的提示词也只是素材，不应成为 Agent 执行其他操作的指令。</p><h2 id="二、前置依赖与按需安装"><a href="#二、前置依赖与按需安装" class="headerlink" title="二、前置依赖与按需安装"></a>二、前置依赖与按需安装</h2><p>需要能读取文件和调用终端的 Agent、Bun，以及可用的 Chrome/Chromium。此技能的运行说明优先要求直接使用 Bun，不应把其他技能的 <code>npx -y bun</code> 回退机械套用到这里。</p><p>仓库 README 提供安装入口，本系列另核实 skills CLI 1.7.2 的按需选择参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-url-to-markdown --agent codex</span><br></pre></td></tr></table></figure><p>按需选择 <code>baoyu-url-to-markdown</code>，不要为保存一篇文章把整个工具集全部加载。如果安装器当前版本不提供所需选择方式，可采用 README 描述的项目级安装：将这个技能的完整目录复制或链接到项目 <code>.agents/skills/</code>，不要只复制 <code>SKILL.md</code>。</p><p>上述命令面向仓库当前版本，最新安装不等于固定快照。要复现本文，应使用提交 <code>1567581c26ec29f4216c6e6835415bf30343b0e3</code> 对应的完整技能目录。源码明确要求：如果该目录下 <code>scripts/node_modules</code> 不存在，再安装第三方运行依赖。以下 <code>&#123;baseDir&#125;</code> 必须替换为实际技能目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bun install --cwd <span class="string">&quot;&#123;baseDir&#125;/scripts&quot;</span></span><br></pre></td></tr></table></figure><p><code>baoyu-fetch</code> 源码已在 <code>scripts/lib</code>，无需另找同名全局包。Windows 路径包含空格时应保留引号；浏览器找不到时让 Agent 指向实际安装位置，而不是猜一个固定路径。</p><h2 id="三、输入、输出与目录规划"><a href="#三、输入、输出与目录规划" class="headerlink" title="三、输入、输出与目录规划"></a>三、输入、输出与目录规划</h2><p>输入是完整 URL，以及可选的输出位置、媒体策略、交互要求。默认 CLI 将 Markdown 写到标准输出；技能工作流则由 Agent 生成路径，再明确传给工具。两者不要混淆。</p><p>按 SKILL 的详细路径算法，每个网页拥有独立目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">url-to-markdown/</span><br><span class="line">  example.com/</span><br><span class="line">    article-topic/</span><br><span class="line">      article-topic.md</span><br><span class="line">      imgs/</span><br><span class="line">      videos/</span><br></pre></td></tr></table></figure><p>首次设置里的简略路径展示没有体现这层独立目录，应以详细算法为准。遇到冲突，Agent 为目录及文件加时间戳，避免覆盖旧资料。启用媒体下载后，图片与视频保存到正文旁边的子目录，并改写为相对路径；不下载则保留远程链接。</p><p>这对 Obsidian 很方便，但搬到 Hexo 时要同时迁移资源目录，并核对站点资源规则。采集文件不是自动可发布的文章：作者、来源、转载权限、摘要和发布元数据仍需要人工处理，不能把资料库一股脑公开。</p><h2 id="四、可复制的自然语言-Agent-请求"><a href="#四、可复制的自然语言-Agent-请求" class="headerlink" title="四、可复制的自然语言 Agent 请求"></a>四、可复制的自然语言 Agent 请求</h2><p>把方括号内容替换成自己的资料即可，不要把占位文字当作网址运行。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-url-to-markdown 保存这个网页：[完整URL]。</span><br><span class="line">这是我有权访问的资料，只保存正文和必要来源信息。</span><br><span class="line">先读取技能配置；没有配置时先问我媒体策略、输出目录和保存范围。</span><br><span class="line">目标目录是 [本地资料目录]，每个网页单独建文件夹。</span><br><span class="line">如果遇到登录或验证码，停下来让我在可见浏览器中正常处理。</span><br><span class="line">先检查 Markdown 是否包含真实正文，再询问是否下载远程媒体。</span><br><span class="line">完成后报告正文路径、资源目录、缺失内容和质量检查结果，不发布。</span><br></pre></td></tr></table></figure><p>已有不完整结果时，可以继续说：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">刚才保存的内容疑似只有导航。请检查标题、正文长度和登录提示，</span><br><span class="line">必要时改用可见浏览器等待我确认正文加载完毕；不要把退出码为零当成成功。</span><br><span class="line">保留旧结果，只有确认新结果更完整后再交付。</span><br></pre></td></tr></table></figure><h2 id="五、操作案例：收集一篇技术长文"><a href="#五、操作案例：收集一篇技术长文" class="headerlink" title="五、操作案例：收集一篇技术长文"></a>五、操作案例：收集一篇技术长文</h2><p>假设要把浏览器里正在阅读的一篇公开数据库文章存为研究资料，里面含两张示意图片和若干代码块。下面演示操作顺序，不声称已经抓取某篇真实页面。</p><ol><li><strong>先固定输入。</strong> 提供文章 URL、资料目录，并说明只做阅读归档，不转载。Agent 检查配置与运行依赖。</li><li><strong>执行初次提取。</strong> 普通公开页先尝试默认无头模式。适配器负责正文抽取，但 Agent 必须读回输出，检查标题是否是目标文章而不是网站首页。</li><li><strong>对照关键段落。</strong> 你指出文章应有“索引选择”和“执行计划”两部分。若输出只含第一节，不能交付为完整文章，需要检查懒加载、展开按钮或页面加载时机。</li><li><strong>必要时恢复。</strong> 自动识别到交互门槛时使用交互等待；需要滚动或复杂操作时使用强制等待，由用户确认页面准备好。源码支持这两种模式，但不保证任何站点都能自动判断完成。</li><li><strong>单独决定媒体。</strong> 正文确认合格后再问是否下载图片。选择下载时，重跑媒体本地化会改写同一份输出中的链接，应检查两张图均存在，而不是只看目录是否创建。</li><li><strong>交付可审计资料。</strong> 报告原始 URL、正文位置、图片结果和仍缺失的内容。将个人阅读笔记另写在独立文章里，保留采集正文作为来源，不在原文里混入模型总结。</li></ol><h2 id="六、配置如何影响执行"><a href="#六、配置如何影响执行" class="headerlink" title="六、配置如何影响执行"></a>六、配置如何影响执行</h2><p><code>EXTEND.md</code> 按以下顺序查找，第一个命中生效，而不是把三处文件合并：</p><ol><li>项目 <code>.baoyu-skills/baoyu-url-to-markdown/EXTEND.md</code>。</li><li><code>$&#123;XDG_CONFIG_HOME:-$HOME/.config&#125;/baoyu-skills/baoyu-url-to-markdown/EXTEND.md</code>。</li><li><code>$HOME/.baoyu-skills/baoyu-url-to-markdown/EXTEND.md</code>。</li></ol><p>可用的核心设置是：</p><figure class="highlight yaml"><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">download_media:</span> <span class="string">ask</span></span><br><span class="line"><span class="attr">default_output_dir:</span> <span class="string">./research-sources/</span></span><br></pre></td></tr></table></figure><p><code>ask</code> 表示每次发现远程媒体后再问；<code>1</code> 表示下载；<code>0</code> 表示保留远程地址。首次没有配置时必须询问，不能静默写默认值。输出目录是 Agent 构造路径的依据，不是某个与配置键同名的 CLI 参数。</p><p><code>BAOYU_CHROME_PROFILE_DIR</code> 可指定浏览器用户数据目录。它可能含登录状态，应放在私有位置，不要进入公开仓库或云同步文章目录。调试产物可能包括页面 HTML、网络记录与正文，同样需要按敏感数据管理。</p><h2 id="七、验收清单"><a href="#七、验收清单" class="headerlink" title="七、验收清单"></a>七、验收清单</h2><ul><li>[ ] 标题、作者及正文主题与目标页面一致。</li><li>[ ] 不是登录、订阅、错误页或只含页脚的外壳。</li><li>[ ] 关键章节、代码、列表和引用没有明显缺失。</li><li>[ ] 下载的图片、视频可从相对路径实际打开。</li><li>[ ] 不下载媒体时已说明离线阅读不完整。</li><li>[ ] 同名文件未被无提示覆盖，来源 URL 可追溯。</li><li>[ ] 在 Obsidian 预览正常；迁入 Hexo 后另做链接与资源检查。</li><li>[ ] 未把浏览器配置、Cookie、网络调试记录放进发布目录。</li></ul><h2 id="八、常见排错"><a href="#八、常见排错" class="headerlink" title="八、常见排错"></a>八、常见排错</h2><p><strong>进程成功，但正文很短。</strong> 优先检查质量，不要立即增加超时。源码特别列出 <code>Application error</code>、通用错误页、登录壳和原始框架数据等失败信号。</p><p><strong>浏览器找不到。</strong> 核实本机浏览器路径，再让 Agent 使用源码支持的浏览器路径选项。不要为这件事默认安装一套新的自动化浏览器。</p><p><strong>YouTube 只保存了描述。</strong> 可能没有可用字幕或播放受到限制；需要字幕缓存和 SRT 时改用专门的字幕技能，而不是把描述称为完整转录。</p><p><strong>图片仍是网络地址。</strong> 先确认媒体策略和本地文件写入结果。媒体下载要求明确输出文件；只把正文输出到终端不会形成可靠的资源包。</p><h2 id="九、相关技能与固定来源"><a href="#九、相关技能与固定来源" class="headerlink" title="九、相关技能与固定来源"></a>九、相关技能与固定来源</h2><ul><li><a href="baoyu-youtube-transcript.md">YouTube 字幕提取</a>：需要字幕语言选择、缓存或 SRT 时使用。</li><li><a href="baoyu-danger-x-to-markdown.md">X 内容归档</a>：专门的 X 接口流程，需要额外风险同意。</li><li><a href="baoyu-translate.md">文章翻译</a>：正文确认完整后再翻译。</li><li><a href="baoyu-format-markdown.md">Markdown 排版</a>：整理自有文章的展示结构。</li></ul><p>固定源码提交：<code>1567581c26ec29f4216c6e6835415bf30343b0e3</code>。</p><ul><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-url-to-markdown/SKILL.md">技能说明</a></li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-url-to-markdown/references/quality-gate.md">质量检查与恢复规则</a></li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-url-to-markdown/references/adapters.md">适配器与媒体规则</a></li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/README.zh.md">安装说明</a></li></ul>]]></content>
    
    
    <summary type="html">从网页正文提取、登录交互到图片本地化，建立可复查的Markdown资料采集流程。</summary>
    
    
    
    
    <category term="AI" scheme="https://www.shallowrecall.top/tags/AI/"/>
    
    <category term="Agent Skills" scheme="https://www.shallowrecall.top/tags/Agent-Skills/"/>
    
    <category term="宝玉Skills" scheme="https://www.shallowrecall.top/tags/%E5%AE%9D%E7%8E%89Skills/"/>
    
  </entry>
  
  <entry>
    <title>宝玉 Skills 实战教程：从安装到搭建 AI 内容工作流</title>
    <link href="https://www.shallowrecall.top/posts/93615e71.html"/>
    <id>https://www.shallowrecall.top/posts/93615e71.html</id>
    <published>2026-10-15T11:30:00.000Z</published>
    <updated>2026-10-15T13:00:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>写完一篇技术文章，真正耗时的部分往往还没结束：整理标题、调 Markdown 格式、做封面、画解释图、压缩图片，再分别准备博客、公众号和分享材料。</p><p><a href="https://github.com/JimLiu/baoyu-skills">宝玉的 baoyu-skills</a> 把这些重复工作整理成了 Agent 能读取并执行的技能。你给它文章和约束，它根据技能里的流程、参考资料与脚本完成任务。最值得学会的，不是记住二十多个名字，而是把适合自己的几个技能串起来。</p><p>这篇教程面向已经能使用 Codex 或 Claude Code、但还没有形成 Skills 工作习惯的读者。完成前半部分，你就能安装一个技能并验证输出；继续往下，可以把同一篇文章加工成博客稿、封面、知识卡片和分享幻灯片。</p><blockquote><p><strong>版本说明</strong>：核对日期为 2026 年 10 月 15 日。本文基于仓库提交 <a href="https://github.com/JimLiu/baoyu-skills/tree/1567581c26ec29f4216c6e6835415bf30343b0e3">1567581c26</a>，插件市场清单版本为 2.5.2。实际执行了技能发现命令并核对脚本与配置；图片生成及社交发布案例是操作示例，不代表已经替读者调用付费接口或发布内容。</p></blockquote><span id="more"></span><style>#article-container img[src*="/img/posts/baoyu-skills-tutorial/"] {  max-width: 100%;  height: auto;}</style><h2 id="一、先理解三个角色：Agent、Skill、工具"><a href="#一、先理解三个角色：Agent、Skill、工具" class="headerlink" title="一、先理解三个角色：Agent、Skill、工具"></a>一、先理解三个角色：Agent、Skill、工具</h2><p>假设你对 AI 说：“把这篇文章做成一套小红书知识卡片。”</p><p>只有一句提示词时，AI 需要临时决定怎么分段、选什么风格、每张卡写多少字、把文件放在哪里。Skill 则把这些约定提前写好了，例如先分析内容、给出大纲、固定视觉风格、保存提示词，再生成图片。</p><div class="table-container"><table><thead><tr><th>角色</th><th>在这项任务里负责什么</th></tr></thead><tbody><tr><td>Agent，例如 Codex、Claude Code</td><td>理解要求，读取文件，选择和执行技能</td></tr><tr><td>Skill，例如 baoyu-xhs-images</td><td>规定分析、拆页、选风格、生成和交付的流程</td></tr><tr><td>工具或后端，例如脚本、浏览器、图片 API</td><td>执行文件转换、读取网页、生成图片等具体操作</td></tr></tbody></table></div><p>一个技能通常不只是单独的提示词文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">baoyu-format-markdown/</span><br><span class="line">├── SKILL.md       # 什么时候使用，以及执行步骤</span><br><span class="line">├── references/    # 按需读取的规则、示例与配置说明</span><br><span class="line">└── scripts/       # 需要实际运行的辅助程序</span><br></pre></td></tr></table></figure><p>因此，安装时要保留整个技能目录。只复制 SKILL.md，可能会在执行到脚本或参考文件时失败。</p><p>还有一个常见误解：安装 Skills 并不等于给模型增加了新接口权限。没有浏览器访问能力，它就不能自动操作网页；没有可用图片后端，配图流程也不会凭空生成图片。</p><p>Codex 对 Skills 采用渐进式加载：先读取名称和描述等元信息，使用时再加载详细说明。因此，“装得越多，每次都把所有 SKILL.md 全文塞进上下文”并不准确；但技能过多仍会增加发现和选择负担。入门阶段按需安装更容易定位问题。参见 <a href="https://developers.openai.com/codex/skills/">Codex 官方 Skills 文档</a>。</p><h2 id="二、先挑技能，不急着全装"><a href="#二、先挑技能，不急着全装" class="headerlink" title="二、先挑技能，不急着全装"></a>二、先挑技能，不急着全装</h2><p>下面按任务分组，重点看“最后拿到什么”。每个技能都有一篇独立教程，点击技能名即可阅读完整的前置条件、操作案例、配置和验收方法。</p><p><strong>阅读导航</strong>：这篇总教程负责安装与组合工作流，下面的 21 篇负责单项技能实战。在 Obsidian 中，表格使用相对 Markdown 链接打开本地笔记；发布到 Hexo 时，发布器会把链接转换为目标文章的固定地址。两端共用一份原稿，不需要手工维护两套正文。</p><div class="table-container"><table><thead><tr><th>你想完成的事</th><th>独立教程</th><th>主要产物与边界</th></tr></thead><tbody><tr><td>收藏网页素材</td><td><a href="宝玉Skills/baoyu-url-to-markdown.md">baoyu-url-to-markdown</a></td><td>网页正文 Markdown；动态页面和登录页需要浏览器与合法访问权限</td></tr><tr><td>提取视频字幕</td><td><a href="宝玉Skills/baoyu-youtube-transcript.md">baoyu-youtube-transcript</a></td><td>字幕、时间戳与封面；字幕缺失不等于自动语音转写</td></tr><tr><td>保存 X 帖子与长文</td><td><a href="宝玉Skills/baoyu-danger-x-to-markdown.md">baoyu-danger-x-to-markdown</a></td><td>Markdown 与媒体；逆向接口需知情同意并可能失效</td></tr><tr><td>总结微信群讨论</td><td><a href="宝玉Skills/baoyu-wechat-summary.md">baoyu-wechat-summary</a></td><td>群聊摘要与增量记录；依赖 wx-cli，注意参与者隐私</td></tr><tr><td>翻译技术文章</td><td><a href="宝玉Skills/baoyu-translate.md">baoyu-translate</a></td><td>翻译稿；quick、normal、refined 对应不同深度</td></tr><tr><td>整理已有文章</td><td><a href="宝玉Skills/baoyu-format-markdown.md">baoyu-format-markdown</a></td><td>排版后的 Markdown；不替代事实核查和内容创作</td></tr><tr><td>生成带样式 HTML</td><td><a href="宝玉Skills/baoyu-markdown-to-html.md">baoyu-markdown-to-html</a></td><td>带样式的 HTML；不是 Hexo 部署器</td></tr><tr><td>给正文配图</td><td><a href="宝玉Skills/baoyu-article-illustrator.md">baoyu-article-illustrator</a></td><td>配图计划、提示词、插图及文图整合</td></tr><tr><td>制作文章封面</td><td><a href="宝玉Skills/baoyu-cover-image.md">baoyu-cover-image</a></td><td>按主题与视觉风格生成封面；需要可用图片后端</td></tr><tr><td>把知识讲成漫画</td><td><a href="宝玉Skills/baoyu-comic.md">baoyu-comic</a></td><td>角色设定、分镜和漫画页面；先确认故事与视觉一致性</td></tr><tr><td>画技术结构图</td><td><a href="宝玉Skills/baoyu-diagram.md">baoyu-diagram</a></td><td>自包含 SVG；适合需要清晰关系和准确标签的图解</td></tr><tr><td>做知识信息图</td><td><a href="宝玉Skills/baoyu-infographic.md">baoyu-infographic</a></td><td>布局与风格驱动的信息图；生成后核对文字和数据</td></tr><tr><td>做小红书卡片</td><td><a href="宝玉Skills/baoyu-xhs-images.md">baoyu-xhs-images</a></td><td>一组图片卡片；生成图片不等于发布到小红书</td></tr><tr><td>做分享幻灯片</td><td><a href="宝玉Skills/baoyu-slide-deck.md">baoyu-slide-deck</a></td><td>逐页图片及合成的 PPTX、PDF；不要假设所有元素可编辑</td></tr><tr><td>调用图片模型</td><td><a href="宝玉Skills/baoyu-image-gen.md">baoyu-image-gen</a></td><td>通过配置的提供商或后端生成图片，费用取决于服务</td></tr><tr><td>使用 Gemini 网页后端</td><td><a href="宝玉Skills/baoyu-danger-gemini-web.md">baoyu-danger-gemini-web</a></td><td>文字与图片；非官方网页接口，需要同意并承担兼容性限制</td></tr><tr><td>优化图片体积</td><td><a href="宝玉Skills/baoyu-compress-image.md">baoyu-compress-image</a></td><td>WebP、PNG 等文件；注意保留原图与透明通道</td></tr><tr><td>分析授权 Electron 应用</td><td><a href="宝玉Skills/baoyu-electron-extract.md">baoyu-electron-extract</a></td><td>提取资源及源码恢复结果；不保证恢复原项目，不用于绕过保护</td></tr><tr><td>送入公众号流程</td><td><a href="宝玉Skills/baoyu-post-to-wechat.md">baoyu-post-to-wechat</a></td><td>草稿或浏览器操作；进入草稿箱不等于正式群发</td></tr><tr><td>准备微博内容</td><td><a href="宝玉Skills/baoyu-post-to-weibo.md">baoyu-post-to-weibo</a></td><td>填入编辑器的微博或头条文章；最终由用户审核发布</td></tr><tr><td>发布 X 帖子与长文</td><td><a href="宝玉Skills/baoyu-post-to-x.md">baoyu-post-to-x</a></td><td>平台内容和发布操作；需对应账号、权限与最终提交授权</td></tr></tbody></table></div><p>上述范围是核对版本中的 21 个 <code>baoyu-*</code> 技能，不包含用于维护技能仓库本身的 <code>release-skills</code>。完整目录见 <a href="https://github.com/JimLiu/baoyu-skills/tree/1567581c26ec29f4216c6e6835415bf30343b0e3/skills">仓库技能目录</a>。</p><p><strong>我的起步建议</strong>：第一天只安装 baoyu-format-markdown。等它能正确读取文件、生成结果，再加封面或网页采集技能。一次只增加一个依赖，排错会轻松很多。</p><h2 id="三、安装：分清终端命令和聊天指令"><a href="#三、安装：分清终端命令和聊天指令" class="headerlink" title="三、安装：分清终端命令和聊天指令"></a>三、安装：分清终端命令和聊天指令</h2><p>本文中的代码块分为两类：</p><ul><li>标为 bash 或 powershell 的命令，在电脑终端执行。</li><li>标为 text 的技能请求，发送给 Codex 或 Claude Code。</li></ul><p>尤其不要把 /plugin 或 /baoyu-format-markdown 当成操作系统命令粘贴到 PowerShell。</p><h3 id="3-1-检查基础环境"><a href="#3-1-检查基础环境" class="headerlink" title="3.1 检查基础环境"></a>3.1 检查基础环境</h3><p>仓库 README 要求已安装 Node.js，并能运行 npx bun。先执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">node --version</span><br><span class="line">npm --version</span><br><span class="line">npx --<span class="built_in">yes</span> bun --version</span><br></pre></td></tr></table></figure><p>三个命令都输出版本号后，再往下走。初次运行 npx 可能需要下载依赖；连接失败和“技能写错了”是两类问题。</p><p>baoyu-url-to-markdown 的具体技能说明要求 Bun 可执行文件。准备使用它时，再检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bun --version</span><br></pre></td></tr></table></figure><p>若没有安装，可以按 <a href="https://bun.sh/docs/installation">Bun 官方安装文档</a> 配置。其他技能究竟使用 bun 还是 npx -y bun，应以各自 SKILL.md 为准，不要假设整套技能的依赖完全一致。</p><h3 id="3-2-通用方式：使用-skills-CLI-按需安装"><a href="#3-2-通用方式：使用-skills-CLI-按需安装" class="headerlink" title="3.2 通用方式：使用 skills CLI 按需安装"></a>3.2 通用方式：使用 skills CLI 按需安装</h3><p>先进入准备写文章的项目目录，再列出可发现的技能：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --list</span><br></pre></td></tr></table></figure><p>这一步只列目录，不安装技能。本次核对时，CLI 发现了 22 项，其中 21 项是 baoyu-* 内容及工具技能，另外还发现了仓库开发使用的 release-skills。列表比 README 中的“20+”多一项，不代表安装失败。</p><p>只为当前项目的 Codex 安装排版技能：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-format-markdown --agent codex</span><br></pre></td></tr></table></figure><p>希望多个项目都能使用，加上全局参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-format-markdown --agent codex --global</span><br></pre></td></tr></table></figure><p>若要给 Claude Code 安装，同一位置的 Agent 名称使用 claude-code：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-format-markdown --agent claude-code</span><br></pre></td></tr></table></figure><p>以后按需安装多个技能，可以在同一个 —skill 后列出多个名称：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills add jimliu/baoyu-skills --skill baoyu-cover-image baoyu-article-illustrator baoyu-compress-image --agent codex</span><br></pre></td></tr></table></figure><p>检查安装结果：</p><figure class="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">npx skills list --agent codex</span><br><span class="line">npx skills list --global --agent codex</span><br></pre></td></tr></table></figure><p>第一条看项目级，第二条看全局。<strong>安装在哪个范围，就到哪个范围检查。</strong> 不加 —global 时，默认是项目级安装。</p><p>以上参数对照了 <a href="https://github.com/vercel-labs/skills">skills CLI 说明</a>，并在本次写作环境的 1.7.2 版本帮助输出中核实。将来行为变化时，以 npx skills —help 为准。</p><h3 id="3-3-Claude-Code-用户：也可以通过插件市场安装"><a href="#3-3-Claude-Code-用户：也可以通过插件市场安装" class="headerlink" title="3.3 Claude Code 用户：也可以通过插件市场安装"></a>3.3 Claude Code 用户：也可以通过插件市场安装</h3><p>在 Claude Code 会话中执行：</p><figure class="highlight text"><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">/plugin marketplace add JimLiu/baoyu-skills</span><br><span class="line">/plugin install baoyu-skills@baoyu-skills</span><br></pre></td></tr></table></figure><p>这是 <strong>Claude Code 的插件命令</strong>，不是所有 Agent 共用的指令。当前仓库的市场清单只提供一个 baoyu-skills 插件，里面注册多个技能，不要照旧教程寻找已经变化的插件分组。</p><p>按需安装更适合精简项目；统一插件更适合希望集中管理整套技能的用户。选一条路径即可，避免同一技能在多个入口重复注册。依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/.claude-plugin/marketplace.json">当前插件市场清单</a>。</p><h3 id="3-4-Codex-用户：项目目录应该长什么样"><a href="#3-4-Codex-用户：项目目录应该长什么样" class="headerlink" title="3.4 Codex 用户：项目目录应该长什么样"></a>3.4 Codex 用户：项目目录应该长什么样</h3><p>官方文档列出的项目级目录是 .agents/skills；用户级目录是 ~/.agents/skills。一个最小项目可以是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">writing-lab/</span><br><span class="line">├── .agents/</span><br><span class="line">│   └── skills/</span><br><span class="line">│       └── baoyu-format-markdown/</span><br><span class="line">│           ├── SKILL.md</span><br><span class="line">│           ├── references/</span><br><span class="line">│           └── scripts/</span><br><span class="line">└── article.md</span><br></pre></td></tr></table></figure><p>Windows PowerShell 下，可以这样检查项目级入口：</p><figure class="highlight powershell"><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">Test-Path</span> <span class="string">&quot;.\.agents\skills\baoyu-format-markdown\SKILL.md&quot;</span></span><br></pre></td></tr></table></figure><p>应返回 True。不同安装方式可能使用软链接或复制文件，关键是 Agent 能看到完整目录。</p><p>文件存在但会话仍然找不到技能时，先检查是否打开了正确项目，再新建会话或重启客户端重试。Codex 还支持通过 $技能名显式选择技能；自然语言中直接点名也便于排查。依据：<a href="https://developers.openai.com/codex/skills/">Codex Skills 的发现与调用机制</a>。</p><h2 id="四、第一个练习：用一篇短文验证安装"><a href="#四、第一个练习：用一篇短文验证安装" class="headerlink" title="四、第一个练习：用一篇短文验证安装"></a>四、第一个练习：用一篇短文验证安装</h2><p>先在项目中创建 article.md，放入下面这段练习素材。它只是排版练习，不需要联网或图片 API。</p><figure class="highlight markdown"><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="section"># 我的第一篇 AI 工作流笔记</span></span><br><span class="line"></span><br><span class="line">我准备把写文章分成三个阶段：收集资料、整理正文、检查发布结果。</span><br><span class="line"></span><br><span class="line">收集资料时保存原始链接，整理正文时统一标题和列表，发布前检查代码块与图片。</span><br><span class="line"></span><br><span class="line">几个约定：</span><br><span class="line"><span class="bullet">-</span> 原始素材放到 sources 目录</span><br><span class="line"><span class="bullet">-</span> 文章正文放到 article.md</span><br><span class="line"><span class="bullet">-</span> 图片放到 images 目录</span><br><span class="line"></span><br><span class="line">命令示例：node --version</span><br></pre></td></tr></table></figure><p>向 Agent 发送：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-format-markdown 处理当前目录的 article.md。</span><br><span class="line"></span><br><span class="line">保留原文事实和所有命令，只优化排版与明显错字。</span><br><span class="line">保留现有标题，自动选择合适的摘要。</span><br><span class="line">输出 article-formatted.md，保留 article.md。</span><br><span class="line">完成后告诉我生成了哪些文件。</span><br></pre></td></tr></table></figure><p>Claude Code 用户也可以按仓库示例使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/baoyu-format-markdown article.md</span><br></pre></td></tr></table></figure><p>第一次调用可能出现格式优化选项；明确要求“优化排版，保留源文件”即可。技能的流程包括内容分析、结构处理和机械排版脚本，不只是运行一个 formatter。</p><h3 id="怎么判断它真的成功了"><a href="#怎么判断它真的成功了" class="headerlink" title="怎么判断它真的成功了"></a>怎么判断它真的成功了</h3><p>不要只看聊天里那句“已完成”，检查文件：</p><ol><li>article.md 仍然存在，原始素材没有被覆盖。</li><li>article-formatted.md 真实存在，标题、列表和命令仍然完整。</li><li>frontmatter、正文层级、代码格式能正常显示。</li><li>如果执行了完整分析流程，可能还有 article-analysis.md。</li></ol><p><strong>一个重要区别</strong>：技能工作流会安排输出文件；直接运行其 scripts/main.ts 是机械排版入口，会处理传入的文件。不要把脚本直接作用于唯一原稿，然后期待它自动完成备份、标题选择和内容分析。</p><p>想补充事实、重写论证或增加案例，应另提写作要求。该技能本身要求保留内容，不能因为排版精致就把未经核实的新内容混进去。依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-format-markdown/SKILL.md">baoyu-format-markdown 的实际流程</a>。</p><h2 id="五、把第一篇文章扩展成内容工作流"><a href="#五、把第一篇文章扩展成内容工作流" class="headerlink" title="五、把第一篇文章扩展成内容工作流"></a>五、把第一篇文章扩展成内容工作流</h2><p>接下来用一篇“Git 常见操作”文章做例子。假设你已经有 article.md，所有示例都围绕这份原稿进行。</p><h3 id="5-1-收集资料：保留来源，再整理内容"><a href="#5-1-收集资料：保留来源，再整理内容" class="headerlink" title="5.1 收集资料：保留来源，再整理内容"></a>5.1 收集资料：保留来源，再整理内容</h3><p>安装 baoyu-url-to-markdown 后，可以这样请求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-url-to-markdown，把 https://git-scm.com/docs/git-status</span><br><span class="line">保存为本地 Markdown 素材，保留原始来源地址。</span><br><span class="line">不下载页面媒体，输出到 sources 目录。</span><br></pre></td></tr></table></figure><p>这个技能底层使用基于 Chrome CDP 的 baoyu-fetch，并针对部分站点提供适配器。首次运行会配置媒体处理和默认输出目录。登录或验证码页面可能需要用户完成交互，技能并不保证所有网站都能自动读取。</p><p>素材文件应保留原文链接、采集日期和引用边界。把网页保存下来不等于完成事实核查，更不等于可以全文转载。写原创教程时，提炼与本次主题相关的知识，再用自己的案例验证。</p><p>依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-url-to-markdown/SKILL.md">URL 转 Markdown 技能说明</a>。</p><h3 id="5-2-整理稿件：把内容写作和排版分成两个步骤"><a href="#5-2-整理稿件：把内容写作和排版分成两个步骤" class="headerlink" title="5.2 整理稿件：把内容写作和排版分成两个步骤"></a>5.2 整理稿件：把内容写作和排版分成两个步骤</h3><p>先让 Agent 根据你提供的素材与验证记录完成文章，再调用 baoyu-format-markdown。这样可以分别检查“内容有没有问题”和“排版是否清楚”。</p><p>一条比“帮我优化一下”更可靠的请求是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-format-markdown 整理 article.md。</span><br><span class="line">保留所有 Git 命令，不改写参数，不新增未经验证的结论。</span><br><span class="line">统一二级、三级标题，把操作顺序改为有序列表。</span><br><span class="line">保留原稿，输出 article-formatted.md。</span><br></pre></td></tr></table></figure><p>命令类教程尤其要保护代码块。排版后的文章更好看，但读者复制命令能不能执行，仍需单独检查。</p><h3 id="5-3-封面和插图：先决定图片要解释什么"><a href="#5-3-封面和插图：先决定图片要解释什么" class="headerlink" title="5.3 封面和插图：先决定图片要解释什么"></a>5.3 封面和插图：先决定图片要解释什么</h3><p>封面负责表达主题，正文插图负责帮助理解。先确定用途，才值得花时间生成。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-cover-image 为 article-formatted.md 生成一张 16:9 封面。</span><br><span class="line">主题是 Git 常见操作，采用 blueprint 风格，不在图片里放长段文字。</span><br><span class="line">先读取已有偏好和可用图片后端，再按技能流程执行。</span><br></pre></td></tr></table></figure><p>正文配图可以这样写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-article-illustrator 分析 article-formatted.md。</span><br><span class="line">只选择最多 3 个值得配图的位置，优先解释工作区、暂存区与提交之间的关系。</span><br><span class="line">先给出位置和每张图要说明的内容，整组保持相同视觉风格。</span><br></pre></td></tr></table></figure><p>baoyu-cover-image 使用类型、配色、渲染、文字与氛围等维度；baoyu-article-illustrator 会保存插图计划和提示词，再调用可用图片后端。两者本身并不是免费图片服务。是否需要 API Key、使用哪个模型，取决于当前 Agent 能用的后端。</p><p>下图为本文新制作的工作流示意图，先明确每个技能处理的输入、输出和验收位置，再决定是否需要额外配图。它不是官方界面截图，也不是已经执行的任务记录。</p><p><img src="hexo-publish/assets/baoyu-skills-tutorial/diagram/baoyu-workflow-map.webp" alt="宝玉 Skills 从资料采集到博客构建的工作流"></p><p>技能依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-cover-image/SKILL.md">封面</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-article-illustrator/SKILL.md">正文配图</a>。</p><h3 id="5-4-技术图、知识卡片、幻灯片怎么选"><a href="#5-4-技术图、知识卡片、幻灯片怎么选" class="headerlink" title="5.4 技术图、知识卡片、幻灯片怎么选"></a>5.4 技术图、知识卡片、幻灯片怎么选</h3><p>如果要解释精确的连接关系，用 baoyu-diagram 生成 SVG；需要面向社交平台的视觉化知识总结，考虑 baoyu-xhs-images 或 baoyu-infographic。</p><p>Claude Code 调用示例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">/baoyu-diagram &quot;Git 工作区、暂存区和提交历史的关系&quot; --type structural --lang zh</span><br><span class="line">/baoyu-xhs-images article-formatted.md --style notion --layout list</span><br><span class="line">/baoyu-slide-deck article-formatted.md --style chalkboard --slides 8 --outline-only</span><br></pre></td></tr></table></figure><p>在 Codex 中，用自然语言点名技能，并保留相同输入、风格和输出要求即可。</p><p>不同视觉产物服务于不同阅读方式。下图为本文独立绘制的选型说明：精确关系适合技术图，连续阅读适合知识卡片，逐页讲述适合幻灯片；它不模仿具体平台界面。</p><p><img src="hexo-publish/assets/baoyu-skills-tutorial/diagram/baoyu-visual-selector.webp" alt="技术图、知识卡片与幻灯片的用途对比"></p><p>幻灯片先用 —outline-only 检查结构，确认每页要讲什么，再生成图片。该技能的 PPTX 合成代码把页面图片放进幻灯片，<strong>不能把输出的 PPTX 理解为所有文字、图表都可独立编辑的原生演示文稿</strong>。需要后续逐字编辑时，应提前改用适合生成原生幻灯片元素的工具。</p><p>来源：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-xhs-images/SKILL.md">卡片技能</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-slide-deck/SKILL.md">幻灯片技能</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-slide-deck/scripts/merge-to-pptx.ts">PPTX 合成实现</a>。</p><h3 id="5-5-图片压缩：明确要求保留原图"><a href="#5-5-图片压缩：明确要求保留原图" class="headerlink" title="5.5 图片压缩：明确要求保留原图"></a>5.5 图片压缩：明确要求保留原图</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-compress-image 压缩 images 目录。</span><br><span class="line">输出 WebP，质量 80，递归处理，并保留原图。</span><br><span class="line">报告压缩前后体积，检查最终图片能否打开。</span><br></pre></td></tr></table></figure><p>对需要手工调用脚本的情况，下面是 <strong>Codex 项目级安装后的 PowerShell 示例</strong>：</p><figure class="highlight powershell"><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="variable">$skillDir</span> = <span class="built_in">Join-Path</span> (<span class="built_in">Get-Location</span>) <span class="string">&quot;.agents\skills\baoyu-compress-image&quot;</span></span><br><span class="line">npx <span class="literal">-y</span> bun <span class="string">&quot;<span class="variable">$skillDir</span>/scripts/main.ts&quot;</span> <span class="string">&quot;.\images&quot;</span> <span class="literal">--recursive</span> <span class="literal">--quality</span> <span class="number">80</span> <span class="literal">--keep</span></span><br></pre></td></tr></table></figure><p>这里的 —keep 很重要：当前技能默认 keep 为 false。脚本也会选择可用压缩器，缺少依赖时应按实际错误补齐。不要只看压缩比例，还要检查代码截图的小字、细线和透明背景是否受损。依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-compress-image/SKILL.md">图片压缩参数</a>。</p><h2 id="六、配置一次：EXTEND-md-和-env-各管什么"><a href="#六、配置一次：EXTEND-md-和-env-各管什么" class="headerlink" title="六、配置一次：EXTEND.md 和 .env 各管什么"></a>六、配置一次：EXTEND.md 和 .env 各管什么</h2><p>这两种文件容易混淆。记住：<strong>EXTEND.md 保存工作偏好，.env 保存接口环境变量和凭证。</strong></p><div class="table-container"><table><thead><tr><th>文件</th><th>适合放什么</th><th>示例</th></tr></thead><tbody><tr><td>EXTEND.md</td><td>输出语言、视觉风格、是否自动选择标题、图片后端等偏好</td><td>.baoyu-skills/baoyu-format-markdown/EXTEND.md</td></tr><tr><td>.env</td><td>API Key、服务地址等环境变量</td><td>.baoyu-skills/.env</td></tr></tbody></table></div><h3 id="6-1-最简单的-EXTEND-md"><a href="#6-1-最简单的-EXTEND-md" class="headerlink" title="6.1 最简单的 EXTEND.md"></a>6.1 最简单的 EXTEND.md</h3><p>如果每次排版都希望自动选标题和摘要，可以在项目中建立：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.baoyu-skills/baoyu-format-markdown/EXTEND.md</span><br></pre></td></tr></table></figure><p>内容：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">auto_select:</span> <span class="literal">true</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>这只是排版技能的配置字段。其他技能有自己的 schema，不能把 auto_select 复制到所有技能，就期待它们都跳过确认。</p><p>本文核对的多个 SKILL.md 约定先查项目目录，再查 XDG 配置目录，最后查用户主目录：</p><ol><li>项目/.baoyu-skills/技能名/EXTEND.md</li><li>XDG_CONFIG_HOME/baoyu-skills/技能名/EXTEND.md（未设置 XDG_CONFIG_HOME 时使用 ~/.config）</li><li>~/.baoyu-skills/技能名/EXTEND.md</li></ol><p>跨平台入门时优先使用项目级路径，容易看见，也更方便排错。</p><p>还要注意：这是技能工作流的查找约定，直接调用某个脚本时，脚本实现未必支持完全相同的搜索路径。例如本文版本的 image-gen 主脚本与 SKILL.md 在 XDG 路径处理上就有差异。<strong>直接跑脚本遇到配置不生效，先检查它真正读取的路径。</strong></p><p>来源：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-format-markdown/SKILL.md">排版技能配置</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-image-gen/scripts/main.ts">image-gen 主脚本</a>。</p><h3 id="6-2-图片生成先选后端，再配密钥"><a href="#6-2-图片生成先选后端，再配密钥" class="headerlink" title="6.2 图片生成先选后端，再配密钥"></a>6.2 图片生成先选后端，再配密钥</h3><p>建议先向 Agent 发这条请求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">请检查 baoyu-image-gen 当前支持的后端和我的可用环境。</span><br><span class="line">列出可选提供商、需要的凭证和配置文件位置。</span><br><span class="line">完成默认提供商、模型、比例的初始化后，再生成一张测试图。</span><br><span class="line">不要输出任何密钥值。</span><br></pre></td></tr></table></figure><p>这样做比照抄一年前的模型名称可靠。图片服务的型号、权限和计费会变化，不要把仓库里的默认模型当成你账户必然可用的模型。</p><p>如果选择 OpenAI API 路径，项目级 .env 的最小形态是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">OPENAI_API_KEY=replace-with-your-own-api-key</span><br></pre></td></tr></table></figure><p>这是占位示例。真正的密钥保留在本机，项目的 .gitignore 加上：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.baoyu-skills/.env</span><br></pre></td></tr></table></figure><p>baoyu-image-gen 的模型选择可受命令行 —model、EXTEND.md 的 default_model、环境变量及内置默认值影响。配置了多个位置时，先检查优先级，避免出现“改了环境变量却没有换模型”的误判。</p><p>该仓库还提供 codex-cli 路径，但 <strong>Codex/ChatGPT 登录态不是 OPENAI_API_KEY</strong>。需要 API Key 的路径和依赖 Codex 登录的路径应分别配置，不能互相替代凭证。当前运行环境若提供原生图片工具，也应先按技能的后端选择规则判断是否可用。</p><p>来源：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-image-gen/SKILL.md">image-gen 使用说明</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-image-gen/references/config/preferences-schema.md">偏好 schema</a>、<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-image-gen/references/codex-oauth-vs-openai-api-key.md">仓库对 OAuth 与 API Key 的说明</a>。</p><h2 id="七、最后一公里：博客、公众号和“已发布”不是同一件事"><a href="#七、最后一公里：博客、公众号和“已发布”不是同一件事" class="headerlink" title="七、最后一公里：博客、公众号和“已发布”不是同一件事"></a>七、最后一公里：博客、公众号和“已发布”不是同一件事</h2><h3 id="7-1-发布到-Hexo：继续使用-Markdown-源稿"><a href="#7-1-发布到-Hexo：继续使用-Markdown-源稿" class="headerlink" title="7.1 发布到 Hexo：继续使用 Markdown 源稿"></a>7.1 发布到 Hexo：继续使用 Markdown 源稿</h3><p>Hexo 本身会渲染 Markdown。因此，将 article-formatted.md 配上 Hexo frontmatter 后放到 source/_posts 即可，没有必要先转成公众号 HTML 再喂给 Hexo。</p><p>一个示例：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">我的</span> <span class="string">Git</span> <span class="string">工作流</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-10-15 19:30:00</span></span><br><span class="line"><span class="attr">tags:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Git</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">AI</span></span><br><span class="line"><span class="attr">categories:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">技术笔记</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">用具体案例理解工作区、暂存区和提交历史。</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>正文里的图片可以使用站点公开路径：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">![<span class="string">Git 工作区关系图</span>](<span class="link">/img/posts/git-workflow/worktree.webp</span>)</span><br></pre></td></tr></table></figure><p>对应文件应放到 Hexo 项目的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">source/img/posts/git-workflow/worktree.webp</span><br></pre></td></tr></table></figure><p>注意，cover 不是所有 Hexo 主题都通用的字段；是否显示封面要看当前主题。图片进入 source 目录后，也需要随站点一起生成和部署。</p><p>在已经安装依赖的 Hexo 项目根目录执行：</p><figure class="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">npx hexo generate</span><br><span class="line">npx hexo server</span><br></pre></td></tr></table></figure><p>本地检查完成后，按已有部署配置执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx hexo deploy</span><br></pre></td></tr></table></figure><p>其中 generate 负责生成静态文件，server 用于预览，deploy 使用你已配置的部署器。仓库没配置部署目标时，运行 deploy 不会自动替你开通托管平台。</p><p>如果你通过 Obsidian 写作，publish、package、note_type 等字段可能是自己的同步脚本约定，<strong>不是 Hexo 默认发布开关</strong>。这篇文章使用的就是“Obsidian 原稿 → 本地导出脚本 → Hexo 源文件 → 静态站点”的方式；换到其他博客时，应替换为该项目自己的发布流程。</p><p>依据：<a href="https://hexo.io/docs/writing">Hexo 写作</a>、<a href="https://hexo.io/docs/generating">生成</a>、<a href="https://hexo.io/docs/one-command-deployment">部署</a>。</p><h3 id="7-2-公众号：优先把-Markdown-直接交给发布技能"><a href="#7-2-公众号：优先把-Markdown-直接交给发布技能" class="headerlink" title="7.2 公众号：优先把 Markdown 直接交给发布技能"></a>7.2 公众号：优先把 Markdown 直接交给发布技能</h3><p>baoyu-post-to-wechat 已内置 Markdown 到公众号内容的处理。当前说明明确要求不要提前把 Markdown 转成 HTML 再套同一发布链路，因为浏览器路径和 API 路径处理图片的方式不同。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">请使用 baoyu-post-to-wechat 处理 article-formatted.md。</span><br><span class="line">使用当前已配置的公众号账户，检查标题、摘要、作者和封面。</span><br><span class="line">本次只创建公众号草稿，不群发。</span><br><span class="line">完成后返回草稿标识和检查结果。</span><br></pre></td></tr></table></figure><p>API 路径涉及公众号凭证、IP 白名单和 draft/add 等接口；浏览器路径则依赖 Chrome 与有效登录状态。<strong>草稿创建成功不等于已经向读者群发。</strong> 验收时要明确结果属于哪一个阶段。</p><p>只有你需要单独交付带样式 HTML 文件时，再使用 baoyu-markdown-to-html。它和公众号发布技能是不同用途，不必强行串联。</p><p>依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/skills/baoyu-post-to-wechat/SKILL.md">公众号发布技能</a>。</p><h2 id="八、排错：按“发现、执行、输出”三层检查"><a href="#八、排错：按“发现、执行、输出”三层检查" class="headerlink" title="八、排错：按“发现、执行、输出”三层检查"></a>八、排错：按“发现、执行、输出”三层检查</h2><div class="table-container"><table><thead><tr><th>现象</th><th>先检查什么</th><th>下一步</th></tr></thead><tbody><tr><td>Agent 说找不到技能</td><td>安装范围、当前项目、完整 SKILL.md 路径</td><td>用 skills list 检查，再显式点名调用</td></tr><tr><td>找到了技能，但找不到脚本</td><td>是否只复制了 SKILL.md</td><td>安装完整技能目录，保留 scripts 与 references</td></tr><tr><td>提示找不到 Bun</td><td>bun 或 npx bun 能否运行</td><td>按该技能要求配置运行时</td></tr><tr><td>第一次停在偏好选择</td><td>是否缺少 EXTEND.md</td><td>按引导完成初始化，确认实际保存位置</td></tr><tr><td>修改偏好后没有变化</td><td>项目配置是否覆盖用户配置，脚本是否读取该路径</td><td>让 Agent 报告所采用的配置路径，勿打印密钥</td></tr><tr><td>网页只能读到登录页</td><td>Chrome 会话、登录与验证码状态</td><td>完成必要交互，再重新采集</td></tr><tr><td>图片接口返回 401、403</td><td>凭证、账户权限、请求地址</td><td>按对应提供商的错误说明排查</td></tr><tr><td>图片接口返回 429</td><td>配额、速率与并发</td><td>减少并发，检查额度，再重试失败项</td></tr><tr><td>提示完成但没文件</td><td>实际输出路径与脚本日志</td><td>用文件存在性和可打开性验收</td></tr><tr><td>图片压缩后原稿不见</td><td>是否遗漏 —keep</td><td>从备份恢复，后续明确保留原图</td></tr><tr><td>PPTX 中的文字不能单独编辑</td><td>是否使用逐页图片合成</td><td>回到原稿或提示词修改后重生成页面</td></tr><tr><td>Hexo 图片 404</td><td>source 中实际文件和正文公开 URL 是否对应</td><td>检查大小写、构建产物和线上地址</td></tr></tbody></table></div><p>Windows 还要留意命令所在环境。PowerShell 中的环境变量写法是 $env:NAME = “value”，不能直接套用 Bash 的 NAME=value command；目录含空格时要加引号。教程里出现占位路径时，应替换成实际路径，不能原样执行。</p><p>名称中带 danger 的工具，仓库说明涉及非官方或逆向网页接口，使用前应阅读其说明并考虑账户、稳定性和平台限制。它们适合有明确需求时评估，不适合作为入门教程默认依赖。依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/README.zh.md#免责声明">项目免责声明</a>。</p><h2 id="九、更新维护：保持可复现，比追新更重要"><a href="#九、更新维护：保持可复现，比追新更重要" class="headerlink" title="九、更新维护：保持可复现，比追新更重要"></a>九、更新维护：保持可复现，比追新更重要</h2><p>用 skills CLI 安装的技能，可以查看和更新：</p><figure class="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">npx skills list --agent codex</span><br><span class="line">npx skills update --project</span><br></pre></td></tr></table></figure><p>如果装在全局：</p><figure class="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">npx skills list --global --agent codex</span><br><span class="line">npx skills update --global</span><br></pre></td></tr></table></figure><p>用 Claude Code 插件市场安装的，就通过 /plugin 的 Marketplaces 界面更新该市场。不要把“更新 Git 克隆”“更新插件市场”和“更新 skills CLI 的安装副本”当成同一个动作。</p><p>对长期写作项目，我建议保留三种记录：</p><ol><li>技能来源与版本或提交号。</li><li>项目级 EXTEND.md 中的非敏感偏好。</li><li>原稿、最终提示词和最终输出文件。</li></ol><p>更新后先跑本文的短文排版练习，确认读写与输出路径正常，再处理正式文章。每次只增加少量技能，也方便判断是安装、配置还是内容本身出了问题。</p><p>维护命令依据：<a href="https://github.com/vercel-labs/skills">skills CLI</a>；插件更新路径依据：<a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/README.zh.md">仓库安装与更新说明</a>。</p><h2 id="十、可直接复用的文章加工请求"><a href="#十、可直接复用的文章加工请求" class="headerlink" title="十、可直接复用的文章加工请求"></a>十、可直接复用的文章加工请求</h2><p>当你已经完成安装、偏好和图片后端初始化后，可以用下面的请求处理正式文章：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">请处理当前项目的 article.md，目标是发布一篇技术博客教程。</span><br><span class="line"></span><br><span class="line">1. 核对提供的来源与示例命令，记录不能确认的地方。</span><br><span class="line">2. 使用 baoyu-format-markdown 整理结构，保留代码与原始事实。</span><br><span class="line">3. 使用 baoyu-cover-image 生成 16:9 封面。</span><br><span class="line">4. 使用 baoyu-article-illustrator 选择最多 3 个必要的配图位置。</span><br><span class="line">5. 使用 baoyu-compress-image 输出 WebP，并保留所有原图。</span><br><span class="line">6. 将最终 Markdown 和图片放到当前博客约定的位置。</span><br><span class="line">7. 运行博客已有构建命令，检查代码块、图片、目录和移动端排版。</span><br><span class="line"></span><br><span class="line">最终交付源稿路径、图片清单、构建结果。</span><br><span class="line">本次仅生成和检查，不执行公开发布。</span><br></pre></td></tr></table></figure><p>最后一句是模板的操作边界：你确认需要上线时，再明确要求发布，并让 Agent 返回真实文章地址及访问验证结果。</p><p>学会这套流程后，真正值得积累的是自己的素材与质量标准。技能把重复步骤固定下来，你就可以把更多精力放在文章是否讲清楚了问题、案例能不能复现，以及读者做完后是否获得了一个真实可用的结果。</p><h2 id="参考资料与图片署名"><a href="#参考资料与图片署名" class="headerlink" title="参考资料与图片署名"></a>参考资料与图片署名</h2><ul><li><a href="https://github.com/JimLiu/baoyu-skills">baoyu-skills 仓库</a>，作者 Jim Liu（宝玉）。</li><li><a href="https://github.com/JimLiu/baoyu-skills/blob/1567581c26ec29f4216c6e6835415bf30343b0e3/README.zh.md">本文核对的中文 README 快照</a>。</li><li><a href="https://github.com/vercel-labs/skills">Skills 安装器</a>。</li><li><a href="https://developers.openai.com/codex/skills/">Codex 官方 Skills 文档</a>。</li><li><a href="https://hexo.io/docs/">Hexo 官方文档</a>。</li><li>本文封面、工作流图和视觉选型图为本次独立绘制的 SVG，转换为 WebP 用于网页展示，同时保留 SVG 与 PNG 原件；不是技能官方截图，不复用本站其他文章的封面。</li></ul>]]></content>
    
    
    <summary type="html">手把手安装 baoyu-skills，用一篇 Markdown 跑通排版、配图和多平台输出；讲清 Codex 与 Claude Code 的调用差异、EXTEND.md 配置、图片后端和常见故障。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="开发工具" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="AI" scheme="https://www.shallowrecall.top/tags/AI/"/>
    
    <category term="Agent Skills" scheme="https://www.shallowrecall.top/tags/Agent-Skills/"/>
    
    <category term="Codex" scheme="https://www.shallowrecall.top/tags/Codex/"/>
    
    <category term="Claude Code" scheme="https://www.shallowrecall.top/tags/Claude-Code/"/>
    
    <category term="内容创作" scheme="https://www.shallowrecall.top/tags/%E5%86%85%E5%AE%B9%E5%88%9B%E4%BD%9C/"/>
    
  </entry>
  
  <entry>
    <title>端口占用排查与进程清理命令</title>
    <link href="https://www.shallowrecall.top/posts/80eccc5a.html"/>
    <id>https://www.shallowrecall.top/posts/80eccc5a.html</id>
    <published>2026-06-25T10:10:00.000Z</published>
    <updated>2026-06-25T10:10:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="🔌-端口占用排查与进程清理命令"><a href="#🔌-端口占用排查与进程清理命令" class="headerlink" title="🔌 端口占用排查与进程清理命令"></a>🔌 端口占用排查与进程清理命令</h1><blockquote><p>✨ 启动服务时看到 <code>address already in use</code>、<code>port is already allocated</code>、<code>EADDRINUSE</code>，通常就是端口已经被某个进程占用了。处理思路很固定：先查端口对应的 PID，再确认进程名，最后结束进程。</p></blockquote><hr><h2 id="🧠-先记住一句话"><a href="#🧠-先记住一句话" class="headerlink" title="🧠 先记住一句话"></a>🧠 先记住一句话</h2><p>端口本身不能被“杀掉”，真正要结束的是占用这个端口的进程。</p><p>常见流程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">端口号 -&gt; PID -&gt; 进程名 -&gt; kill / taskkill / Stop-Process</span><br></pre></td></tr></table></figure><p>比如你要处理 <code>8080</code> 端口，就先查谁在监听 <code>8080</code>，再结束对应 PID。</p><span id="more"></span><hr><h2 id="⚡-最快速查"><a href="#⚡-最快速查" class="headerlink" title="⚡ 最快速查"></a>⚡ 最快速查</h2><h3 id="Windows-PowerShell"><a href="#Windows-PowerShell" class="headerlink" title="Windows PowerShell"></a>Windows PowerShell</h3><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8080</span> | <span class="built_in">Select-Object</span> LocalAddress, LocalPort, State, OwningProcess</span><br><span class="line"><span class="built_in">Get-Process</span> <span class="literal">-Id</span> &lt;PID&gt;</span><br><span class="line"><span class="built_in">Stop-Process</span> <span class="literal">-Id</span> &lt;PID&gt; <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><h3 id="Windows-CMD"><a href="#Windows-CMD" class="headerlink" title="Windows CMD"></a>Windows CMD</h3><figure class="highlight bat"><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">netstat -ano | <span class="built_in">findstr</span> :<span class="number">8080</span></span><br><span class="line">tasklist /FI &quot;PID eq &lt;PID&gt;&quot;</span><br><span class="line"><span class="built_in">taskkill</span> /PID &lt;PID&gt; /F</span><br></pre></td></tr></table></figure><h3 id="macOS-Linux"><a href="#macOS-Linux" class="headerlink" title="macOS / Linux"></a>macOS / Linux</h3><figure class="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">lsof -nP -iTCP:8080 -sTCP:LISTEN</span><br><span class="line"><span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -9 &lt;PID&gt;</span><br></pre></td></tr></table></figure><hr><h2 id="🪟-Windows-PowerShell"><a href="#🪟-Windows-PowerShell" class="headerlink" title="🪟 Windows PowerShell"></a>🪟 Windows PowerShell</h2><p>PowerShell 里推荐优先用 <code>Get-NetTCPConnection</code>，输出结构化，适合继续管道处理。</p><h3 id="查端口对应-PID"><a href="#查端口对应-PID" class="headerlink" title="查端口对应 PID"></a>查端口对应 PID</h3><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8080</span></span><br></pre></td></tr></table></figure><p>只看关键字段：</p><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8080</span> |</span><br><span class="line">  <span class="built_in">Select-Object</span> LocalAddress, LocalPort, State, OwningProcess</span><br></pre></td></tr></table></figure><p><code>OwningProcess</code> 就是 PID。</p><p>如果只想查正在监听的进程：</p><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8080</span> <span class="literal">-State</span> Listen |</span><br><span class="line">  <span class="built_in">Select-Object</span> LocalAddress, LocalPort, OwningProcess</span><br></pre></td></tr></table></figure><h3 id="根据-PID-查看进程名"><a href="#根据-PID-查看进程名" class="headerlink" title="根据 PID 查看进程名"></a>根据 PID 查看进程名</h3><figure class="highlight powershell"><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">Get-Process</span> <span class="literal">-Id</span> &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>例如：</p><figure class="highlight powershell"><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">Get-Process</span> <span class="literal">-Id</span> <span class="number">12345</span></span><br></pre></td></tr></table></figure><h3 id="结束进程"><a href="#结束进程" class="headerlink" title="结束进程"></a>结束进程</h3><p>温和结束：</p><figure class="highlight powershell"><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">Stop-Process</span> <span class="literal">-Id</span> &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight powershell"><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">Stop-Process</span> <span class="literal">-Id</span> &lt;PID&gt; <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><h3 id="一行命令结束占用端口的进程"><a href="#一行命令结束占用端口的进程" class="headerlink" title="一行命令结束占用端口的进程"></a>一行命令结束占用端口的进程</h3><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">8080</span> <span class="literal">-State</span> Listen |</span><br><span class="line">  <span class="built_in">Select-Object</span> <span class="literal">-ExpandProperty</span> OwningProcess <span class="literal">-Unique</span> |</span><br><span class="line">  <span class="built_in">ForEach-Object</span> &#123; <span class="built_in">Stop-Process</span> <span class="literal">-Id</span> <span class="variable">$_</span> <span class="literal">-Force</span> &#125;</span><br></pre></td></tr></table></figure><blockquote><p>[!warning]<br>一行 kill 命令很爽，但最好先用 <code>Get-Process -Id &lt;PID&gt;</code> 看一下是不是你自己的开发服务。不要随手结束数据库、Docker、系统服务。</p></blockquote><hr><h2 id="🪟-Windows-CMD"><a href="#🪟-Windows-CMD" class="headerlink" title="🪟 Windows CMD"></a>🪟 Windows CMD</h2><p>CMD 里最常用的是 <code>netstat + findstr + taskkill</code>。</p><h3 id="查端口对应-PID-1"><a href="#查端口对应-PID-1" class="headerlink" title="查端口对应 PID"></a>查端口对应 PID</h3><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">netstat -ano | <span class="built_in">findstr</span> :<span class="number">8080</span></span><br></pre></td></tr></table></figure><p>你会看到类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TCP    0.0.0.0:8080    0.0.0.0:0    LISTENING    12345</span><br></pre></td></tr></table></figure><p>最后一列 <code>12345</code> 就是 PID。</p><p>如果只想找监听状态：</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">netstat -ano | <span class="built_in">findstr</span> :<span class="number">8080</span> | <span class="built_in">findstr</span> LISTENING</span><br></pre></td></tr></table></figure><h3 id="根据-PID-查看进程名-1"><a href="#根据-PID-查看进程名-1" class="headerlink" title="根据 PID 查看进程名"></a>根据 PID 查看进程名</h3><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tasklist /FI &quot;PID eq <span class="number">12345</span>&quot;</span><br></pre></td></tr></table></figure><h3 id="结束进程-1"><a href="#结束进程-1" class="headerlink" title="结束进程"></a>结束进程</h3><figure class="highlight bat"><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">taskkill</span> /PID <span class="number">12345</span> /F</span><br></pre></td></tr></table></figure><h3 id="一行命令结束占用端口的进程-1"><a href="#一行命令结束占用端口的进程-1" class="headerlink" title="一行命令结束占用端口的进程"></a>一行命令结束占用端口的进程</h3><p>在 CMD 里直接执行：</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> /f &quot;tokens=<span class="number">5</span>&quot; %a <span class="keyword">in</span> (&#x27;netstat -ano ^| <span class="built_in">findstr</span> :<span class="number">8080</span> ^| <span class="built_in">findstr</span> LISTENING&#x27;) <span class="keyword">do</span> <span class="built_in">taskkill</span> /F /PID %a</span><br></pre></td></tr></table></figure><p>如果写在 <code>.bat</code> 文件里，变量要写成 <code>%%a</code>：</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> /f &quot;tokens=<span class="number">5</span>&quot; <span class="variable">%%a</span> <span class="keyword">in</span> (&#x27;netstat -ano ^| <span class="built_in">findstr</span> :<span class="number">8080</span> ^| <span class="built_in">findstr</span> LISTENING&#x27;) <span class="keyword">do</span> <span class="built_in">taskkill</span> /F /PID <span class="variable">%%a</span></span><br></pre></td></tr></table></figure><hr><h2 id="🍎-macOS"><a href="#🍎-macOS" class="headerlink" title="🍎 macOS"></a>🍎 macOS</h2><p>macOS 上最常用的是 <code>lsof</code>。</p><h3 id="查端口对应-PID-2"><a href="#查端口对应-PID-2" class="headerlink" title="查端口对应 PID"></a>查端口对应 PID</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -nP -iTCP:8080 -sTCP:LISTEN</span><br></pre></td></tr></table></figure><p>参数含义：</p><ul><li><code>-n</code>: 不解析主机名，速度更快</li><li><code>-P</code>: 不解析端口名，直接显示端口号</li><li><code>-iTCP:8080</code>: 查 TCP 8080 端口</li><li><code>-sTCP:LISTEN</code>: 只看监听状态</li></ul><p>只输出 PID：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -ti tcp:8080</span><br></pre></td></tr></table></figure><h3 id="查看进程信息"><a href="#查看进程信息" class="headerlink" title="查看进程信息"></a>查看进程信息</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -p &lt;PID&gt; -o pid,ppid,user,<span class="built_in">comm</span>,args</span><br></pre></td></tr></table></figure><h3 id="结束进程-2"><a href="#结束进程-2" class="headerlink" title="结束进程"></a>结束进程</h3><p>优先温和结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>还没退出时再强制结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -9 &lt;PID&gt;</span><br></pre></td></tr></table></figure><h3 id="一行命令结束占用端口的进程-2"><a href="#一行命令结束占用端口的进程-2" class="headerlink" title="一行命令结束占用端口的进程"></a>一行命令结束占用端口的进程</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -ti tcp:8080 | xargs <span class="built_in">kill</span> -15</span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -ti tcp:8080 | xargs <span class="built_in">kill</span> -9</span><br></pre></td></tr></table></figure><p>如果端口没有进程，<code>xargs</code> 可能仍然执行 <code>kill</code>。更稳一点可以这样：</p><figure class="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">pid=$(lsof -ti tcp:8080)</span><br><span class="line">[ -n <span class="string">&quot;<span class="variable">$pid</span>&quot;</span> ] &amp;&amp; <span class="built_in">kill</span> -15 <span class="variable">$pid</span></span><br></pre></td></tr></table></figure><hr><h2 id="🐧-Linux"><a href="#🐧-Linux" class="headerlink" title="🐧 Linux"></a>🐧 Linux</h2><p>Linux 上常见选择有 <code>ss</code>、<code>lsof</code>、<code>fuser</code>。其中 <code>ss</code> 通常是系统自带的新工具，<code>lsof</code> 更直观，<code>fuser</code> 适合快速清理。</p><h3 id="用-ss-查端口"><a href="#用-ss-查端口" class="headerlink" title="用 ss 查端口"></a>用 ss 查端口</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ss -ltnp <span class="string">&#x27;sport = :8080&#x27;</span></span><br></pre></td></tr></table></figure><p>或者：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ss -ltnp | grep <span class="string">&#x27;:8080&#x27;</span></span><br></pre></td></tr></table></figure><p>输出里通常会看到类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">users:((&quot;node&quot;,pid=12345,fd=21))</span><br></pre></td></tr></table></figure><p>这里的 <code>12345</code> 就是 PID。</p><h3 id="用-lsof-查端口"><a href="#用-lsof-查端口" class="headerlink" title="用 lsof 查端口"></a>用 lsof 查端口</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo lsof -nP -iTCP:8080 -sTCP:LISTEN</span><br></pre></td></tr></table></figure><p>只输出 PID：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo lsof -ti tcp:8080</span><br></pre></td></tr></table></figure><h3 id="查看进程信息-1"><a href="#查看进程信息-1" class="headerlink" title="查看进程信息"></a>查看进程信息</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -p &lt;PID&gt; -o pid,ppid,user,<span class="built_in">comm</span>,args</span><br></pre></td></tr></table></figure><h3 id="结束进程-3"><a href="#结束进程-3" class="headerlink" title="结束进程"></a>结束进程</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -9 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>如果不是当前用户的进程，需要加 <code>sudo</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo <span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><h3 id="用-fuser-直接释放端口"><a href="#用-fuser-直接释放端口" class="headerlink" title="用 fuser 直接释放端口"></a>用 fuser 直接释放端口</h3><p>查看端口：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fuser 8080/tcp</span><br></pre></td></tr></table></figure><p>直接结束占用这个 TCP 端口的进程：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fuser -k 8080/tcp</span><br></pre></td></tr></table></figure><p><code>fuser -k</code> 很直接，适合你确认这个端口就是开发服务时使用。</p><hr><h2 id="🐚-Bash-Zsh"><a href="#🐚-Bash-Zsh" class="headerlink" title="🐚 Bash / Zsh"></a>🐚 Bash / Zsh</h2><p>macOS 和大多数 Linux 发行版都可以用类似写法。</p><h3 id="查端口"><a href="#查端口" class="headerlink" title="查端口"></a>查端口</h3><figure class="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">PORT=8080</span><br><span class="line">lsof -nP -iTCP:<span class="variable">$PORT</span> -sTCP:LISTEN</span><br></pre></td></tr></table></figure><h3 id="结束端口进程"><a href="#结束端口进程" class="headerlink" title="结束端口进程"></a>结束端口进程</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">PORT=8080</span><br><span class="line">PID=$(lsof -ti tcp:<span class="variable">$PORT</span>)</span><br><span class="line">[ -n <span class="string">&quot;<span class="variable">$PID</span>&quot;</span> ] &amp;&amp; <span class="built_in">kill</span> -15 <span class="variable">$PID</span></span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">PORT=8080</span><br><span class="line">PID=$(lsof -ti tcp:<span class="variable">$PORT</span>)</span><br><span class="line">[ -n <span class="string">&quot;<span class="variable">$PID</span>&quot;</span> ] &amp;&amp; <span class="built_in">kill</span> -9 <span class="variable">$PID</span></span><br></pre></td></tr></table></figure><h3 id="封装成函数"><a href="#封装成函数" class="headerlink" title="封装成函数"></a>封装成函数</h3><p>可以放到 <code>~/.zshrc</code> 或 <code>~/.bashrc</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="function"><span class="title">killport</span></span>() &#123;</span><br><span class="line">  <span class="built_in">local</span> port=<span class="string">&quot;<span class="variable">$1</span>&quot;</span></span><br><span class="line">  <span class="keyword">if</span> [ -z <span class="string">&quot;<span class="variable">$port</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Usage: killport &lt;port&gt;&quot;</span></span><br><span class="line">    <span class="built_in">return</span> 1</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">  <span class="built_in">local</span> pid</span><br><span class="line">  pid=$(lsof -ti tcp:<span class="string">&quot;<span class="variable">$port</span>&quot;</span>)</span><br><span class="line">  <span class="keyword">if</span> [ -z <span class="string">&quot;<span class="variable">$pid</span>&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;No process is listening on port <span class="variable">$port</span>&quot;</span></span><br><span class="line">    <span class="built_in">return</span> 0</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Killing port <span class="variable">$port</span>, PID: <span class="variable">$pid</span>&quot;</span></span><br><span class="line">  <span class="built_in">kill</span> -15 <span class="variable">$pid</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">killport 8080</span><br></pre></td></tr></table></figure><hr><h2 id="🐟-Fish-Shell"><a href="#🐟-Fish-Shell" class="headerlink" title="🐟 Fish Shell"></a>🐟 Fish Shell</h2><p>Fish 的变量写法和 Bash/Zsh 不一样。</p><h3 id="查端口-1"><a href="#查端口-1" class="headerlink" title="查端口"></a>查端口</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">set port 8080</span><br><span class="line">lsof -nP -iTCP:$port -sTCP:LISTEN</span><br></pre></td></tr></table></figure><h3 id="结束端口进程-1"><a href="#结束端口进程-1" class="headerlink" title="结束端口进程"></a>结束端口进程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">set port 8080</span><br><span class="line">set pid (lsof -ti tcp:$port)</span><br><span class="line">test -n &quot;$pid&quot;; and kill -15 $pid</span><br></pre></td></tr></table></figure><p>强制结束：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">set port 8080</span><br><span class="line">set pid (lsof -ti tcp:$port)</span><br><span class="line">test -n &quot;$pid&quot;; and kill -9 $pid</span><br></pre></td></tr></table></figure><h3 id="封装成函数-1"><a href="#封装成函数-1" class="headerlink" title="封装成函数"></a>封装成函数</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">function killport</span><br><span class="line">    set port $argv[1]</span><br><span class="line">    if test -z &quot;$port&quot;</span><br><span class="line">        echo &quot;Usage: killport &lt;port&gt;&quot;</span><br><span class="line">        return 1</span><br><span class="line">    end</span><br><span class="line"></span><br><span class="line">    set pid (lsof -ti tcp:$port)</span><br><span class="line">    if test -z &quot;$pid&quot;</span><br><span class="line">        echo &quot;No process is listening on port $port&quot;</span><br><span class="line">        return 0</span><br><span class="line">    end</span><br><span class="line"></span><br><span class="line">    echo &quot;Killing port $port, PID: $pid&quot;</span><br><span class="line">    kill -15 $pid</span><br><span class="line">end</span><br></pre></td></tr></table></figure><p>保存为永久函数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">funcsave killport</span><br></pre></td></tr></table></figure><hr><h2 id="🧩-Git-Bash-WSL-怎么办"><a href="#🧩-Git-Bash-WSL-怎么办" class="headerlink" title="🧩 Git Bash / WSL 怎么办"></a>🧩 Git Bash / WSL 怎么办</h2><h3 id="Git-Bash"><a href="#Git-Bash" class="headerlink" title="Git Bash"></a>Git Bash</h3><p>Git Bash 跑在 Windows 上，所以端口真正属于 Windows 系统。最稳的是直接调用 Windows 命令：</p><figure class="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">netstat -ano | findstr :8080</span><br><span class="line">taskkill //PID 12345 //F</span><br></pre></td></tr></table></figure><p>有些 Git Bash 环境会把 <code>/PID</code> 当路径处理，所以可以写成 <code>//PID</code> 和 <code>//F</code>。</p><h3 id="WSL"><a href="#WSL" class="headerlink" title="WSL"></a>WSL</h3><p>如果服务是在 WSL 里启动的，优先在 WSL 里用 Linux 命令：</p><figure class="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">sudo lsof -nP -iTCP:8080 -sTCP:LISTEN</span><br><span class="line">sudo <span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>如果服务是在 Windows 里启动的，就回到 PowerShell 或 CMD 查。</p><hr><h2 id="🧯-常见场景"><a href="#🧯-常见场景" class="headerlink" title="🧯 常见场景"></a>🧯 常见场景</h2><h3 id="Vite-React-Vue-提示端口被占用"><a href="#Vite-React-Vue-提示端口被占用" class="headerlink" title="Vite / React / Vue 提示端口被占用"></a>Vite / React / Vue 提示端口被占用</h3><p>一般是上一次 dev server 没退干净。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -ti tcp:5173 | xargs <span class="built_in">kill</span> -15</span><br></pre></td></tr></table></figure><p>Windows PowerShell：</p><figure class="highlight powershell"><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">Get-NetTCPConnection</span> <span class="literal">-LocalPort</span> <span class="number">5173</span> <span class="literal">-State</span> Listen |</span><br><span class="line">  <span class="built_in">Select-Object</span> <span class="literal">-ExpandProperty</span> OwningProcess <span class="literal">-Unique</span> |</span><br><span class="line">  <span class="built_in">ForEach-Object</span> &#123; <span class="built_in">Stop-Process</span> <span class="literal">-Id</span> <span class="variable">$_</span> <span class="literal">-Force</span> &#125;</span><br></pre></td></tr></table></figure><h3 id="Node-服务提示-EADDRINUSE"><a href="#Node-服务提示-EADDRINUSE" class="headerlink" title="Node 服务提示 EADDRINUSE"></a>Node 服务提示 EADDRINUSE</h3><p>先查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -nP -iTCP:3000 -sTCP:LISTEN</span><br></pre></td></tr></table></figure><p>再结束：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -15 &lt;PID&gt;</span><br></pre></td></tr></table></figure><p>Windows CMD：</p><figure class="highlight bat"><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">netstat -ano | <span class="built_in">findstr</span> :<span class="number">3000</span></span><br><span class="line"><span class="built_in">taskkill</span> /PID &lt;PID&gt; /F</span><br></pre></td></tr></table></figure><h3 id="Docker-提示-port-is-already-allocated"><a href="#Docker-提示-port-is-already-allocated" class="headerlink" title="Docker 提示 port is already allocated"></a>Docker 提示 port is already allocated</h3><p>先确认宿主机端口是谁占了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">lsof -nP -iTCP:8080 -sTCP:LISTEN</span><br></pre></td></tr></table></figure><p>如果是 Docker 容器占用，查看容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker ps --filter publish=8080</span><br></pre></td></tr></table></figure><p>停止容器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker stop &lt;container_id&gt;</span><br></pre></td></tr></table></figure><hr><h2 id="✅-建议习惯"><a href="#✅-建议习惯" class="headerlink" title="✅ 建议习惯"></a>✅ 建议习惯</h2><ol><li>先查端口，不要直接 kill。</li><li>先看进程名，确认是不是自己的开发服务。</li><li>优先用 <code>kill -15</code> 或 <code>Stop-Process</code> 温和结束。</li><li>进程无响应时再用 <code>kill -9</code>、<code>taskkill /F</code>、<code>Stop-Process -Force</code>。</li><li>如果总是同一个端口冲突，可以给开发服务换端口，或者检查是否有后台自启动服务。</li></ol><hr><h2 id="🧾-命令对照表"><a href="#🧾-命令对照表" class="headerlink" title="🧾 命令对照表"></a>🧾 命令对照表</h2><div class="table-container"><table><thead><tr><th>系统 / Shell</th><th>查端口</th><th>查进程</th><th>结束进程</th></tr></thead><tbody><tr><td>PowerShell</td><td><code>Get-NetTCPConnection -LocalPort 8080</code></td><td><code>Get-Process -Id &lt;PID&gt;</code></td><td><code>Stop-Process -Id &lt;PID&gt; -Force</code></td></tr><tr><td>CMD</td><td>`netstat -ano \</td><td>findstr :8080`</td><td><code>tasklist /FI &quot;PID eq &lt;PID&gt;&quot;</code></td><td><code>taskkill /PID &lt;PID&gt; /F</code></td></tr><tr><td>macOS</td><td><code>lsof -nP -iTCP:8080 -sTCP:LISTEN</code></td><td><code>ps -p &lt;PID&gt; -o pid,ppid,user,comm,args</code></td><td><code>kill -15 &lt;PID&gt;</code></td></tr><tr><td>Linux</td><td><code>sudo ss -ltnp &#39;sport = :8080&#39;</code></td><td><code>ps -p &lt;PID&gt; -o pid,ppid,user,comm,args</code></td><td><code>sudo kill -15 &lt;PID&gt;</code></td></tr><tr><td>Bash / Zsh</td><td><code>lsof -ti tcp:8080</code></td><td><code>ps -p &lt;PID&gt; -o comm,args</code></td><td><code>kill -15 &lt;PID&gt;</code></td></tr><tr><td>Fish</td><td><code>lsof -ti tcp:$port</code></td><td><code>ps -p $pid -o comm,args</code></td><td><code>kill -15 $pid</code></td></tr></tbody></table></div><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>端口占用排查没有玄学，核心就是三步：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">查端口 -&gt; 找 PID -&gt; 结束进程</span><br></pre></td></tr></table></figure><p>Windows 记住 <code>netstat / taskkill</code> 或 <code>Get-NetTCPConnection / Stop-Process</code>，macOS 和 Linux 记住 <code>lsof / kill</code>，Linux 服务器上再补一个 <code>ss</code> 和 <code>fuser</code>。这些命令够覆盖绝大多数开发场景。</p>]]></content>
    
    
    <summary type="html">Windows、macOS、Linux 中查看端口占用、定位 PID、结束对应进程的常用命令速查。</summary>
    
    
    
    <category term="开发工具" scheme="https://www.shallowrecall.top/categories/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="Windows" scheme="https://www.shallowrecall.top/tags/Windows/"/>
    
    <category term="macOS" scheme="https://www.shallowrecall.top/tags/macOS/"/>
    
    <category term="Linux" scheme="https://www.shallowrecall.top/tags/Linux/"/>
    
    <category term="Shell" scheme="https://www.shallowrecall.top/tags/Shell/"/>
    
    <category term="端口" scheme="https://www.shallowrecall.top/tags/%E7%AB%AF%E5%8F%A3/"/>
    
    <category term="进程" scheme="https://www.shallowrecall.top/tags/%E8%BF%9B%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Homebrew</title>
    <link href="https://www.shallowrecall.top/posts/17ad264f.html"/>
    <id>https://www.shallowrecall.top/posts/17ad264f.html</id>
    <published>2026-06-24T02:20:00.000Z</published>
    <updated>2026-06-24T02:20:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="🍺-Homebrew"><a href="#🍺-Homebrew" class="headerlink" title="🍺 Homebrew"></a>🍺 Homebrew</h1><span id="more"></span><blockquote><p>✨ Mac 上最好用的包管理工具之一。适合安装命令行工具、GUI 应用、后台服务，也很适合新电脑初始化。</p></blockquote><hr><h2 id="🧠-它是什么"><a href="#🧠-它是什么" class="headerlink" title="🧠 它是什么"></a>🧠 它是什么</h2><p>Homebrew 可以理解成 macOS 上的“应用商店 + 命令行安装器”。</p><p>它最常见的用途：</p><ul><li>安装命令行工具，比如 <code>git</code>、<code>wget</code>、<code>node</code></li><li>安装图形应用，比如 <code>google-chrome</code>、<code>visual-studio-code</code></li><li>管理后台服务，比如 <code>mysql</code>、<code>redis</code></li><li>统一软件的安装、升级、卸载和清理</li></ul><p>一句话理解：</p><blockquote><p>💡 不想手动找官网、下载、拖进应用目录时，很多东西都可以先看看能不能 <code>brew install</code>。</p></blockquote><hr><h2 id="💻-安装-Homebrew"><a href="#💻-安装-Homebrew" class="headerlink" title="💻 安装 Homebrew"></a>💻 安装 Homebrew</h2><p>官方首页：</p><ul><li>Homebrew 官网：<a href="https://brew.sh/">https://brew.sh/</a></li><li>安装文档：<a href="https://docs.brew.sh/Installation">https://docs.brew.sh/Installation</a></li></ul><h3 id="1-🧰-安装-Xcode-Command-Line-Tools"><a href="#1-🧰-安装-Xcode-Command-Line-Tools" class="headerlink" title="1. 🧰 安装 Xcode Command Line Tools"></a>1. 🧰 安装 Xcode Command Line Tools</h3><p>Homebrew 在 macOS 上通常需要先准备好 Command Line Tools：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xcode-select --install</span><br></pre></td></tr></table></figure><p>如果系统提示已经安装过，就可以继续下一步。</p><h3 id="2-🚀-安装-Homebrew"><a href="#2-🚀-安装-Homebrew" class="headerlink" title="2. 🚀 安装 Homebrew"></a>2. 🚀 安装 Homebrew</h3><p>官方安装命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/bin/bash -c <span class="string">&quot;<span class="subst">$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="3-✅-检查是否安装成功"><a href="#3-✅-检查是否安装成功" class="headerlink" title="3. ✅ 检查是否安装成功"></a>3. ✅ 检查是否安装成功</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew --version</span><br></pre></td></tr></table></figure><p>如果能正常显示版本号，说明安装成功。</p><hr><h2 id="🧭-安装后的路径概念"><a href="#🧭-安装后的路径概念" class="headerlink" title="🧭 安装后的路径概念"></a>🧭 安装后的路径概念</h2><p>Homebrew 官方推荐的默认安装位置：</p><ul><li>Apple Silicon Mac：<code>/opt/homebrew</code></li><li>Intel Mac：<code>/usr/local</code></li></ul><p>这是官方支持的默认前缀，很多二进制包也依赖这个位置。</p><blockquote><p>📌 也就是说，M1 / M2 / M3 之类机器上看到 <code>/opt/homebrew</code> 很正常。</p></blockquote><hr><h2 id="⚙️-如果安装后提示-brew-命令不存在"><a href="#⚙️-如果安装后提示-brew-命令不存在" class="headerlink" title="⚙️ 如果安装后提示 brew 命令不存在"></a>⚙️ 如果安装后提示 <code>brew</code> 命令不存在</h2><p>有时候安装完成后，当前 shell 还没加载 Homebrew 环境。</p><h3 id="Apple-Silicon-常见写法"><a href="#Apple-Silicon-常见写法" class="headerlink" title="Apple Silicon 常见写法"></a>Apple Silicon 常见写法</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(/opt/homebrew/bin/brew shellenv)</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="Intel-Mac-常见写法"><a href="#Intel-Mac-常见写法" class="headerlink" title="Intel Mac 常见写法"></a>Intel Mac 常见写法</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(/usr/local/bin/brew shellenv)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>如果想长期生效，可以把对应命令写进你的 shell 配置文件，比如：</p><ul><li><code>~/.zprofile</code></li><li><code>~/.zshrc</code></li></ul><p>检查：</p><figure class="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">which</span> brew</span><br><span class="line">brew --version</span><br></pre></td></tr></table></figure><hr><h2 id="📦-Homebrew-安装什么"><a href="#📦-Homebrew-安装什么" class="headerlink" title="📦 Homebrew 安装什么"></a>📦 Homebrew 安装什么</h2><p>Homebrew 里你会经常遇到 3 类东西：</p><h3 id="1-🧱-Formula"><a href="#1-🧱-Formula" class="headerlink" title="1. 🧱 Formula"></a>1. 🧱 Formula</h3><p>命令行工具和库，通常这样装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew install git</span><br><span class="line">brew install node</span><br><span class="line">brew install wget</span><br></pre></td></tr></table></figure><h3 id="2-🪟-Cask"><a href="#2-🪟-Cask" class="headerlink" title="2. 🪟 Cask"></a>2. 🪟 Cask</h3><p>图形界面应用，通常这样装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew install --cask google-chrome</span><br><span class="line">brew install --cask visual-studio-code</span><br><span class="line">brew install --cask iterm2</span><br></pre></td></tr></table></figure><h3 id="3-🔁-Services"><a href="#3-🔁-Services" class="headerlink" title="3. 🔁 Services"></a>3. 🔁 Services</h3><p>后台服务，比如数据库、缓存、队列：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">brew install mysql</span><br><span class="line">brew install redis</span><br><span class="line">brew services start mysql</span><br><span class="line">brew services start redis</span><br></pre></td></tr></table></figure><hr><h2 id="🚀-最常用命令"><a href="#🚀-最常用命令" class="headerlink" title="🚀 最常用命令"></a>🚀 最常用命令</h2><h3 id="🔍-搜索软件"><a href="#🔍-搜索软件" class="headerlink" title="🔍 搜索软件"></a>🔍 搜索软件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew search 关键词</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew search git</span><br><span class="line">brew search python</span><br><span class="line">brew search chrome</span><br></pre></td></tr></table></figure><h3 id="📥-安装"><a href="#📥-安装" class="headerlink" title="📥 安装"></a>📥 安装</h3><p>安装命令行工具：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install 包名</span><br></pre></td></tr></table></figure><p>安装 GUI 应用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install --cask 应用名</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew install git</span><br><span class="line">brew install python</span><br><span class="line">brew install --cask visual-studio-code</span><br></pre></td></tr></table></figure><h3 id="📋-查看已安装内容"><a href="#📋-查看已安装内容" class="headerlink" title="📋 查看已安装内容"></a>📋 查看已安装内容</h3><figure class="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">brew list</span><br><span class="line">brew list --cask</span><br></pre></td></tr></table></figure><h3 id="ℹ️-查看软件信息"><a href="#ℹ️-查看软件信息" class="headerlink" title="ℹ️ 查看软件信息"></a>ℹ️ 查看软件信息</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew info 包名</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="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">brew info git</span><br><span class="line">brew info node</span><br></pre></td></tr></table></figure><h3 id="🗑️-卸载"><a href="#🗑️-卸载" class="headerlink" title="🗑️ 卸载"></a>🗑️ 卸载</h3><p>卸载命令行工具：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew uninstall 包名</span><br></pre></td></tr></table></figure><p>卸载 GUI 应用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew uninstall --cask 应用名</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="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">brew uninstall git</span><br><span class="line">brew uninstall --cask google-chrome</span><br></pre></td></tr></table></figure><hr><h2 id="🔄-更新与升级"><a href="#🔄-更新与升级" class="headerlink" title="🔄 更新与升级"></a>🔄 更新与升级</h2><h3 id="更新-Homebrew-本身和软件索引"><a href="#更新-Homebrew-本身和软件索引" class="headerlink" title="更新 Homebrew 本身和软件索引"></a>更新 Homebrew 本身和软件索引</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew update</span><br></pre></td></tr></table></figure><h3 id="查看哪些软件可升级"><a href="#查看哪些软件可升级" class="headerlink" title="查看哪些软件可升级"></a>查看哪些软件可升级</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew outdated</span><br></pre></td></tr></table></figure><h3 id="升级所有已安装软件"><a href="#升级所有已安装软件" class="headerlink" title="升级所有已安装软件"></a>升级所有已安装软件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew upgrade</span><br></pre></td></tr></table></figure><h3 id="升级单个软件"><a href="#升级单个软件" class="headerlink" title="升级单个软件"></a>升级单个软件</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew upgrade 包名</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="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">brew upgrade git</span><br><span class="line">brew upgrade node</span><br></pre></td></tr></table></figure><blockquote><p>💡 官方 FAQ 提到：<code>brew upgrade</code> 后，Homebrew 会自动清理旧版本，并且还会定期做额外清理。</p></blockquote><hr><h2 id="🧹-清理与体检"><a href="#🧹-清理与体检" class="headerlink" title="🧹 清理与体检"></a>🧹 清理与体检</h2><h3 id="清理旧版本和缓存"><a href="#清理旧版本和缓存" class="headerlink" title="清理旧版本和缓存"></a>清理旧版本和缓存</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew cleanup</span><br></pre></td></tr></table></figure><h3 id="检查当前环境是否有问题"><a href="#检查当前环境是否有问题" class="headerlink" title="检查当前环境是否有问题"></a>检查当前环境是否有问题</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew doctor</span><br></pre></td></tr></table></figure><h3 id="查看-Homebrew-配置"><a href="#查看-Homebrew-配置" class="headerlink" title="查看 Homebrew 配置"></a>查看 Homebrew 配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew config</span><br></pre></td></tr></table></figure><p>这三个命令很适合排错时一起用。</p><hr><h2 id="🔁-服务管理"><a href="#🔁-服务管理" class="headerlink" title="🔁 服务管理"></a>🔁 服务管理</h2><p>如果你用 Homebrew 安装了服务类软件，比如 <code>mysql</code>、<code>postgresql</code>、<code>redis</code>，常用命令是：</p><h3 id="启动服务"><a href="#启动服务" class="headerlink" title="启动服务"></a>启动服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services start 服务名</span><br></pre></td></tr></table></figure><h3 id="停止服务"><a href="#停止服务" class="headerlink" title="停止服务"></a>停止服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services stop 服务名</span><br></pre></td></tr></table></figure><h3 id="重启服务"><a href="#重启服务" class="headerlink" title="重启服务"></a>重启服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services restart 服务名</span><br></pre></td></tr></table></figure><h3 id="查看服务状态"><a href="#查看服务状态" class="headerlink" title="查看服务状态"></a>查看服务状态</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services list</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew services start redis</span><br><span class="line">brew services restart mysql</span><br><span class="line">brew services list</span><br></pre></td></tr></table></figure><hr><h2 id="🧰-常见安装示例"><a href="#🧰-常见安装示例" class="headerlink" title="🧰 常见安装示例"></a>🧰 常见安装示例</h2><h3 id="开发常用工具"><a href="#开发常用工具" class="headerlink" title="开发常用工具"></a>开发常用工具</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">brew install git</span><br><span class="line">brew install node</span><br><span class="line">brew install python</span><br><span class="line">brew install wget</span><br><span class="line">brew install pnpm</span><br></pre></td></tr></table></figure><h3 id="常见-GUI-应用"><a href="#常见-GUI-应用" class="headerlink" title="常见 GUI 应用"></a>常见 GUI 应用</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">brew install --cask visual-studio-code</span><br><span class="line">brew install --cask google-chrome</span><br><span class="line">brew install --cask iterm2</span><br><span class="line">brew install --cask docker</span><br></pre></td></tr></table></figure><h3 id="常见服务"><a href="#常见服务" class="headerlink" title="常见服务"></a>常见服务</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew install mysql</span><br><span class="line">brew install redis</span><br><span class="line">brew install postgresql</span><br></pre></td></tr></table></figure><hr><h2 id="⚡-高频命令速查"><a href="#⚡-高频命令速查" class="headerlink" title="⚡ 高频命令速查"></a>⚡ 高频命令速查</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">brew --version</span><br><span class="line">brew search 关键词</span><br><span class="line">brew install 包名</span><br><span class="line">brew install --cask 应用名</span><br><span class="line">brew list</span><br><span class="line">brew list --cask</span><br><span class="line">brew info 包名</span><br><span class="line">brew update</span><br><span class="line">brew outdated</span><br><span class="line">brew upgrade</span><br><span class="line">brew upgrade 包名</span><br><span class="line">brew uninstall 包名</span><br><span class="line">brew uninstall --cask 应用名</span><br><span class="line">brew cleanup</span><br><span class="line">brew doctor</span><br><span class="line">brew config</span><br><span class="line">brew services list</span><br><span class="line">brew services start 服务名</span><br><span class="line">brew services stop 服务名</span><br><span class="line">brew services restart 服务名</span><br></pre></td></tr></table></figure><hr><h2 id="🩹-常见问题"><a href="#🩹-常见问题" class="headerlink" title="🩹 常见问题"></a>🩹 常见问题</h2><h3 id="❌-brew-command-not-found"><a href="#❌-brew-command-not-found" class="headerlink" title="❌ brew: command not found"></a>❌ <code>brew: command not found</code></h3><p>先试：</p><figure class="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">which</span> brew</span><br><span class="line">brew --version</span><br></pre></td></tr></table></figure><p>如果不行，通常是 shell 环境没加载。</p><p>Apple Silicon：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(/opt/homebrew/bin/brew shellenv)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>Intel：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(/usr/local/bin/brew shellenv)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>然后把对应命令写进：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/.zprofile</span><br></pre></td></tr></table></figure><p>或：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/.zshrc</span><br></pre></td></tr></table></figure><h3 id="❌-安装很慢"><a href="#❌-安装很慢" class="headerlink" title="❌ 安装很慢"></a>❌ 安装很慢</h3><p>常见原因：</p><ul><li>网络慢</li><li>下载源码而不是二进制包</li><li>当前软件依赖较多</li></ul><p>先确认 Homebrew 当前状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew update</span><br><span class="line">brew doctor</span><br><span class="line">brew config</span><br></pre></td></tr></table></figure><h3 id="❌-brew-doctor-提示异常"><a href="#❌-brew-doctor-提示异常" class="headerlink" title="❌ brew doctor 提示异常"></a>❌ <code>brew doctor</code> 提示异常</h3><p>先别慌，<code>brew doctor</code> 有时会给出一些“建议修复项”。</p><p>处理思路：</p><ol><li>先读清楚提示内容</li><li>只处理和当前问题相关的项</li><li>改完再重新运行：</li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew doctor</span><br></pre></td></tr></table></figure><h3 id="❌-服务起不来"><a href="#❌-服务起不来" class="headerlink" title="❌ 服务起不来"></a>❌ 服务起不来</h3><p>先看服务列表：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services list</span><br></pre></td></tr></table></figure><p>再看软件信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew info 服务名</span><br></pre></td></tr></table></figure><p>必要时尝试重启：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew services restart 服务名</span><br></pre></td></tr></table></figure><hr><h2 id="🎯-我的推荐使用习惯"><a href="#🎯-我的推荐使用习惯" class="headerlink" title="🎯 我的推荐使用习惯"></a>🎯 我的推荐使用习惯</h2><p>日常可以这样用：</p><h3 id="安装新工具"><a href="#安装新工具" class="headerlink" title="安装新工具"></a>安装新工具</h3><figure class="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">brew search 关键词</span><br><span class="line">brew install 包名</span><br></pre></td></tr></table></figure><h3 id="每隔一段时间更新"><a href="#每隔一段时间更新" class="headerlink" title="每隔一段时间更新"></a>每隔一段时间更新</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">brew update</span><br><span class="line">brew upgrade</span><br><span class="line">brew cleanup</span><br></pre></td></tr></table></figure><h3 id="遇到问题先体检"><a href="#遇到问题先体检" class="headerlink" title="遇到问题先体检"></a>遇到问题先体检</h3><figure class="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">brew doctor</span><br><span class="line">brew config</span><br></pre></td></tr></table></figure><p>这套就已经很够用了。</p><hr><h2 id="🚀-新-Mac-初始化时的常用顺序"><a href="#🚀-新-Mac-初始化时的常用顺序" class="headerlink" title="🚀 新 Mac 初始化时的常用顺序"></a>🚀 新 Mac 初始化时的常用顺序</h2><p>如果是新电脑，我一般会按这个顺序来：</p><ol><li>安装 Xcode Command Line Tools</li><li>安装 Homebrew</li><li>配好 <code>brew shellenv</code></li><li>检查 <code>brew --version</code></li><li>安装开发工具</li><li>安装 GUI 应用</li><li>按需启动服务</li></ol><p>命令流：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">xcode-select --install</span><br><span class="line">/bin/bash -c <span class="string">&quot;<span class="subst">$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)</span>&quot;</span></span><br><span class="line"><span class="built_in">eval</span> <span class="string">&quot;<span class="subst">$(/opt/homebrew/bin/brew shellenv)</span>&quot;</span></span><br><span class="line">brew --version</span><br><span class="line">brew install git node python</span><br><span class="line">brew install --cask visual-studio-code iterm2</span><br></pre></td></tr></table></figure><blockquote><p>📌 如果是 Intel Mac，把 <code>shellenv</code> 路径替换成 <code>/usr/local/bin/brew</code>。</p></blockquote><hr><h2 id="🔗-官方参考"><a href="#🔗-官方参考" class="headerlink" title="🔗 官方参考"></a>🔗 官方参考</h2><ul><li>官网：<a href="https://brew.sh/">https://brew.sh/</a></li><li>安装文档：<a href="https://docs.brew.sh/Installation">https://docs.brew.sh/Installation</a></li><li>文档总入口：<a href="https://docs.brew.sh/">https://docs.brew.sh/</a></li><li>命令手册：<a href="https://docs.brew.sh/Manpage"><code>brew(1)</code></a></li><li>FAQ：<a href="https://docs.brew.sh/FAQ">https://docs.brew.sh/FAQ</a></li></ul><hr><h2 id="🪄-超短版"><a href="#🪄-超短版" class="headerlink" title="🪄 超短版"></a>🪄 超短版</h2><p>只记住这些就已经能覆盖大多数日常场景：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">brew install 包名</span><br><span class="line">brew install --cask 应用名</span><br><span class="line">brew list</span><br><span class="line">brew update</span><br><span class="line">brew upgrade</span><br><span class="line">brew cleanup</span><br><span class="line">brew doctor</span><br><span class="line">brew services list</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">Homebrew 的安装、路径概念和常用操作整理。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="开发工具" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
    
    <category term="macOS" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/macOS/"/>
    
    
    <category term="开发环境" scheme="https://www.shallowrecall.top/tags/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/"/>
    
    <category term="macOS" scheme="https://www.shallowrecall.top/tags/macOS/"/>
    
    <category term="Homebrew" scheme="https://www.shallowrecall.top/tags/Homebrew/"/>
    
    <category term="包管理" scheme="https://www.shallowrecall.top/tags/%E5%8C%85%E7%AE%A1%E7%90%86/"/>
    
  </entry>
  
  <entry>
    <title>Git 与 GitHub SSH 指南</title>
    <link href="https://www.shallowrecall.top/posts/a084796a.html"/>
    <id>https://www.shallowrecall.top/posts/a084796a.html</id>
    <published>2026-06-24T02:10:00.000Z</published>
    <updated>2026-06-24T02:10:00.000Z</updated>
    
    <content type="html"><![CDATA[<h1 id="🧰-Git-与-GitHub-SSH-指南"><a href="#🧰-Git-与-GitHub-SSH-指南" class="headerlink" title="🧰 Git 与 GitHub SSH 指南"></a>🧰 Git 与 GitHub SSH 指南</h1><span id="more"></span><blockquote><p>✨ 一篇够用的 Git / GitHub / SSH 笔记，适合新电脑初始化、日常查命令、排查连接问题。</p></blockquote><hr><h2 id="🧠-先记住一句话"><a href="#🧠-先记住一句话" class="headerlink" title="🧠 先记住一句话"></a>🧠 先记住一句话</h2><p>Git 负责版本管理，GitHub 负责远程托管。</p><p>日常最核心的流程就是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">修改文件 -&gt; git add -&gt; git commit -&gt; git push</span><br></pre></td></tr></table></figure><hr><h2 id="📦-Git-是什么"><a href="#📦-Git-是什么" class="headerlink" title="📦 Git 是什么"></a>📦 Git 是什么</h2><p>可以把 Git 理解成一个“文件时间机器 + 协作记录器”。</p><p>几个高频概念先混个脸熟：</p><ul><li><code>workspace</code>：工作区，你正在改的文件</li><li><code>staging area</code>：暂存区，准备提交的内容</li><li><code>repository</code>：仓库，保存历史记录的地方</li><li><code>commit</code>：一次提交</li><li><code>branch</code>：分支</li><li><code>remote</code>：远程仓库，比如 GitHub</li></ul><blockquote><p>💡 简化理解：工作区像草稿纸，暂存区像待寄出信封，提交就是正式存档。</p></blockquote><hr><h2 id="💻-安装-Git"><a href="#💻-安装-Git" class="headerlink" title="💻 安装 Git"></a>💻 安装 Git</h2><h3 id="🪟-Windows"><a href="#🪟-Windows" class="headerlink" title="🪟 Windows"></a>🪟 Windows</h3><p>官方入口：</p><ul><li>Git 安装页：<a href="https://git-scm.com/install/windows">https://git-scm.com/install/windows</a></li><li>Git 下载页：<a href="https://git-scm.com/download/win">https://git-scm.com/download/win</a></li></ul><p>安装建议：</p><ol><li>直接安装 <code>Git for Windows</code></li><li>大部分选项保持默认即可</li><li>保留 <code>Git Bash</code></li><li>PowerShell 里也可以照常用 Git</li></ol><p>安装后检查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git --version</span><br></pre></td></tr></table></figure><p>看到类似输出就说明没问题：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git version 2.xx.x</span><br></pre></td></tr></table></figure><h3 id="🍎-macOS"><a href="#🍎-macOS" class="headerlink" title="🍎 macOS"></a>🍎 macOS</h3><figure class="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">brew install git</span><br><span class="line">git --version</span><br></pre></td></tr></table></figure><h3 id="🐧-Linux"><a href="#🐧-Linux" class="headerlink" title="🐧 Linux"></a>🐧 Linux</h3><p>Debian / Ubuntu：</p><figure class="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">sudo apt update</span><br><span class="line">sudo apt install git</span><br></pre></td></tr></table></figure><p>Fedora：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo dnf install git</span><br></pre></td></tr></table></figure><p>CentOS / RHEL：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo yum install git</span><br></pre></td></tr></table></figure><hr><h2 id="⚙️-首次全局配置"><a href="#⚙️-首次全局配置" class="headerlink" title="⚙️ 首次全局配置"></a>⚙️ 首次全局配置</h2><p>安装完 Git，第一件事就是配身份信息：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global user.name <span class="string">&quot;你的名字&quot;</span></span><br><span class="line">git config --global user.email <span class="string">&quot;你的邮箱&quot;</span></span><br></pre></td></tr></table></figure><p>查看配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global --list</span><br></pre></td></tr></table></figure><p>查看单项：</p><figure class="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 config --global user.name</span><br><span class="line">git config --global user.email</span><br></pre></td></tr></table></figure><p>顺手建议配上这两个：</p><figure class="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 config --global init.defaultBranch main</span><br><span class="line">git config --global color.ui auto</span><br></pre></td></tr></table></figure><p>查看所有配置来源：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --list --show-origin</span><br></pre></td></tr></table></figure><blockquote><p>✅ <code>user.email</code> 最好和 GitHub 上使用的邮箱保持一致。</p></blockquote><hr><h2 id="🔐-GitHub-SSH-配置"><a href="#🔐-GitHub-SSH-配置" class="headerlink" title="🔐 GitHub SSH 配置"></a>🔐 GitHub SSH 配置</h2><p>SSH 的好处：</p><ul><li>不用每次推送都输账号密码</li><li>更适合长期开发</li><li>多仓库、多设备时更顺手</li></ul><h3 id="1-👀-先看本地有没有-key"><a href="#1-👀-先看本地有没有-key" class="headerlink" title="1. 👀 先看本地有没有 key"></a>1. 👀 先看本地有没有 key</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> ~/.ssh</span><br></pre></td></tr></table></figure><p>常见文件：</p><ul><li><code>id_ed25519</code>：私钥</li><li><code>id_ed25519.pub</code>：公钥</li><li><code>id_rsa</code></li><li><code>id_rsa.pub</code></li></ul><p>如果已经有现成的 key，可以先看看是不是你要继续用的那一套。</p><h3 id="2-🪄-生成新的-SSH-key"><a href="#2-🪄-生成新的-SSH-key" class="headerlink" title="2. 🪄 生成新的 SSH key"></a>2. 🪄 生成新的 SSH key</h3><p>优先推荐 <code>ed25519</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -C <span class="string">&quot;你的GitHub邮箱&quot;</span></span><br></pre></td></tr></table></figure><p>如果环境太老不支持，再用 <code>rsa</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t rsa -b 4096 -C <span class="string">&quot;你的GitHub邮箱&quot;</span></span><br></pre></td></tr></table></figure><p>过程中会提示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Enter file in which to save the key</span><br></pre></td></tr></table></figure><p>直接回车即可，默认会保存到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/.ssh/id_ed25519</span><br></pre></td></tr></table></figure><p>然后会让你输入 <code>passphrase</code>：</p><ul><li>直接回车：方便</li><li>设置密码：更安全</li></ul><h3 id="3-🚀-启动-ssh-agent-并添加-key"><a href="#3-🚀-启动-ssh-agent-并添加-key" class="headerlink" title="3. 🚀 启动 ssh-agent 并添加 key"></a>3. 🚀 启动 ssh-agent 并添加 key</h3><h4 id="Windows-Git-Bash"><a href="#Windows-Git-Bash" class="headerlink" title="Windows Git Bash"></a>Windows Git Bash</h4><figure class="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">eval</span> <span class="string">&quot;<span class="subst">$(ssh-agent -s)</span>&quot;</span></span><br><span class="line">ssh-add ~/.ssh/id_ed25519</span><br></pre></td></tr></table></figure><h4 id="Windows-PowerShell"><a href="#Windows-PowerShell" class="headerlink" title="Windows PowerShell"></a>Windows PowerShell</h4><p>先看服务：</p><figure class="highlight powershell"><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">Get-Service</span> ssh<span class="literal">-agent</span></span><br></pre></td></tr></table></figure><p>启动服务：</p><figure class="highlight powershell"><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">Start-Service</span> ssh<span class="literal">-agent</span></span><br></pre></td></tr></table></figure><p>添加私钥：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh<span class="literal">-add</span> <span class="variable">$HOME</span>\.ssh\id_ed25519</span><br></pre></td></tr></table></figure><h4 id="macOS-Linux"><a href="#macOS-Linux" class="headerlink" title="macOS / Linux"></a>macOS / Linux</h4><figure class="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">eval</span> <span class="string">&quot;<span class="subst">$(ssh-agent -s)</span>&quot;</span></span><br><span class="line">ssh-add ~/.ssh/id_ed25519</span><br></pre></td></tr></table></figure><h3 id="4-📋-复制公钥"><a href="#4-📋-复制公钥" class="headerlink" title="4. 📋 复制公钥"></a>4. 📋 复制公钥</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> ~/.ssh/id_ed25519.pub</span><br></pre></td></tr></table></figure><p>复制整行内容，形态大概像这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-ed25519 AAAA.... your_email@example.com</span><br></pre></td></tr></table></figure><h3 id="5-🌍-添加到-GitHub"><a href="#5-🌍-添加到-GitHub" class="headerlink" title="5. 🌍 添加到 GitHub"></a>5. 🌍 添加到 GitHub</h3><p>GitHub 路径：</p><ol><li><code>Settings</code></li><li><code>SSH and GPG keys</code></li><li><code>New SSH key</code></li><li><code>Title</code> 填设备名，比如 <code>My Laptop</code></li><li><code>Key</code> 粘贴公钥</li><li>保存</li></ol><p>官方文档：</p><ul><li>SSH 总入口：<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh">https://docs.github.com/en/authentication/connecting-to-github-with-ssh</a></li><li>生成 SSH key：<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent">https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent</a></li><li>添加 SSH key：<a href="https://docs.github.com/articles/adding-a-new-ssh-key-to-your-github-account">https://docs.github.com/articles/adding-a-new-ssh-key-to-your-github-account</a></li></ul><h3 id="6-✅-测试-SSH-连接"><a href="#6-✅-测试-SSH-连接" class="headerlink" title="6. ✅ 测试 SSH 连接"></a>6. ✅ 测试 SSH 连接</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -T git@github.com</span><br></pre></td></tr></table></figure><p>第一次通常会看到：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">The authenticity of host &#x27;github.com (...)&#x27; can&#x27;t be established.</span><br><span class="line">Are you sure you want to continue connecting (yes/no/[fingerprint])?</span><br></pre></td></tr></table></figure><p>输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yes</span><br></pre></td></tr></table></figure><p>成功后一般会看到类似内容：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Hi &lt;your-name&gt;! You&#x27;ve successfully authenticated...</span><br></pre></td></tr></table></figure><p>测试文档：</p><ul><li><a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection">https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection</a></li></ul><hr><h2 id="🧾-SSH-配置文件"><a href="#🧾-SSH-配置文件" class="headerlink" title="🧾 SSH 配置文件"></a>🧾 SSH 配置文件</h2><p>如果你有多个 GitHub 账号，或者想指定不同私钥，可以配置 <code>~/.ssh/config</code>。</p><h3 id="单账号示例"><a href="#单账号示例" class="headerlink" title="单账号示例"></a>单账号示例</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Host github.com</span><br><span class="line">  HostName github.com</span><br><span class="line">  User git</span><br><span class="line">  IdentityFile ~/.ssh/id_ed25519</span><br><span class="line">  IdentitiesOnly yes</span><br></pre></td></tr></table></figure><h3 id="多账号示例"><a href="#多账号示例" class="headerlink" title="多账号示例"></a>多账号示例</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Host github-personal</span><br><span class="line">  HostName github.com</span><br><span class="line">  User git</span><br><span class="line">  IdentityFile ~/.ssh/id_ed25519_personal</span><br><span class="line">  IdentitiesOnly yes</span><br><span class="line"></span><br><span class="line">Host github-work</span><br><span class="line">  HostName github.com</span><br><span class="line">  User git</span><br><span class="line">  IdentityFile ~/.ssh/id_ed25519_work</span><br><span class="line">  IdentitiesOnly yes</span><br></pre></td></tr></table></figure><p>这时远程地址可以写成：</p><figure class="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@github-personal:username/repo.git</span><br><span class="line">git@github-work:company/repo.git</span><br></pre></td></tr></table></figure><blockquote><p>🔁 多账号时，<code>~/.ssh/config</code> 很值。</p></blockquote><hr><h2 id="🏗️-新仓库常用流程"><a href="#🏗️-新仓库常用流程" class="headerlink" title="🏗️ 新仓库常用流程"></a>🏗️ 新仓库常用流程</h2><h3 id="📁-本地新建仓库"><a href="#📁-本地新建仓库" class="headerlink" title="📁 本地新建仓库"></a>📁 本地新建仓库</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git init</span><br><span class="line">git status</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;init&quot;</span></span><br></pre></td></tr></table></figure><h3 id="🔗-关联远程-GitHub-仓库"><a href="#🔗-关联远程-GitHub-仓库" class="headerlink" title="🔗 关联远程 GitHub 仓库"></a>🔗 关联远程 GitHub 仓库</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git remote add origin git@github.com:用户名/仓库名.git</span><br><span class="line">git remote -v</span><br><span class="line">git push -u origin main</span><br></pre></td></tr></table></figure><p>说明：</p><ul><li><code>origin</code>：远程仓库默认名</li><li><code>-u</code>：建立本地和远程分支跟踪关系</li></ul><h3 id="📥-克隆远程仓库"><a href="#📥-克隆远程仓库" class="headerlink" title="📥 克隆远程仓库"></a>📥 克隆远程仓库</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><p>克隆到指定目录：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> git@github.com:用户名/仓库名.git my-project</span><br></pre></td></tr></table></figure><hr><h2 id="⚡-常用命令速查"><a href="#⚡-常用命令速查" class="headerlink" title="⚡ 常用命令速查"></a>⚡ 常用命令速查</h2><h3 id="🔎-看状态-看历史"><a href="#🔎-看状态-看历史" class="headerlink" title="🔎 看状态 / 看历史"></a>🔎 看状态 / 看历史</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git status</span><br><span class="line">git <span class="built_in">log</span></span><br><span class="line">git <span class="built_in">log</span> --oneline</span><br><span class="line">git <span class="built_in">log</span> --oneline --graph --decorate --all</span><br><span class="line">git show</span><br></pre></td></tr></table></figure><h3 id="📌-暂存-提交"><a href="#📌-暂存-提交" class="headerlink" title="📌 暂存 / 提交"></a>📌 暂存 / 提交</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git add .</span><br><span class="line">git add 文件名</span><br><span class="line">git commit -m <span class="string">&quot;提交说明&quot;</span></span><br><span class="line">git commit --amend</span><br></pre></td></tr></table></figure><p><code>git commit --amend</code>：修改最近一次提交。</p><h3 id="🪞-查看差异"><a href="#🪞-查看差异" class="headerlink" title="🪞 查看差异"></a>🪞 查看差异</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git diff</span><br><span class="line">git diff --staged</span><br><span class="line">git diff 分支1..分支2</span><br></pre></td></tr></table></figure><h3 id="🌿-分支操作"><a href="#🌿-分支操作" class="headerlink" title="🌿 分支操作"></a>🌿 分支操作</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">git branch</span><br><span class="line">git branch -a</span><br><span class="line">git branch 分支名</span><br><span class="line">git switch 分支名</span><br><span class="line">git switch -c 新分支名</span><br><span class="line">git checkout 分支名</span><br><span class="line">git merge 分支名</span><br><span class="line">git branch -d 分支名</span><br><span class="line">git branch -D 分支名</span><br></pre></td></tr></table></figure><p>小抄：</p><ul><li><code>git switch</code>：更适合切分支</li><li><code>git checkout</code>：老牌多功能选手</li><li><code>-d</code>：安全删除</li><li><code>-D</code>：强制删除</li></ul><h3 id="☁️-远程相关"><a href="#☁️-远程相关" class="headerlink" title="☁️ 远程相关"></a>☁️ 远程相关</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">git remote -v</span><br><span class="line">git fetch</span><br><span class="line">git pull</span><br><span class="line">git push</span><br><span class="line">git push -u origin main</span><br><span class="line">git remote remove origin</span><br><span class="line">git remote set-url origin git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><h3 id="🧹-撤销与恢复"><a href="#🧹-撤销与恢复" class="headerlink" title="🧹 撤销与恢复"></a>🧹 撤销与恢复</h3><p>撤销工作区修改：</p><figure class="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 restore 文件名</span><br><span class="line">git restore .</span><br></pre></td></tr></table></figure><p>撤销暂存区：</p><figure class="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 restore --staged 文件名</span><br><span class="line">git restore --staged .</span><br></pre></td></tr></table></figure><p>回退提交：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git reset --soft HEAD~1</span><br><span class="line">git reset --mixed HEAD~1</span><br><span class="line">git reset --hard HEAD~1</span><br></pre></td></tr></table></figure><p>区别：</p><ul><li><code>--soft</code>：回退提交，保留暂存区和工作区</li><li><code>--mixed</code>：回退提交和暂存区，保留工作区</li><li><code>--hard</code>：全部回退，危险</li></ul><p>反向生成一次新提交：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git revert 提交哈希</span><br></pre></td></tr></table></figure><h3 id="🧳-暂存现场"><a href="#🧳-暂存现场" class="headerlink" title="🧳 暂存现场"></a>🧳 暂存现场</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git stash</span><br><span class="line">git stash list</span><br><span class="line">git stash pop</span><br><span class="line">git stash apply</span><br><span class="line">git stash drop</span><br></pre></td></tr></table></figure><hr><h2 id="🔁-常见协作流程"><a href="#🔁-常见协作流程" class="headerlink" title="🔁 常见协作流程"></a>🔁 常见协作流程</h2><h3 id="日常提交"><a href="#日常提交" class="headerlink" title="日常提交"></a>日常提交</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git status</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;feat: 增加 xxx&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure><h3 id="拉取最新代码"><a href="#拉取最新代码" class="headerlink" title="拉取最新代码"></a>拉取最新代码</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git pull</span><br></pre></td></tr></table></figure><p>更稳一点的拆开写法：</p><figure class="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 fetch</span><br><span class="line">git merge</span><br></pre></td></tr></table></figure><h3 id="新功能分支开发"><a href="#新功能分支开发" class="headerlink" title="新功能分支开发"></a>新功能分支开发</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git switch -c feature/login</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;feat: add login page&quot;</span></span><br><span class="line">git push -u origin feature/login</span><br></pre></td></tr></table></figure><h3 id="合并分支"><a href="#合并分支" class="headerlink" title="合并分支"></a>合并分支</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git switch main</span><br><span class="line">git pull</span><br><span class="line">git merge feature/login</span><br><span class="line">git push</span><br></pre></td></tr></table></figure><hr><h2 id="🏷️-提交信息怎么写"><a href="#🏷️-提交信息怎么写" class="headerlink" title="🏷️ 提交信息怎么写"></a>🏷️ 提交信息怎么写</h2><p>常见前缀：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">feat: 新功能</span><br><span class="line">fix: 修复问题</span><br><span class="line">docs: 文档修改</span><br><span class="line">style: 格式调整</span><br><span class="line">refactor: 重构</span><br><span class="line">test: 测试相关</span><br><span class="line">chore: 杂项维护</span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git commit -m <span class="string">&quot;feat: add user login&quot;</span></span><br><span class="line">git commit -m <span class="string">&quot;fix: resolve avatar loading error&quot;</span></span><br><span class="line">git commit -m <span class="string">&quot;docs: update README&quot;</span></span><br></pre></td></tr></table></figure><hr><h2 id="🩹-常见问题排查"><a href="#🩹-常见问题排查" class="headerlink" title="🩹 常见问题排查"></a>🩹 常见问题排查</h2><h3 id="❌-git-命令找不到"><a href="#❌-git-命令找不到" class="headerlink" title="❌ git 命令找不到"></a>❌ <code>git</code> 命令找不到</h3><p>先试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git --version</span><br></pre></td></tr></table></figure><p>如果不行，排查：</p><ol><li>Git 是否安装成功</li><li>终端是否重新打开</li><li>环境变量里是否有 Git</li></ol><h3 id="❌-SSH-测试失败"><a href="#❌-SSH-测试失败" class="headerlink" title="❌ SSH 测试失败"></a>❌ SSH 测试失败</h3><p>检查本地 key：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> ~/.ssh</span><br></pre></td></tr></table></figure><p>检查 agent 里有没有加载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-add -l</span><br></pre></td></tr></table></figure><p>重新添加：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-add ~/.ssh/id_ed25519</span><br></pre></td></tr></table></figure><p>详细调试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -vT git@github.com</span><br></pre></td></tr></table></figure><h3 id="❌-推送时报-Permission-denied-publickey"><a href="#❌-推送时报-Permission-denied-publickey" class="headerlink" title="❌ 推送时报 Permission denied (publickey)"></a>❌ 推送时报 <code>Permission denied (publickey)</code></h3><p>按这个顺序排查：</p><ol><li>本地有没有 SSH key</li><li>公钥有没有加到 GitHub</li><li>远程地址是不是 SSH 格式</li><li><code>ssh-agent</code> 有没有加载私钥</li><li><code>~/.ssh/config</code> 有没有写错</li></ol><p>查看远程地址：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote -v</span><br></pre></td></tr></table></figure><p>SSH 格式应类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><p>如果当前是 HTTPS，可切换成 SSH：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote set-url origin git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><h3 id="⚔️-合并冲突"><a href="#⚔️-合并冲突" class="headerlink" title="⚔️ 合并冲突"></a>⚔️ 合并冲突</h3><p>手动处理冲突文件后：</p><figure class="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 add .</span><br><span class="line">git commit -m <span class="string">&quot;fix: resolve merge conflict&quot;</span></span><br></pre></td></tr></table></figure><p>查看当前状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git status</span><br></pre></td></tr></table></figure><hr><h2 id="🧷-最常用命令清单"><a href="#🧷-最常用命令清单" class="headerlink" title="🧷 最常用命令清单"></a>🧷 最常用命令清单</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">git --version</span><br><span class="line">git config --global --list</span><br><span class="line">git init</span><br><span class="line">git <span class="built_in">clone</span> git@github.com:用户名/仓库名.git</span><br><span class="line">git status</span><br><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;message&quot;</span></span><br><span class="line">git <span class="built_in">log</span> --oneline</span><br><span class="line">git diff</span><br><span class="line">git branch</span><br><span class="line">git switch -c new-branch</span><br><span class="line">git merge branch-name</span><br><span class="line">git pull</span><br><span class="line">git push</span><br><span class="line">git remote -v</span><br><span class="line">git stash</span><br><span class="line">ssh-keygen -t ed25519 -C <span class="string">&quot;你的GitHub邮箱&quot;</span></span><br><span class="line">ssh-add ~/.ssh/id_ed25519</span><br><span class="line">ssh -T git@github.com</span><br></pre></td></tr></table></figure><hr><h2 id="🎯-推荐记忆顺序"><a href="#🎯-推荐记忆顺序" class="headerlink" title="🎯 推荐记忆顺序"></a>🎯 推荐记忆顺序</h2><p>先记这 8 个：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span></span><br><span class="line">git status</span><br><span class="line">git add</span><br><span class="line">git commit -m</span><br><span class="line">git pull</span><br><span class="line">git push</span><br><span class="line">git switch -c</span><br><span class="line">git <span class="built_in">log</span> --oneline</span><br></pre></td></tr></table></figure><p>再补这些：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">git diff</span><br><span class="line">git merge</span><br><span class="line">git stash</span><br><span class="line">git restore</span><br><span class="line">git remote -v</span><br><span class="line">ssh -T git@github.com</span><br></pre></td></tr></table></figure><hr><h2 id="🚀-新电脑初始化顺序"><a href="#🚀-新电脑初始化顺序" class="headerlink" title="🚀 新电脑初始化顺序"></a>🚀 新电脑初始化顺序</h2><p>推荐照这个顺序来：</p><ol><li>安装 Git</li><li><code>git --version</code></li><li>配置 <code>user.name</code> 和 <code>user.email</code></li><li>生成 SSH key</li><li>添加到 GitHub</li><li>测试 SSH</li><li><code>git clone</code> 一个仓库试跑</li></ol><p>命令版：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">git --version</span><br><span class="line">git config --global user.name <span class="string">&quot;你的名字&quot;</span></span><br><span class="line">git config --global user.email <span class="string">&quot;你的邮箱&quot;</span></span><br><span class="line">git config --global init.defaultBranch main</span><br><span class="line">ssh-keygen -t ed25519 -C <span class="string">&quot;你的GitHub邮箱&quot;</span></span><br><span class="line">ssh-add ~/.ssh/id_ed25519</span><br><span class="line">ssh -T git@github.com</span><br><span class="line">git <span class="built_in">clone</span> git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><hr><h2 id="🔗-官方参考"><a href="#🔗-官方参考" class="headerlink" title="🔗 官方参考"></a>🔗 官方参考</h2><ul><li>Git Windows 安装：<a href="https://git-scm.com/install/windows">https://git-scm.com/install/windows</a></li><li>Git 安装总入口：<a href="https://git-scm.com/install/">https://git-scm.com/install/</a></li><li>Git 安装说明：<a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git">https://git-scm.com/book/en/v2/Getting-Started-Installing-Git</a></li><li>GitHub SSH 总入口：<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh">https://docs.github.com/en/authentication/connecting-to-github-with-ssh</a></li><li>生成 SSH key：<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent">https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent</a></li><li>添加 SSH key：<a href="https://docs.github.com/articles/adding-a-new-ssh-key-to-your-github-account">https://docs.github.com/articles/adding-a-new-ssh-key-to-your-github-account</a></li><li>测试 SSH 连接：<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection">https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection</a></li></ul><hr><h2 id="🪄-终极超短版"><a href="#🪄-终极超短版" class="headerlink" title="🪄 终极超短版"></a>🪄 终极超短版</h2><p>只看最精简流程的话，就是这几步：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">git config --global user.name <span class="string">&quot;你的名字&quot;</span></span><br><span class="line">git config --global user.email <span class="string">&quot;你的邮箱&quot;</span></span><br><span class="line">ssh-keygen -t ed25519 -C <span class="string">&quot;你的GitHub邮箱&quot;</span></span><br><span class="line">ssh-add ~/.ssh/id_ed25519</span><br><span class="line">ssh -T git@github.com</span><br><span class="line">git <span class="built_in">clone</span> git@github.com:用户名/仓库名.git</span><br></pre></td></tr></table></figure><p>然后日常：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git add .</span><br><span class="line">git commit -m <span class="string">&quot;message&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">一篇够用的 Git、GitHub 和 SSH 配置与日常使用笔记。</summary>
    
    
    
    <category term="技术笔记" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/"/>
    
    <category term="开发工具" scheme="https://www.shallowrecall.top/categories/%E6%8A%80%E6%9C%AF%E7%AC%94%E8%AE%B0/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="Git" scheme="https://www.shallowrecall.top/tags/Git/"/>
    
    <category term="GitHub" scheme="https://www.shallowrecall.top/tags/GitHub/"/>
    
    <category term="SSH" scheme="https://www.shallowrecall.top/tags/SSH/"/>
    
    <category term="开发环境" scheme="https://www.shallowrecall.top/tags/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/"/>
    
  </entry>
  
  <entry>
    <title>MyBatis-Plus</title>
    <link href="https://www.shallowrecall.top/posts/1276cc28.html"/>
    <id>https://www.shallowrecall.top/posts/1276cc28.html</id>
    <published>2024-05-02T22:41:52.000Z</published>
    <updated>2026-10-16T12:30:20.000Z</updated>
    
    <content type="html"><![CDATA[<p>大家在日常开发中应该能发现，单表的CRUD功能代码重复度很高，也没有什么难度。而这部分代码量往往比较大，开发起来比较费时。</p><p>因此，目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.</p><p>当然，MybatisPlus不仅仅可以简化单表操作，而且还对Mybatis的功能有很多的增强。可以让我们的开发更加的简单，高效。</p><p>通过今天的学习，我们要达成下面的目标：</p><ul><li>能利用MybatisPlus实现基本的CRUD</li><li>会使用条件构建造器构建查询和更新语句</li><li>会使用MybatisPlus中的常用注解</li><li>会使用MybatisPlus处理枚举、JSON类型字段</li><li>会使用MybatisPlus实现分页</li></ul><h1 id="1-快速入门"><a class="headerlink" href="#1-快速入门" title="1.快速入门"></a><strong>1.快速入门</strong></h1><p>为了方便测试，我们先创建一个新的项目，并准备一些基础数据。</p><h2 id="1-1-环境准备"><a class="headerlink" href="#1-1-环境准备" title="1.1.环境准备"></a><strong>1.1.环境准备</strong></h2><p>复制课前资料提供好的一个项目到你的工作空间（不要包含空格和特殊字符）：</p><p><img alt="image-20240502225914784" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225914784.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225914784.png"/></p><p>然后用你的IDEA工具打开，项目结构如下：</p><p><img alt="image-20240502225922637" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225922637.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225922637.png"/></p><p>注意配置一下项目的JDK版本为JDK11。首先点击项目结构设置：</p><p><img alt="image-20240502225929704" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225929704.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225929704.png"/></p><p>在弹窗中配置JDK：</p><p><img alt="image-20240502225936311" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225936311.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225936311.png"/></p><p>接下来，要导入两张表，在课前资料中已经提供了SQL文件：</p><p><img alt="image-20240502225941808" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225941808.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225941808.png"/></p><p>对应的数据库表结构如下：</p><p><img alt="image-20240502225946414" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225946414.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225946414.png"/></p><p>最后，在<code>application.yaml</code>中修改jdbc参数为你自己的数据库参数：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><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="attr">datasource:</span></span><br/><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;serverTimezone=Asia/Shanghai</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">username:</span> <span class="string">root</span></span><br/><span class="line">    <span class="attr">password:</span> <span class="string">MySQL123</span></span><br/><span class="line"><span class="attr">logging:</span></span><br/><span class="line">  <span class="attr">level:</span></span><br/><span class="line">    <span class="attr">com.itheima:</span> <span class="string">debug</span></span><br/><span class="line">  <span class="attr">pattern:</span></span><br/><span class="line">    <span class="attr">dateformat:</span> <span class="string">HH:mm:ss</span></span><br/></pre></td></tr></table></figure><h2 id="1-2-快速开始"><a class="headerlink" href="#1-2-快速开始" title="1.2.快速开始"></a><strong>1.2.快速开始</strong></h2><p>比如我们要实现User表的CRUD，只需要下面几步：</p><ul><li>引入MybatisPlus依赖</li><li>定义Mapper</li></ul><h3 id="1-2-1引入依赖"><a class="headerlink" href="#1-2-1引入依赖" title="1.2.1引入依赖"></a><strong>1.2.1引入依赖</strong></h3><p>MybatisPlus提供了starter，实现了自动Mybatis以及MybatisPlus的自动装配功能，坐标如下：</p><figure class="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.baomidou<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-boot-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.3.1<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><p>由于这个starter包含对mybatis的自动装配，因此完全可以替换掉Mybatis的starter。 最终，项目的依赖如下：</p><figure class="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/></pre></td><td class="code"><pre><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>com.baomidou<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-boot-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.3.1<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>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">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">dependencies</span>&gt;</span></span><br/></pre></td></tr></table></figure><h3 id="1-2-2-定义Mapper"><a class="headerlink" href="#1-2-2-定义Mapper" title="1.2.2.定义Mapper"></a><strong>1.2.2.定义Mapper</strong></h3><p>为了简化单表CRUD，MybatisPlus提供了一个基础的<code>BaseMapper</code>接口，其中已经实现了单表的CRUD：</p><p><img alt="image-20240502225958501" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502225958501.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502225958501.png"/></p><p>因此我们自定义的Mapper只要实现了这个<code>BaseMapper</code>，就无需自己实现单表CRUD了。 修改mp-demo中的<code>com.itheima.mp.mapper</code>包下的<code>UserMapper</code>接口，让其继承<code>BaseMapper</code>：</p><p><img alt="image-20240502230003985" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230003985.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230003985.png"/></p><p>代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.mapper;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.core.mapper.BaseMapper;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserMapper</span> <span class="keyword">extends</span> <span class="title class_">BaseMapper</span>&lt;User&gt; {</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="1-2-3-测试"><a class="headerlink" href="#1-2-3-测试" title="1.2.3.测试"></a><strong>1.2.3.测试</strong></h3><p>新建一个测试类，编写几个单元测试，测试基本的CRUD功能：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/><span class="line">42</span><br/><span class="line">43</span><br/><span class="line">44</span><br/><span class="line">45</span><br/><span class="line">46</span><br/><span class="line">47</span><br/><span class="line">48</span><br/><span class="line">49</span><br/><span class="line">50</span><br/><span class="line">51</span><br/><span class="line">52</span><br/><span class="line">53</span><br/><span class="line">54</span><br/><span class="line">55</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.mapper;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.Test;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br/><span class="line"><span class="keyword">import</span> java.util.List;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@SpringBootTest</span></span><br/><span class="line"><span class="keyword">class</span> <span class="title class_">UserMapperTest</span> {</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> UserMapper userMapper;</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_">testInsert</span><span class="params">()</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><br/><span class="line">        user.setId(<span class="number">5L</span>);</span><br/><span class="line">        user.setUsername(<span class="string">"Lucy"</span>);</span><br/><span class="line">        user.setPassword(<span class="string">"123"</span>);</span><br/><span class="line">        user.setPhone(<span class="string">"18688990011"</span>);</span><br/><span class="line">        user.setBalance(<span class="number">200</span>);</span><br/><span class="line">        user.setInfo(<span class="string">"{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}"</span>);</span><br/><span class="line">        user.setCreateTime(LocalDateTime.now());</span><br/><span class="line">        user.setUpdateTime(LocalDateTime.now());</span><br/><span class="line">        userMapper.insert(user);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">testSelectById</span><span class="params">()</span> {</span><br/><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userMapper.selectById(<span class="number">5L</span>);</span><br/><span class="line">        System.out.println(<span class="string">"user = "</span> + user);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">testSelectByIds</span><span class="params">()</span> {</span><br/><span class="line">        List&lt;User&gt; users = userMapper.selectBatchIds(List.of(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">3L</span>, <span class="number">4L</span>, <span class="number">5L</span>));</span><br/><span class="line">        users.forEach(System.out::println);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">testUpdateById</span><span class="params">()</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><br/><span class="line">        user.setId(<span class="number">5L</span>);</span><br/><span class="line">        user.setBalance(<span class="number">20000</span>);</span><br/><span class="line">        userMapper.updateById(user);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">testDelete</span><span class="params">()</span> {</span><br/><span class="line">        userMapper.deleteById(<span class="number">5L</span>);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>可以看到，在运行过程中打印出的SQL日志，非常标准：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="number">11</span>:<span class="number">05</span>:<span class="number">01</span>  INFO <span class="number">15524</span> <span class="comment">--- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...</span></span><br/><span class="line"><span class="number">11</span>:<span class="number">05</span>:<span class="number">02</span>  INFO <span class="number">15524</span> <span class="comment">--- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.</span></span><br/><span class="line"><span class="number">11</span>:<span class="number">05</span>:<span class="number">02</span> DEBUG <span class="number">15524</span> <span class="comment">--- [           main] c.i.mp.mapper.UserMapper.selectById      : ==&gt;  Preparing: SELECT id,username,password,phone,info,status,balance,create_time,update_time FROM user WHERE id=?</span></span><br/><span class="line"><span class="number">11</span>:<span class="number">05</span>:<span class="number">02</span> DEBUG <span class="number">15524</span> <span class="comment">--- [           main] c.i.mp.mapper.UserMapper.selectById      : ==&gt; Parameters: 5(Long)</span></span><br/><span class="line"><span class="number">11</span>:<span class="number">05</span>:<span class="number">02</span> DEBUG <span class="number">15524</span> <span class="comment">--- [           main] c.i.mp.mapper.UserMapper.selectById      : &lt;==      Total: 1</span></span><br/><span class="line"><span class="keyword">user</span> <span class="operator">=</span> <span class="keyword">User</span>(id<span class="operator">=</span><span class="number">5</span>, username<span class="operator">=</span>Lucy, password<span class="operator">=</span><span class="number">123</span>, phone<span class="operator">=</span><span class="number">18688990011</span>, info<span class="operator">=</span>{"age": <span class="number">21</span>}, status<span class="operator">=</span><span class="number">1</span>, balance<span class="operator">=</span><span class="number">20000</span>, createTime<span class="operator">=</span>Fri Jun <span class="number">30</span> <span class="number">11</span>:<span class="number">02</span>:<span class="number">30</span> CST <span class="number">2023</span>, updateTime<span class="operator">=</span>Fri Jun <span class="number">30</span> <span class="number">11</span>:<span class="number">02</span>:<span class="number">30</span> CST <span class="number">2023</span>)</span><br/></pre></td></tr></table></figure><p>只需要继承BaseMapper就能省去所有的单表CRUD，是不是非常简单！</p><h2 id="1-3-常见注解"><a class="headerlink" href="#1-3-常见注解" title="1.3.常见注解"></a><strong>1.3.常见注解</strong></h2><p>在刚刚的入门案例中，我们仅仅引入了依赖，继承了BaseMapper就能使用MybatisPlus，非常简单。但是问题来了： MybatisPlus如何知道我们要查询的是哪张表？表中有哪些字段呢？</p><p>大家回忆一下，UserMapper在继承BaseMapper的时候指定了一个泛型：</p><p><img alt="image-20240502230020175" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230020175.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230020175.png"/></p><p>泛型中的User就是与数据库对应的PO.</p><p>MybatisPlus就是根据PO实体的信息来推断出表的信息，从而生成SQL的。默认情况下：</p><ul><li>MybatisPlus会把PO实体的类名驼峰转下划线作为表名</li><li>MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名，并根据变量类型推断字段类型</li><li>MybatisPlus会把名为id的字段作为主键</li></ul><p>但很多情况下，默认的实现与实际场景不符，因此MybatisPlus提供了一些注解便于我们声明表信息。</p><h3 id="1-3-1-TableName"><a class="headerlink" href="#1-3-1-TableName" title="1.3.1.@TableName"></a><strong>1.3.1.@TableName</strong></h3><p>说明：</p><ul><li>描述：表名注解，标识实体类对应的表</li><li>使用位置：实体类</li></ul><p>示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@TableName("user")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> {</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><br/></pre></td></tr></table></figure><p>TableName注解除了指定表名以外，还可以指定很多其它属性：</p><div class="table-container"><table><thead><tr><th><strong>属性</strong></th><th><strong>类型</strong></th><th><strong>必须指定</strong></th><th><strong>默认值</strong></th><th><strong>描述</strong></th></tr></thead><tbody><tr><td>value</td><td>String</td><td>否</td><td>“”</td><td>表名</td></tr><tr><td>schema</td><td>String</td><td>否</td><td>“”</td><td>schema</td></tr><tr><td>keepGlobalPrefix</td><td>boolean</td><td>否</td><td>false</td><td>是否保持使用全局的 tablePrefix 的值（当全局 tablePrefix 生效时）</td></tr><tr><td>resultMap</td><td>String</td><td>否</td><td>“”</td><td>xml 中 resultMap 的 id（用于满足特定类型的实体类对象绑定）</td></tr><tr><td>autoResultMap</td><td>boolean</td><td>否</td><td>false</td><td>是否自动构建 resultMap 并使用（如果设置 resultMap 则不会进行 resultMap 的自动构建与注入）</td></tr><tr><td>excludeProperty</td><td>String[]</td><td>否</td><td>{}</td><td>需要排除的属性名 @since 3.3.1</td></tr></tbody></table></div><h3 id="1-3-2-TableId"><a class="headerlink" href="#1-3-2-TableId" title="1.3.2.@TableId"></a><strong>1.3.2.@TableId</strong></h3><p>说明：</p><ul><li>描述：主键注解，标识实体类中的主键字段</li><li>使用位置：实体类的主键字段</li></ul><p>示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@TableName("user")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> {</span><br/><span class="line">    <span class="meta">@TableId</span></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><br/></pre></td></tr></table></figure><p><code>TableId</code>注解支持两个属性：</p><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>属性</strong></th><th style="text-align:left"><strong>类型</strong></th><th style="text-align:left"><strong>必须指定</strong></th><th style="text-align:left"><strong>默认值</strong></th><th style="text-align:left"><strong>描述</strong></th></tr></thead><tbody><tr><td style="text-align:left">value</td><td style="text-align:left">String</td><td style="text-align:left">否</td><td style="text-align:left">“”</td><td style="text-align:left">表名</td></tr><tr><td style="text-align:left">type</td><td style="text-align:left">Enum</td><td style="text-align:left">否</td><td style="text-align:left">IdType.NONE</td><td style="text-align:left">指定主键类型</td></tr></tbody></table></div><p><code>IdType</code>支持的类型有：</p><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>值</strong></th><th style="text-align:left"><strong>描述</strong></th></tr></thead><tbody><tr><td style="text-align:left">AUTO</td><td style="text-align:left">数据库 ID 自增</td></tr><tr><td style="text-align:left">NONE</td><td style="text-align:left">无状态，该类型为未设置主键类型（注解里等于跟随全局，全局里约等于 INPUT）</td></tr><tr><td style="text-align:left">INPUT</td><td style="text-align:left">insert 前自行 set 主键值</td></tr><tr><td style="text-align:left">ASSIGN_ID</td><td style="text-align:left">分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)</td></tr><tr><td style="text-align:left">ASSIGN_UUID</td><td style="text-align:left">分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)</td></tr><tr><td style="text-align:left">ID_WORKER</td><td style="text-align:left">分布式全局唯一 ID 长整型类型(please use ASSIGN_ID)</td></tr><tr><td style="text-align:left">UUID</td><td style="text-align:left">32 位 UUID 字符串(please use ASSIGN_UUID)</td></tr><tr><td style="text-align:left">ID_WORKER_STR</td><td style="text-align:left">分布式全局唯一 ID 字符串类型(please use ASSIGN_ID)</td></tr></tbody></table></div><p>这里比较常见的有三种：</p><ul><li><code>AUTO</code>：利用数据库的id自增长</li><li><code>INPUT</code>：手动生成id</li><li><code>ASSIGN_ID</code>：雪花算法生成<code>Long</code>类型的全局唯一id，这是默认的ID策略</li></ul><h3 id="1-3-3-TableField"><a class="headerlink" href="#1-3-3-TableField" title="1.3.3.@TableField"></a><strong>1.3.3.@TableField</strong></h3><p>说明：</p><blockquote><p>描述：普通字段注解</p></blockquote><p>示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><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="meta">@TableName("user")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> {</span><br/><span class="line">    <span class="meta">@TableId</span></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> Integer age;</span><br/><span class="line">    <span class="meta">@TableField("isMarried")</span></span><br/><span class="line">    <span class="keyword">private</span> Boolean isMarried;</span><br/><span class="line">    <span class="meta">@TableField("concat")</span></span><br/><span class="line">    <span class="keyword">private</span> String concat;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>一般情况下我们并不需要给字段添加<code>@TableField</code>注解，一些特殊情况除外：</p><ul><li>成员变量名与数据库字段名不一致</li><li>成员变量是以<code>isXXX</code>命名，按照<code>JavaBean</code>的规范，<code>MybatisPlus</code>识别字段时会把<code>is</code>去除，这就导致与数据库不符。</li><li>成员变量名与数据库一致，但是与数据库的关键字冲突。使用<code>@TableField</code>注解给字段名添加转义字符：````</li></ul><p>支持的其它属性如下：<br/><img alt="image-20240502233148508" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502233148508.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502233148508.png"/></p><h2 id="1-4-常见配置"><a class="headerlink" href="#1-4-常见配置" title="1.4.常见配置"></a><strong>1.4.常见配置</strong></h2><p>MybatisPlus也支持基于yaml文件的自定义配置，详见官方文档：</p><p><a href="https://www.baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE" rel="noopener" target="_blank">https://www.baomidou.com/pages/56bac0/#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE</a></p><p>大多数的配置都有默认值，因此我们都无需配置。但还有一些是没有默认值的，例如:</p><ul><li>实体类的别名扫描包</li><li>全局id类型</li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="attr">mybatis-plus:</span></span><br/><span class="line">  <span class="attr">type-aliases-package:</span> <span class="string">com.itheima.mp.domain.po</span></span><br/><span class="line">  <span class="attr">global-config:</span></span><br/><span class="line">    <span class="attr">db-config:</span></span><br/><span class="line">      <span class="attr">id-type:</span> <span class="string">auto</span> <span class="comment"># 全局id类型为自增长</span></span><br/></pre></td></tr></table></figure><p>需要注意的是，MyBatisPlus也支持手写SQL的，而mapper文件的读取地址可以自己配置：</p><figure class="highlight yaml"><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">mybatis-plus:</span></span><br/><span class="line">  <span class="attr">mapper-locations:</span> <span class="string">"classpath*:/mapper/**/*.xml"</span> <span class="comment"># Mapper.xml文件地址，当前这个是默认值。</span></span><br/></pre></td></tr></table></figure><p>可以看到默认值是<code>classpath*:/mapper/**/*.xml</code>，也就是说我们只要把mapper.xml文件放置这个目录下就一定会被加载。</p><p>例如，我们新建一个<code>UserMapper.xml</code>文件：</p><p><img alt="image-20240502230050365" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230050365.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230050365.png"/></p><p>然后在其中定义一个方法：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?&gt;</span></span><br/><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">mapper</span> <span class="keyword">PUBLIC</span> <span class="string">"-//mybatis.org//DTD Mapper 3.0//EN"</span> <span class="string">"http://mybatis.org/dtd/mybatis-3-mapper.dtd"</span>&gt;</span></span><br/><span class="line"><span class="tag">&lt;<span class="name">mapper</span> <span class="attr">namespace</span>=<span class="string">"com.itheima.mp.mapper.UserMapper"</span>&gt;</span></span><br/><span class="line"></span><br/><span class="line">    <span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"queryById"</span> <span class="attr">resultType</span>=<span class="string">"User"</span>&gt;</span></span><br/><span class="line">        SELECT * FROM user WHERE id = #{id}</span><br/><span class="line">    <span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br/><span class="line"><span class="tag">&lt;/<span class="name">mapper</span>&gt;</span></span><br/></pre></td></tr></table></figure><p>然后在测试类<code>UserMapperTest</code>中测试该方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testQuery</span><span class="params">()</span> {</span><br/><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userMapper.queryById(<span class="number">1L</span>);</span><br/><span class="line">    System.out.println(<span class="string">"user = "</span> + user);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h1 id="2-核心功能"><a class="headerlink" href="#2-核心功能" title="2.核心功能"></a><strong>2.核心功能</strong></h1><p>刚才的案例中都是以id为条件的简单CRUD，一些复杂条件的SQL语句就要用到一些更高级的功能了。</p><h2 id="2-1-条件构造器"><a class="headerlink" href="#2-1-条件构造器" title="2.1.条件构造器"></a><strong>2.1.条件构造器</strong></h2><p>除了新增以外，修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以<code>id</code>作为<code>where</code>条件以外，还支持更加复杂的<code>where</code>条件。</p><p><img alt="image-20240502230058272" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230058272.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230058272.png"/></p><p>参数中的<code>Wrapper</code>就是条件构造的抽象类，其下有很多默认实现，继承关系如图：</p><p><img alt="image-20240502230103049" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230103049.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230103049.png"/></p><p><code>Wrapper</code>的子类<code>AbstractWrapper</code>提供了where中包含的所有条件构造方法：</p><p><img alt="image-20240502230108002" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230108002.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230108002.png"/></p><p>而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法，允许指定查询字段：</p><p><img alt="image-20240502230114564" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230114564.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230114564.png"/></p><p>而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法，允许指定SQL中的SET部分：</p><p><img alt="image-20240502230120130" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230120130.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230120130.png"/></p><p>接下来，我们就来看看如何利用<code>Wrapper</code>实现复杂查询。</p><h3 id="2-1-1-QueryWrapper"><a class="headerlink" href="#2-1-1-QueryWrapper" title="2.1.1.QueryWrapper"></a><strong>2.1.1.QueryWrapper</strong></h3><p>无论是修改、删除、查询，都可以使用QueryWrapper来构建查询条件。接下来看一些例子： <strong>查询</strong>：查询出名字中带<code>o</code>的，存款大于等于1000元的人。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testQueryWrapper</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.构建查询条件 where name like "%o%" AND balance &gt;= 1000</span></span><br/><span class="line">    QueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;User&gt;()</span><br/><span class="line">            .select(<span class="string">"id"</span>, <span class="string">"username"</span>, <span class="string">"info"</span>, <span class="string">"balance"</span>)</span><br/><span class="line">            .like(<span class="string">"username"</span>, <span class="string">"o"</span>)</span><br/><span class="line">            .ge(<span class="string">"balance"</span>, <span class="number">1000</span>);</span><br/><span class="line">    <span class="comment">// 2.查询数据</span></span><br/><span class="line">    List&lt;User&gt; users = userMapper.selectList(wrapper);</span><br/><span class="line">    users.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p><strong>更新</strong>：更新用户名为jack的用户的余额为2000，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testUpdateByQueryWrapper</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.构建查询条件 where name = "Jack"</span></span><br/><span class="line">    QueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;User&gt;().eq(<span class="string">"username"</span>, <span class="string">"Jack"</span>);</span><br/><span class="line">    <span class="comment">// 2.更新数据，user中非null字段都会作为set语句</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><br/><span class="line">    user.setBalance(<span class="number">2000</span>);</span><br/><span class="line">    userMapper.update(user, wrapper);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="2-1-2-UpdateWrapper"><a class="headerlink" href="#2-1-2-UpdateWrapper" title="2.1.2.UpdateWrapper"></a><strong>2.1.2.UpdateWrapper</strong></h3><p>基于BaseMapper中的update方法更新时只能直接赋值，对于一些复杂的需求就难以实现。 例如：更新id为<code>1,2,4</code>的用户的余额，扣200，对应的SQL应该是：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/></pre></td><td class="code"><pre><span class="line">UPDATE user <span class="type">SET</span> <span class="variable">balance</span> <span class="operator">=</span> balance - <span class="number">200</span> WHERE id <span class="title function_">in</span> <span class="params">(<span class="number">1</span>, <span class="number">2</span>, <span class="number">4</span>)</span></span><br/></pre></td></tr></table></figure><p>SET的赋值结果是基于字段现有值的，这个时候就要利用UpdateWrapper中的setSql功能了：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testUpdateWrapper</span><span class="params">()</span> {</span><br/><span class="line">    List&lt;Long&gt; ids = List.of(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">4L</span>);</span><br/><span class="line">    <span class="comment">// 1.生成SQL</span></span><br/><span class="line">    UpdateWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">UpdateWrapper</span>&lt;User&gt;()</span><br/><span class="line">            .setSql(<span class="string">"balance = balance - 200"</span>) <span class="comment">// SET balance = balance - 200</span></span><br/><span class="line">            .in(<span class="string">"id"</span>, ids); <span class="comment">// WHERE id in (1, 2, 4)</span></span><br/><span class="line">        <span class="comment">// 2.更新，注意第一个参数可以给null，也就是不填更新字段和数据，</span></span><br/><span class="line">    <span class="comment">// 而是基于UpdateWrapper中的setSQL来更新</span></span><br/><span class="line">    userMapper.update(<span class="literal">null</span>, wrapper);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="2-1-3-LambdaQueryWrapper"><a class="headerlink" href="#2-1-3-LambdaQueryWrapper" title="2.1.3.LambdaQueryWrapper"></a><strong>2.1.3.LambdaQueryWrapper</strong></h3><p>无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称，会出现字符串<code>魔法值</code>。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名，又能知道字段名呢？</p><p>其中一种办法是基于变量的<code>gettter</code>方法结合反射技术。因此我们只要将条件对应的字段的<code>getter</code>方法传递给MybatisPlus，它就能计算出对应的变量名了。而传递方法可以使用JDK8中的<code>方法引用</code>和<code>Lambda</code>表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper，包含两个：</p><ul><li>LambdaQueryWrapper</li><li>LambdaUpdateWrapper</li></ul><p>分别对应QueryWrapper和UpdateWrapper</p><p>其使用方式如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testLambdaQueryWrapper</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.构建条件 WHERE username LIKE "%o%" AND balance &gt;= 1000</span></span><br/><span class="line">    QueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;&gt;();</span><br/><span class="line">    wrapper.lambda()</span><br/><span class="line">            .select(User::getId, User::getUsername, User::getInfo, User::getBalance)</span><br/><span class="line">            .like(User::getUsername, <span class="string">"o"</span>)</span><br/><span class="line">            .ge(User::getBalance, <span class="number">1000</span>);</span><br/><span class="line">    <span class="comment">// 2.查询</span></span><br/><span class="line">    List&lt;User&gt; users = userMapper.selectList(wrapper);</span><br/><span class="line">    users.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h2 id="2-2-自定义-SQL"><a class="headerlink" href="#2-2-自定义-SQL" title="2.2.自定义**SQL**"></a><strong>2.2.自定义**</strong>SQL**</h2><p>在演示UpdateWrapper的案例中，我们在代码中编写了更新的SQL语句：</p><p><img alt="image-20240502230138511" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230138511.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230138511.png"/></p><p>这种写法在某些企业也是不允许的，因为SQL语句最好都维护在持久层，而不是业务层。就当前案例来说，由于条件是in语句，只能将SQL写在Mapper.xml文件，利用foreach来生成动态SQL。 这实在是太麻烦了。假如查询条件更复杂，动态SQL的编写也会更加复杂。</p><p>所以，MybatisPlus提供了自定义SQL功能，可以让我们利用Wrapper生成查询条件，再结合Mapper.xml编写SQL</p><h3 id="2-2-1-基本用法"><a class="headerlink" href="#2-2-1-基本用法" title="2.2.1.基本用法"></a><strong>2.2.1.基本用法</strong></h3><p>以当前案例来说，我们可以这样写：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testCustomWrapper</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.准备自定义查询条件</span></span><br/><span class="line">    List&lt;Long&gt; ids = List.of(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">4L</span>);</span><br/><span class="line">    QueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;User&gt;().in(<span class="string">"id"</span>, ids);</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">// 2.调用mapper的自定义方法，直接传递Wrapper</span></span><br/><span class="line">    userMapper.deductBalanceByIds(<span class="number">200</span>, wrapper);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后在UserMapper中自定义SQL：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.mapper;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.core.mapper.BaseMapper;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Param;</span><br/><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Update;</span><br/><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Param;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserMapper</span> <span class="keyword">extends</span> <span class="title class_">BaseMapper</span>&lt;User&gt; {</span><br/><span class="line">    <span class="meta">@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">deductBalanceByIds</span><span class="params">(<span class="meta">@Param("money")</span> <span class="type">int</span> money, <span class="meta">@Param("ew")</span> QueryWrapper&lt;User&gt; wrapper)</span>;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>这样就省去了编写复杂查询条件的烦恼了。</p><h3 id="2-2-2-多表关联"><a class="headerlink" href="#2-2-2-多表关联" title="2.2.2.多表关联"></a><strong>2.2.2.多表关联</strong></h3><p>理论上来讲MyBatisPlus是不支持多表查询的，不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。 例如，我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户 要是自己基于mybatis实现SQL，大概是这样的：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"queryUserByIdAndAddr"</span> <span class="attr">resultType</span>=<span class="string">"com.itheima.mp.domain.po.User"</span>&gt;</span></span><br/><span class="line">      SELECT *</span><br/><span class="line">      FROM user u</span><br/><span class="line">      INNER JOIN address a ON u.id = a.user_id</span><br/><span class="line">      WHERE u.id</span><br/><span class="line">      <span class="tag">&lt;<span class="name">foreach</span> <span class="attr">collection</span>=<span class="string">"ids"</span> <span class="attr">separator</span>=<span class="string">","</span> <span class="attr">item</span>=<span class="string">"id"</span> <span class="attr">open</span>=<span class="string">"IN ("</span> <span class="attr">close</span>=<span class="string">")"</span>&gt;</span></span><br/><span class="line">          #{id}</span><br/><span class="line">      <span class="tag">&lt;/<span class="name">foreach</span>&gt;</span></span><br/><span class="line">      AND a.city = #{city}</span><br/><span class="line">  <span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br/></pre></td></tr></table></figure><p>可以看出其中最复杂的就是WHERE条件的编写，如果业务复杂一些，这里的SQL会更变态。</p><p>但是基于自定义SQL结合Wrapper的玩法，我们就可以利用Wrapper来构建查询条件，然后手写SELECT及FROM部分，实现多表查询。</p><p>查询条件这样来构建：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testCustomJoinWrapper</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.准备自定义查询条件</span></span><br/><span class="line">    QueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;User&gt;()</span><br/><span class="line">            .in(<span class="string">"u.id"</span>, List.of(<span class="number">1L</span>, <span class="number">2L</span>, <span class="number">4L</span>))</span><br/><span class="line">            .eq(<span class="string">"a.city"</span>, <span class="string">"北京"</span>);</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">// 2.调用mapper的自定义方法</span></span><br/><span class="line">    List&lt;User&gt; users = userMapper.queryUserByWrapper(wrapper);</span><br/><span class="line"></span><br/><span class="line">    users.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后在UserMapper中自定义方法：</p><figure class="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">@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")</span></span><br/><span class="line">List&lt;User&gt; <span class="title function_">queryUserByWrapper</span><span class="params">(<span class="meta">@Param("ew")</span>QueryWrapper&lt;User&gt; wrapper)</span>;</span><br/></pre></td></tr></table></figure><p>当然，也可以在<code>UserMapper.xml</code>中写SQL：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">select</span> <span class="attr">id</span>=<span class="string">"queryUserByIdAndAddr"</span> <span class="attr">resultType</span>=<span class="string">"com.itheima.mp.domain.po.User"</span>&gt;</span></span><br/><span class="line">    SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}</span><br/><span class="line"><span class="tag">&lt;/<span class="name">select</span>&gt;</span></span><br/></pre></td></tr></table></figure><h2 id="2-3-Service接口"><a class="headerlink" href="#2-3-Service接口" title="2.3.Service接口"></a><strong>2.3.Service接口</strong></h2><p>MybatisPlus不仅提供了BaseMapper，还提供了通用的Service接口及默认实现，封装了一些常用的service模板方法。 通用接口为<code>IService</code>，默认实现为<code>ServiceImpl</code>，其中封装的方法可以分为以下几类：</p><ul><li><code>save</code>：新增</li><li><code>remove</code>：删除</li><li><code>update</code>：更新</li><li><code>get</code>：查询单个结果</li><li><code>list</code>：查询集合结果</li><li><code>count</code>：计数</li><li><code>page</code>：分页查询</li></ul><h3 id="2-3-1-CRUD"><a class="headerlink" href="#2-3-1-CRUD" title="2.3.1.CRUD"></a><strong>2.3.1.CRUD</strong></h3><p>我们先俩看下基本的CRUD接口。 <strong>新增</strong>：</p><p><img alt="image-20240502230205447" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230205447.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230205447.png"/></p><ul><li><code>save</code>是新增单个元素</li><li><code>saveBatch</code>是批量新增</li><li><code>saveOrUpdate</code>是根据id判断，如果数据存在就更新，不存在则新增</li><li><code>saveOrUpdateBatch</code>是批量的新增或修改</li></ul><p><strong>删除：</strong></p><p><img alt="image-20240502230210680" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230210680.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230210680.png"/></p><ul><li><code>removeById</code>：根据id删除</li><li><code>removeByIds</code>：根据id批量删除</li><li><code>removeByMap</code>：根据Map中的键值对为条件删除</li><li><code>remove(Wrapper&lt;T&gt;)</code>：根据Wrapper条件删除</li><li><code>~~removeBatchByIds~~</code>：暂不支持</li></ul><p><strong>修改：</strong></p><p><img alt="image-20240502230215814" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230215814.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230215814.png"/></p><ul><li><code>updateById</code>：根据id修改</li><li><code>update(Wrapper&lt;T&gt;)</code>：根据<code>UpdateWrapper</code>修改，<code>Wrapper</code>中包含<code>set</code>和<code>where</code>部分</li><li><code>update(T，Wrapper&lt;T&gt;)</code>：按照<code>T</code>内的数据修改与<code>Wrapper</code>匹配到的数据</li><li><code>updateBatchById</code>：根据id批量修改</li></ul><p><strong>Get：</strong></p><p><img alt="image-20240502230221154" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230221154.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230221154.png"/></p><ul><li><code>getById</code>：根据id查询1条数据</li><li><code>getOne(Wrapper&lt;T&gt;)</code>：根据<code>Wrapper</code>查询1条数据</li><li><code>getBaseMapper</code>：获取<code>Service</code>内的<code>BaseMapper</code>实现，某些时候需要直接调用<code>Mapper</code>内的自定义<code>SQL</code>时可以用这个方法获取到<code>Mapper</code></li></ul><p><strong>List：</strong></p><p><img alt="image-20240502230227351" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230227351.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230227351.png"/></p><ul><li><code>listByIds</code>：根据id批量查询</li><li><code>list(Wrapper&lt;T&gt;)</code>：根据Wrapper条件查询多条数据</li><li><code>list()</code>：查询所有</li></ul><p><strong>Count</strong>：</p><p><img alt="image-20240502230232781" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230232781.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230232781.png"/></p><ul><li><code>count()</code>：统计所有数量</li><li><code>count(Wrapper&lt;T&gt;)</code>：统计符合<code>Wrapper</code>条件的数据数量</li></ul><p><strong>getBaseMapper</strong>： 当我们在service中要调用Mapper中自定义SQL时，就必须获取service对应的Mapper，就可以通过这个方法：</p><p><img alt="image-20240502230237701" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230237701.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230237701.png"/></p><h3 id="2-3-2-基本用法"><a class="headerlink" href="#2-3-2-基本用法" title="2.3.2.基本用法"></a><strong>2.3.2.基本用法</strong></h3><p>由于<code>Service</code>中经常需要定义与业务有关的自定义方法，因此我们不能直接使用<code>IService</code>，而是自定义<code>Service</code>接口，然后继承<code>IService</code>以拓展方法。同时，让自定义的<code>Service实现类</code>继承<code>ServiceImpl</code>，这样就不用自己实现<code>IService</code>中的接口了。</p><p>首先，定义<code>IUserService</code>，继承<code>IService</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.service;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.IService;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">IUserService</span> <span class="keyword">extends</span> <span class="title class_">IService</span>&lt;User&gt; {</span><br/><span class="line">    <span class="comment">// 拓展自定义方法</span></span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后，编写<code>UserServiceImpl</code>类，继承<code>ServiceImpl</code>，实现<code>UserService</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.service.impl;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.service.IUserService;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.mapper.UserMapper;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br/><span class="line"></span><br/><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_">UserServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;UserMapper, User&gt;</span><br/><span class="line">                                                                                                        <span class="keyword">implements</span> <span class="title class_">IUserService</span> {</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>项目结构如下：</p><p><img alt="image-20240502230256993" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230256993.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230256993.png"/></p><p>接下来，我们快速实现下面4个接口：</p><div class="table-container"><table><thead><tr><th style="text-align:left"><strong>编号</strong></th><th style="text-align:left"><strong>接口</strong></th><th style="text-align:left"><strong>请求方式</strong></th><th style="text-align:left"><strong>请求路径</strong></th><th style="text-align:left"><strong>请求参数</strong></th><th style="text-align:left"><strong>返回值</strong></th></tr></thead><tbody><tr><td style="text-align:left">1</td><td style="text-align:left">新增用户</td><td style="text-align:left">POST</td><td style="text-align:left">/users</td><td style="text-align:left">用户表单实体</td><td style="text-align:left">无</td></tr><tr><td style="text-align:left">2</td><td style="text-align:left">删除用户</td><td style="text-align:left">DELETE</td><td style="text-align:left">/users/{id}</td><td style="text-align:left">用户id</td><td style="text-align:left">无</td></tr><tr><td style="text-align:left">3</td><td style="text-align:left">根据id查询用户</td><td style="text-align:left">GET</td><td style="text-align:left">/users/{id}</td><td style="text-align:left">用户id</td><td style="text-align:left">用户VO</td></tr><tr><td style="text-align:left">4</td><td style="text-align:left">根据id批量查询</td><td style="text-align:left">GET</td><td style="text-align:left">/users</td><td style="text-align:left">用户id集合</td><td style="text-align:left">用户VO集合</td></tr></tbody></table></div><p>首先，我们在项目中引入几个依赖：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--swagger--&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.github.xiaoymin<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>knife4j-openapi2-spring-boot-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>4.1.0<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="comment">&lt;!--web--&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-web<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/></pre></td></tr></table></figure><p>然后需要配置swagger信息：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><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="attr">knife4j:</span></span><br/><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br/><span class="line">  <span class="attr">openapi:</span></span><br/><span class="line">    <span class="attr">title:</span> <span class="string">用户管理接口文档</span></span><br/><span class="line">    <span class="attr">description:</span> <span class="string">"用户管理接口文档"</span></span><br/><span class="line">    <span class="attr">email:</span> <span class="string">zhanghuyi@itcast.cn</span></span><br/><span class="line">    <span class="attr">concat:</span> <span class="string">虎哥</span></span><br/><span class="line">    <span class="attr">url:</span> <span class="string">https://www.itcast.cn</span></span><br/><span class="line">    <span class="attr">version:</span> <span class="string">v1.0.0</span></span><br/><span class="line">    <span class="attr">group:</span></span><br/><span class="line">      <span class="attr">default:</span></span><br/><span class="line">        <span class="attr">group-name:</span> <span class="string">default</span></span><br/><span class="line">        <span class="attr">api-rule:</span> <span class="string">package</span></span><br/><span class="line">        <span class="attr">api-rule-resources:</span></span><br/><span class="line">          <span class="bullet">-</span> <span class="string">com.itheima.mp.controller</span></span><br/></pre></td></tr></table></figure><p>然后，接口需要两个实体：</p><ul><li>UserFormDTO：代表新增时的用户表单</li><li>UserVO：代表查询的返回结果</li></ul><p>首先是UserFormDTO：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.dto;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableField;</span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "用户表单实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserFormDTO</span> {</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("id")</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">@ApiModelProperty("用户名")</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">@ApiModelProperty("密码")</span></span><br/><span class="line">    <span class="keyword">private</span> String password;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("注册手机号")</span></span><br/><span class="line">    <span class="keyword">private</span> String phone;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("详细信息，JSON风格")</span></span><br/><span class="line">    <span class="keyword">private</span> String info;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("账户余额")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer balance;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后是UserVO：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.vo;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "用户VO实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserVO</span> {</span><br/><span class="line">    </span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户id")</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">@ApiModelProperty("用户名")</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">@ApiModelProperty("详细信息")</span></span><br/><span class="line">    <span class="keyword">private</span> String info;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("使用状态（1正常 2冻结）")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer status;</span><br/><span class="line">    </span><br/><span class="line">    <span class="meta">@ApiModelProperty("账户余额")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer balance;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最后，按照Restful风格编写Controller接口方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/><span class="line">42</span><br/><span class="line">43</span><br/><span class="line">44</span><br/><span class="line">45</span><br/><span class="line">46</span><br/><span class="line">47</span><br/><span class="line">48</span><br/><span class="line">49</span><br/><span class="line">50</span><br/><span class="line">51</span><br/><span class="line">52</span><br/><span class="line">53</span><br/><span class="line">54</span><br/><span class="line">55</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.controller;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> cn.hutool.core.bean.BeanUtil;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.dto.UserFormDTO;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.vo.UserVO;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.service.IUserService;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.Api;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiOperation;</span><br/><span class="line"><span class="keyword">import</span> lombok.RequiredArgsConstructor;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.util.List;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Api(tags = "用户管理接口")</span></span><br/><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br/><span class="line"><span class="meta">@RestController</span></span><br/><span class="line"><span class="meta">@RequestMapping("users")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> IUserService userService;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@PostMapping</span></span><br/><span class="line">    <span class="meta">@ApiOperation("新增用户")</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveUser</span><span class="params">(<span class="meta">@RequestBody</span> UserFormDTO userFormDTO)</span>{</span><br/><span class="line">        <span class="comment">// 1.转换DTO为PO</span></span><br/><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> BeanUtil.copyProperties(userFormDTO, User.class);</span><br/><span class="line">        <span class="comment">// 2.新增</span></span><br/><span class="line">        userService.save(user);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@DeleteMapping("/{id}")</span></span><br/><span class="line">    <span class="meta">@ApiOperation("删除用户")</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">removeUserById</span><span class="params">(<span class="meta">@PathVariable("id")</span> Long userId)</span>{</span><br/><span class="line">        userService.removeById(userId);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@GetMapping("/{id}")</span></span><br/><span class="line">    <span class="meta">@ApiOperation("根据id查询用户")</span></span><br/><span class="line">    <span class="keyword">public</span> UserVO <span class="title function_">queryUserById</span><span class="params">(<span class="meta">@PathVariable("id")</span> Long userId)</span>{</span><br/><span class="line">        <span class="comment">// 1.查询用户</span></span><br/><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.getById(userId);</span><br/><span class="line">        <span class="comment">// 2.处理vo</span></span><br/><span class="line">        <span class="keyword">return</span> BeanUtil.copyProperties(user, UserVO.class);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@GetMapping</span></span><br/><span class="line">    <span class="meta">@ApiOperation("根据id集合查询用户")</span></span><br/><span class="line">    <span class="keyword">public</span> List&lt;UserVO&gt; <span class="title function_">queryUserByIds</span><span class="params">(<span class="meta">@RequestParam("ids")</span> List&lt;Long&gt; ids)</span>{</span><br/><span class="line">        <span class="comment">// 1.查询用户</span></span><br/><span class="line">        List&lt;User&gt; users = userService.listByIds(ids);</span><br/><span class="line">        <span class="comment">// 2.处理vo</span></span><br/><span class="line">        <span class="keyword">return</span> BeanUtil.copyToList(users, UserVO.class);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>可以看到上述接口都直接在controller即可实现，无需编写任何service代码，非常方便。</p><p>不过，一些带有业务逻辑的接口则需要在service中自定义实现了。例如下面的需求：</p><ul><li>根据id扣减用户余额</li></ul><p>这看起来是个简单修改功能，只要修改用户余额即可。但这个业务包含一些业务逻辑处理：</p><ul><li>判断用户状态是否正常</li><li>判断用户余额是否充足</li></ul><p>这些业务逻辑都要在service层来做，另外更新余额需要自定义SQL，要在mapper中来实现。因此，我们除了要编写controller以外，具体的业务还要在service和mapper中编写。</p><p>首先在UserController中定义一个方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@PutMapping("{id}/deduction/{money}")</span></span><br/><span class="line"><span class="meta">@ApiOperation("扣减用户余额")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deductBalance</span><span class="params">(<span class="meta">@PathVariable("id")</span> Long id, <span class="meta">@PathVariable("money")</span>Integer money)</span>{</span><br/><span class="line">    userService.deductBalance(id, money);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后是UserService接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.service;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.IService;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">IUserService</span> <span class="keyword">extends</span> <span class="title class_">IService</span>&lt;User&gt; {</span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">deductBalance</span><span class="params">(Long id, Integer money)</span>;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最后是UserServiceImpl实现类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.service.impl;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.mapper.UserMapper;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.service.IUserService;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br/><span class="line"></span><br/><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_">UserServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;UserMapper, User&gt; <span class="keyword">implements</span> <span class="title class_">IUserService</span> {</span><br/><span class="line">    <span class="meta">@Override</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deductBalance</span><span class="params">(Long id, Integer money)</span> {</span><br/><span class="line">        <span class="comment">// 1.查询用户</span></span><br/><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> getById(id);</span><br/><span class="line">        <span class="comment">// 2.判断用户状态</span></span><br/><span class="line">        <span class="keyword">if</span> (user == <span class="literal">null</span> || user.getStatus() == <span class="number">2</span>) {</span><br/><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"用户状态异常"</span>);</span><br/><span class="line">        }</span><br/><span class="line">        <span class="comment">// 3.判断用户余额</span></span><br/><span class="line">        <span class="keyword">if</span> (user.getBalance() &lt; money) {</span><br/><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"用户余额不足"</span>);</span><br/><span class="line">        }</span><br/><span class="line">        <span class="comment">// 4.扣减余额</span></span><br/><span class="line">        baseMapper.deductMoneyById(id, money);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最后是mapper：</p><figure class="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">@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">deductMoneyById</span><span class="params">(<span class="meta">@Param("id")</span> Long id, <span class="meta">@Param("money")</span> Integer money)</span>;</span><br/></pre></td></tr></table></figure><h3 id="2-3-3-Lambda"><a class="headerlink" href="#2-3-3-Lambda" title="2.3.3.Lambda"></a><strong>2.3.3.Lambda</strong></h3><p>IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。我们通过两个案例来学习一下。</p><p>案例一：实现一个根据复杂条件查询用户的接口，查询条件如下：</p><ul><li>name：用户名关键字，可以为空</li><li>status：用户状态，可以为空</li><li>minBalance：最小余额，可以为空</li><li>maxBalance：最大余额，可以为空</li></ul><p>可以理解成一个用户的后台管理界面，管理员可以自己选择条件来筛选用户，因此上述条件不一定存在，需要做判断。</p><p>我们首先需要定义一个查询条件实体，UserQuery实体：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.query;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "用户查询条件实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserQuery</span> {</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户名关键字")</span></span><br/><span class="line">    <span class="keyword">private</span> String name;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户状态：1-正常，2-冻结")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer status;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最小值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer minBalance;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最大值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer maxBalance;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>接下来我们在UserController中定义一个controller方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/list")</span></span><br/><span class="line"><span class="meta">@ApiOperation("根据id集合查询用户")</span></span><br/><span class="line"><span class="keyword">public</span> List&lt;UserVO&gt; <span class="title function_">queryUsers</span><span class="params">(UserQuery query)</span>{</span><br/><span class="line">    <span class="comment">// 1.组织条件</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> query.getName();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">status</span> <span class="operator">=</span> query.getStatus();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">minBalance</span> <span class="operator">=</span> query.getMinBalance();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">maxBalance</span> <span class="operator">=</span> query.getMaxBalance();</span><br/><span class="line">    LambdaQueryWrapper&lt;User&gt; wrapper = <span class="keyword">new</span> <span class="title class_">QueryWrapper</span>&lt;User&gt;().lambda()</span><br/><span class="line">            .like(username != <span class="literal">null</span>, User::getUsername, username)</span><br/><span class="line">            .eq(status != <span class="literal">null</span>, User::getStatus, status)</span><br/><span class="line">            .ge(minBalance != <span class="literal">null</span>, User::getBalance, minBalance)</span><br/><span class="line">            .le(maxBalance != <span class="literal">null</span>, User::getBalance, maxBalance);</span><br/><span class="line">    <span class="comment">// 2.查询用户</span></span><br/><span class="line">    List&lt;User&gt; users = userService.list(wrapper);</span><br/><span class="line">    <span class="comment">// 3.处理vo</span></span><br/><span class="line">    <span class="keyword">return</span> BeanUtil.copyToList(users, UserVO.class);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>在组织查询条件的时候，我们加入了 <code>username != null</code> 这样的参数，意思就是当条件成立时才会添加这个查询条件，类似Mybatis的mapper.xml文件中的<code>&lt;if&gt;</code>标签。这样就实现了动态查询条件效果了。</p><p>不过，上述条件构建的代码太麻烦了。 因此Service中对<code>LambdaQueryWrapper</code>和<code>LambdaUpdateWrapper</code>的用法进一步做了简化。我们无需自己通过<code>new</code>的方式来创建<code>Wrapper</code>，而是直接调用<code>lambdaQuery</code>和<code>lambdaUpdate</code>方法：</p><p>基于Lambda查询：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/list")</span></span><br/><span class="line"><span class="meta">@ApiOperation("根据id集合查询用户")</span></span><br/><span class="line"><span class="keyword">public</span> List&lt;UserVO&gt; <span class="title function_">queryUsers</span><span class="params">(UserQuery query)</span>{</span><br/><span class="line">    <span class="comment">// 1.组织条件</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> query.getName();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">status</span> <span class="operator">=</span> query.getStatus();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">minBalance</span> <span class="operator">=</span> query.getMinBalance();</span><br/><span class="line">    <span class="type">Integer</span> <span class="variable">maxBalance</span> <span class="operator">=</span> query.getMaxBalance();</span><br/><span class="line">    <span class="comment">// 2.查询用户</span></span><br/><span class="line">    List&lt;User&gt; users = userService.lambdaQuery()</span><br/><span class="line">            .like(username != <span class="literal">null</span>, User::getUsername, username)</span><br/><span class="line">            .eq(status != <span class="literal">null</span>, User::getStatus, status)</span><br/><span class="line">            .ge(minBalance != <span class="literal">null</span>, User::getBalance, minBalance)</span><br/><span class="line">            .le(maxBalance != <span class="literal">null</span>, User::getBalance, maxBalance)</span><br/><span class="line">            .list();</span><br/><span class="line">    <span class="comment">// 3.处理vo</span></span><br/><span class="line">    <span class="keyword">return</span> BeanUtil.copyToList(users, UserVO.class);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>可以发现lambdaQuery方法中除了可以构建条件，还需要在链式编程的最后添加一个<code>list()</code>，这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用<code>list()</code>，可选的方法有：</p><ul><li><code>.one()</code>：最多1个结果</li><li><code>.list()</code>：返回集合结果</li><li><code>.count()</code>：返回计数结果</li></ul><p>MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。</p><p>与lambdaQuery方法类似，IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。</p><p>例如下面的需求：</p><blockquote><p>需求：改造根据id修改用户余额的接口，要求如下</p><ul><li>如果扣减后余额为0，则将用户status修改为冻结状态（2）</li></ul></blockquote><p>也就是说我们在扣减用户余额时，需要对用户剩余余额做出判断，如果发现剩余余额为0，则应该将status修改为2，这就是说update语句的set部分是动态的。</p><p>实现如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br/><span class="line"><span class="meta">@Transactional</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deductBalance</span><span class="params">(Long id, Integer money)</span> {</span><br/><span class="line">    <span class="comment">// 1.查询用户</span></span><br/><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> getById(id);</span><br/><span class="line">    <span class="comment">// 2.校验用户状态</span></span><br/><span class="line">    <span class="keyword">if</span> (user == <span class="literal">null</span> || user.getStatus() == <span class="number">2</span>) {</span><br/><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"用户状态异常！"</span>);</span><br/><span class="line">    }</span><br/><span class="line">    <span class="comment">// 3.校验余额是否充足</span></span><br/><span class="line">    <span class="keyword">if</span> (user.getBalance() &lt; money) {</span><br/><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"用户余额不足！"</span>);</span><br/><span class="line">    }</span><br/><span class="line">    <span class="comment">// 4.扣减余额 update tb_user set balance = balance - ?</span></span><br/><span class="line">    <span class="type">int</span> <span class="variable">remainBalance</span> <span class="operator">=</span> user.getBalance() - money;</span><br/><span class="line">    lambdaUpdate()</span><br/><span class="line">            .set(User::getBalance, remainBalance) <span class="comment">// 更新余额</span></span><br/><span class="line">            .set(remainBalance == <span class="number">0</span>, User::getStatus, <span class="number">2</span>) <span class="comment">// 动态判断，是否更新status</span></span><br/><span class="line">            .eq(User::getId, id)</span><br/><span class="line">            .eq(User::getBalance, user.getBalance()) <span class="comment">// 乐观锁</span></span><br/><span class="line">            .update();</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="2-3-4-批量新增"><a class="headerlink" href="#2-3-4-批量新增" title="2.3.4.批量新增"></a><strong>2.3.4.批量新增</strong></h3><p>IService中的批量新增功能使用起来非常方便，但有一点注意事项，我们先来测试一下。 首先我们测试逐条插入数据：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testSaveOneByOne</span><span class="params">()</span> {</span><br/><span class="line">    <span class="type">long</span> <span class="variable">b</span> <span class="operator">=</span> System.currentTimeMillis();</span><br/><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>; i &lt;= <span class="number">100000</span>; i++) {</span><br/><span class="line">        userService.save(buildUser(i));</span><br/><span class="line">    }</span><br/><span class="line">    <span class="type">long</span> <span class="variable">e</span> <span class="operator">=</span> System.currentTimeMillis();</span><br/><span class="line">    System.out.println(<span class="string">"耗时："</span> + (e - b));</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">private</span> User <span class="title function_">buildUser</span><span class="params">(<span class="type">int</span> i)</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><br/><span class="line">    user.setUsername(<span class="string">"user_"</span> + i);</span><br/><span class="line">    user.setPassword(<span class="string">"123"</span>);</span><br/><span class="line">    user.setPhone(<span class="string">""</span> + (<span class="number">18688190000L</span> + i));</span><br/><span class="line">    user.setBalance(<span class="number">2000</span>);</span><br/><span class="line">    user.setInfo(<span class="string">"{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}"</span>);</span><br/><span class="line">    user.setCreateTime(LocalDateTime.now());</span><br/><span class="line">    user.setUpdateTime(user.getCreateTime());</span><br/><span class="line">    <span class="keyword">return</span> user;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>执行结果如下：</p><p><img alt="image-20240502230334303" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230334303.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230334303.png"/></p><p>可以看到速度非常慢。</p><p>然后再试试MybatisPlus的批处理：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testSaveBatch</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 准备10万条数据</span></span><br/><span class="line">    List&lt;User&gt; list = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(<span class="number">1000</span>);</span><br/><span class="line">    <span class="type">long</span> <span class="variable">b</span> <span class="operator">=</span> System.currentTimeMillis();</span><br/><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>; i &lt;= <span class="number">100000</span>; i++) {</span><br/><span class="line">        list.add(buildUser(i));</span><br/><span class="line">        <span class="comment">// 每1000条批量插入一次</span></span><br/><span class="line">        <span class="keyword">if</span> (i % <span class="number">1000</span> == <span class="number">0</span>) {</span><br/><span class="line">            userService.saveBatch(list);</span><br/><span class="line">            list.clear();</span><br/><span class="line">        }</span><br/><span class="line">    }</span><br/><span class="line">    <span class="type">long</span> <span class="variable">e</span> <span class="operator">=</span> System.currentTimeMillis();</span><br/><span class="line">    System.out.println(<span class="string">"耗时："</span> + (e - b));</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>执行最终耗时如下：</p><p><img alt="image-20240502230340751" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230340751.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230340751.png"/></p><p>可以看到使用了批处理以后，比逐条新增效率提高了10倍左右，性能还是不错的。</p><p>不过，我们简单查看一下<code>MybatisPlus</code>源码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Transactional(rollbackFor = Exception.class)</span></span><br/><span class="line"><span class="meta">@Override</span></span><br/><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">saveBatch</span><span class="params">(Collection&lt;T&gt; entityList, <span class="type">int</span> batchSize)</span> {</span><br/><span class="line">    <span class="type">String</span> <span class="variable">sqlStatement</span> <span class="operator">=</span> getSqlStatement(SqlMethod.INSERT_ONE);</span><br/><span class="line">    <span class="keyword">return</span> executeBatch(entityList, batchSize, (sqlSession, entity) -&gt; sqlSession.insert(sqlStatement, entity));</span><br/><span class="line">}</span><br/><span class="line"><span class="comment">// ...SqlHelper</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> &lt;E&gt; <span class="type">boolean</span> <span class="title function_">executeBatch</span><span class="params">(Class&lt;?&gt; entityClass, Log log, Collection&lt;E&gt; list, <span class="type">int</span> batchSize, BiConsumer&lt;SqlSession, E&gt; consumer)</span> {</span><br/><span class="line">    Assert.isFalse(batchSize &lt; <span class="number">1</span>, <span class="string">"batchSize must not be less than one"</span>);</span><br/><span class="line">    <span class="keyword">return</span> !CollectionUtils.isEmpty(list) &amp;&amp; executeBatch(entityClass, log, sqlSession -&gt; {</span><br/><span class="line">        <span class="type">int</span> <span class="variable">size</span> <span class="operator">=</span> list.size();</span><br/><span class="line">        <span class="type">int</span> <span class="variable">idxLimit</span> <span class="operator">=</span> Math.min(batchSize, size);</span><br/><span class="line">        <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>;</span><br/><span class="line">        <span class="keyword">for</span> (E element : list) {</span><br/><span class="line">            consumer.accept(sqlSession, element);</span><br/><span class="line">            <span class="keyword">if</span> (i == idxLimit) {</span><br/><span class="line">                sqlSession.flushStatements();</span><br/><span class="line">                idxLimit = Math.min(idxLimit + batchSize, size);</span><br/><span class="line">            }</span><br/><span class="line">            i++;</span><br/><span class="line">        }</span><br/><span class="line">    });</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>可以发现其实<code>MybatisPlus</code>的批处理是基于<code>PrepareStatement</code>的预编译模式，然后批量提交，最终在数据库执行时还是会有多条insert语句，逐条插入数据。SQL类似这样：</p><figure class="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/></pre></td><td class="code"><pre><span class="line">Preparing: <span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="keyword">user</span> ( username, password, phone, info, balance, create_time, update_time ) <span class="keyword">VALUES</span> ( ?, ?, ?, ?, ?, ?, ? )</span><br/><span class="line">Parameters: user_1, <span class="number">123</span>, <span class="number">18688190001</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span></span><br/><span class="line">Parameters: user_2, <span class="number">123</span>, <span class="number">18688190002</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span></span><br/><span class="line">Parameters: user_3, <span class="number">123</span>, <span class="number">18688190003</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span></span><br/></pre></td></tr></table></figure><p>而如果想要得到最佳性能，最好是将多条SQL合并为一条，像这样：</p><figure class="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/></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> <span class="keyword">user</span> ( username, password, phone, info, balance, create_time, update_time )</span><br/><span class="line"><span class="keyword">VALUES</span> </span><br/><span class="line">(user_1, <span class="number">123</span>, <span class="number">18688190001</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>),</span><br/><span class="line">(user_2, <span class="number">123</span>, <span class="number">18688190002</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>),</span><br/><span class="line">(user_3, <span class="number">123</span>, <span class="number">18688190003</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>),</span><br/><span class="line">(user_4, <span class="number">123</span>, <span class="number">18688190004</span>, "", <span class="number">2000</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>, <span class="number">2023</span><span class="number">-07</span><span class="number">-01</span>);</span><br/></pre></td></tr></table></figure><p>该怎么做呢？</p><p>MySQL的客户端连接参数中有这样的一个参数：<code>rewriteBatchedStatements</code>。顾名思义，就是重写批处理的<code>statement</code>语句。参考文档：</p><p><a href="https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements" rel="noopener" target="_blank">https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements</a></p><p>这个参数的默认值是false，我们需要修改连接参数，将其配置为true</p><p>修改项目中的application.yml文件，在jdbc的url后面添加参数<code>&amp;rewriteBatchedStatements=true</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</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">url:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;serverTimezone=Asia/Shanghai&amp;rewriteBatchedStatements=true</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">username:</span> <span class="string">root</span></span><br/><span class="line">    <span class="attr">password:</span> <span class="string">MySQL123</span></span><br/></pre></td></tr></table></figure><p>再次测试插入10万条数据，可以发现速度有非常明显的提升：</p><p><img alt="image-20240502230352648" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230352648.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230352648.png"/></p><p>在<code>ClientPreparedStatement</code>的<code>executeBatchInternal</code>中，有判断<code>rewriteBatchedStatements</code>值是否为true并重写SQL的功能：</p><p>最终，SQL被重写了：</p><p><img alt="image-20240502230401230" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230401230.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230401230.png"/></p><h1 id="3-扩展功能"><a class="headerlink" href="#3-扩展功能" title="3.扩展功能"></a><strong>3.扩展功能</strong></h1><h2 id="3-1-代码生成"><a class="headerlink" href="#3-1-代码生成" title="3.1.代码生成"></a><strong>3.1.代码生成</strong></h2><p>在使用MybatisPlus以后，基础的<code>Mapper</code>、<code>Service</code>、<code>PO</code>代码相对固定，重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成<code>PO</code>、<code>Mapper</code>、<code>Service</code>等相关代码。只不过代码生成器同样要编码使用，也很麻烦。</p><p>这里推荐大家使用一款<code>MybatisPlus</code>的插件，它可以基于图形化界面完成<code>MybatisPlus</code>的代码生成，非常简单。</p><h3 id="3-1-1-安装插件"><a class="headerlink" href="#3-1-1-安装插件" title="3.1.1.安装插件"></a><strong>3.1.1.安装插件</strong></h3><p>在<code>Idea</code>的plugins市场中搜索并安装<code>MyBatisPlus</code>插件：</p><p><img alt="image-20240502230410676" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230410676.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230410676.png"/></p><p>然后重启你的Idea即可使用。</p><h3 id="3-1-2-使用"><a class="headerlink" href="#3-1-2-使用" title="3.1.2.使用"></a><strong>3.1.2.使用</strong></h3><p>刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址，在Idea顶部菜单中，找到<code>other</code>，选择<code>Config Database</code>：</p><p><img alt="image-20240502230415932" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230415932.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230415932.png"/></p><p>在弹出的窗口中填写数据库连接的基本信息：</p><p><img alt="image-20240502230427648" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230427648.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230427648.png"/></p><p>点击OK保存。</p><p>然后再次点击Idea顶部菜单中的other，然后选择<code>Code Generator</code>:</p><p><img alt="image-20240502230438133" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230438133.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230438133.png"/></p><p>在弹出的表单中填写信息：</p><p><img alt="image-20240502230442273" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230442273.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230442273.png"/></p><p>最终，代码自动生成到指定的位置了：</p><h2 id="3-2-静态工具"><a class="headerlink" href="#3-2-静态工具" title="3.2.静态工具"></a><strong>3.2.静态工具</strong></h2><p>有的时候Service之间也会相互调用，为了避免出现循环依赖问题，MybatisPlus提供一个静态工具类：<code>Db</code>，其中的一些静态方法与<code>IService</code>中方法签名基本一致，也可以帮助我们实现CRUD功能：</p><p><img alt="image-20240502230448099" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230448099.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230448099.png"/></p><p>示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testDbGet</span><span class="params">()</span> {</span><br/><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> Db.getById(<span class="number">1L</span>, User.class);</span><br/><span class="line">    System.out.println(user);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testDbList</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 利用Db实现复杂条件查询</span></span><br/><span class="line">    List&lt;User&gt; list = Db.lambdaQuery(User.class)</span><br/><span class="line">            .like(User::getUsername, <span class="string">"o"</span>)</span><br/><span class="line">            .ge(User::getBalance, <span class="number">1000</span>)</span><br/><span class="line">            .list();</span><br/><span class="line">    list.forEach(System.out::println);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testDbUpdate</span><span class="params">()</span> {</span><br/><span class="line">    Db.lambdaUpdate(User.class)</span><br/><span class="line">            .set(User::getBalance, <span class="number">2000</span>)</span><br/><span class="line">            .eq(User::getUsername, <span class="string">"Rose"</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>需求：改造根据id用户查询的接口，查询用户的同时返回用户收货地址列表</p><p>首先，我们要添加一个收货地址的VO对象：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.vo;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "收货地址VO")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AddressVO</span>{</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("id")</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">@ApiModelProperty("用户ID")</span></span><br/><span class="line">    <span class="keyword">private</span> Long userId;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("省")</span></span><br/><span class="line">    <span class="keyword">private</span> String province;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("市")</span></span><br/><span class="line">    <span class="keyword">private</span> String city;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("县/区")</span></span><br/><span class="line">    <span class="keyword">private</span> String town;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("手机")</span></span><br/><span class="line">    <span class="keyword">private</span> String mobile;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("详细地址")</span></span><br/><span class="line">    <span class="keyword">private</span> String street;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("联系人")</span></span><br/><span class="line">    <span class="keyword">private</span> String contact;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("是否是默认 1默认 0否")</span></span><br/><span class="line">    <span class="keyword">private</span> Boolean isDefault;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@ApiModelProperty("备注")</span></span><br/><span class="line">    <span class="keyword">private</span> String notes;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后，改造原来的UserVO，添加一个地址属性：</p><p><img alt="image-20240502230500887" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230500887.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230500887.png"/></p><p>接下来，修改UserController中根据id查询用户的业务接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping("/{id}")</span></span><br/><span class="line"><span class="meta">@ApiOperation("根据id查询用户")</span></span><br/><span class="line"><span class="keyword">public</span> UserVO <span class="title function_">queryUserById</span><span class="params">(<span class="meta">@PathVariable("id")</span> Long userId)</span>{</span><br/><span class="line">    <span class="comment">// 基于自定义service方法查询</span></span><br/><span class="line">    <span class="keyword">return</span> userService.queryUserAndAddressById(userId);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>由于查询业务复杂，所以要在service层来实现。首先在IUserService中定义方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.service;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.service.IService;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.po.User;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.vo.UserVO;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">IUserService</span> <span class="keyword">extends</span> <span class="title class_">IService</span>&lt;User&gt; {</span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">deduct</span><span class="params">(Long id, Integer money)</span>;</span><br/><span class="line"></span><br/><span class="line">    UserVO <span class="title function_">queryUserAndAddressById</span><span class="params">(Long userId)</span>;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后，在UserServiceImpl中实现该方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br/><span class="line"><span class="keyword">public</span> UserVO <span class="title function_">queryUserAndAddressById</span><span class="params">(Long userId)</span> {</span><br/><span class="line">    <span class="comment">// 1.查询用户</span></span><br/><span class="line">    <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> getById(userId);</span><br/><span class="line">    <span class="keyword">if</span> (user == <span class="literal">null</span>) {</span><br/><span class="line">        <span class="keyword">return</span> <span class="literal">null</span>;</span><br/><span class="line">    }</span><br/><span class="line">    <span class="comment">// 2.查询收货地址</span></span><br/><span class="line">    List&lt;Address&gt; addresses = Db.lambdaQuery(Address.class)</span><br/><span class="line">            .eq(Address::getUserId, userId)</span><br/><span class="line">            .list();</span><br/><span class="line">    <span class="comment">// 3.处理vo</span></span><br/><span class="line">    <span class="type">UserVO</span> <span class="variable">userVO</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserVO.class);</span><br/><span class="line">    userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));</span><br/><span class="line">    <span class="keyword">return</span> userVO;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>在查询地址时，我们采用了Db的静态方法，因此避免了注入AddressService，减少了循环依赖的风险。</p><p>再来实现一个功能：</p><ul><li>根据id批量查询用户，并查询出用户对应的所有地址</li></ul><h2 id="3-3-逻辑删除"><a class="headerlink" href="#3-3-逻辑删除" title="3.3.逻辑删除"></a><strong>3.3.逻辑删除</strong></h2><p>对于一些比较重要的数据，我们往往会采用逻辑删除的方案，即：</p><ul><li>在表中添加一个字段标记数据是否被删除</li><li>当删除数据时把标记置为true</li><li>查询时过滤掉标记为true的数据</li></ul><p>一旦采用了逻辑删除，所有的查询和删除逻辑都要跟着变化，非常麻烦。</p><p>为了解决这个问题，MybatisPlus就添加了对逻辑删除的支持。</p><p><strong>注意</strong>，只有MybatisPlus生成的SQL语句才支持自动的逻辑删除，自定义SQL需要自己手动处理逻辑删除。</p><p>例如，我们给<code>address</code>表添加一个逻辑删除字段：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">alter</span> <span class="keyword">table</span> address <span class="keyword">add</span> deleted bit <span class="keyword">default</span> b<span class="string">'0'</span> <span class="keyword">null</span> comment <span class="string">'逻辑删除'</span>;</span><br/></pre></td></tr></table></figure><p>然后给<code>Address</code>实体添加<code>deleted</code>字段：</p><p><img alt="image-20240502230519804" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230519804.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230519804.png"/></p><p>接下来，我们要在<code>application.yml</code>中配置逻辑删除字段：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/></pre></td><td class="code"><pre><span class="line"><span class="attr">mybatis-plus:</span></span><br/><span class="line">  <span class="attr">global-config:</span></span><br/><span class="line">    <span class="attr">db-config:</span></span><br/><span class="line">      <span class="attr">logic-delete-field:</span> <span class="string">deleted</span> <span class="comment"># 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)</span></span><br/><span class="line">      <span class="attr">logic-delete-value:</span> <span class="number">1</span> <span class="comment"># 逻辑已删除值(默认为 1)</span></span><br/><span class="line">      <span class="attr">logic-not-delete-value:</span> <span class="number">0</span> <span class="comment"># 逻辑未删除值(默认为 0)</span></span><br/></pre></td></tr></table></figure><p>测试： 首先，我们执行一个删除操作：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><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">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testDeleteByLogic</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 删除方法与以前没有区别</span></span><br/><span class="line">    addressService.removeById(<span class="number">59L</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>方法与普通删除一模一样，但是底层的SQL逻辑变了：</p><p><img alt="image-20240502230528718" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230528718.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230528718.png"/></p><p>查询一下试试：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testQuery</span><span class="params">()</span> {</span><br/><span class="line">    List&lt;Address&gt; list = addressService.list();</span><br/><span class="line">    list.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>会发现id为59的确实没有查询出来，而且SQL中也对逻辑删除字段做了判断：</p><p><img alt="image-20240502230534044" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230534044.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230534044.png"/></p><p>综上， 开启了逻辑删除功能以后，我们就可以像普通删除一样做CRUD，基本不用考虑代码逻辑问题。还是非常方便的。</p><p><strong>注意</strong>： 逻辑删除本身也有自己的问题，比如：</p><ul><li>会导致数据库表垃圾数据越来越多，从而影响查询效率</li><li>SQL中全都需要对逻辑删除字段做判断，影响查询效率</li></ul><p>因此，我不太推荐采用逻辑删除功能，如果数据不能删除，可以采用把数据迁移到其它表的办法。</p><h2 id="3-3-通用枚举"><a class="headerlink" href="#3-3-通用枚举" title="3.3.通用枚举"></a><strong>3.3.通用枚举</strong></h2><p>User类中有一个用户状态字段：</p><p><img alt="image-20240502230540216" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230540216.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230540216.png"/></p><p>像这种字段我们一般会定义一个枚举，做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是<code>int</code>类型，对应的PO也是<code>Integer</code>。因此业务操作时必须手动把<code>枚举</code>与<code>Integer</code>转换，非常麻烦。</p><p>因此，MybatisPlus提供了一个处理枚举的类型转换器，可以帮我们<strong>把枚举类型与数据库类型自动转换</strong>。</p><h3 id="3-3-1-定义枚举"><a class="headerlink" href="#3-3-1-定义枚举" title="3.3.1.定义枚举"></a><strong>3.3.1.定义枚举</strong></h3><p>我们定义一个用户状态的枚举：</p><p><img alt="image-20240502230546733" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230546733.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230546733.png"/></p><p>代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.enums;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.EnumValue;</span><br/><span class="line"><span class="keyword">import</span> lombok.Getter;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Getter</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">UserStatus</span> {</span><br/><span class="line">    NORMAL(<span class="number">1</span>, <span class="string">"正常"</span>),</span><br/><span class="line">    FREEZE(<span class="number">2</span>, <span class="string">"冻结"</span>)</span><br/><span class="line">    ;</span><br/><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> value;</span><br/><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String desc;</span><br/><span class="line"></span><br/><span class="line">    UserStatus(<span class="type">int</span> value, String desc) {</span><br/><span class="line">        <span class="built_in">this</span>.value = value;</span><br/><span class="line">        <span class="built_in">this</span>.desc = desc;</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后把<code>User</code>类中的<code>status</code>字段改为<code>UserStatus</code> 类型：</p><p><img alt="image-20240502230555899" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230555899.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230555899.png"/></p><p>要让<code>MybatisPlus</code>处理枚举与数据库类型自动转换，我们必须告诉<code>MybatisPlus</code>，枚举中的哪个字段的值作为数据库值。 <code>MybatisPlus</code>提供了<code>@EnumValue</code>注解来标记枚举属性：</p><p><img alt="image-20240502230600710" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230600710.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230600710.png"/></p><h3 id="3-3-2-配置枚举处理器"><a class="headerlink" href="#3-3-2-配置枚举处理器" title="3.3.2.**配置枚举处理器**"></a><strong>3.3.2.**</strong>配置枚举处理器**</h3><p>在application.yaml文件中添加配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/></pre></td><td class="code"><pre><span class="line"><span class="attr">mybatis-plus:</span></span><br/><span class="line">  <span class="attr">configuration:</span></span><br/><span class="line">    <span class="attr">default-enum-type-handler:</span> <span class="string">com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler</span></span><br/></pre></td></tr></table></figure><h3 id="3-3-3-测试"><a class="headerlink" href="#3-3-3-测试" title="3.3.3.测试"></a><strong>3.3.3.测试</strong></h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testService</span><span class="params">()</span> {</span><br/><span class="line">    List&lt;User&gt; list = userService.list();</span><br/><span class="line">    list.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最终，查询出的<code>User</code>类的<code>status</code>字段会是枚举类型：</p><p><img alt="image-20240502230609616" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230609616.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230609616.png"/></p><p>同时，为了使页面查询结果也是枚举格式，我们需要修改UserVO中的status属性：</p><p><img alt="image-20240502230614246" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230614246.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230614246.png"/></p><p>并且，在UserStatus枚举中通过<code>@JsonValue</code>注解标记JSON序列化时展示的字段：</p><p><img alt="image-20240502230619039" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230619039.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230619039.png"/></p><p>最后，在页面查询，结果如下：</p><p><img alt="image-20240502230623618" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230623618.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230623618.png"/></p><h2 id="3-4-JSON-类型处理器"><a class="headerlink" href="#3-4-JSON-类型处理器" title="3.4.**JSON**类型处理器"></a><strong>3.4.**</strong>JSON<strong>**类型处理器</strong></h2><p>数据库的user表中有一个<code>info</code>字段，是JSON类型：</p><p><img alt="image-20240502230629114" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230629114.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230629114.png"/></p><p>格式像这样：</p><figure class="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">{</span><span class="attr">"age"</span><span class="punctuation">:</span> <span class="number">20</span><span class="punctuation">,</span> <span class="attr">"intro"</span><span class="punctuation">:</span> <span class="string">"佛系青年"</span><span class="punctuation">,</span> <span class="attr">"gender"</span><span class="punctuation">:</span> <span class="string">"male"</span><span class="punctuation">}</span></span><br/></pre></td></tr></table></figure><p>而目前<code>User</code>实体类中却是<code>String</code>类型：</p><p><img alt="image-20240502230634184" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230634184.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230634184.png"/></p><p>这样一来，我们要读取info中的属性时就非常不方便。如果要方便获取，info的类型最好是一个<code>Map</code>或者实体类。</p><p>而一旦我们把<code>info</code>改为<code>对象</code>类型，就需要在写入数据库时手动转为<code>String</code>，再读取数据库时，手动转换为<code>对象</code>，这会非常麻烦。</p><p>因此MybatisPlus提供了很多特殊类型字段的类型处理器，解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用<code>JacksonTypeHandler</code>处理器。</p><p>接下来，我们就来看看这个处理器该如何使用。</p><h3 id="3-4-1-定义实体"><a class="headerlink" href="#3-4-1-定义实体" title="3.4.1.定义实体"></a><strong>3.4.1.定义实体</strong></h3><p>首先，我们定义一个单独实体类来与info字段的属性匹配：</p><p><img alt="image-20240502230642139" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230642139.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230642139.png"/></p><p>代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.po;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserInfo</span> {</span><br/><span class="line">    <span class="keyword">private</span> Integer age;</span><br/><span class="line">    <span class="keyword">private</span> String intro;</span><br/><span class="line">    <span class="keyword">private</span> String gender;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-4-2-使用类型处理器"><a class="headerlink" href="#3-4-2-使用类型处理器" title="3.4.2.使用类型处理器"></a><strong>3.4.2.使用类型处理器</strong></h3><p>接下来，将User类的info字段修改为UserInfo类型，并声明类型处理器：</p><p><img alt="image-20240502230649052" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230649052.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230649052.png"/></p><p>测试可以发现，所有数据都正确封装到UserInfo当中了：</p><p><img alt="image-20240502230654102" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230654102.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230654102.png"/></p><p>同时，为了让页面返回的结果也以对象格式返回，我们要修改UserVO中的info字段：</p><p><img alt="image-20240502230658889" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230658889.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230658889.png"/></p><p>此时，在页面查询结果如下：</p><p><img alt="image-20240502230703687" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230703687.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230703687.png"/></p><h2 id="3-5-配置加密（选学）"><a class="headerlink" href="#3-5-配置加密（选学）" title="3.5.配置加密（选学）"></a><strong>3.5.配置加密（选学）</strong></h2><p>目前我们配置文件中的很多参数都是明文，如果开发人员发生流动，很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。</p><p>我们以数据库的用户名和密码为例。</p><h3 id="3-5-1-生成-秘钥"><a class="headerlink" href="#3-5-1-生成-秘钥" title="3.5.1.生成**秘钥**"></a><strong>3.5.1.生成**</strong>秘钥**</h3><p>首先，我们利用AES工具生成一个随机秘钥，然后对用户名、密码加密：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.core.toolkit.AES;</span><br/><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.Test;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">class</span> <span class="title class_">MpDemoApplicationTests</span> {</span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">void</span> <span class="title function_">contextLoads</span><span class="params">()</span> {</span><br/><span class="line">        <span class="comment">// 生成 16 位随机 AES 密钥</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">randomKey</span> <span class="operator">=</span> AES.generateRandomKey();</span><br/><span class="line">        System.out.println(<span class="string">"randomKey = "</span> + randomKey);</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 利用密钥对用户名加密</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> AES.encrypt(<span class="string">"root"</span>, randomKey);</span><br/><span class="line">        System.out.println(<span class="string">"username = "</span> + username);</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 利用密钥对用户名加密</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> AES.encrypt(<span class="string">"MySQL123"</span>, randomKey);</span><br/><span class="line">        System.out.println(<span class="string">"password = "</span> + password);</span><br/><span class="line"></span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>打印结果如下：</p><figure class="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/></pre></td><td class="code"><pre><span class="line">randomKey <span class="operator">=</span> <span class="number">6234633</span>a66fb399f</span><br/><span class="line">username <span class="operator">=</span> px2bAbnUfiY8K<span class="operator">/</span>IgsKvscg<span class="operator">=</span><span class="operator">=</span></span><br/><span class="line">password <span class="operator">=</span> FGvCSEaOuga3ulDAsxw68Q<span class="operator">=</span><span class="operator">=</span></span><br/></pre></td></tr></table></figure><h3 id="3-5-2-修改配置"><a class="headerlink" href="#3-5-2-修改配置" title="3.5.2.修改配置"></a><strong>3.5.2.修改配置</strong></h3><p>修改application.yaml文件，把jdbc的用户名、密码修改为刚刚加密生成的密文：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</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">url:</span> <span class="string">jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;serverTimezone=Asia/Shanghai&amp;rewriteBatchedStatements=true</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">username:</span> <span class="string">mpw:QWWVnk1Oal3258x5rVhaeQ==</span> <span class="comment"># 密文要以 mpw:开头</span></span><br/><span class="line">    <span class="attr">password:</span> <span class="string">mpw:EUFmeH3cNAzdRGdOQcabWg==</span> <span class="comment"># 密文要以 mpw:开头</span></span><br/></pre></td></tr></table></figure><h3 id="3-5-3-测试"><a class="headerlink" href="#3-5-3-测试" title="3.5.3.测试"></a><strong>3.5.3.测试</strong></h3><p>在启动项目的时候，需要把刚才生成的秘钥添加到启动参数中，像这样：</p><p>—mpw.key=6234633a66fb399f</p><p>单元测试的时候不能添加启动参数，所以要在测试类的注解上配置：</p><p><img alt="image-20240502230714664" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230714664.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230714664.png"/></p><p>然后随意运行一个单元测试，可以发现数据库查询正常。</p><h1 id="4-插件功能"><a class="headerlink" href="#4-插件功能" title="4.插件功能"></a><strong>4.插件功能</strong></h1><p>MybatisPlus提供了很多的插件功能，进一步拓展其功能。目前已有的插件有：</p><ul><li><code>PaginationInnerInterceptor</code>：自动分页</li><li><code>TenantLineInnerInterceptor</code>：多租户</li><li><code>DynamicTableNameInnerInterceptor</code>：动态表名</li><li><code>OptimisticLockerInnerInterceptor</code>：乐观锁</li><li><code>IllegalSQLInnerInterceptor</code>：sql 性能规范</li><li><code>BlockAttackInnerInterceptor</code>：防止全表更新与删除</li></ul><p><strong>注意：</strong> 使用多个分页插件的时候需要注意插件定义顺序，建议使用顺序如下：</p><ul><li>多租户,动态表名</li><li>分页,乐观锁</li><li>sql 性能规范,防止全表更新与删除</li></ul><p>这里我们以分页插件为里来学习插件的用法。</p><h2 id="4-1-分页插件"><a class="headerlink" href="#4-1-分页插件" title="4.1.分页插件"></a><strong>4.1.分页插件</strong></h2><p>在未引入分页插件的情况下，<code>MybatisPlus</code>是不支持分页功能的，<code>IService</code>和<code>BaseMapper</code>中的分页方法都无法正常起效。 所以，我们必须配置分页插件。</p><h3 id="4-1-1-配置分页插件"><a class="headerlink" href="#4-1-1-配置分页插件" title="4.1.1.配置分页插件"></a><strong>4.1.1.配置分页插件</strong></h3><p>在项目中新建一个配置类：</p><p><img alt="image-20240502230727420" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230727420.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230727420.png"/></p><p>其代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.config;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.DbType;</span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;</span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br/><span class="line"></span><br/><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_">MybatisConfig</span> {</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> MybatisPlusInterceptor <span class="title function_">mybatisPlusInterceptor</span><span class="params">()</span> {</span><br/><span class="line">        <span class="comment">// 初始化核心插件</span></span><br/><span class="line">        <span class="type">MybatisPlusInterceptor</span> <span class="variable">interceptor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MybatisPlusInterceptor</span>();</span><br/><span class="line">        <span class="comment">// 添加分页插件</span></span><br/><span class="line">        interceptor.addInnerInterceptor(<span class="keyword">new</span> <span class="title class_">PaginationInnerInterceptor</span>(DbType.MYSQL));</span><br/><span class="line">        <span class="keyword">return</span> interceptor;</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="4-1-2-分页-API"><a class="headerlink" href="#4-1-2-分页-API" title="4.1.2.分页**API**"></a><strong>4.1.2.分页**</strong>API**</h3><p>编写一个分页查询的测试：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">void</span> <span class="title function_">testPageQuery</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 1.分页查询，new Page()的两个参数分别是：页码、每页大小</span></span><br/><span class="line">    Page&lt;User&gt; p = userService.page(<span class="keyword">new</span> <span class="title class_">Page</span>&lt;&gt;(<span class="number">2</span>, <span class="number">2</span>));</span><br/><span class="line">    <span class="comment">// 2.总条数</span></span><br/><span class="line">    System.out.println(<span class="string">"total = "</span> + p.getTotal());</span><br/><span class="line">    <span class="comment">// 3.总页数</span></span><br/><span class="line">    System.out.println(<span class="string">"pages = "</span> + p.getPages());</span><br/><span class="line">    <span class="comment">// 4.数据</span></span><br/><span class="line">    List&lt;User&gt; records = p.getRecords();</span><br/><span class="line">    records.forEach(System.out::println);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>运行的SQL如下：</p><p><img alt="image-20240502230738953" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230738953.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230738953.png"/></p><p>这里用到了分页参数，Page，即可以支持分页参数，也可以支持排序参数。常见的API如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="variable">pageNo</span> <span class="operator">=</span> <span class="number">1</span>, pageSize = <span class="number">5</span>;</span><br/><span class="line"><span class="comment">// 分页参数</span></span><br/><span class="line">Page&lt;User&gt; page = Page.of(pageNo, pageSize);</span><br/><span class="line"><span class="comment">// 排序参数, 通过OrderItem来指定</span></span><br/><span class="line">page.addOrder(<span class="keyword">new</span> <span class="title class_">OrderItem</span>(<span class="string">"balance"</span>, <span class="literal">false</span>));</span><br/><span class="line"></span><br/><span class="line">userService.page(page);</span><br/></pre></td></tr></table></figure><h2 id="4-2-通用分页实体"><a class="headerlink" href="#4-2-通用分页实体" title="4.2.通用分页实体"></a><strong>4.2.通用分页实体</strong></h2><p>现在要实现一个用户分页查询的接口，接口规范如下：</p><div class="table-container"><table><thead><tr><th><strong>参数</strong></th><th><strong>说明</strong></th></tr></thead><tbody><tr><td>请求方式</td><td>GET</td></tr><tr><td>请求路径</td><td>/users/page</td></tr><tr><td>请求参数</td><td><code>&#123;    "pageNo": 1,    "pageSize": 5,    "sortBy": "balance",    "isAsc": false,    "name": "o",    "status": 1 &#125;</code></td></tr><tr><td>返回值</td><td><code>&#123;    "total": 100006,    "pages": 50003,    "list": [        &#123;            "id": 1685100878975279298,            "username": "user_9****",            "info": &#123;                "age": 24,                "intro": "英文老师",                "gender": "female"            &#125;,            "status": "正常",            "balance": 2000        &#125;    ] &#125;</code></td></tr><tr><td>特殊说明</td><td>如果排序字段为空，默认按照更新时间排序排序字段不为空，则按照排序字段排序</td></tr></tbody></table></div><p>这里需要定义3个实体：</p><ul><li><code>UserQuery</code>：分页查询条件的实体，包含分页、排序参数、过滤条件</li><li><code>PageDTO</code>：分页结果实体，包含总条数、总页数、当前页数据</li><li><code>UserVO</code>：用户页面视图实体</li></ul><h3 id="4-2-1-实体"><a class="headerlink" href="#4-2-1-实体" title="4.2.1.实体"></a><strong>4.2.1.实体</strong></h3><p>由于UserQuery之前已经定义过了，并且其中已经包含了过滤条件，具体代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.query;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "用户查询条件实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserQuery</span> {</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户名关键字")</span></span><br/><span class="line">    <span class="keyword">private</span> String name;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户状态：1-正常，2-冻结")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer status;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最小值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer minBalance;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最大值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer maxBalance;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>其中缺少的仅仅是分页条件，而分页条件不仅仅用户分页查询需要，以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个<code>PageQuery</code>实体：</p><p><img alt="image-20240502230758930" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230758930.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230758930.png"/></p><p><code>PageQuery</code>是前端提交的查询参数，一般包含四个属性：</p><ul><li><code>pageNo</code>：页码</li><li><code>pageSize</code>：每页数据条数</li><li><code>sortBy</code>：排序字段</li><li><code>isAsc</code>：是否升序</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "分页查询实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PageQuery</span> {</span><br/><span class="line">    <span class="meta">@ApiModelProperty("页码")</span></span><br/><span class="line">    <span class="keyword">private</span> Long pageNo;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("页码")</span></span><br/><span class="line">    <span class="keyword">private</span> Long pageSize;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("排序字段")</span></span><br/><span class="line">    <span class="keyword">private</span> String sortBy;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("是否升序")</span></span><br/><span class="line">    <span class="keyword">private</span> Boolean isAsc;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后，让我们的UserQuery继承这个实体：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.query;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"><span class="keyword">import</span> lombok.EqualsAndHashCode;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@EqualsAndHashCode(callSuper = true)</span></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "用户查询条件实体")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserQuery</span> <span class="keyword">extends</span> <span class="title class_">PageQuery</span> {</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户名关键字")</span></span><br/><span class="line">    <span class="keyword">private</span> String name;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("用户状态：1-正常，2-冻结")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer status;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最小值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer minBalance;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("余额最大值")</span></span><br/><span class="line">    <span class="keyword">private</span> Integer maxBalance;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>返回值的用户实体沿用之前定一个<code>UserVO</code>实体：</p><p><img alt="image-20240502230810948" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230810948.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230810948.png"/></p><p>最后，则是分页实体PageDTO:</p><p><img alt="image-20240502230825299" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230825299.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230825299.png"/></p><p>代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.dto;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br/><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.util.List;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@ApiModel(description = "分页结果")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PageDTO</span>&lt;T&gt; {</span><br/><span class="line">    <span class="meta">@ApiModelProperty("总条数")</span></span><br/><span class="line">    <span class="keyword">private</span> Long total;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("总页数")</span></span><br/><span class="line">    <span class="keyword">private</span> Long pages;</span><br/><span class="line">    <span class="meta">@ApiModelProperty("集合")</span></span><br/><span class="line">    <span class="keyword">private</span> List&lt;T&gt; list;</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="4-2-2-开发接口"><a class="headerlink" href="#4-2-2-开发接口" title="4.2.2.开发接口"></a><strong>4.2.2.开发接口</strong></h3><p>我们在<code>UserController</code>中定义分页查询用户的接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.controller;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.dto.PageDTO;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.query.PageQuery;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.domain.vo.UserVO;</span><br/><span class="line"><span class="keyword">import</span> com.itheima.mp.service.UserService;</span><br/><span class="line"><span class="keyword">import</span> lombok.RequiredArgsConstructor;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RestController</span></span><br/><span class="line"><span class="meta">@RequestMapping("users")</span></span><br/><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> {</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> UserService userService;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@GetMapping("/page")</span></span><br/><span class="line">    <span class="keyword">public</span> PageDTO&lt;UserVO&gt; <span class="title function_">queryUsersPage</span><span class="params">(UserQuery query)</span>{</span><br/><span class="line">        <span class="keyword">return</span> userService.queryUsersPage(query);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">// 。。。 略</span></span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>然后在<code>IUserService</code>中创建<code>queryUsersPage</code>方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/></pre></td><td class="code"><pre><span class="line">PageDTO&lt;UserVO&gt; <span class="title function_">queryUsersPage</span><span class="params">(PageQuery query)</span>;</span><br/></pre></td></tr></table></figure><p>接下来，在UserServiceImpl中实现该方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br/><span class="line"><span class="keyword">public</span> PageDTO&lt;UserVO&gt; <span class="title function_">queryUsersPage</span><span class="params">(PageQuery query)</span> {</span><br/><span class="line">    <span class="comment">// 1.构建条件</span></span><br/><span class="line">    <span class="comment">// 1.1.分页条件</span></span><br/><span class="line">    Page&lt;User&gt; page = Page.of(query.getPageNo(), query.getPageSize());</span><br/><span class="line">    <span class="comment">// 1.2.排序条件</span></span><br/><span class="line">    <span class="keyword">if</span> (query.getSortBy() != <span class="literal">null</span>) {</span><br/><span class="line">        page.addOrder(<span class="keyword">new</span> <span class="title class_">OrderItem</span>(query.getSortBy(), query.getIsAsc()));</span><br/><span class="line">    }<span class="keyword">else</span>{</span><br/><span class="line">        <span class="comment">// 默认按照更新时间排序</span></span><br/><span class="line">        page.addOrder(<span class="keyword">new</span> <span class="title class_">OrderItem</span>(<span class="string">"update_time"</span>, <span class="literal">false</span>));</span><br/><span class="line">    }</span><br/><span class="line">    <span class="comment">// 2.查询</span></span><br/><span class="line">    page(page);</span><br/><span class="line">    <span class="comment">// 3.数据非空校验</span></span><br/><span class="line">    List&lt;User&gt; records = page.getRecords();</span><br/><span class="line">    <span class="keyword">if</span> (records == <span class="literal">null</span> || records.size() &lt;= <span class="number">0</span>) {</span><br/><span class="line">        <span class="comment">// 无数据，返回空结果</span></span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageDTO</span>&lt;&gt;(page.getTotal(), page.getPages(), Collections.emptyList());</span><br/><span class="line">    }</span><br/><span class="line">    <span class="comment">// 4.有数据，转换</span></span><br/><span class="line">    List&lt;UserVO&gt; list = BeanUtil.copyToList(records, UserVO.class);</span><br/><span class="line">    <span class="comment">// 5.封装返回</span></span><br/><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageDTO</span>&lt;UserVO&gt;(page.getTotal(), page.getPages(), list);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>启动项目，在页面查看：</p><p><img alt="image-20240502230839447" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230839447.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230839447.png"/></p><h3 id="4-2-3-改造PageQuery实体"><a class="headerlink" href="#4-2-3-改造PageQuery实体" title="4.2.3.改造PageQuery实体"></a><strong>4.2.3.改造PageQuery实体</strong></h3><p>在刚才的代码中，从<code>PageQuery</code>到<code>MybatisPlus</code>的<code>Page</code>之间转换的过程还是比较麻烦的。</p><p>我们完全可以在<code>PageQuery</code>这个实体中定义一个工具方法，简化开发。 像这样：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.query;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.core.metadata.OrderItem;</span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.plugins.pagination.Page;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PageQuery</span> {</span><br/><span class="line">    <span class="keyword">private</span> Integer pageNo;</span><br/><span class="line">    <span class="keyword">private</span> Integer pageSize;</span><br/><span class="line">    <span class="keyword">private</span> String sortBy;</span><br/><span class="line">    <span class="keyword">private</span> Boolean isAsc;</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">public</span> &lt;T&gt;  Page&lt;T&gt; <span class="title function_">toMpPage</span><span class="params">(OrderItem ... orders)</span>{</span><br/><span class="line">        <span class="comment">// 1.分页条件</span></span><br/><span class="line">        Page&lt;T&gt; p = Page.of(pageNo, pageSize);</span><br/><span class="line">        <span class="comment">// 2.排序条件</span></span><br/><span class="line">        <span class="comment">// 2.1.先看前端有没有传排序字段</span></span><br/><span class="line">        <span class="keyword">if</span> (sortBy != <span class="literal">null</span>) {</span><br/><span class="line">            p.addOrder(<span class="keyword">new</span> <span class="title class_">OrderItem</span>(sortBy, isAsc));</span><br/><span class="line">            <span class="keyword">return</span> p;</span><br/><span class="line">        }</span><br/><span class="line">        <span class="comment">// 2.2.再看有没有手动指定排序字段</span></span><br/><span class="line">        <span class="keyword">if</span>(orders != <span class="literal">null</span>){</span><br/><span class="line">            p.addOrder(orders);</span><br/><span class="line">        }</span><br/><span class="line">        <span class="keyword">return</span> p;</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">public</span> &lt;T&gt; Page&lt;T&gt; <span class="title function_">toMpPage</span><span class="params">(String defaultSortBy, <span class="type">boolean</span> isAsc)</span>{</span><br/><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.toMpPage(<span class="keyword">new</span> <span class="title class_">OrderItem</span>(defaultSortBy, isAsc));</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">public</span> &lt;T&gt; Page&lt;T&gt; <span class="title function_">toMpPageDefaultSortByCreateTimeDesc</span><span class="params">()</span> {</span><br/><span class="line">        <span class="keyword">return</span> toMpPage(<span class="string">"create_time"</span>, <span class="literal">false</span>);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">public</span> &lt;T&gt; Page&lt;T&gt; <span class="title function_">toMpPageDefaultSortByUpdateTimeDesc</span><span class="params">()</span> {</span><br/><span class="line">        <span class="keyword">return</span> toMpPage(<span class="string">"update_time"</span>, <span class="literal">false</span>);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>这样我们在开发也时就可以省去对从<code>PageQuery</code>到<code>Page</code>的的转换：</p><figure class="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="comment">// 1.构建条件</span></span><br/><span class="line">Page&lt;User&gt; page = query.toMpPageDefaultSortByCreateTimeDesc();</span><br/></pre></td></tr></table></figure><h3 id="4-2-4-改造PageDTO实体"><a class="headerlink" href="#4-2-4-改造PageDTO实体" title="4.2.4.改造PageDTO实体"></a><strong>4.2.4.改造PageDTO实体</strong></h3><p>在查询出分页结果后，数据的非空校验，数据的vo转换都是模板代码，编写起来很麻烦。</p><p>我们完全可以将其封装到PageDTO的工具方法中，简化整个过程：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/><span class="line">42</span><br/><span class="line">43</span><br/><span class="line">44</span><br/><span class="line">45</span><br/><span class="line">46</span><br/><span class="line">47</span><br/><span class="line">48</span><br/><span class="line">49</span><br/><span class="line">50</span><br/><span class="line">51</span><br/><span class="line">52</span><br/><span class="line">53</span><br/><span class="line">54</span><br/><span class="line">55</span><br/><span class="line">56</span><br/><span class="line">57</span><br/><span class="line">58</span><br/><span class="line">59</span><br/><span class="line">60</span><br/><span class="line">61</span><br/><span class="line">62</span><br/><span class="line">63</span><br/><span class="line">64</span><br/><span class="line">65</span><br/><span class="line">66</span><br/><span class="line">67</span><br/><span class="line">68</span><br/><span class="line">69</span><br/><span class="line">70</span><br/><span class="line">71</span><br/><span class="line">72</span><br/><span class="line">73</span><br/><span class="line">74</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itheima.mp.domain.dto;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> cn.hutool.core.bean.BeanUtil;</span><br/><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.extension.plugins.pagination.Page;</span><br/><span class="line"><span class="keyword">import</span> lombok.AllArgsConstructor;</span><br/><span class="line"><span class="keyword">import</span> lombok.Data;</span><br/><span class="line"><span class="keyword">import</span> lombok.NoArgsConstructor;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.util.Collections;</span><br/><span class="line"><span class="keyword">import</span> java.util.List;</span><br/><span class="line"><span class="keyword">import</span> java.util.function.Function;</span><br/><span class="line"><span class="keyword">import</span> java.util.stream.Collectors;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@Data</span></span><br/><span class="line"><span class="meta">@NoArgsConstructor</span></span><br/><span class="line"><span class="meta">@AllArgsConstructor</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PageDTO</span>&lt;V&gt; {</span><br/><span class="line">    <span class="keyword">private</span> Long total;</span><br/><span class="line">    <span class="keyword">private</span> Long pages;</span><br/><span class="line">    <span class="keyword">private</span> List&lt;V&gt; list;</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 class="doctag">@param</span> p MybatisPlus的分页结果</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;V&gt; 目标VO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;P&gt; 原始PO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@return</span> VO的分页对象</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;V, P&gt; PageDTO&lt;V&gt; <span class="title function_">empty</span><span class="params">(Page&lt;P&gt; p)</span>{</span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageDTO</span>&lt;&gt;(p.getTotal(), p.getPages(), Collections.emptyList());</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 将MybatisPlus分页结果转为 VO分页结果</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> p MybatisPlus的分页结果</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> voClass 目标VO类型的字节码</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;V&gt; 目标VO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;P&gt; 原始PO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@return</span> VO的分页对象</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;V, P&gt; PageDTO&lt;V&gt; <span class="title function_">of</span><span class="params">(Page&lt;P&gt; p, Class&lt;V&gt; voClass)</span> {</span><br/><span class="line">        <span class="comment">// 1.非空校验</span></span><br/><span class="line">        List&lt;P&gt; records = p.getRecords();</span><br/><span class="line">        <span class="keyword">if</span> (records == <span class="literal">null</span> || records.size() &lt;= <span class="number">0</span>) {</span><br/><span class="line">            <span class="comment">// 无数据，返回空结果</span></span><br/><span class="line">            <span class="keyword">return</span> empty(p);</span><br/><span class="line">        }</span><br/><span class="line">        <span class="comment">// 2.数据转换</span></span><br/><span class="line">        List&lt;V&gt; vos = BeanUtil.copyToList(records, voClass);</span><br/><span class="line">        <span class="comment">// 3.封装返回</span></span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageDTO</span>&lt;&gt;(p.getTotal(), p.getPages(), vos);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 将MybatisPlus分页结果转为 VO分页结果，允许用户自定义PO到VO的转换方式</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> p MybatisPlus的分页结果</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> convertor PO到VO的转换函数</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;V&gt; 目标VO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;P&gt; 原始PO类型</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@return</span> VO的分页对象</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;V, P&gt; PageDTO&lt;V&gt; <span class="title function_">of</span><span class="params">(Page&lt;P&gt; p, Function&lt;P, V&gt; convertor)</span> {</span><br/><span class="line">        <span class="comment">// 1.非空校验</span></span><br/><span class="line">        List&lt;P&gt; records = p.getRecords();</span><br/><span class="line">        <span class="keyword">if</span> (records == <span class="literal">null</span> || records.size() &lt;= <span class="number">0</span>) {</span><br/><span class="line">            <span class="comment">// 无数据，返回空结果</span></span><br/><span class="line">            <span class="keyword">return</span> empty(p);</span><br/><span class="line">        }</span><br/><span class="line">        <span class="comment">// 2.数据转换</span></span><br/><span class="line">        List&lt;V&gt; vos = records.stream().map(convertor).collect(Collectors.toList());</span><br/><span class="line">        <span class="comment">// 3.封装返回</span></span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageDTO</span>&lt;&gt;(p.getTotal(), p.getPages(), vos);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最终，业务层的代码可以简化为：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><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">@Override</span></span><br/><span class="line"><span class="keyword">public</span> PageDTO&lt;UserVO&gt; <span class="title function_">queryUserByPage</span><span class="params">(PageQuery query)</span> {</span><br/><span class="line">    <span class="comment">// 1.构建条件</span></span><br/><span class="line">    Page&lt;User&gt; page = query.toMpPageDefaultSortByCreateTimeDesc();</span><br/><span class="line">    <span class="comment">// 2.查询</span></span><br/><span class="line">    page(page);</span><br/><span class="line">    <span class="comment">// 3.封装返回</span></span><br/><span class="line">    <span class="keyword">return</span> PageDTO.of(page, UserVO.class);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>如果是希望自定义PO到VO的转换过程，可以这样做：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br/><span class="line"><span class="keyword">public</span> PageDTO&lt;UserVO&gt; <span class="title function_">queryUserByPage</span><span class="params">(PageQuery query)</span> {</span><br/><span class="line">    <span class="comment">// 1.构建条件</span></span><br/><span class="line">    Page&lt;User&gt; page = query.toMpPageDefaultSortByCreateTimeDesc();</span><br/><span class="line">    <span class="comment">// 2.查询</span></span><br/><span class="line">    page(page);</span><br/><span class="line">    <span class="comment">// 3.封装返回</span></span><br/><span class="line">    <span class="keyword">return</span> PageDTO.of(page, user -&gt; {</span><br/><span class="line">        <span class="comment">// 拷贝属性到VO</span></span><br/><span class="line">        <span class="type">UserVO</span> <span class="variable">vo</span> <span class="operator">=</span> BeanUtil.copyProperties(user, UserVO.class);</span><br/><span class="line">        <span class="comment">// 用户名脱敏</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> vo.getUsername();</span><br/><span class="line">        vo.setUsername(username.substring(<span class="number">0</span>, username.length() - <span class="number">2</span>) + <span class="string">"**"</span>);</span><br/><span class="line">        <span class="keyword">return</span> vo;</span><br/><span class="line">    });</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>最终查询的结果如下：</p><p><img alt="image-20240502230906331" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20240502230906331.png" src="https://img.shallowrecall.top/images/2024/4/image-20240502230906331.png"/></p><h1 id="5-作业"><a class="headerlink" href="#5-作业" title="5.作业"></a><strong>5.作业</strong></h1><p>尝试改造项目一中的<code>Service</code>层和<code>Mapper</code>层实现，用<code>MybatisPlus</code>代替单表的CRUD</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;大家在日常开发中应该能发现，单表的CRUD功能代码重复度很高，也没有什么难度。而这部分代码量往往比较大，开发起来比较费时。&lt;/p&gt;
&lt;p&gt;因此，目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus.&lt;/p&gt;
&lt;</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="MyBatis" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/MyBatis/"/>
    
    
    <category term="MyBatis-Plus" scheme="https://www.shallowrecall.top/tags/MyBatis-Plus/"/>
    
  </entry>
  
  <entry>
    <title>RabbitMQ</title>
    <link href="https://www.shallowrecall.top/posts/b543ced0.html"/>
    <id>https://www.shallowrecall.top/posts/b543ced0.html</id>
    <published>2024-04-26T23:57:05.000Z</published>
    <updated>2026-10-16T12:30:20.000Z</updated>
    
    <content type="html"><![CDATA[<p><h1 id="RabbitMQ"><a class="headerlink" href="#RabbitMQ" title="RabbitMQ"></a>RabbitMQ</h1><h1 id="1-初识MQ"><a class="headerlink" href="#1-初识MQ" title="1.初识MQ"></a>1.初识MQ</h1><h2 id="1-1-同步和异步通讯"><a class="headerlink" href="#1-1-同步和异步通讯" title="1.1.同步和异步通讯"></a>1.1.同步和异步通讯</h2><p>微服务间通讯有同步和异步两种方式：</p></p><p><p>同步通讯：就像打电话，需要实时响应。</p></p><p><p>异步通讯：就像发邮件，不需要马上回复。</p></p><p><p><img alt="image-20210717161939695" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717161939695.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717161939695.png"/></p></p><p><p>两种方式各有优劣，打电话可以立即得到响应，但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件，但是往往响应会有延迟。</p></p><p><h3 id="1-1-1-同步通讯"><a class="headerlink" href="#1-1-1-同步通讯" title="1.1.1.同步通讯"></a>1.1.1.同步通讯</h3><p>我们之前学习的Feign调用就属于同步方式，虽然调用可以实时得到结果，但存在下面的问题：</p></p><p><p><img alt="image-20210717162004285" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717162004285.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717162004285.png"/></p></p><p><p>总结：</p></p><p><p>同步调用的优点：</p></p><p><ul></p><p><li>时效性较强，可以立即得到结果</li><br>&lt;/ul&gt;</p><p><p>同步调用的问题：</p></p><p><ul></p><p><li>耦合度高</li></p><p><li>性能和吞吐能力下降</li></p><p><li>有额外的资源消耗</li></p><p><li>有级联失败问题</li><br>&lt;/ul&gt;</p><p><h3 id="1-1-2-异步通讯"><a class="headerlink" href="#1-1-2-异步通讯" title="1.1.2.异步通讯"></a>1.1.2.异步通讯</h3><p>异步调用则可以避免上述问题：</p></p><p><p>我们以购买商品为例，用户支付后需要调用订单服务完成订单状态修改，调用物流服务，从仓库分配响应的库存并准备发货。</p></p><p><p>在事件模式中，支付服务是事件发布者（publisher），在支付完成后只需要发布一个支付成功的事件（event），事件中带上订单id。</p></p><p><p>订单服务和物流服务是事件订阅者（Consumer），订阅支付成功的事件，监听到事件后完成自己业务即可。</p></p><p><p>为了解除事件发布者与订阅者之间的耦合，两者并不是直接通信，而是有一个中间人（Broker）。发布者发布事件到Broker，不关心谁来订阅事件。订阅者从Broker订阅事件，不关心谁发来的消息。</p></p><p><p><img alt="image-20210422095356088" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210422095356088.png" src="https://img.shallowrecall.top/images/2024/4/image-20210422095356088.png"/></p></p><p><p>Broker 是一个像数据总线一样的东西，所有的服务要接收数据和发送数据都发到这个总线上，这个总线就像协议一样，让服务间的通讯变得标准和可控。</p></p><p><p>好处：</p></p><p><ul></p><p><li><p>吞吐量提升：无需等待订阅者处理完成，响应更快速</p></li></p><p><li><p>故障隔离：服务没有直接调用，不存在级联失败问题</p></li></p><p><li>调用间没有阻塞，不会造成无效的资源占用</li></p><p><li>耦合度极低，每个服务都可以灵活插拔，可替换</li></p><p><li>流量削峰：不管发布事件的流量波动多大，都由Broker接收，订阅者可以按照自己的速度去处理事件</li><br>&lt;/ul&gt;</p><p><p>缺点：</p></p><p><ul></p><p><li>架构复杂了，业务没有明显的流程线，不好管理</li></p><p><li>需要依赖于Broker的可靠、安全、性能</li><br>&lt;/ul&gt;</p><p><p>好在现在开源软件或云平台上 Broker 的软件是非常成熟的，比较常见的一种就是我们今天要学习的MQ技术。</p></p><p><h2 id="1-2-技术对比："><a class="headerlink" href="#1-2-技术对比：" title="1.2.技术对比："></a>1.2.技术对比：</h2><p>MQ，中文是消息队列（MessageQueue），字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。</p></p><p><p>比较常见的MQ实现：</p></p><p><ul></p><p><li>ActiveMQ</li></p><p><li>RabbitMQ</li></p><p><li>RocketMQ</li></p><p><li>Kafka</li><br>&lt;/ul&gt;</p><p><p>几种常见MQ的对比：</p></p><p><div class="table-container"></p><p><table></p><p><thead></p><p><tr></p><p><th></th></p><p><th><strong>RabbitMQ</strong></th></p><p><th><strong>ActiveMQ</strong></th></p><p><th><strong>RocketMQ</strong></th></p><p><th><strong>Kafka</strong></th><br>&lt;/tr&gt;<br>&lt;/thead&gt;</p><p><tbody></p><p><tr></p><p><td>公司/社区</td></p><p><td>Rabbit</td></p><p><td>Apache</td></p><p><td>阿里</td></p><p><td>Apache</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>开发语言</td></p><p><td>Erlang</td></p><p><td>Java</td></p><p><td>Java</td></p><p><td>Scala&amp;Java</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>协议支持</td></p><p><td>AMQP，XMPP，SMTP，STOMP</td></p><p><td>OpenWire,STOMP，REST,XMPP,AMQP</td></p><p><td>自定义协议</td></p><p><td>自定义协议</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>可用性</td></p><p><td>高</td></p><p><td>一般</td></p><p><td>高</td></p><p><td>高</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>单机吞吐量</td></p><p><td>一般</td></p><p><td>差</td></p><p><td>高</td></p><p><td>非常高</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>消息延迟</td></p><p><td>微秒级</td></p><p><td>毫秒级</td></p><p><td>毫秒级</td></p><p><td>毫秒以内</td><br>&lt;/tr&gt;</p><p><tr></p><p><td>消息可靠性</td></p><p><td>高</td></p><p><td>一般</td></p><p><td>高</td></p><p><td>一般</td><br>&lt;/tr&gt;<br>&lt;/tbody&gt;<br>&lt;/table&gt;<br>&lt;/div&gt;</p><p><p>追求可用性：Kafka、 RocketMQ 、RabbitMQ</p></p><p><p>追求可靠性：RabbitMQ、RocketMQ</p></p><p><p>追求吞吐能力：RocketMQ、Kafka</p></p><p><p>追求消息低延迟：RabbitMQ、Kafka</p></p><p><h1 id="2-快速入门"><a class="headerlink" href="#2-快速入门" title="2.快速入门"></a>2.快速入门</h1><h2 id="2-1-安装RabbitMQ"><a class="headerlink" href="#2-1-安装RabbitMQ" title="2.1.安装RabbitMQ"></a>2.1.安装RabbitMQ</h2><p>安装RabbitMQ，参考课前资料：</p></p><p><p><img alt="image-20210717162628635" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717162628635.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717162628635.png"/></p></p><p><p>MQ的基本结构：</p></p><p><p><img alt="image-20210717162752376" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717162752376.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717162752376.png"/></p></p><p><p>RabbitMQ中的一些角色：</p></p><p><ul></p><p><li>publisher：生产者</li></p><p><li>consumer：消费者</li></p><p><li>exchange个：交换机，负责消息路由</li></p><p><li>queue：队列，存储消息</li></p><p><li>virtualHost：虚拟主机，隔离不同租户的exchange、queue、消息的隔离</li><br>&lt;/ul&gt;</p><p><h2 id="2-2-RabbitMQ消息模型"><a class="headerlink" href="#2-2-RabbitMQ消息模型" title="2.2.RabbitMQ消息模型"></a>2.2.RabbitMQ消息模型</h2><p>RabbitMQ官方提供了5个不同的Demo示例，对应了不同的消息模型：</p></p><p><p><img alt="image-20210717163332646" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717163332646.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717163332646.png"/></p></p><p><h2 id="2-3-导入Demo工程"><a class="headerlink" href="#2-3-导入Demo工程" title="2.3.导入Demo工程"></a>2.3.导入Demo工程</h2><p>课前资料提供了一个Demo工程，mq-demo:</p></p><p><p><img alt="image-20210717163253264" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717163253264.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717163253264.png"/></p></p><p><p>导入后可以看到结构如下：</p></p><p><p><img alt="image-20210717163604330" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717163604330.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717163604330.png"/></p></p><p><p>包括三部分：</p></p><p><ul></p><p><li>mq-demo：父工程，管理项目依赖</li></p><p><li>publisher：消息的发送者</li></p><p><li>consumer：消息的消费者</li><br>&lt;/ul&gt;</p><p><h2 id="2-4-入门案例"><a class="headerlink" href="#2-4-入门案例" title="2.4.入门案例"></a>2.4.入门案例</h2><p>简单队列模式的模型图：</p></p><p><p><img alt="image-20210717163434647" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717163434647.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717163434647.png"/></p></p><p><p>官方的HelloWorld是基于最基础的消息队列模型来实现的，只包括三个角色：</p></p><p><ul></p><p><li>publisher：消息发布者，将消息发送到队列queue</li></p><p><li>queue：消息队列，负责接受并缓存消息</li></p><p><li>consumer：订阅队列，处理队列中的消息</li><br>&lt;/ul&gt;</p><p><h3 id="2-4-1-publisher实现"><a class="headerlink" href="#2-4-1-publisher实现" title="2.4.1.publisher实现"></a>2.4.1.publisher实现</h3><p>思路：</p></p><p><ul></p><p><li>建立连接</li></p><p><li>创建Channel</li></p><p><li>声明队列</li></p><p><li>发送消息</li></p><p><li>关闭连接和channel</li><br>&lt;/ul&gt;</p><p><p>代码实现：</p></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/><span class="line">42</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.helloworld;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.rabbitmq.client.Channel;</span><br/><span class="line"><span class="keyword">import</span> com.rabbitmq.client.Connection;</span><br/><span class="line"><span class="keyword">import</span> com.rabbitmq.client.ConnectionFactory;</span><br/><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br/><span class="line"><span class="keyword">import</span> java.util.concurrent.TimeoutException;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PublisherTest</span> {</span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendMessage</span><span class="params">()</span> <span class="keyword">throws</span> IOException, TimeoutException {</span><br/><span class="line">        <span class="comment">// 1.建立连接</span></span><br/><span class="line">        <span class="type">ConnectionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConnectionFactory</span>();</span><br/><span class="line">        <span class="comment">// 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码</span></span><br/><span class="line">        factory.setHost(<span class="string">"192.168.150.101"</span>);</span><br/><span class="line">        factory.setPort(<span class="number">5672</span>);</span><br/><span class="line">        factory.setVirtualHost(<span class="string">"/"</span>);</span><br/><span class="line">        factory.setUsername(<span class="string">"itcast"</span>);</span><br/><span class="line">        factory.setPassword(<span class="string">"123321"</span>);</span><br/><span class="line">        <span class="comment">// 1.2.建立连接</span></span><br/><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> factory.newConnection();</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 2.创建通道Channel</span></span><br/><span class="line">        <span class="type">Channel</span> <span class="variable">channel</span> <span class="operator">=</span> connection.createChannel();</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 3.创建队列</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"simple.queue"</span>;</span><br/><span class="line">        channel.queueDeclare(queueName, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">null</span>);</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 4.发送消息</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"hello, rabbitmq!"</span>;</span><br/><span class="line">        channel.basicPublish(<span class="string">""</span>, queueName, <span class="literal">null</span>, message.getBytes());</span><br/><span class="line">        System.out.println(<span class="string">"发送消息成功：【"</span> + message + <span class="string">"】"</span>);</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 5.关闭通道和连接</span></span><br/><span class="line">        channel.close();</span><br/><span class="line">        connection.close();</span><br/><span class="line"></span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="2-4-2-consumer实现"><a class="headerlink" href="#2-4-2-consumer实现" title="2.4.2.consumer实现"></a>2.4.2.consumer实现</h3><p>代码思路：</p><ul><li>建立连接</li><li>创建Channel</li><li>声明队列</li><li>订阅消息</li></ul><p>代码实现：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.helloworld;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> com.rabbitmq.client.*;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br/><span class="line"><span class="keyword">import</span> java.util.concurrent.TimeoutException;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ConsumerTest</span> {</span><br/><span class="line"></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException, TimeoutException {</span><br/><span class="line">        <span class="comment">// 1.建立连接</span></span><br/><span class="line">        <span class="type">ConnectionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConnectionFactory</span>();</span><br/><span class="line">        <span class="comment">// 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码</span></span><br/><span class="line">        factory.setHost(<span class="string">"192.168.150.101"</span>);</span><br/><span class="line">        factory.setPort(<span class="number">5672</span>);</span><br/><span class="line">        factory.setVirtualHost(<span class="string">"/"</span>);</span><br/><span class="line">        factory.setUsername(<span class="string">"itcast"</span>);</span><br/><span class="line">        factory.setPassword(<span class="string">"123321"</span>);</span><br/><span class="line">        <span class="comment">// 1.2.建立连接</span></span><br/><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> factory.newConnection();</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 2.创建通道Channel</span></span><br/><span class="line">        <span class="type">Channel</span> <span class="variable">channel</span> <span class="operator">=</span> connection.createChannel();</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 3.创建队列</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"simple.queue"</span>;</span><br/><span class="line">        channel.queueDeclare(queueName, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">null</span>);</span><br/><span class="line"></span><br/><span class="line">        <span class="comment">// 4.订阅消息</span></span><br/><span class="line">        channel.basicConsume(queueName, <span class="literal">true</span>, <span class="keyword">new</span> <span class="title class_">DefaultConsumer</span>(channel){</span><br/><span class="line">            <span class="meta">@Override</span></span><br/><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleDelivery</span><span class="params">(String consumerTag, Envelope envelope,</span></span><br/><span class="line"><span class="params">                                       AMQP.BasicProperties properties, <span class="type">byte</span>[] body)</span> <span class="keyword">throws</span> IOException {</span><br/><span class="line">                <span class="comment">// 5.处理消息</span></span><br/><span class="line">                <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(body);</span><br/><span class="line">                System.out.println(<span class="string">"接收到消息：【"</span> + message + <span class="string">"】"</span>);</span><br/><span class="line">            }</span><br/><span class="line">        });</span><br/><span class="line">        System.out.println(<span class="string">"等待接收消息。。。。"</span>);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h2 id="2-5-总结"><a class="headerlink" href="#2-5-总结" title="2.5.总结"></a>2.5.总结</h2><p>基本消息队列的消息发送流程：</p><ol><li><p>建立connection</p></li><li><p>创建channel</p></li><li><p>利用channel声明队列</p></li><li><p>利用channel向队列发送消息</p></li></ol><p>基本消息队列的消息接收流程：</p><ol><li><p>建立connection</p></li><li><p>创建channel</p></li><li><p>利用channel声明队列</p></li><li><p>定义consumer的消费行为handleDelivery()</p></li><li><p>利用channel将消费者与队列绑定</p></li></ol><h1 id="3-SpringAMQP"><a class="headerlink" href="#3-SpringAMQP" title="3.SpringAMQP"></a>3.SpringAMQP</h1><p>SpringAMQP是基于RabbitMQ封装的一套模板，并且还利用SpringBoot对其实现了自动装配，使用起来非常方便。</p><p>SpringAmqp的官方地址：<a href="https://spring.io/projects/spring-amqp" rel="noopener" target="_blank">https://spring.io/projects/spring-amqp</a></p><p><img alt="image-20210717164024967" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717164024967.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717164024967.png"/></p><p><img alt="image-20210717164038678" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717164038678.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717164038678.png"/></p><p>SpringAMQP提供了三个功能：</p><ul><li>自动声明队列、交换机及其绑定关系</li><li>基于注解的监听器模式，异步接收消息</li><li>封装了RabbitTemplate工具，用于发送消息</li></ul><h2 id="3-1-Basic-Queue-简单队列模型"><a class="headerlink" href="#3-1-Basic-Queue-简单队列模型" title="3.1.Basic Queue 简单队列模型"></a>3.1.Basic Queue 简单队列模型</h2><p>在父工程mq-demo中引入依赖</p><figure class="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="comment">&lt;!--AMQP依赖，包含RabbitMQ--&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-amqp<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/></pre></td></tr></table></figure><h3 id="3-1-1-消息发送"><a class="headerlink" href="#3-1-1-消息发送" title="3.1.1.消息发送"></a>3.1.1.消息发送</h3><p>首先配置MQ地址，在publisher服务的application.yml中添加配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><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">spring:</span></span><br/><span class="line">  <span class="attr">rabbitmq:</span></span><br/><span class="line">    <span class="attr">host:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span> <span class="comment"># 主机名</span></span><br/><span class="line">    <span class="attr">port:</span> <span class="number">5672</span> <span class="comment"># 端口</span></span><br/><span class="line">    <span class="attr">virtual-host:</span> <span class="string">/</span> <span class="comment"># 虚拟主机</span></span><br/><span class="line">    <span class="attr">username:</span> <span class="string">itcast</span> <span class="comment"># 用户名</span></span><br/><span class="line">    <span class="attr">password:</span> <span class="number">123321</span> <span class="comment"># 密码</span></span><br/></pre></td></tr></table></figure><p>然后在publisher服务中编写测试类SpringAmqpTest，并利用RabbitTemplate实现消息发送：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.spring;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br/><span class="line"><span class="keyword">import</span> org.junit.runner.RunWith;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.core.RabbitTemplate;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.test.context.junit4.SpringRunner;</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RunWith(SpringRunner.class)</span></span><br/><span class="line"><span class="meta">@SpringBootTest</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringAmqpTest</span> {</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> RabbitTemplate rabbitTemplate;</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@Test</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSimpleQueue</span><span class="params">()</span> {</span><br/><span class="line">        <span class="comment">// 队列名称</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"simple.queue"</span>;</span><br/><span class="line">        <span class="comment">// 消息</span></span><br/><span class="line">        <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"hello, spring amqp!"</span>;</span><br/><span class="line">        <span class="comment">// 发送消息</span></span><br/><span class="line">        rabbitTemplate.convertAndSend(queueName, message);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-1-2-消息接收"><a class="headerlink" href="#3-1-2-消息接收" title="3.1.2.消息接收"></a>3.1.2.消息接收</h3><p>首先配置MQ地址，在consumer服务的application.yml中添加配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><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">spring:</span></span><br/><span class="line">  <span class="attr">rabbitmq:</span></span><br/><span class="line">    <span class="attr">host:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span> <span class="comment"># 主机名</span></span><br/><span class="line">    <span class="attr">port:</span> <span class="number">5672</span> <span class="comment"># 端口</span></span><br/><span class="line">    <span class="attr">virtual-host:</span> <span class="string">/</span> <span class="comment"># 虚拟主机</span></span><br/><span class="line">    <span class="attr">username:</span> <span class="string">itcast</span> <span class="comment"># 用户名</span></span><br/><span class="line">    <span class="attr">password:</span> <span class="number">123321</span> <span class="comment"># 密码</span></span><br/></pre></td></tr></table></figure><p>然后在consumer服务的<code>cn.itcast.mq.listener</code>包中新建一个类SpringRabbitListener，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.listener;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.annotation.RabbitListener;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br/><span class="line"></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_">SpringRabbitListener</span> {</span><br/><span class="line"></span><br/><span class="line">    <span class="meta">@RabbitListener(queues = "simple.queue")</span></span><br/><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenSimpleQueueMessage</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException {</span><br/><span class="line">        System.out.println(<span class="string">"spring 消费者接收到消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-1-3-测试"><a class="headerlink" href="#3-1-3-测试" title="3.1.3.测试"></a>3.1.3.测试</h3><p>启动consumer服务，然后在publisher服务中运行测试代码，发送MQ消息</p><h2 id="3-2-WorkQueue"><a class="headerlink" href="#3-2-WorkQueue" title="3.2.WorkQueue"></a>3.2.WorkQueue</h2><p>Work queues，也被称为（Task queues），任务模型。简单来说就是<strong>让多个消费者绑定到一个队列，共同消费队列中的消息</strong>。</p><p><img alt="image-20210717164238910" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717164238910.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717164238910.png"/></p><p>当消息处理比较耗时的时候，可能生产消息的速度会远远大于消息的消费速度。长此以往，消息就会堆积越来越多，无法及时处理。</p><p>此时就可以使用work 模型，多个消费者共同处理消息处理，速度就能大大提高了。</p><h3 id="3-2-1-消息发送"><a class="headerlink" href="#3-2-1-消息发送" title="3.2.1.消息发送"></a>3.2.1.消息发送</h3><p>这次我们循环发送，模拟大量消息堆积现象。</p><p>在publisher服务中的SpringAmqpTest类中添加一个测试方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * workQueue</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="meta">@Test</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testWorkQueue</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException {</span><br/><span class="line">    <span class="comment">// 队列名称</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">"simple.queue"</span>;</span><br/><span class="line">    <span class="comment">// 消息</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"hello, message_"</span>;</span><br/><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">50</span>; i++) {</span><br/><span class="line">        <span class="comment">// 发送消息</span></span><br/><span class="line">        rabbitTemplate.convertAndSend(queueName, message + i);</span><br/><span class="line">        Thread.sleep(<span class="number">20</span>);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-2-2-消息接收"><a class="headerlink" href="#3-2-2-消息接收" title="3.2.2.消息接收"></a>3.2.2.消息接收</h3><p>要模拟多个消费者绑定同一个队列，我们在consumer服务的SpringRabbitListener中添加2个新的方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(queues = "simple.queue")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenWorkQueue1</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException {</span><br/><span class="line">    System.out.println(<span class="string">"消费者1接收到消息：【"</span> + msg + <span class="string">"】"</span> + LocalTime.now());</span><br/><span class="line">    Thread.sleep(<span class="number">20</span>);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RabbitListener(queues = "simple.queue")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenWorkQueue2</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException {</span><br/><span class="line">    System.err.println(<span class="string">"消费者2........接收到消息：【"</span> + msg + <span class="string">"】"</span> + LocalTime.now());</span><br/><span class="line">    Thread.sleep(<span class="number">200</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>注意到这个消费者sleep了1000秒，模拟任务耗时。</p><h3 id="3-2-3-测试"><a class="headerlink" href="#3-2-3-测试" title="3.2.3.测试"></a>3.2.3.测试</h3><p>启动ConsumerApplication后，在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。</p><p>可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。</p><p>也就是说消息是平均分配给每个消费者，并没有考虑到消费者的处理能力。这样显然是有问题的。</p><h3 id="3-2-4-能者多劳"><a class="headerlink" href="#3-2-4-能者多劳" title="3.2.4.能者多劳"></a>3.2.4.能者多劳</h3><p>在spring中有一个简单的配置，可以解决这个问题。我们修改consumer服务的application.yml文件，添加配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><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">rabbitmq:</span></span><br/><span class="line">    <span class="attr">listener:</span></span><br/><span class="line">      <span class="attr">simple:</span></span><br/><span class="line">        <span class="attr">prefetch:</span> <span class="number">1</span> <span class="comment"># 每次只能获取一条消息，处理完成才能获取下一个消息</span></span><br/></pre></td></tr></table></figure><h3 id="3-2-5-总结"><a class="headerlink" href="#3-2-5-总结" title="3.2.5.总结"></a>3.2.5.总结</h3><p>Work模型的使用：</p><ul><li>多个消费者绑定到一个队列，同一条消息只会被一个消费者处理</li><li>通过设置prefetch来控制消费者预取的消息数量</li></ul><h2 id="3-3-发布-订阅"><a class="headerlink" href="#3-3-发布-订阅" title="3.3.发布/订阅"></a>3.3.发布/订阅</h2><p>发布订阅的模型如图：</p><p><img alt="image-20210717165309625" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717165309625.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717165309625.png"/></p><p>可以看到，在订阅模型中，多了一个exchange角色，而且过程略有变化：</p><ul><li>Publisher：生产者，也就是要发送消息的程序，但是不再发送到队列中，而是发给X（交换机）</li><li>Exchange：交换机，图中的X。一方面，接收生产者发送的消息。另一方面，知道如何处理消息，例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作，取决于Exchange的类型。Exchange有以下3种类型：<ul><li>Fanout：广播，将消息交给所有绑定到交换机的队列</li><li>Direct：定向，把消息交给符合指定routing key 的队列</li><li>Topic：通配符，把消息交给符合routing pattern（路由模式） 的队列</li></ul></li><li>Consumer：消费者，与以前一样，订阅队列，没有变化</li><li>Queue：消息队列也与以前一样，接收消息、缓存消息。</li></ul><p><strong>Exchange（交换机）只负责转发消息，不具备存储消息的能力</strong>，因此如果没有任何队列与Exchange绑定，或者没有符合路由规则的队列，那么消息会丢失！</p><h2 id="3-4-Fanout"><a class="headerlink" href="#3-4-Fanout" title="3.4.Fanout"></a>3.4.Fanout</h2><p>Fanout，英文翻译是扇出，我觉得在MQ中叫广播更合适。</p><p><img alt="image-20210717165438225" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717165438225.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717165438225.png"/></p><p>在广播模式下，消息发送流程是这样的：</p><ul><li>1）  可以有多个队列</li><li>2）  每个队列都要绑定到Exchange（交换机）</li><li>3）  生产者发送的消息，只能发送到交换机，交换机来决定要发给哪个队列，生产者无法决定</li><li>4）  交换机把消息发送给绑定过的所有队列</li><li>5）  订阅队列的消费者都能拿到消息</li></ul><p>我们的计划是这样的：</p><ul><li>创建一个交换机 itcast.fanout，类型是Fanout</li><li>创建两个队列fanout.queue1和fanout.queue2，绑定到交换机itcast.fanout</li></ul><p><img alt="image-20210717165509466" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717165509466.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717165509466.png"/></p><h3 id="3-4-1-声明队列和交换机"><a class="headerlink" href="#3-4-1-声明队列和交换机" title="3.4.1.声明队列和交换机"></a>3.4.1.声明队列和交换机</h3><p>Spring提供了一个接口Exchange，来表示所有不同类型的交换机：</p><p><img alt="image-20210717165552676" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717165552676.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717165552676.png"/></p><p>在consumer中创建一个类，声明队列和交换机：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/><span class="line">18</span><br/><span class="line">19</span><br/><span class="line">20</span><br/><span class="line">21</span><br/><span class="line">22</span><br/><span class="line">23</span><br/><span class="line">24</span><br/><span class="line">25</span><br/><span class="line">26</span><br/><span class="line">27</span><br/><span class="line">28</span><br/><span class="line">29</span><br/><span class="line">30</span><br/><span class="line">31</span><br/><span class="line">32</span><br/><span class="line">33</span><br/><span class="line">34</span><br/><span class="line">35</span><br/><span class="line">36</span><br/><span class="line">37</span><br/><span class="line">38</span><br/><span class="line">39</span><br/><span class="line">40</span><br/><span class="line">41</span><br/><span class="line">42</span><br/><span class="line">43</span><br/><span class="line">44</span><br/><span class="line">45</span><br/><span class="line">46</span><br/><span class="line">47</span><br/><span class="line">48</span><br/><span class="line">49</span><br/><span class="line">50</span><br/><span class="line">51</span><br/><span class="line">52</span><br/></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.config;</span><br/><span class="line"></span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.Binding;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.BindingBuilder;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.FanoutExchange;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.Queue;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br/><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br/><span class="line"></span><br/><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_">FanoutConfig</span> {</span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 声明交换机</span></span><br/><span class="line"><span class="comment">     * <span class="doctag">@return</span> Fanout类型交换机</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="meta">@Bean</span></span><br/><span class="line">    <span class="keyword">public</span> FanoutExchange <span class="title function_">fanoutExchange</span><span class="params">()</span>{</span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">FanoutExchange</span>(<span class="string">"itcast.fanout"</span>);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 第1个队列</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="meta">@Bean</span></span><br/><span class="line">    <span class="keyword">public</span> Queue <span class="title function_">fanoutQueue1</span><span class="params">()</span>{</span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Queue</span>(<span class="string">"fanout.queue1"</span>);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 绑定队列和交换机</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="meta">@Bean</span></span><br/><span class="line">    <span class="keyword">public</span> Binding <span class="title function_">bindingQueue1</span><span class="params">(Queue fanoutQueue1, FanoutExchange fanoutExchange)</span>{</span><br/><span class="line">        <span class="keyword">return</span> BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 第2个队列</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="meta">@Bean</span></span><br/><span class="line">    <span class="keyword">public</span> Queue <span class="title function_">fanoutQueue2</span><span class="params">()</span>{</span><br/><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Queue</span>(<span class="string">"fanout.queue2"</span>);</span><br/><span class="line">    }</span><br/><span class="line"></span><br/><span class="line">    <span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * 绑定队列和交换机</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line">    <span class="meta">@Bean</span></span><br/><span class="line">    <span class="keyword">public</span> Binding <span class="title function_">bindingQueue2</span><span class="params">(Queue fanoutQueue2, FanoutExchange fanoutExchange)</span>{</span><br/><span class="line">        <span class="keyword">return</span> BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);</span><br/><span class="line">    }</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-4-2-消息发送"><a class="headerlink" href="#3-4-2-消息发送" title="3.4.2.消息发送"></a>3.4.2.消息发送</h3><p>在publisher服务的SpringAmqpTest类中添加测试方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testFanoutExchange</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 队列名称</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">"itcast.fanout"</span>;</span><br/><span class="line">    <span class="comment">// 消息</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"hello, everyone!"</span>;</span><br/><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">""</span>, message);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-4-3-消息接收"><a class="headerlink" href="#3-4-3-消息接收" title="3.4.3.消息接收"></a>3.4.3.消息接收</h3><p>在consumer服务的SpringRabbitListener中添加两个方法，作为消费者：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(queues = "fanout.queue1")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenFanoutQueue1</span><span class="params">(String msg)</span> {</span><br/><span class="line">    System.out.println(<span class="string">"消费者1接收到Fanout消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RabbitListener(queues = "fanout.queue2")</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenFanoutQueue2</span><span class="params">(String msg)</span> {</span><br/><span class="line">    System.out.println(<span class="string">"消费者2接收到Fanout消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-4-4-总结"><a class="headerlink" href="#3-4-4-总结" title="3.4.4.总结"></a>3.4.4.总结</h3><p>交换机的作用是什么？</p><ul><li>接收publisher发送的消息</li><li>将消息按照规则路由到与之绑定的队列</li><li>不能缓存消息，路由失败，消息丢失</li><li>FanoutExchange的会将消息路由到每个绑定的队列</li></ul><p>声明队列、交换机、绑定关系的Bean是什么？</p><ul><li>Queue</li><li>FanoutExchange</li><li>Binding</li></ul><h2 id="3-5-Direct"><a class="headerlink" href="#3-5-Direct" title="3.5.Direct"></a>3.5.Direct</h2><p>在Fanout模式中，一条消息，会被所有订阅的队列都消费。但是，在某些场景下，我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。</p><p><img alt="image-20210717170041447" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717170041447.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717170041447.png"/></p><p>在Direct模型下：</p><ul><li>队列与交换机的绑定，不能是任意绑定了，而是要指定一个<code>RoutingKey</code>（路由key）</li><li>消息的发送方在 向 Exchange发送消息时，也必须指定消息的 <code>RoutingKey</code>。</li><li>Exchange不再把消息交给每一个绑定的队列，而是根据消息的<code>Routing Key</code>进行判断，只有队列的<code>Routingkey</code>与消息的 <code>Routing key</code>完全一致，才会接收到消息</li></ul><p><strong>案例需求如下</strong>：</p><ol><li><p>利用@RabbitListener声明Exchange、Queue、RoutingKey</p></li><li><p>在consumer服务中，编写两个消费者方法，分别监听direct.queue1和direct.queue2</p></li><li><p>在publisher中编写测试方法，向itcast. direct发送消息</p></li></ol><p><img alt="image-20210717170223317" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717170223317.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717170223317.png"/></p><h3 id="3-5-1-基于注解声明队列和交换机"><a class="headerlink" href="#3-5-1-基于注解声明队列和交换机" title="3.5.1.基于注解声明队列和交换机"></a>3.5.1.基于注解声明队列和交换机</h3><p>基于@Bean的方式声明队列和交换机比较麻烦，Spring还提供了基于注解方式来声明。</p><p>在consumer的SpringRabbitListener中添加两个消费者，同时基于注解来声明队列和交换机：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br/><span class="line"><span class="meta">    value = @Queue(name = "direct.queue1"),</span></span><br/><span class="line"><span class="meta">    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),</span></span><br/><span class="line"><span class="meta">    key = {"red", "blue"}</span></span><br/><span class="line"><span class="meta">))</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenDirectQueue1</span><span class="params">(String msg)</span>{</span><br/><span class="line">    System.out.println(<span class="string">"消费者接收到direct.queue1的消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br/><span class="line"><span class="meta">    value = @Queue(name = "direct.queue2"),</span></span><br/><span class="line"><span class="meta">    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),</span></span><br/><span class="line"><span class="meta">    key = {"red", "yellow"}</span></span><br/><span class="line"><span class="meta">))</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenDirectQueue2</span><span class="params">(String msg)</span>{</span><br/><span class="line">    System.out.println(<span class="string">"消费者接收到direct.queue2的消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-5-2-消息发送"><a class="headerlink" href="#3-5-2-消息发送" title="3.5.2.消息发送"></a>3.5.2.消息发送</h3><p>在publisher服务的SpringAmqpTest类中添加测试方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendDirectExchange</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 交换机名称</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">"itcast.direct"</span>;</span><br/><span class="line">    <span class="comment">// 消息</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"红色警报！日本乱排核废水，导致海洋生物变异，惊现哥斯拉！"</span>;</span><br/><span class="line">    <span class="comment">// 发送消息</span></span><br/><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">"red"</span>, message);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-5-3-总结"><a class="headerlink" href="#3-5-3-总结" title="3.5.3.总结"></a>3.5.3.总结</h3><p>描述下Direct交换机与Fanout交换机的差异？</p><ul><li>Fanout交换机将消息路由给每一个与之绑定的队列</li><li>Direct交换机根据RoutingKey判断路由给哪个队列</li><li>如果多个队列具有相同的RoutingKey，则与Fanout功能类似</li></ul><p>基于@RabbitListener注解声明队列和交换机有哪些常见注解？</p><ul><li>@Queue</li><li>@Exchange</li></ul><h2 id="3-6-Topic"><a class="headerlink" href="#3-6-Topic" title="3.6.Topic"></a>3.6.Topic</h2><h3 id="3-6-1-说明"><a class="headerlink" href="#3-6-1-说明" title="3.6.1.说明"></a>3.6.1.说明</h3><p><code>Topic</code>类型的<code>Exchange</code>与<code>Direct</code>相比，都是可以根据<code>RoutingKey</code>把消息路由到不同的队列。只不过<code>Topic</code>类型<code>Exchange</code>可以让队列在绑定<code>Routing key</code> 的时候使用通配符！</p><p><code>Routingkey</code> 一般都是有一个或多个单词组成，多个单词之间以”.”分割，例如： <code>item.insert</code></p><p>通配符规则：</p><p><code>#</code>：匹配一个或多个词</p><p><code>*</code>：匹配不多不少恰好1个词</p><p>举例：</p><p><code>item.#</code>：能够匹配<code>item.spu.insert</code> 或者 <code>item.spu</code></p><p><code>item.*</code>：只能匹配<code>item.spu</code></p><p>​</p><p>图示：</p><p><img alt="image-20210717170705380" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717170705380.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717170705380.png"/></p><p>解释：</p><ul><li>Queue1：绑定的是<code>china.#</code> ，因此凡是以 <code>china.</code>开头的<code>routing key</code> 都会被匹配到。包括china.news和china.weather</li><li>Queue2：绑定的是<code>#.news</code> ，因此凡是以 <code>.news</code>结尾的 <code>routing key</code> 都会被匹配。包括china.news和japan.news</li></ul><p>案例需求：</p><p>实现思路如下：</p><ol><li><p>并利用@RabbitListener声明Exchange、Queue、RoutingKey</p></li><li><p>在consumer服务中，编写两个消费者方法，分别监听topic.queue1和topic.queue2</p></li><li><p>在publisher中编写测试方法，向itcast. topic发送消息</p></li></ol><p><img alt="image-20210717170829229" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210717170829229.png" src="https://img.shallowrecall.top/images/2024/4/image-20210717170829229.png"/></p><h3 id="3-6-2-消息发送"><a class="headerlink" href="#3-6-2-消息发送" title="3.6.2.消息发送"></a>3.6.2.消息发送</h3><p>在publisher服务的SpringAmqpTest类中添加测试方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br/><span class="line"><span class="comment">     * topicExchange</span></span><br/><span class="line"><span class="comment">     */</span></span><br/><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendTopicExchange</span><span class="params">()</span> {</span><br/><span class="line">    <span class="comment">// 交换机名称</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">"itcast.topic"</span>;</span><br/><span class="line">    <span class="comment">// 消息</span></span><br/><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">"喜报！孙悟空大战哥斯拉，胜!"</span>;</span><br/><span class="line">    <span class="comment">// 发送消息</span></span><br/><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">"china.news"</span>, message);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-6-3-消息接收"><a class="headerlink" href="#3-6-3-消息接收" title="3.6.3.消息接收"></a>3.6.3.消息接收</h3><p>在consumer服务的SpringRabbitListener中添加方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/><span class="line">10</span><br/><span class="line">11</span><br/><span class="line">12</span><br/><span class="line">13</span><br/><span class="line">14</span><br/><span class="line">15</span><br/><span class="line">16</span><br/><span class="line">17</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br/><span class="line"><span class="meta">    value = @Queue(name = "topic.queue1"),</span></span><br/><span class="line"><span class="meta">    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),</span></span><br/><span class="line"><span class="meta">    key = "china.#"</span></span><br/><span class="line"><span class="meta">))</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenTopicQueue1</span><span class="params">(String msg)</span>{</span><br/><span class="line">    System.out.println(<span class="string">"消费者接收到topic.queue1的消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/><span class="line"></span><br/><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br/><span class="line"><span class="meta">    value = @Queue(name = "topic.queue2"),</span></span><br/><span class="line"><span class="meta">    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),</span></span><br/><span class="line"><span class="meta">    key = "#.news"</span></span><br/><span class="line"><span class="meta">))</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenTopicQueue2</span><span class="params">(String msg)</span>{</span><br/><span class="line">    System.out.println(<span class="string">"消费者接收到topic.queue2的消息：【"</span> + msg + <span class="string">"】"</span>);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><h3 id="3-6-4-总结"><a class="headerlink" href="#3-6-4-总结" title="3.6.4.总结"></a>3.6.4.总结</h3><p>描述下Direct交换机与Topic交换机的差异？</p><ul><li>Topic交换机接收的消息RoutingKey必须是多个单词，以 <code>**.**</code> 分割</li><li>Topic交换机与队列绑定时的bindingKey可以指定通配符</li><li><code>#</code>：代表0个或多个词</li><li><code>*</code>：代表1个词</li></ul><h2 id="3-7-消息转换器"><a class="headerlink" href="#3-7-消息转换器" title="3.7.消息转换器"></a>3.7.消息转换器</h2><p>之前说过，Spring会把你发送的消息序列化为字节发送给MQ，接收消息的时候，还会把字节反序列化为Java对象。</p><p><img alt="image-20200525170410401" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20200525170410401.png" src="https://img.shallowrecall.top/images/2024/4/image-20200525170410401.png"/></p><p>只不过，默认情况下Spring采用的序列化方式是JDK序列化。众所周知，JDK序列化存在下列问题：</p><ul><li>数据体积过大</li><li>有安全漏洞</li><li>可读性差</li></ul><p>我们来测试一下。</p><h3 id="3-7-1-测试默认转换器"><a class="headerlink" href="#3-7-1-测试默认转换器" title="3.7.1.测试默认转换器"></a>3.7.1.测试默认转换器</h3><p>我们修改消息发送的代码，发送一个Map对象：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/><span class="line">5</span><br/><span class="line">6</span><br/><span class="line">7</span><br/><span class="line">8</span><br/><span class="line">9</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br/><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendMap</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException {</span><br/><span class="line">    <span class="comment">// 准备消息</span></span><br/><span class="line">    Map&lt;String,Object&gt; msg = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br/><span class="line">    msg.put(<span class="string">"name"</span>, <span class="string">"Jack"</span>);</span><br/><span class="line">    msg.put(<span class="string">"age"</span>, <span class="number">21</span>);</span><br/><span class="line">    <span class="comment">// 发送消息</span></span><br/><span class="line">    rabbitTemplate.convertAndSend(<span class="string">"simple.queue"</span>,<span class="string">""</span>, msg);</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure><p>停止consumer服务</p><p>发送消息后查看控制台：</p><p><img alt="image-20210422232835363" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210422232835363.png" src="https://img.shallowrecall.top/images/2024/4/image-20210422232835363.png"/></p><h3 id="3-7-2-配置JSON转换器"><a class="headerlink" href="#3-7-2-配置JSON转换器" title="3.7.2.配置JSON转换器"></a>3.7.2.配置JSON转换器</h3><p>显然，JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高，因此可以使用JSON方式来做序列化和反序列化。</p><p>在publisher和consumer两个服务中都引入依赖：</p><figure class="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.fasterxml.jackson.dataformat<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>jackson-dataformat-xml<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>2.9.10<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><p>配置消息转换器。</p><p>在启动类中添加一个Bean即可：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/><span class="line">4</span><br/></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br/><span class="line"><span class="keyword">public</span> MessageConverter <span class="title function_">jsonMessageConverter</span><span class="params">()</span>{</span><br/><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Jackson2JsonMessageConverter</span>();</span><br/><span class="line">}</span><br/></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;h1 id=&quot;RabbitMQ&quot;&gt;&lt;a class=&quot;headerlink&quot; href=&quot;#RabbitMQ&quot; title=&quot;RabbitMQ&quot;&gt;&lt;/a&gt;RabbitMQ&lt;/h1&gt;&lt;h1 id=&quot;1-初识MQ&quot;&gt;&lt;a class=&quot;headerlink&quot; href=&quot;#1</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="RabbitMQ" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/RabbitMQ/"/>
    
    
    <category term="RabbitMQ" scheme="https://www.shallowrecall.top/tags/RabbitMQ/"/>
    
  </entry>
  
  <entry>
    <title>RabbitMQ部署指南</title>
    <link href="https://www.shallowrecall.top/posts/ddc45bf.html"/>
    <id>https://www.shallowrecall.top/posts/ddc45bf.html</id>
    <published>2024-04-26T23:53:16.000Z</published>
    <updated>2026-10-16T12:30:20.000Z</updated>
    
    <content type="html"><![CDATA[<p><h1 id="RabbitMQ部署指南"><a class="headerlink" href="#RabbitMQ部署指南" title="RabbitMQ部署指南"></a>RabbitMQ部署指南</h1><h1 id="1-单机部署"><a class="headerlink" href="#1-单机部署" title="1.单机部署"></a>1.单机部署</h1><p>我们在Centos7虚拟机中使用Docker来安装。</p></p><p><h2 id="1-1-下载镜像"><a class="headerlink" href="#1-1-下载镜像" title="1.1.下载镜像"></a>1.1.下载镜像</h2><p>方式一：在线拉取</p></p><p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br/></pre></td><td class="code"><pre><span class="line">docker pull rabbitmq:3-management</span><br/></pre></td></tr></table></figure></p><p>方式二：从本地加载</p><p>在课前资料已经提供了镜像包：</p><p><img alt="image-20210423191210349" data-lazy-src="https://img.shallowrecall.top/images/2024/4/image-20210423191210349.png" src="https://img.shallowrecall.top/images/2024/4/image-20210423191210349.png"/></p><p>上传到虚拟机中后，使用命令加载镜像即可：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br/></pre></td><td class="code"><pre><span class="line">docker load -i mq.tar</span><br/></pre></td></tr></table></figure><h2 id="1-2-安装MQ"><a class="headerlink" href="#1-2-安装MQ" title="1.2.安装MQ"></a>1.2.安装MQ</h2><p>执行下面的命令来运行MQ容器：</p><figure class="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/><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">docker run \</span><br/><span class="line"> -e RABBITMQ_DEFAULT_USER=itcast \</span><br/><span class="line"> -e RABBITMQ_DEFAULT_PASS=123321 \</span><br/><span class="line"> --name mq \</span><br/><span class="line"> --hostname mq1 \</span><br/><span class="line"> -p 15672:15672 \</span><br/><span class="line"> -p 5672:5672 \</span><br/><span class="line"> -d \</span><br/><span class="line"> rabbitmq:3-management</span><br/></pre></td></tr></table></figure><h1 id="2-集群部署"><a class="headerlink" href="#2-集群部署" title="2.集群部署"></a>2.集群部署</h1><p>接下来，我们看看如何安装RabbitMQ的集群。</p><h2 id="2-1-集群分类"><a class="headerlink" href="#2-1-集群分类" title="2.1.集群分类"></a>2.1.集群分类</h2><p>在RabbitMQ的官方文档中，讲述了两种集群的配置方式：</p><ul><li>普通模式：普通模式集群不进行数据同步，每个MQ都有自己的队列、数据信息（其它元数据信息如交换机等会同步）。例如我们有2个MQ：mq1，和mq2，如果你的消息在mq1，而你连接到了mq2，那么mq2会去mq1拉取消息，然后返回给你。如果mq1宕机，消息就会丢失。</li><li>镜像模式：与普通模式不同，队列会在各个mq的镜像节点之间同步，因此你连接到任何一个镜像节点，均可获取到消息。而且如果一个节点宕机，并不会导致数据丢失。不过，这种方式增加了数据同步的带宽消耗。</li></ul><p>我们先来看普通模式集群。</p><h2 id="2-2-设置网络"><a class="headerlink" href="#2-2-设置网络" title="2.2.设置网络"></a>2.2.设置网络</h2><p>首先，我们需要让3台MQ互相知道对方的存在。</p><p>分别在3台机器中，设置 /etc/hosts文件，添加如下内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br/><span class="line">2</span><br/><span class="line">3</span><br/></pre></td><td class="code"><pre><span class="line">192.168.150.101 mq1</span><br/><span class="line">192.168.150.102 mq2</span><br/><span class="line">192.168.150.103 mq3</span><br/></pre></td></tr></table></figure><p>并在每台机器上测试，是否可以ping通对方：</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;h1 id=&quot;RabbitMQ部署指南&quot;&gt;&lt;a class=&quot;headerlink&quot; href=&quot;#RabbitMQ部署指南&quot; title=&quot;RabbitMQ部署指南&quot;&gt;&lt;/a&gt;RabbitMQ部署指南&lt;/h1&gt;&lt;h1 id=&quot;1-单机部署&quot;&gt;&lt;a class=&quot;head</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="RabbitMQ" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/RabbitMQ/"/>
    
    
    <category term="RabbitMQ" scheme="https://www.shallowrecall.top/tags/RabbitMQ/"/>
    
  </entry>
  
  <entry>
    <title>Docker实用篇</title>
    <link href="https://www.shallowrecall.top/posts/55bce094.html"/>
    <id>https://www.shallowrecall.top/posts/55bce094.html</id>
    <published>2024-04-24T12:40:39.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Docker实用篇"><a href="#Docker实用篇" class="headerlink" title="Docker实用篇"></a>Docker实用篇</h1><h1 id="1-初识Docker"><a href="#1-初识Docker" class="headerlink" title="1.初识Docker"></a>1.初识Docker</h1><h2 id="1-1-什么是Docker"><a href="#1-1-什么是Docker" class="headerlink" title="1.1.什么是Docker"></a>1.1.什么是Docker</h2><p>微服务虽然具备各种各样的优势，但服务的拆分通用给部署带来了很大的麻烦。</p><ul><li>分布式系统中，依赖的组件非常多，不同组件之间部署时往往会产生一些冲突。</li><li>在数百上千台服务中重复部署，环境不一定一致，会遇到各种问题</li></ul><h3 id="1-1-1-应用部署的环境问题"><a href="#1-1-1-应用部署的环境问题" class="headerlink" title="1.1.1.应用部署的环境问题"></a>1.1.1.应用部署的环境问题</h3><p>大型项目组件较多，运行环境也较为复杂，部署时会碰到一些问题：</p><ul><li><p>依赖关系复杂，容易出现兼容性问题</p></li><li><p>开发、测试、生产环境有差异</p></li></ul><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731141907366.png" alt="image-20210731141907366"></p><p>例如一个项目中，部署时需要依赖于node.js、Redis、RabbitMQ、MySQL等，这些服务部署时所需要的函数库、依赖项各不相同，甚至会有冲突。给部署带来了极大的困难。</p><h3 id="1-1-2-Docker解决依赖兼容问题"><a href="#1-1-2-Docker解决依赖兼容问题" class="headerlink" title="1.1.2.Docker解决依赖兼容问题"></a>1.1.2.Docker解决依赖兼容问题</h3><p>而Docker确巧妙的解决了这些问题，Docker是如何实现的呢？</p><p>Docker为了解决依赖的兼容问题的，采用了两个手段：</p><ul><li><p>将应用的Libs（函数库）、Deps（依赖）、配置与应用一起打包</p></li><li><p>将每个应用放到一个隔离<strong>容器</strong>去运行，避免互相干扰</p></li></ul><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731142219735.png" alt="image-20210731142219735"></p><p>这样打包好的应用包中，既包含应用本身，也保护应用所需要的Libs、Deps，无需再操作系统上安装这些，自然就不存在不同应用之间的兼容问题了。</p><p>虽然解决了不同应用的兼容问题，但是开发、测试等环境会存在差异，操作系统版本也会有差异，怎么解决这些问题呢？</p><h3 id="1-1-3-Docker解决操作系统环境差异"><a href="#1-1-3-Docker解决操作系统环境差异" class="headerlink" title="1.1.3.Docker解决操作系统环境差异"></a>1.1.3.Docker解决操作系统环境差异</h3><p>要解决不同操作系统环境差异问题，必须先了解操作系统结构。以一个Ubuntu操作系统为例，结构如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731143401460.png" alt="image-20210731143401460"></p><p>结构包括：</p><ul><li>计算机硬件：例如CPU、内存、磁盘等</li><li>系统内核：所有Linux发行版的内核都是Linux，例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互，对外提供<strong>内核指令</strong>，用于操作计算机硬件。</li><li>系统应用：操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装，使用更加方便。</li></ul><p>应用于计算机交互的流程如下：</p><p>1）应用调用操作系统应用（函数库），实现各种功能</p><p>2）系统函数库是对内核指令集的封装，会调用内核指令</p><p>3）内核指令操作计算机硬件</p><p>Ubuntu和CentOSpringBoot都是基于Linux内核，无非是系统应用不同，提供的函数库有差异：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731144304990.png" alt="image-20210731144304990"></p><p>此时，如果将一个Ubuntu版本的MySQL应用安装到CentOS系统，MySQL在调用Ubuntu函数库时，会发现找不到或者不匹配，就会报错了：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731144458680.png" alt="image-20210731144458680"></p><p>Docker如何解决不同系统环境的问题？</p><ul><li>Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包</li><li>Docker运行到不同操作系统时，直接基于打包的函数库，借助于操作系统的Linux内核来运行</li></ul><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731144820638.png" alt="image-20210731144820638"></p><h3 id="1-1-4-小结"><a href="#1-1-4-小结" class="headerlink" title="1.1.4.小结"></a>1.1.4.小结</h3><p>Docker如何解决大型项目依赖关系复杂，不同组件依赖的兼容性问题？</p><ul><li>Docker允许开发中将应用、依赖、函数库、配置一起<strong>打包</strong>，形成可移植镜像</li><li>Docker应用运行在容器中，使用沙箱机制，相互<strong>隔离</strong></li></ul><p>Docker如何解决开发、测试、生产环境有差异的问题？</p><ul><li>Docker镜像中包含完整运行环境，包括系统函数库，仅依赖系统的Linux内核，因此可以在任意Linux操作系统上运行</li></ul><p>Docker是一个快速交付应用、运行应用的技术，具备下列优势：</p><ul><li>可以将程序及其依赖、运行环境一起打包为一个镜像，可以迁移到任意Linux操作系统</li><li>运行时利用沙箱机制形成隔离容器，各个应用互不干扰</li><li>启动、移除都可以通过一行命令完成，方便快捷</li></ul><h2 id="1-2-Docker和虚拟机的区别"><a href="#1-2-Docker和虚拟机的区别" class="headerlink" title="1.2.Docker和虚拟机的区别"></a>1.2.Docker和虚拟机的区别</h2><p>Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机，也能在一个操作系统中，运行另外一个操作系统，保护系统中的任何应用。</p><p>两者有什么差异呢？</p><p><strong>虚拟机</strong>（virtual machine）是在操作系统中<strong>模拟</strong>硬件设备，然后运行另一个操作系统，比如在 Windows 系统里面运行 Ubuntu 系统，这样就可以运行任意的Ubuntu应用了。</p><p><strong>Docker</strong>仅仅是封装函数库，并没有模拟完整的操作系统，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731145914960.png" alt="image-20210731145914960"></p><p>对比来看：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731152243765.png" alt="image-20210731152243765"></p><p>小结：</p><p>Docker和虚拟机的差异：</p><ul><li><p>docker是一个系统进程；虚拟机是在操作系统中的操作系统</p></li><li><p>docker体积小、启动速度快、性能好；虚拟机体积大、启动速度慢、性能一般</p></li></ul><h2 id="1-3-Docker架构"><a href="#1-3-Docker架构" class="headerlink" title="1.3.Docker架构"></a>1.3.Docker架构</h2><h3 id="1-3-1-镜像和容器"><a href="#1-3-1-镜像和容器" class="headerlink" title="1.3.1.镜像和容器"></a>1.3.1.镜像和容器</h3><p>Docker中有几个重要的概念：</p><p><strong>镜像（Image）</strong>：Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起，称为镜像。</p><p><strong>容器（Container）</strong>：镜像中的应用程序运行后形成的进程就是<strong>容器</strong>，只是Docker会给容器进程做隔离，对外不可见。</p><p>一切应用最终都是代码组成，都是硬盘中的一个个的字节形成的<strong>文件</strong>。只有运行时，才会加载到内存，形成进程。</p><p>而<strong>镜像</strong>，就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。</p><p><strong>容器</strong>呢，就是将这些文件中编写的程序、函数加载到内存中允许，形成进程，只不过要隔离起来。因此一个镜像可以启动多次，形成多个容器进程。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731153059464.png" alt="image-20210731153059464"></p><p>例如你下载了一个QQ，如果我们将QQ在磁盘上的运行<strong>文件</strong>及其运行的操作系统依赖打包，形成QQ镜像。然后你可以启动多次，双开、甚至三开QQ，跟多个妹子聊天。</p><h3 id="1-3-2-DockerHub"><a href="#1-3-2-DockerHub" class="headerlink" title="1.3.2.DockerHub"></a>1.3.2.DockerHub</h3><p>开源应用程序非常多，打包这些应用往往是重复的劳动。为了避免这些重复劳动，人们就会将自己打包的应用镜像，例如Redis、MySQL镜像放到网络上，共享使用，就像GitHub的代码共享一样。</p><ul><li><p>DockerHub：DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。</p></li><li><p>国内也有类似于DockerHub 的公开服务，比如 <a href="https://c.163yun.com/hub">网易云镜像服务</a>、<a href="https://cr.console.aliyun.com/">阿里云镜像库</a>等。</p></li></ul><p>我们一方面可以将自己的镜像共享到DockerHub，另一方面也可以从DockerHub拉取镜像：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731153743354.png" alt="image-20210731153743354"></p><h3 id="1-3-3-Docker架构"><a href="#1-3-3-Docker架构" class="headerlink" title="1.3.3.Docker架构"></a>1.3.3.Docker架构</h3><p>我们要使用Docker来操作镜像、容器，就必须要安装Docker。</p><p>Docker是一个CS架构的程序，由两部分组成：</p><ul><li><p>服务端(server)：Docker守护进程，负责处理Docker指令，管理镜像、容器等</p></li><li><p>客户端(client)：通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。</p></li></ul><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731154257653.png" alt="image-20210731154257653"></p><h3 id="1-3-4-小结"><a href="#1-3-4-小结" class="headerlink" title="1.3.4.小结"></a>1.3.4.小结</h3><p>镜像：</p><ul><li>将应用程序及其依赖、环境、配置打包在一起</li></ul><p>容器：</p><ul><li>镜像运行起来就是容器，一个镜像可以运行多个容器</li></ul><p>Docker结构：</p><ul><li><p>服务端：接收命令或远程请求，操作镜像或容器</p></li><li><p>客户端：发送命令或者请求到Docker服务端</p></li></ul><p>DockerHub：</p><ul><li>一个镜像托管的服务器，类似的还有阿里云镜像服务，统称为DockerRegistry</li></ul><h2 id="1-4-安装Docker"><a href="#1-4-安装Docker" class="headerlink" title="1.4.安装Docker"></a>1.4.安装Docker</h2><p>企业部署一般都是采用Linux操作系统，而其中又数CentOS发行版占比最多，因此我们在CentOS下安装Docker。参考课前资料中的文档：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155002425.png" alt="image-20210731155002425"></p><h1 id="2-Docker的基本操作"><a href="#2-Docker的基本操作" class="headerlink" title="2.Docker的基本操作"></a>2.Docker的基本操作</h1><h2 id="2-1-镜像操作"><a href="#2-1-镜像操作" class="headerlink" title="2.1.镜像操作"></a>2.1.镜像操作</h2><h3 id="2-1-1-镜像名称"><a href="#2-1-1-镜像名称" class="headerlink" title="2.1.1.镜像名称"></a>2.1.1.镜像名称</h3><p>首先来看下镜像的名称组成：</p><ul><li>镜名称一般分两部分组成：[repository]:[tag]。</li><li>在没有指定tag时，默认是latest，代表最新版本的镜像</li></ul><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155141362.png" alt="image-20210731155141362"></p><p>这里的mysql就是repository，5.7就是tag，合一起就是镜像名称，代表5.7版本的MySQL镜像。</p><h3 id="2-1-2-镜像命令"><a href="#2-1-2-镜像命令" class="headerlink" title="2.1.2.镜像命令"></a>2.1.2.镜像命令</h3><p>常见的镜像操作命令如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155649535.png" alt="image-20210731155649535"></p><h3 id="2-1-3-案例1-拉取、查看镜像"><a href="#2-1-3-案例1-拉取、查看镜像" class="headerlink" title="2.1.3.案例1-拉取、查看镜像"></a>2.1.3.案例1-拉取、查看镜像</h3><p>需求：从DockerHub中拉取一个nginx镜像并查看</p><p>1）首先去镜像仓库搜索nginx镜像，比如<a href="https://hub.docker.com/">DockerHub</a>:</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155844368.png" alt="image-20210731155844368"></p><p>2）根据查看到的镜像名称，拉取自己需要的镜像，通过命令：docker pull nginx</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155856199.png" alt="image-20210731155856199"></p><p>3）通过命令：docker images 查看拉取到的镜像</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731155903037.png" alt="image-20210731155903037"></p><h3 id="2-1-4-案例2-保存、导入镜像"><a href="#2-1-4-案例2-保存、导入镜像" class="headerlink" title="2.1.4.案例2-保存、导入镜像"></a>2.1.4.案例2-保存、导入镜像</h3><p>需求：利用docker save将nginx镜像导出磁盘，然后再通过load加载回来</p><p>1）利用docker xx —help命令查看docker save和docker load的语法</p><p>例如，查看save命令用法，可以输入命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731161104732.png" alt="image-20210731161104732"></p><p>命令格式：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save -o [保存的目标文件名称] [镜像名称]</span><br></pre></td></tr></table></figure><p>2）使用docker save导出镜像到磁盘</p><p>运行命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save -o nginx.tar nginx:latest</span><br></pre></td></tr></table></figure><p>结果如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731161354344.png" alt="image-20210731161354344"></p><p>3）使用docker load加载镜像</p><p>先删除本地的nginx镜像：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker rmi nginx:latest</span><br></pre></td></tr></table></figure><p>然后运行命令，加载本地文件：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker load -i nginx.tar</span><br></pre></td></tr></table></figure><p>结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731161746245.png" alt="image-20210731161746245"></p><h3 id="2-1-5-练习"><a href="#2-1-5-练习" class="headerlink" title="2.1.5.练习"></a>2.1.5.练习</h3><p>需求：去DockerHub搜索并拉取一个Redis镜像</p><p>目标：</p><p>1）去DockerHub搜索Redis镜像</p><p>2）查看Redis镜像的名称和版本</p><p>3）利用docker pull命令拉取镜像</p><p>4）利用docker save命令将 redis:latest打包为一个redis.tar包</p><p>5）利用docker rmi 删除本地的redis:latest</p><p>6）利用docker load 重新加载 redis.tar文件</p><h2 id="2-2-容器操作"><a href="#2-2-容器操作" class="headerlink" title="2.2.容器操作"></a>2.2.容器操作</h2><h3 id="2-2-1-容器相关命令"><a href="#2-2-1-容器相关命令" class="headerlink" title="2.2.1.容器相关命令"></a>2.2.1.容器相关命令</h3><p>容器操作的命令如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731161950495.png" alt="image-20210731161950495"></p><p>容器保护三个状态：</p><ul><li>运行：进程正常运行</li><li>暂停：进程暂停，CPU不再运行，并不释放内存</li><li>停止：进程终止，回收进程占用的内存、CPU等资源</li></ul><p>其中：</p><ul><li>docker run：创建并运行一个容器，处于运行状态</li><li>docker pause：让一个运行的容器暂停</li><li>docker unpause：让一个容器从暂停状态恢复运行</li><li>docker stop：停止一个运行的容器</li><li><p>docker start：让一个停止的容器再次运行</p></li><li><p>docker rm：删除一个容器</p></li></ul><h3 id="2-2-2-案例-创建并运行一个容器"><a href="#2-2-2-案例-创建并运行一个容器" class="headerlink" title="2.2.2.案例-创建并运行一个容器"></a>2.2.2.案例-创建并运行一个容器</h3><p>创建并运行nginx容器的命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name containerName -p 80:80 -d nginx</span><br></pre></td></tr></table></figure><p>命令解读：</p><ul><li>docker run ：创建并运行一个容器</li><li>—name : 给容器起一个名字，比如叫做mn</li><li>-p ：将宿主机端口与容器端口映射，冒号左侧是宿主机端口，右侧是容器端口</li><li>-d：后台运行容器</li><li>nginx：镜像名称，例如nginx</li></ul><p>这里的<code>-p</code>参数，是将容器端口映射到宿主机端口。</p><p>默认情况下，容器是隔离环境，我们直接访问宿主机的80端口，肯定访问不到容器中的nginx。</p><p>现在，将容器的80与宿主机的80关联起来，当我们访问宿主机的80端口时，就会被映射到容器的80，这样就能访问到nginx了：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731163255863.png" alt="image-20210731163255863"></p><h3 id="2-2-3-案例-进入容器，修改文件"><a href="#2-2-3-案例-进入容器，修改文件" class="headerlink" title="2.2.3.案例-进入容器，修改文件"></a>2.2.3.案例-进入容器，修改文件</h3><p><strong>需求</strong>：进入Nginx容器，修改HTML文件内容，添加“传智教育欢迎您”</p><p><strong>提示</strong>：进入容器要用到docker exec命令。</p><p><strong>步骤</strong>：</p><p>1）进入容器。进入我们刚刚创建的nginx容器的命令为：</p><figure class="highlight sh"><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">exec</span> -it mn bash</span><br></pre></td></tr></table></figure><p>命令解读：</p><ul><li><p>docker exec ：进入容器内部，执行一个命令</p></li><li><p>-it : 给当前进入的容器创建一个标准输入、输出终端，允许我们与容器交互</p></li><li><p>mn ：要进入的容器的名称</p></li><li><p>bash：进入容器后执行的命令，bash是一个linux终端交互命令</p></li></ul><p>2）进入nginx的HTML所在目录 /usr/share/nginx/html</p><p>容器内部会模拟一个独立的Linux文件系统，看起来如同一个linux服务器一样：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731164159811.png" alt="image-20210731164159811"></p><p>nginx的环境、配置、运行文件全部都在这个文件系统中，包括我们要修改的html文件。</p><p>查看DockerHub网站中的nginx页面，可以知道nginx的html目录位置在<code>/usr/share/nginx/html</code></p><p>我们执行命令，进入该目录：</p><figure class="highlight sh"><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">cd</span> /usr/share/nginx/html</span><br></pre></td></tr></table></figure><p>查看目录下文件：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731164455818.png" alt="image-20210731164455818"></p><p>3）修改index.html的内容</p><p>容器内没有vi命令，无法直接修改，我们用下面的命令来修改：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i -e <span class="string">&#x27;s#Welcome to nginx#传智教育欢迎您#g&#x27;</span> -e <span class="string">&#x27;s#&lt;head&gt;#&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;#g&#x27;</span> index.html</span><br></pre></td></tr></table></figure><p>在浏览器访问自己的虚拟机地址，例如我的是：<a href="http://192.168.150.101，即可看到结果：">http://192.168.150.101，即可看到结果：</a></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731164717604.png" alt="image-20210731164717604"></p><h3 id="2-2-4-小结"><a href="#2-2-4-小结" class="headerlink" title="2.2.4.小结"></a>2.2.4.小结</h3><p>docker run命令的常见参数有哪些？</p><ul><li>—name：指定容器名称</li><li>-p：指定端口映射</li><li>-d：让容器后台运行</li></ul><p>查看容器日志的命令：</p><ul><li>docker logs</li><li>添加 -f 参数可以持续查看日志</li></ul><p>查看容器状态：</p><ul><li>docker ps</li><li>docker ps -a 查看所有容器，包括已经停止的</li></ul><h2 id="2-3-数据卷（容器数据管理）"><a href="#2-3-数据卷（容器数据管理）" class="headerlink" title="2.3.数据卷（容器数据管理）"></a>2.3.数据卷（容器数据管理）</h2><p>在之前的nginx案例中，修改nginx的html页面时，需要进入nginx内部。并且因为没有编辑器，修改文件也很麻烦。</p><p>这就是因为容器与数据（容器内文件）耦合带来的后果。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731172440275.png" alt="image-20210731172440275"></p><p>要解决这个问题，必须将数据与容器解耦，这就要用到数据卷了。</p><h3 id="2-3-1-什么是数据卷"><a href="#2-3-1-什么是数据卷" class="headerlink" title="2.3.1.什么是数据卷"></a>2.3.1.什么是数据卷</h3><p><strong>数据卷（volume）</strong>是一个虚拟目录，指向宿主机文件系统中的某个目录。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731173541846.png" alt="image-20210731173541846"></p><p>一旦完成数据卷挂载，对容器的一切操作都会作用在数据卷对应的宿主机目录了。</p><p>这样，我们操作宿主机的/var/lib/docker/volumes/html目录，就等于操作容器内的/usr/share/nginx/html目录了</p><h3 id="2-3-2-数据集操作命令"><a href="#2-3-2-数据集操作命令" class="headerlink" title="2.3.2.数据集操作命令"></a>2.3.2.数据集操作命令</h3><p>数据卷操作的基本语法如下：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume [COMMAND]</span><br></pre></td></tr></table></figure><p>docker volume命令是数据卷操作，根据命令后跟随的command来确定下一步的操作：</p><ul><li>create 创建一个volume</li><li>inspect 显示一个或多个volume的信息</li><li>ls 列出所有的volume</li><li>prune 删除未使用的volume</li><li>rm 删除一个或多个指定的volume</li></ul><h3 id="2-3-3-创建和查看数据卷"><a href="#2-3-3-创建和查看数据卷" class="headerlink" title="2.3.3.创建和查看数据卷"></a>2.3.3.创建和查看数据卷</h3><p><strong>需求</strong>：创建一个数据卷，并查看数据卷在宿主机的目录位置</p><p>① 创建数据卷</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume create html</span><br></pre></td></tr></table></figure><p>② 查看所有数据</p><figure class="highlight sh"><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><p>结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731173746910.png" alt="image-20210731173746910"></p><p>③ 查看数据卷详细信息卷</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume inspect html</span><br></pre></td></tr></table></figure><p>结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731173809877.png" alt="image-20210731173809877"></p><p>可以看到，我们创建的html这个数据卷关联的宿主机目录为<code>/var/lib/docker/volumes/html/_data</code>目录。</p><p><strong>小结</strong>：</p><p>数据卷的作用：</p><ul><li>将容器与数据分离，解耦合，方便操作容器内数据，保证数据安全</li></ul><p>数据卷操作：</p><ul><li>docker volume create：创建数据卷</li><li>docker volume ls：查看所有数据卷</li><li>docker volume inspect：查看数据卷详细信息，包括关联的宿主机目录位置</li><li>docker volume rm：删除指定数据卷</li><li>docker volume prune：删除所有未使用的数据卷</li></ul><h3 id="2-3-4-挂载数据卷"><a href="#2-3-4-挂载数据卷" class="headerlink" title="2.3.4.挂载数据卷"></a>2.3.4.挂载数据卷</h3><p>我们在创建容器时，可以通过 -v 参数来挂载一个数据卷到某个容器内目录，命令格式如下：</p><figure class="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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">  --name mn \</span><br><span class="line">  -v html:/root/html \</span><br><span class="line">  -p 8080:80</span><br><span class="line">  nginx \</span><br></pre></td></tr></table></figure><p>这里的-v就是挂载数据卷的命令：</p><ul><li><code>-v html:/root/htm</code> ：把html数据卷挂载到容器内的/root/html这个目录中</li></ul><h3 id="2-3-5-案例-给nginx挂载数据卷"><a href="#2-3-5-案例-给nginx挂载数据卷" class="headerlink" title="2.3.5.案例-给nginx挂载数据卷"></a>2.3.5.案例-给nginx挂载数据卷</h3><p><strong>需求</strong>：创建一个nginx容器，修改容器内的html目录内的index.html内容</p><p><strong>分析</strong>：上个案例中，我们进入nginx容器内部，已经知道nginx的html目录所在位置/usr/share/nginx/html ，我们需要把这个目录挂载到html这个数据卷上，方便操作其中的内容。</p><p><strong>提示</strong>：运行容器时使用 -v 参数挂载数据卷</p><p>步骤：</p><p>① 创建容器并挂载数据卷到容器内的HTML目录</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx</span><br></pre></td></tr></table></figure><p>② 进入html数据卷所在位置，并修改HTML内容</p><figure class="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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看html数据卷的位置</span></span><br><span class="line">docker volume inspect html</span><br><span class="line"><span class="comment"># 进入该目录</span></span><br><span class="line"><span class="built_in">cd</span> /var/lib/docker/volumes/html/_data</span><br><span class="line"><span class="comment"># 修改文件</span></span><br><span class="line">vi index.html</span><br></pre></td></tr></table></figure><h3 id="2-3-6-案例-给MySQL挂载本地目录"><a href="#2-3-6-案例-给MySQL挂载本地目录" class="headerlink" title="2.3.6.案例-给MySQL挂载本地目录"></a>2.3.6.案例-给MySQL挂载本地目录</h3><p>容器不仅仅可以挂载数据卷，也可以直接挂载到宿主机目录上。关联关系如下：</p><ul><li>带数据卷模式：宿主机目录 —&gt; 数据卷 —-&gt; 容器内目录</li><li>直接挂载模式：宿主机目录 —-&gt; 容器内目录</li></ul><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731175155453.png" alt="image-20210731175155453"></p><p><strong>语法</strong>：</p><p>目录挂载与数据卷挂载的语法是类似的：</p><ul><li>-v [宿主机目录]:[容器内目录]</li><li>-v [宿主机文件]:[容器内文件]</li></ul><p><strong>需求</strong>：创建并运行一个MySQL容器，将宿主机目录直接挂载到容器</p><p>实现思路如下：</p><p>1）在将课前资料中的mysql.tar文件上传到虚拟机，通过load命令加载为镜像</p><p>2）创建目录/tmp/mysql/data</p><p>3）创建目录/tmp/mysql/conf，将课前资料提供的hmy.cnf文件上传到/tmp/mysql/conf</p><p>4）去DockerHub查阅资料，创建并运行MySQL容器，要求：</p><p>① 挂载/tmp/mysql/data到mysql容器内数据存储目录</p><p>② 挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件</p><p>③ 设置MySQL密码</p><h3 id="2-3-7-小结"><a href="#2-3-7-小结" class="headerlink" title="2.3.7.小结"></a>2.3.7.小结</h3><p>docker run的命令中通过 -v 参数挂载文件或目录到容器中：</p><ul><li>-v volume名称:容器内目录</li><li>-v 宿主机文件:容器内文</li><li>-v 宿主机目录:容器内目录</li></ul><p>数据卷挂载与目录直接挂载的</p><ul><li>数据卷挂载耦合度低，由docker来管理目录，但是目录较深，不好找</li><li>目录挂载耦合度高，需要我们自己管理目录，不过目录容易寻找查看</li></ul><h1 id="3-Dockerfile自定义镜像"><a href="#3-Dockerfile自定义镜像" class="headerlink" title="3.Dockerfile自定义镜像"></a>3.Dockerfile自定义镜像</h1><p>常见的镜像在DockerHub就能找到，但是我们自己写的项目就必须自己构建镜像了。</p><p>而要自定义镜像，就必须先了解镜像的结构才行。</p><h2 id="3-1-镜像结构"><a href="#3-1-镜像结构" class="headerlink" title="3.1.镜像结构"></a>3.1.镜像结构</h2><p>镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。</p><p>我们以MySQL为例，来看看镜像的组成结构：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731175806273.png" alt="image-20210731175806273"></p><p>简单来说，镜像就是在系统函数库、运行环境基础上，添加应用程序文件、配置文件、依赖文件等组合，然后编写好启动脚本打包在一起形成的文件。</p><p>我们要构建镜像，其实就是实现上述打包的过程。</p><h2 id="3-2-Dockerfile语法"><a href="#3-2-Dockerfile语法" class="headerlink" title="3.2.Dockerfile语法"></a>3.2.Dockerfile语法</h2><p>构建自定义的镜像时，并不需要一个个文件去拷贝，打包。</p><p>我们只需要告诉Docker，我们的镜像的组成，需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么，将来Docker会帮助我们构建镜像。</p><p>而描述上述信息的文件就是Dockerfile文件。</p><p><strong>Dockerfile</strong>就是一个文本文件，其中包含一个个的<strong>指令(Instruction)</strong>，用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731180321133.png" alt="image-20210731180321133"></p><p>更新详细语法说明，请参考官网文档： <a href="https://docs.docker.com/engine/reference/builder">https://docs.docker.com/engine/reference/builder</a></p><h2 id="3-3-构建Java项目"><a href="#3-3-构建Java项目" class="headerlink" title="3.3.构建Java项目"></a>3.3.构建Java项目</h2><h3 id="3-3-1-基于Ubuntu构建Java项目"><a href="#3-3-1-基于Ubuntu构建Java项目" class="headerlink" title="3.3.1.基于Ubuntu构建Java项目"></a>3.3.1.基于Ubuntu构建Java项目</h3><p>需求：基于Ubuntu镜像构建一个新镜像，运行一个java项目</p><ul><li><p>步骤1：新建一个空文件夹docker-demo</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801101207444.png" alt="image-20210801101207444"></p></li><li><p>步骤2：拷贝课前资料中的docker-demo.jar文件到docker-demo这个目录</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801101314816.png" alt="image-20210801101314816"></p></li><li><p>步骤3：拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801101410200.png" alt="image-20210801101410200"></p></li><li><p>步骤4：拷贝课前资料提供的Dockerfile到docker-demo这个目录</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801101455590.png" alt="image-20210801101455590"></p><p>其中的内容如下：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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="comment"># 指定基础镜像</span></span><br><span class="line"><span class="keyword">FROM</span> ubuntu:<span class="number">16.04</span></span><br><span class="line"><span class="comment"># 配置环境变量，JDK的安装目录</span></span><br><span class="line"><span class="keyword">ENV</span> JAVA_DIR=/usr/local</span><br><span class="line"></span><br><span class="line"><span class="comment"># 拷贝jdk和java项目的包</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./jdk8.tar.gz <span class="variable">$JAVA_DIR</span>/</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./docker-demo.jar /tmp/app.jar</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装JDK</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> <span class="built_in">cd</span> <span class="variable">$JAVA_DIR</span> \</span></span><br><span class="line"><span class="language-bash"> &amp;&amp; tar -xf ./jdk8.tar.gz \</span></span><br><span class="line"><span class="language-bash"> &amp;&amp; <span class="built_in">mv</span> ./jdk1.8.0_144 ./java8</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置环境变量</span></span><br><span class="line"><span class="keyword">ENV</span> JAVA_HOME=$JAVA_DIR/java8</span><br><span class="line"><span class="keyword">ENV</span> PATH=$PATH:$JAVA_HOME/bin</span><br><span class="line"></span><br><span class="line"><span class="comment"># 暴露端口</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8090</span></span><br><span class="line"><span class="comment"># 入口，java项目的启动命令</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> java -jar /tmp/app.jar</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>步骤5：进入docker-demo</p><p>将准备好的docker-demo上传到虚拟机任意目录，然后进入docker-demo目录下</p></li><li><p>步骤6：运行命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -t javaweb:1.0 .</span><br></pre></td></tr></table></figure></li></ul><p>最后访问 <a href="http://192.168.150.101:8090/hello/count，其中的ip改成你的虚拟机ip">http://192.168.150.101:8090/hello/count，其中的ip改成你的虚拟机ip</a></p><h3 id="3-3-2-基于java8构建Java项目"><a href="#3-3-2-基于java8构建Java项目" class="headerlink" title="3.3.2.基于java8构建Java项目"></a>3.3.2.基于java8构建Java项目</h3><p>虽然我们可以基于Ubuntu基础镜像，添加任意自己需要的安装包，构建镜像，但是却比较麻烦。所以大多数情况下，我们都可以在一些安装了部分软件的基础镜像上做改造。</p><p>例如，构建java项目的镜像，可以在已经准备了JDK的基础镜像基础上构建。</p><p>需求：基于java:8-alpine镜像，将一个Java项目构建为镜像</p><p>实现思路如下：</p><ul><li><p>① 新建一个空的目录，然后在目录中新建一个文件，命名为Dockerfile</p></li><li><p>② 拷贝课前资料提供的docker-demo.jar到这个目录中</p></li><li><p>③ 编写Dockerfile文件：</p><ul><li><p>a ）基于java:8-alpine作为基础镜像</p></li><li><p>b ）将app.jar拷贝到镜像中</p></li><li><p>c ）暴露端口</p></li><li><p>d ）编写入口ENTRYPOINT</p><p>内容如下：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> java:<span class="number">8</span>-alpine</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./app.jar /tmp/app.jar</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8090</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> java -jar /tmp/app.jar</span></span><br></pre></td></tr></table></figure></li></ul></li></ul><ul><li><p>④ 使用docker build命令构建镜像</p></li><li><p>⑤ 使用docker run创建容器并运行</p></li></ul><h2 id="3-4-小结"><a href="#3-4-小结" class="headerlink" title="3.4.小结"></a>3.4.小结</h2><p>小结：</p><ol><li><p>Dockerfile的本质是一个文件，通过指令描述镜像的构建过程</p></li><li><p>Dockerfile的第一行必须是FROM，从一个基础镜像来构建</p></li><li><p>基础镜像可以是基本操作系统，如Ubuntu。也可以是其他人制作好的镜像，例如：java:8-alpine</p></li></ol><h1 id="4-Docker-Compose"><a href="#4-Docker-Compose" class="headerlink" title="4.Docker-Compose"></a>4.Docker-Compose</h1><p>Docker Compose可以基于Compose文件帮我们快速的部署分布式应用，而无需手动一个个创建和运行容器！</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731180921742.png" alt="image-20210731180921742"></p><h2 id="4-1-初识DockerCompose"><a href="#4-1-初识DockerCompose" class="headerlink" title="4.1.初识DockerCompose"></a>4.1.初识DockerCompose</h2><p>Compose文件是一个文本文件，通过指令定义集群中的每个容器如何运行。格式如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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">version<span class="punctuation">:</span> <span class="string">&quot;3.8&quot;</span></span><br><span class="line"> services<span class="punctuation">:</span></span><br><span class="line">  mysql<span class="punctuation">:</span></span><br><span class="line">    image<span class="punctuation">:</span> mysql<span class="punctuation">:</span><span class="number">5.7</span><span class="number">.25</span></span><br><span class="line">    environment<span class="punctuation">:</span></span><br><span class="line">     MYSQL_ROOT_PASSWORD<span class="punctuation">:</span> <span class="number">123</span> </span><br><span class="line">    volumes<span class="punctuation">:</span></span><br><span class="line">     - <span class="string">&quot;/tmp/mysql/data:/var/lib/mysql&quot;</span></span><br><span class="line">     - <span class="string">&quot;/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf&quot;</span></span><br><span class="line">  web<span class="punctuation">:</span></span><br><span class="line">    build<span class="punctuation">:</span> .</span><br><span class="line">    ports<span class="punctuation">:</span></span><br><span class="line">     - <span class="string">&quot;8090:8090&quot;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>上面的Compose文件就描述一个项目，其中包含两个容器：</p><ul><li>mysql：一个基于<code>mysql:5.7.25</code>镜像构建的容器，并且挂载了两个目录</li><li>web：一个基于<code>docker build</code>临时构建的镜像容器，映射端口时8090</li></ul><p>DockerCompose的详细语法参考官网：<a href="https://docs.docker.com/compose/compose-file/">https://docs.docker.com/compose/compose-file/</a></p><p>其实DockerCompose文件可以看做是将多个docker run命令写到一个文件，只是语法稍有差异。</p><h2 id="4-2-安装DockerCompose"><a href="#4-2-安装DockerCompose" class="headerlink" title="4.2.安装DockerCompose"></a>4.2.安装DockerCompose</h2><p>参考课前资料</p><h2 id="4-3-部署微服务集群"><a href="#4-3-部署微服务集群" class="headerlink" title="4.3.部署微服务集群"></a>4.3.部署微服务集群</h2><p><strong>需求</strong>：将之前学习的cloud-demo微服务集群利用DockerCompose部署</p><p><strong>实现思路</strong>：</p><p>① 查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件</p><p>② 修改自己的cloud-demo项目，将数据库、nacos地址都命名为docker-compose中的服务名</p><p>③ 使用maven打包工具，将项目中的每个微服务都打包为app.jar</p><p>④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中</p><p>⑤ 将cloud-demo上传至虚拟机，利用 docker-compose up -d 来部署</p><h3 id="4-3-1-compose文件"><a href="#4-3-1-compose文件" class="headerlink" title="4.3.1.compose文件"></a>4.3.1.compose文件</h3><p>查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件，而且每个微服务都准备了一个独立的目录：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210731181341330.png" alt="image-20210731181341330"></p><p>内容如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">version:</span> <span class="string">&quot;3.2&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">nacos:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nacos/nacos-server</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MODE:</span> <span class="string">standalone</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8848:8848&quot;</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:5.7.25</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">123</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;$PWD/mysql/data:/var/lib/mysql&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;$PWD/mysql/conf:/etc/mysql/conf.d/&quot;</span></span><br><span class="line">  <span class="attr">userservice:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./user-service</span></span><br><span class="line">  <span class="attr">orderservice:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./order-service</span></span><br><span class="line">  <span class="attr">gateway:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./gateway</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;10010:10010&quot;</span></span><br></pre></td></tr></table></figure><p>可以看到，其中包含5个service服务：</p><ul><li><code>nacos</code>：作为注册中心和配置中心<ul><li><code>image: nacos/nacos-server</code>： 基于nacos/nacos-server镜像构建</li><li><code>environment</code>：环境变量<ul><li><code>MODE: standalone</code>：单点模式启动</li></ul></li><li><code>ports</code>：端口映射，这里暴露了8848端口</li></ul></li><li><code>mysql</code>：数据库<ul><li><code>image: mysql:5.7.25</code>：镜像版本是mysql:5.7.25</li><li><code>environment</code>：环境变量<ul><li><code>MYSQL_ROOT_PASSWORD: 123</code>：设置数据库root账户的密码为123</li></ul></li><li><code>volumes</code>：数据卷挂载，这里挂载了mysql的data、conf目录，其中有我提前准备好的数据</li></ul></li><li><code>userservice</code>、<code>orderservice</code>、<code>gateway</code>：都是基于Dockerfile临时构建的</li></ul><p>查看mysql目录，可以看到其中已经准备好了cloud_order、cloud_user表：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801095205034.png" alt="image-20210801095205034"></p><p>查看微服务目录，可以看到都包含Dockerfile文件：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801095320586.png" alt="image-20210801095320586"></p><p>内容如下：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> java:<span class="number">8</span>-alpine</span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./app.jar /tmp/app.jar</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> java -jar /tmp/app.jar</span></span><br></pre></td></tr></table></figure><h3 id="4-3-2-修改微服务配置"><a href="#4-3-2-修改微服务配置" class="headerlink" title="4.3.2.修改微服务配置"></a>4.3.2.修改微服务配置</h3><p>因为微服务将来要部署为docker容器，而容器之间互联不是通过IP地址，而是通过容器名。这里我们将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于容器名的访问。</p><p>如下所示：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">datasource:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://mysql:3306/cloud_order?useSSL=false</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">123</span></span><br><span class="line">    <span class="attr">driver-class-name:</span> <span class="string">com.mysql.jdbc.Driver</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">orderservice</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">nacos:8848</span> <span class="comment"># nacos服务地址</span></span><br></pre></td></tr></table></figure><h3 id="4-3-3-打包"><a href="#4-3-3-打包" class="headerlink" title="4.3.3.打包"></a>4.3.3.打包</h3><p>接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar，因此我们的每个微服务都需要用这个名称。</p><p>可以通过修改pom.xml中的打包名称来实现，每个微服务都需要修改：</p><figure class="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></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">build</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">finalName</span>&gt;</span>app<span class="tag">&lt;/<span class="name">finalName</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">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><p>打包后：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801095951030.png" alt="image-20210801095951030"></p><h3 id="4-3-4-拷贝jar包到部署目录"><a href="#4-3-4-拷贝jar包到部署目录" class="headerlink" title="4.3.4.拷贝jar包到部署目录"></a>4.3.4.拷贝jar包到部署目录</h3><p>编译打包好的app.jar文件，需要放到Dockerfile的同级目录中。注意：每个微服务的app.jar放到与服务名称对应的目录，别搞错了。</p><p>user-service：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801100201253.png" alt="image-20210801100201253"></p><p>order-service：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801100231495.png" alt="image-20210801100231495"></p><p>gateway：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801100308102.png" alt="image-20210801100308102"></p><h3 id="4-3-5-部署"><a href="#4-3-5-部署" class="headerlink" title="4.3.5.部署"></a>4.3.5.部署</h3><p>最后，我们需要将文件整个cloud-demo文件夹上传到虚拟机中，理由DockerCompose部署。</p><p>上传到任意目录：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210801100955653.png" alt="image-20210801100955653"></p><p>部署：</p><p>进入cloud-demo目录，然后运行下面的命令：</p><figure class="highlight sh"><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><h1 id="5-Docker镜像仓库"><a href="#5-Docker镜像仓库" class="headerlink" title="5.Docker镜像仓库"></a>5.Docker镜像仓库</h1><h2 id="5-1-搭建私有镜像仓库"><a href="#5-1-搭建私有镜像仓库" class="headerlink" title="5.1.搭建私有镜像仓库"></a>5.1.搭建私有镜像仓库</h2><p>参考课前资料《CentOS7安装Docker.md》</p><h2 id="5-2-推送、拉取镜像"><a href="#5-2-推送、拉取镜像" class="headerlink" title="5.2.推送、拉取镜像"></a>5.2.推送、拉取镜像</h2><p>推送镜像到私有镜像服务必须先tag，步骤如下：</p><p>① 重新tag本地镜像，名称前缀为私有仓库的地址：192.168.150.101:8080/</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 </span><br></pre></td></tr></table></figure><p>② 推送镜像</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker push 192.168.150.101:8080/nginx:1.0 </span><br></pre></td></tr></table></figure><p>③ 拉取镜像</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull 192.168.150.101:8080/nginx:1.0 </span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Docker实用篇&quot;&gt;&lt;a href=&quot;#Docker实用篇&quot; class=&quot;headerlink&quot; title=&quot;Docker实用篇&quot;&gt;&lt;/a&gt;Docker实用篇&lt;/h1&gt;&lt;h1 id=&quot;1-初识Docker&quot;&gt;&lt;a href=&quot;#1-初识Docker&quot; cla</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="Docker" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/Docker/"/>
    
    
    <category term="Docker" scheme="https://www.shallowrecall.top/tags/Docker/"/>
    
  </entry>
  
  <entry>
    <title>Centos7安装Docker</title>
    <link href="https://www.shallowrecall.top/posts/a6573b44.html"/>
    <id>https://www.shallowrecall.top/posts/a6573b44.html</id>
    <published>2024-04-24T12:39:48.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="0-安装Docker"><a href="#0-安装Docker" class="headerlink" title="0.安装Docker"></a>0.安装Docker</h1><p>Docker 分为 CE 和 EE 两大版本。CE 即社区版（免费，支持周期 7 个月），EE 即企业版，强调安全，付费使用，支持周期 24 个月。</p><p>Docker CE 分为 <code>stable</code> <code>test</code> 和 <code>nightly</code> 三个更新频道。</p><p>官方网站上有各种环境下的 <a href="https://docs.docker.com/install/">安装指南</a>，这里主要介绍 Docker CE 在 CentOS上的安装。</p><h1 id="1-CentOS安装Docker"><a href="#1-CentOS安装Docker" class="headerlink" title="1.CentOS安装Docker"></a>1.CentOS安装Docker</h1><p>Docker CE 支持 64 位版本 CentOS 7，并且要求内核版本不低于 3.10， CentOS 7 满足最低内核的要求，所以我们在CentOS 7安装Docker。</p><h2 id="1-1-卸载（可选）"><a href="#1-1-卸载（可选）" class="headerlink" title="1.1.卸载（可选）"></a>1.1.卸载（可选）</h2><p>如果之前安装过旧版本的Docker，可以使用下面命令卸载：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">yum remove docker \</span><br><span class="line">                  docker-client \</span><br><span class="line">                  docker-client-latest \</span><br><span class="line">                  docker-common \</span><br><span class="line">                  docker-latest \</span><br><span class="line">                  docker-latest-logrotate \</span><br><span class="line">                  docker-logrotate \</span><br><span class="line">                  docker-selinux \</span><br><span class="line">                  docker-engine-selinux \</span><br><span class="line">                  docker-engine \</span><br><span class="line">                  docker-ce</span><br></pre></td></tr></table></figure><h2 id="1-2-安装docker"><a href="#1-2-安装docker" class="headerlink" title="1.2.安装docker"></a>1.2.安装docker</h2><p>首先需要大家虚拟机联网，安装yum工具</p><figure class="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></pre></td><td class="code"><pre><span class="line">yum install -y yum-utils \</span><br><span class="line">           device-mapper-persistent-data \</span><br><span class="line">           lvm2 --skip-broken</span><br></pre></td></tr></table></figure><p>然后更新本地镜像源：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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 prompt_"># </span><span class="language-bash">设置docker镜像源</span></span><br><span class="line">yum-config-manager \</span><br><span class="line">    --add-repo \</span><br><span class="line">    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo</span><br><span class="line">    </span><br><span class="line">sed -i &#x27;s/download.docker.com/mirrors.aliyun.com\/docker-ce/g&#x27; /etc/yum.repos.d/docker-ce.repo</span><br><span class="line"></span><br><span class="line">yum makecache fast</span><br></pre></td></tr></table></figure><p>然后输入命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y docker-ce</span><br></pre></td></tr></table></figure><p>docker-ce为社区免费版本。稍等片刻，docker即可安装成功。</p><h2 id="1-3-启动docker"><a href="#1-3-启动docker" class="headerlink" title="1.3.启动docker"></a>1.3.启动docker</h2><p>Docker应用需要用到各种端口，逐一去修改防火墙设置。非常麻烦，因此建议大家直接关闭防火墙！</p><p>启动docker前，一定要关闭防火墙后！！</p><p>启动docker前，一定要关闭防火墙后！！</p><p>启动docker前，一定要关闭防火墙后！！</p><figure class="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="comment"># 关闭</span></span><br><span class="line">systemctl stop firewalld</span><br><span class="line"><span class="comment"># 禁止开机启动防火墙</span></span><br><span class="line">systemctl <span class="built_in">disable</span> firewalld</span><br></pre></td></tr></table></figure><p>通过命令启动docker：</p><figure class="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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">systemctl start docker  <span class="comment"># 启动docker服务</span></span><br><span class="line"></span><br><span class="line">systemctl stop docker  <span class="comment"># 停止docker服务</span></span><br><span class="line"></span><br><span class="line">systemctl restart docker  <span class="comment"># 重启docker服务</span></span><br></pre></td></tr></table></figure><p>然后输入命令，可以查看docker版本：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker -v</span><br></pre></td></tr></table></figure><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210418154704436.png" alt="image-20210418154704436"></p><h2 id="1-4-配置镜像加速"><a href="#1-4-配置镜像加速" class="headerlink" title="1.4.配置镜像加速"></a>1.4.配置镜像加速</h2><p>docker官方镜像仓库网速较差，我们需要设置国内镜像服务：</p><p>参考阿里云的镜像加速文档：<a href="https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors">https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors</a></p><h1 id="2-CentOS7安装DockerCompose"><a href="#2-CentOS7安装DockerCompose" class="headerlink" title="2.CentOS7安装DockerCompose"></a>2.CentOS7安装DockerCompose</h1><h2 id="2-1-下载"><a href="#2-1-下载" class="headerlink" title="2.1.下载"></a>2.1.下载</h2><p>Linux下需要通过命令下载：</p><figure class="highlight sh"><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"># 安装</span></span><br><span class="line">curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`<span class="built_in">uname</span> -s`-`<span class="built_in">uname</span> -m` &gt; /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure><p>如果下载速度较慢，或者下载失败，可以使用课前资料提供的docker-compose文件：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210417133020614.png" alt="image-20210417133020614"></p><p>上传到<code>/usr/local/bin/</code>目录也可以。</p><h2 id="2-2-修改文件权限"><a href="#2-2-修改文件权限" class="headerlink" title="2.2.修改文件权限"></a>2.2.修改文件权限</h2><p>修改文件权限：</p><figure class="highlight sh"><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"># 修改权限</span></span><br><span class="line"><span class="built_in">chmod</span> +x /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure><h2 id="2-3-Base自动补全命令："><a href="#2-3-Base自动补全命令：" class="headerlink" title="2.3.Base自动补全命令："></a>2.3.Base自动补全命令：</h2><figure class="highlight sh"><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"># 补全命令</span></span><br><span class="line">curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose &gt; /etc/bash_completion.d/docker-compose</span><br></pre></td></tr></table></figure><p>如果这里出现错误，需要修改自己的hosts文件：</p><figure class="highlight sh"><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;199.232.68.133 raw.githubusercontent.com&quot;</span> &gt;&gt; /etc/hosts</span><br></pre></td></tr></table></figure><h1 id="3-Docker镜像仓库"><a href="#3-Docker镜像仓库" class="headerlink" title="3.Docker镜像仓库"></a>3.Docker镜像仓库</h1><p>搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。</p><p>官网地址：<a href="https://hub.docker.com/_/registry">https://hub.docker.com/_/registry</a></p><h2 id="3-1-简化版镜像仓库"><a href="#3-1-简化版镜像仓库" class="headerlink" title="3.1.简化版镜像仓库"></a>3.1.简化版镜像仓库</h2><p>Docker官方的Docker Registry是一个基础版本的Docker镜像仓库，具备仓库管理的完整功能，但是没有图形化界面。</p><p>搭建方式比较简单，命令如下：</p><figure class="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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">    --restart=always \</span><br><span class="line">    --name registry\</span><br><span class="line">    -p 5000:5000 \</span><br><span class="line">    -v registry-data:/var/lib/registry \</span><br><span class="line">    registry</span><br></pre></td></tr></table></figure><p>命令中挂载了一个数据卷registry-data到容器内的/var/lib/registry 目录，这是私有镜像库存放数据的目录。</p><p>访问<a href="http://YourIp:5000/v2/_catalog">http://YourIp:5000/v2/_catalog</a> 可以查看当前私有镜像服务中包含的镜像</p><h2 id="3-2-带有图形化界面版本"><a href="#3-2-带有图形化界面版本" class="headerlink" title="3.2.带有图形化界面版本"></a>3.2.带有图形化界面版本</h2><p>使用DockerCompose部署带有图象界面的DockerRegistry，命令如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">version:</span> <span class="string">&#x27;3.0&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">registry:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">registry</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./registry-data:/var/lib/registry</span></span><br><span class="line">  <span class="attr">ui:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">joxit/docker-registry-ui:static</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">8080</span><span class="string">:80</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REGISTRY_TITLE=传智教育私有仓库</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REGISTRY_URL=http://registry:5000</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">registry</span></span><br></pre></td></tr></table></figure><h2 id="3-3-配置Docker信任地址"><a href="#3-3-配置Docker信任地址" class="headerlink" title="3.3.配置Docker信任地址"></a>3.3.配置Docker信任地址</h2><p>我们的私服采用的是http协议，默认不被Docker信任，所以需要做一个配置：</p><figure class="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><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="comment"># 打开要修改的文件</span></span><br><span class="line">vi /etc/docker/daemon.json</span><br><span class="line"><span class="comment"># 添加内容：</span></span><br><span class="line"><span class="string">&quot;insecure-registries&quot;</span>:[<span class="string">&quot;http://192.168.150.101:8080&quot;</span>]</span><br><span class="line"><span class="comment"># 重加载</span></span><br><span class="line">systemctl daemon-reload</span><br><span class="line"><span class="comment"># 重启docker</span></span><br><span class="line">systemctl restart docker</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;0-安装Docker&quot;&gt;&lt;a href=&quot;#0-安装Docker&quot; class=&quot;headerlink&quot; title=&quot;0.安装Docker&quot;&gt;&lt;/a&gt;0.安装Docker&lt;/h1&gt;&lt;p&gt;Docker 分为 CE 和 EE 两大版本。CE 即社区版（免费，支持周期</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="Docker" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/Docker/"/>
    
    
    <category term="docker" scheme="https://www.shallowrecall.top/tags/docker/"/>
    
  </entry>
  
  <entry>
    <title>Nacos集群搭建</title>
    <link href="https://www.shallowrecall.top/posts/bf8bcde.html"/>
    <id>https://www.shallowrecall.top/posts/bf8bcde.html</id>
    <published>2024-04-22T12:57:42.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Nacos集群搭建"><a href="#Nacos集群搭建" class="headerlink" title="Nacos集群搭建"></a>Nacos集群搭建</h1><h1 id="1-集群结构图"><a href="#1-集群结构图" class="headerlink" title="1.集群结构图"></a>1.集群结构图</h1><p>官方给出的Nacos集群图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210409210621117.png" alt="image-20210409210621117"></p><p>其中包含3个nacos节点，然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。</p><p>我们计划的集群结构：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210409211355037.png" alt="image-20210409211355037"></p><p>三个nacos节点的地址：</p><div class="table-container"><table><thead><tr><th>节点</th><th>ip</th><th>port</th></tr></thead><tbody><tr><td>nacos1</td><td>192.168.150.1</td><td>8845</td></tr><tr><td>nacos2</td><td>192.168.150.1</td><td>8846</td></tr><tr><td>nacos3</td><td>192.168.150.1</td><td>8847</td></tr></tbody></table></div><h1 id="2-搭建集群"><a href="#2-搭建集群" class="headerlink" title="2.搭建集群"></a>2.搭建集群</h1><p>搭建集群的基本步骤：</p><ul><li>搭建数据库，初始化数据库表结构</li><li>下载nacos安装包</li><li>配置nacos</li><li>启动nacos集群</li><li>nginx反向代理</li></ul><h2 id="2-1-初始化数据库"><a href="#2-1-初始化数据库" class="headerlink" title="2.1.初始化数据库"></a>2.1.初始化数据库</h2><p>Nacos默认数据存储在内嵌数据库Derby中，不属于生产可用的数据库。</p><p>官方推荐的最佳实践是使用带有主从的高可用数据库集群，主从模式的高可用数据库可以参考<strong>传智教育</strong>的后续高手课程。</p><p>这里我们以单点的数据库为例来讲解。</p><p>首先新建一个数据库，命名为nacos，而后导入下面的SQL：</p><figure class="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><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  `c_desc` <span class="type">varchar</span>(<span class="number">256</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `c_use` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `effect` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `type` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `c_schema` text,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_aggr   */</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> `config_info_aggr` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `datum_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;datum_id&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;内容&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;增加租户字段&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_beta   */</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> `config_info_beta` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `beta_ips` <span class="type">varchar</span>(<span class="number">1024</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;betaIps&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info_beta&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_tag   */</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> `config_info_tag` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `tag_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info_tag&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_tags_relation   */</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> `config_tags_relation` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `tag_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_name&#x27;</span>,</span><br><span class="line">  `tag_type` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_type&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `nid` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`nid`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),</span><br><span class="line">  KEY `idx_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_tag_relation&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = group_capacity   */</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> `group_capacity` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键ID&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;Group ID，空字符表示整个集群&#x27;</span>,</span><br><span class="line">  `quota` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;配额，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `usage` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;使用量&#x27;</span>,</span><br><span class="line">  `max_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;聚合子配置最大个数，，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_history_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;最大变更历史数量&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_group_id` (`group_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;集群、各Group容量信息表&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = his_config_info   */</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> `his_config_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">64</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `nid` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  `data_id` <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">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line">  `src_user` text,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `op_type` <span class="type">char</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`nid`),</span><br><span class="line">  KEY `idx_gmt_create` (`gmt_create`),</span><br><span class="line">  KEY `idx_gmt_modified` (`gmt_modified`),</span><br><span class="line">  KEY `idx_did` (`data_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;多租户改造&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = tenant_capacity   */</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> `tenant_capacity` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键ID&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;Tenant ID&#x27;</span>,</span><br><span class="line">  `quota` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;配额，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `usage` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;使用量&#x27;</span>,</span><br><span class="line">  `max_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;聚合子配置最大个数&#x27;</span>,</span><br><span class="line">  `max_aggr_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_history_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;最大变更历史数量&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;租户容量信息表&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `tenant_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `kp` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;kp&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">default</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `tenant_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">default</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_name&#x27;</span>,</span><br><span class="line">  `tenant_desc` <span class="type">varchar</span>(<span class="number">256</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tenant_desc&#x27;</span>,</span><br><span class="line">  `create_source` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;create_source&#x27;</span>,</span><br><span class="line">  `gmt_create` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),</span><br><span class="line">  KEY `idx_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;tenant_info&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `users` (</span><br><span class="line">`username` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line">`password` <span class="type">varchar</span>(<span class="number">500</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">`enabled` <span class="type">boolean</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `roles` (</span><br><span class="line">`username` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">`role` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line"><span class="keyword">UNIQUE</span> INDEX `idx_user_role` (`username` <span class="keyword">ASC</span>, `role` <span class="keyword">ASC</span>) <span class="keyword">USING</span> BTREE</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `permissions` (</span><br><span class="line">    `role` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    `resource` <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">    `action` <span class="type">varchar</span>(<span class="number">8</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `uk_role_permission` (`role`,`resource`,`action`) <span class="keyword">USING</span> BTREE</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> users (username, password, enabled) <span class="keyword">VALUES</span> (<span class="string">&#x27;nacos&#x27;</span>, <span class="string">&#x27;$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu&#x27;</span>, <span class="literal">TRUE</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> roles (username, role) <span class="keyword">VALUES</span> (<span class="string">&#x27;nacos&#x27;</span>, <span class="string">&#x27;ROLE_ADMIN&#x27;</span>);</span><br></pre></td></tr></table></figure><h2 id="2-2-下载nacos"><a href="#2-2-下载nacos" class="headerlink" title="2.2.下载nacos"></a>2.2.下载nacos</h2><p>nacos在GitHub上有下载地址：<a href="https://github.com/alibaba/nacos/tags，可以选择任意版本下载。">https://github.com/alibaba/nacos/tags，可以选择任意版本下载。</a></p><p>本例中才用1.4.1版本：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210409212119411.png" alt="image-20210409212119411"></p><h2 id="2-3-配置Nacos"><a href="#2-3-配置Nacos" class="headerlink" title="2.3.配置Nacos"></a>2.3.配置Nacos</h2><p>将这个包解压到任意非中文目录下，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161843337.png" alt="image-20210402161843337"></p><p>目录说明：</p><ul><li>bin：启动脚本</li><li>conf：配置文件</li></ul><p>进入nacos的conf目录，修改配置文件cluster.conf.example，重命名为cluster.conf：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210409212459292.png" alt="image-20210409212459292"></p><p>然后添加内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:8845</span><br><span class="line">127.0.0.1.8846</span><br><span class="line">127.0.0.1.8847</span><br></pre></td></tr></table></figure><p>然后修改application.properties文件，添加数据库配置</p><figure class="highlight properties"><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">spring.datasource.platform</span>=<span class="string">mysql</span></span><br><span class="line"></span><br><span class="line"><span class="attr">db.num</span>=<span class="string">1</span></span><br><span class="line"></span><br><span class="line"><span class="attr">db.url.0</span>=<span class="string">jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=UTC</span></span><br><span class="line"><span class="attr">db.user.0</span>=<span class="string">root</span></span><br><span class="line"><span class="attr">db.password.0</span>=<span class="string">123</span></span><br></pre></td></tr></table></figure><h2 id="2-4-启动"><a href="#2-4-启动" class="headerlink" title="2.4.启动"></a>2.4.启动</h2><p>将nacos文件夹复制三份，分别命名为：nacos1、nacos2、nacos3</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210409213335538.png" alt="image-20210409213335538"></p><p>然后分别修改三个文件夹中的application.properties，</p><p>nacos1:</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8845</span></span><br></pre></td></tr></table></figure><p>nacos2:</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8846</span></span><br></pre></td></tr></table></figure><p>nacos3:</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8847</span></span><br></pre></td></tr></table></figure><p>然后分别启动三个nacos节点：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">startup.cmd</span><br></pre></td></tr></table></figure><h2 id="2-5-nginx反向代理"><a href="#2-5-nginx反向代理" class="headerlink" title="2.5.nginx反向代理"></a>2.5.nginx反向代理</h2><p>找到课前资料提供的nginx安装包：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210410103253355.png" alt="image-20210410103253355"></p><p>解压到任意非中文目录下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210410103322874.png" alt="image-20210410103322874"></p><p>修改conf/nginx.conf文件，配置如下：</p><figure class="highlight nginx"><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="section">upstream</span> nacos-cluster &#123;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">127.0.0.1:8845</span>;</span><br><span class="line"><span class="attribute">server</span> <span class="number">127.0.0.1:8846</span>;</span><br><span class="line"><span class="attribute">server</span> <span class="number">127.0.0.1:8847</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span>       <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span>  localhost;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> /nacos &#123;</span><br><span class="line">        <span class="attribute">proxy_pass</span> http://nacos-cluster;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而后在浏览器访问：<a href="http://localhost/nacos即可。">http://localhost/nacos即可。</a></p><p>代码中application.yml文件配置如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:80</span> <span class="comment"># Nacos地址</span></span><br></pre></td></tr></table></figure><h2 id="2-6-优化"><a href="#2-6-优化" class="headerlink" title="2.6.优化"></a>2.6.优化</h2><ul><li><p>实际部署时，需要给做反向代理的nginx服务器设置一个域名，这样后续如果有服务器迁移nacos的客户端也无需更改配置.</p></li><li><p>Nacos的各个节点应该部署到多个不同服务器，做好容灾和隔离</p></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Nacos集群搭建&quot;&gt;&lt;a href=&quot;#Nacos集群搭建&quot; class=&quot;headerlink&quot; title=&quot;Nacos集群搭建&quot;&gt;&lt;/a&gt;Nacos集群搭建&lt;/h1&gt;&lt;h1 id=&quot;1-集群结构图&quot;&gt;&lt;a href=&quot;#1-集群结构图&quot; class=&quot;he</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="Nacos" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/Nacos/"/>
    
    
    <category term="Nacos" scheme="https://www.shallowrecall.top/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>Nacos安装指南</title>
    <link href="https://www.shallowrecall.top/posts/51556c56.html"/>
    <id>https://www.shallowrecall.top/posts/51556c56.html</id>
    <published>2024-04-22T12:54:40.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Nacos安装指南"><a href="#Nacos安装指南" class="headerlink" title="Nacos安装指南"></a>Nacos安装指南</h1><h1 id="1-Windows安装"><a href="#1-Windows安装" class="headerlink" title="1.Windows安装"></a>1.Windows安装</h1><p>开发阶段采用单机安装即可。</p><h2 id="1-1-下载安装包"><a href="#1-1-下载安装包" class="headerlink" title="1.1.下载安装包"></a>1.1.下载安装包</h2><p>在Nacos的GitHub页面，提供有下载链接，可以下载编译好的Nacos服务端或者源代码：</p><p>GitHub主页：<a href="https://github.com/alibaba/nacos">https://github.com/alibaba/nacos</a></p><p>GitHub的Release下载页：<a href="https://github.com/alibaba/nacos/releases">https://github.com/alibaba/nacos/releases</a></p><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161102887.png" alt="image-20210402161102887"></p><p>本课程采用1.4.1.版本的Nacos，课前资料已经准备了安装包：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161130261.png" alt="image-20210402161130261"></p><p>windows版本使用<code>nacos-server-1.4.1.zip</code>包即可。</p><h2 id="1-2-解压"><a href="#1-2-解压" class="headerlink" title="1.2.解压"></a>1.2.解压</h2><p>将这个包解压到任意非中文目录下，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161843337.png" alt="image-20210402161843337"></p><p>目录说明：</p><ul><li>bin：启动脚本</li><li>conf：配置文件</li></ul><h2 id="1-3-端口配置"><a href="#1-3-端口配置" class="headerlink" title="1.3.端口配置"></a>1.3.端口配置</h2><p>Nacos的默认端口是8848，如果你电脑上的其它进程占用了8848端口，请先尝试关闭该进程。</p><p><strong>如果无法关闭占用8848端口的进程</strong>，也可以进入nacos的conf目录，修改配置文件中的端口：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162008280.png" alt="image-20210402162008280"></p><p>修改其中的内容：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162251093.png" alt="image-20210402162251093"></p><h2 id="1-4-启动"><a href="#1-4-启动" class="headerlink" title="1.4.启动"></a>1.4.启动</h2><p>启动非常简单，进入bin目录，结构如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162350977.png" alt="image-20210402162350977"></p><p>然后执行命令即可：</p><ul><li><p>windows命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">startup.cmd -m standalone</span><br></pre></td></tr></table></figure></li></ul><p>执行后的效果如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162526774.png" alt="image-20210402162526774"></p><h2 id="1-5-访问"><a href="#1-5-访问" class="headerlink" title="1.5.访问"></a>1.5.访问</h2><p>在浏览器输入地址：<a href="http://127.0.0.1:8848/nacos即可：">http://127.0.0.1:8848/nacos即可：</a></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162630427.png" alt="image-20210402162630427"></p><p>默认的账号和密码都是nacos，进入后：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402162709515.png" alt="image-20210402162709515"></p><h1 id="2-Linux安装"><a href="#2-Linux安装" class="headerlink" title="2.Linux安装"></a>2.Linux安装</h1><p>Linux或者Mac安装方式与Windows类似。</p><h2 id="2-1-安装JDK"><a href="#2-1-安装JDK" class="headerlink" title="2.1.安装JDK"></a>2.1.安装JDK</h2><p>Nacos依赖于JDK运行，索引Linux上也需要安装JDK才行。</p><p>上传jdk安装包：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402172334810.png" alt="image-20210402172334810"></p><p>上传到某个目录，例如：<code>/usr/local/</code></p><p>然后解压缩：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -xvf jdk-8u144-linux-x64.tar.gz</span><br></pre></td></tr></table></figure><p>然后重命名为java</p><p>配置环境变量：</p><figure class="highlight sh"><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">export</span> JAVA_HOME=/usr/local/java</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$PATH</span>:<span class="variable">$JAVA_HOME</span>/bin</span><br></pre></td></tr></table></figure><p>设置环境变量：</p><figure class="highlight sh"><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">source</span> /etc/profile</span><br></pre></td></tr></table></figure><h2 id="2-2-上传安装包"><a href="#2-2-上传安装包" class="headerlink" title="2.2.上传安装包"></a>2.2.上传安装包</h2><p>如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161102887.png" alt="image-20210402161102887"></p><p>也可以直接使用课前资料中的tar.gz：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402161130261.png" alt="image-20210402161130261"></p><p>上传到Linux服务器的某个目录，例如<code>/usr/local/src</code>目录下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402163715580.png" alt="image-20210402163715580"></p><h2 id="2-3-解压"><a href="#2-3-解压" class="headerlink" title="2.3.解压"></a>2.3.解压</h2><p>命令解压缩安装包：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tar -xvf nacos-server-1.4.1.tar.gz</span><br></pre></td></tr></table></figure><p>然后删除安装包：</p><figure class="highlight sh"><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 nacos-server-1.4.1.tar.gz</span><br></pre></td></tr></table></figure><p>目录中最终样式：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402163858429.png" alt="image-20210402163858429"></p><p>目录内部：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210402164414827.png" alt="image-20210402164414827"></p><h2 id="2-4-端口配置"><a href="#2-4-端口配置" class="headerlink" title="2.4.端口配置"></a>2.4.端口配置</h2><p>与windows中类似</p><h2 id="2-5-启动"><a href="#2-5-启动" class="headerlink" title="2.5.启动"></a>2.5.启动</h2><p>在nacos/bin目录中，输入命令启动Nacos：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sh startup.sh -m standalone</span><br></pre></td></tr></table></figure><h1 id="3-Nacos的依赖"><a href="#3-Nacos的依赖" class="headerlink" title="3.Nacos的依赖"></a>3.Nacos的依赖</h1><p>父工程：</p><figure class="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></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.alibaba.cloud<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-cloud-alibaba-dependencies<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>2.2.5.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<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></pre></td></tr></table></figure><p>客户端：</p><figure class="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="comment">&lt;!-- nacos客户端依赖包 --&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.alibaba.cloud<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-cloud-starter-alibaba-nacos-discovery<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></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;Nacos安装指南&quot;&gt;&lt;a href=&quot;#Nacos安装指南&quot; class=&quot;headerlink&quot; title=&quot;Nacos安装指南&quot;&gt;&lt;/a&gt;Nacos安装指南&lt;/h1&gt;&lt;h1 id=&quot;1-Windows安装&quot;&gt;&lt;a href=&quot;#1-Windows安装&quot; c</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    <category term="Nacos" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/Nacos/"/>
    
    
    <category term="Nacos" scheme="https://www.shallowrecall.top/tags/Nacos/"/>
    
  </entry>
  
  <entry>
    <title>SpringCloud02</title>
    <link href="https://www.shallowrecall.top/posts/4f54bfad.html"/>
    <id>https://www.shallowrecall.top/posts/4f54bfad.html</id>
    <published>2024-04-22T12:40:07.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="SpringCloud02"><a href="#SpringCloud02" class="headerlink" title="SpringCloud02"></a>SpringCloud02</h1><h1 id="1-Nacos配置管理"><a href="#1-Nacos配置管理" class="headerlink" title="1.Nacos配置管理"></a>1.Nacos配置管理</h1><p>Nacos除了可以做注册中心，同样可以做配置管理来使用。</p><h2 id="1-1-统一配置管理"><a href="#1-1-统一配置管理" class="headerlink" title="1.1.统一配置管理"></a>1.1.统一配置管理</h2><p>当微服务部署的实例越来越多，达到数十、数百时，逐个修改微服务配置就会让人抓狂，而且很容易出错。我们需要一种统一配置管理方案，可以集中管理所有实例的配置。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714164426792.png" alt="image-20210714164426792"></p><p>Nacos一方面可以将配置集中管理，另一方可以在配置变更时，及时通知微服务，实现配置的热更新。</p><h3 id="1-1-1-在nacos中添加配置文件"><a href="#1-1-1-在nacos中添加配置文件" class="headerlink" title="1.1.1.在nacos中添加配置文件"></a>1.1.1.在nacos中添加配置文件</h3><p>如何在nacos中管理配置呢？</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714164742924.png" alt="image-20210714164742924"></p><p>然后在弹出的表单中，填写配置信息：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714164856664.png" alt="image-20210714164856664"></p><blockquote><p>注意：项目的核心配置，需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。</p></blockquote><h3 id="1-1-2-从微服务拉取配置"><a href="#1-1-2-从微服务拉取配置" class="headerlink" title="1.1.2.从微服务拉取配置"></a>1.1.2.从微服务拉取配置</h3><p>微服务要拉取nacos中管理的配置，并且与本地的application.yml配置合并，才能完成项目启动。</p><p>但如果尚未读取application.yml，又如何得知nacos地址呢？</p><p>因此spring引入了一种新的配置文件：bootstrap.yaml文件，会在application.yml之前被读取，流程如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/L0iFYNF.png" alt="img"></p><p>1）引入nacos-config依赖</p><p>首先，在user-service服务中，引入nacos-config的客户端依赖：</p><figure class="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="comment">&lt;!--nacos配置管理依赖--&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.alibaba.cloud<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-cloud-starter-alibaba-nacos-config<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></pre></td></tr></table></figure><p>2）添加bootstrap.yaml</p><p>然后，在user-service中添加一个bootstrap.yaml文件，内容如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="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">userservice</span> <span class="comment"># 服务名称</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 class="comment">#开发环境，这里是dev </span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span> <span class="comment"># Nacos地址</span></span><br><span class="line">      <span class="attr">config:</span></span><br><span class="line">        <span class="attr">file-extension:</span> <span class="string">yaml</span> <span class="comment"># 文件后缀名</span></span><br></pre></td></tr></table></figure><p>这里会根据spring.cloud.nacos.server-addr获取nacos地址，再根据</p><p><code>$&#123;spring.application.name&#125;-$&#123;spring.profiles.active&#125;.$&#123;spring.cloud.nacos.config.file-extension&#125;</code>作为文件id，来读取配置。</p><p>本例中，就是去读取<code>userservice-dev.yaml</code>：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714170845901.png" alt="image-20210714170845901"></p><p>3）读取nacos配置</p><p>在user-service中的UserController中添加业务逻辑，读取pattern.dateformat配置：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714170337448.png" alt="image-20210714170337448"></p><p>完整代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.web;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.pojo.User;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.service.UserService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Value;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.time.format.DateTimeFormatter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</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> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;pattern.dateformat&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@GetMapping(&quot;now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...略</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在页面访问，可以看到效果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714170449612.png" alt="image-20210714170449612"></p><h2 id="1-2-配置热更新"><a href="#1-2-配置热更新" class="headerlink" title="1.2.配置热更新"></a>1.2.配置热更新</h2><p>我们最终的目的，是修改nacos中的配置后，微服务中无需重启即可让配置生效，也就是<strong>配置热更新</strong>。</p><p>要实现配置热更新，可以使用两种方式：</p><h3 id="1-2-1-方式一"><a href="#1-2-1-方式一" class="headerlink" title="1.2.1.方式一"></a>1.2.1.方式一</h3><p>在@Value注入的变量所在类上添加注解@RefreshScope：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714171036335.png" alt="image-20210714171036335"></p><h3 id="1-2-2-方式二"><a href="#1-2-2-方式二" class="headerlink" title="1.2.2.方式二"></a>1.2.2.方式二</h3><p>使用@ConfigurationProperties注解代替@Value注解。</p><p>在user-service服务中，添加一个类，读取patterrn.dateformat属性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.context.properties.ConfigurationProperties;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;pattern&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PatternProperties</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在UserController中使用这个类代替@Value：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714171316124.png" alt="image-20210714171316124"></p><p>完整代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.web;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.config.PatternProperties;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.pojo.User;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.service.UserService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.time.format.DateTimeFormatter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</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> UserService userService;</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> PatternProperties patternProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));</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><h2 id="1-3-配置共享"><a href="#1-3-配置共享" class="headerlink" title="1.3.配置共享"></a>1.3.配置共享</h2><p>其实微服务启动时，会去nacos读取多个配置文件，例如：</p><ul><li><p><code>[spring.application.name]-[spring.profiles.active].yaml</code>，例如：userservice-dev.yaml</p></li><li><p><code>[spring.application.name].yaml</code>，例如：userservice.yaml</p></li></ul><p>而<code>[spring.application.name].yaml</code>不包含环境，因此可以被多个环境共享。</p><p>下面我们通过案例来测试配置共享</p><h3 id="1）添加一个环境共享配置"><a href="#1）添加一个环境共享配置" class="headerlink" title="1）添加一个环境共享配置"></a>1）添加一个环境共享配置</h3><p>我们在nacos中添加一个userservice.yaml文件：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714173233650.png" alt="image-20210714173233650"></p><h3 id="2）在user-service中读取共享配置"><a href="#2）在user-service中读取共享配置" class="headerlink" title="2）在user-service中读取共享配置"></a>2）在user-service中读取共享配置</h3><p>在user-service服务中，修改PatternProperties类，读取新添加的属性：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714173324231.png" alt="image-20210714173324231"></p><p>在user-service服务中，修改UserController，添加一个方法：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714173721309.png" alt="image-20210714173721309"></p><h3 id="3）运行两个UserApplication，使用不同的profile"><a href="#3）运行两个UserApplication，使用不同的profile" class="headerlink" title="3）运行两个UserApplication，使用不同的profile"></a>3）运行两个UserApplication，使用不同的profile</h3><p>修改UserApplication2这个启动项，改变其profile值：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714173538538.png" alt="image-20210714173538538"></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714173519963.png" alt="image-20210714173519963"></p><p>这样，UserApplication(8081)使用的profile是dev，UserApplication2(8082)使用的profile是test。</p><p>启动UserApplication和UserApplication2</p><p>访问<a href="http://localhost:8081/user/prop，结果：">http://localhost:8081/user/prop，结果：</a></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174313344.png" alt="image-20210714174313344"></p><p>访问<a href="http://localhost:8082/user/prop，结果：">http://localhost:8082/user/prop，结果：</a></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174424818.png" alt="image-20210714174424818"></p><p>可以看出来，不管是dev，还是test环境，都读取到了envSharedValue这个属性的值。</p><h3 id="4）配置共享的优先级"><a href="#4）配置共享的优先级" class="headerlink" title="4）配置共享的优先级"></a>4）配置共享的优先级</h3><p>当nacos、服务本地同时出现相同属性时，优先级有高低之分：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174623557.png" alt="image-20210714174623557"></p><h2 id="1-4-搭建Nacos集群"><a href="#1-4-搭建Nacos集群" class="headerlink" title="1.4.搭建Nacos集群"></a>1.4.搭建Nacos集群</h2><p>Nacos生产环境下一定要部署为集群状态，部署方式参考课前资料中的文档：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174728042.png" alt="image-20210714174728042"></p><h1 id="2-Feign远程调用"><a href="#2-Feign远程调用" class="headerlink" title="2.Feign远程调用"></a>2.Feign远程调用</h1><p>先来看我们以前利用RestTemplate发起远程调用的代码：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174814204.png" alt="image-20210714174814204"></p><p>存在下面的问题：</p><p>•代码可读性差，编程体验不统一</p><p>•参数复杂URL难以维护</p><p>Feign是一个声明式的http客户端，官方地址：<a href="https://github.com/OpenFeign/feign">https://github.com/OpenFeign/feign</a></p><p>其作用就是帮助我们优雅的实现http请求的发送，解决上面提到的问题。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714174918088.png" alt="image-20210714174918088"></p><h2 id="2-1-Feign替代RestTemplate"><a href="#2-1-Feign替代RestTemplate" class="headerlink" title="2.1.Feign替代RestTemplate"></a>2.1.Feign替代RestTemplate</h2><p>Fegin的使用步骤如下：</p><h3 id="1）引入依赖"><a href="#1）引入依赖" class="headerlink" title="1）引入依赖"></a>1）引入依赖</h3><p>我们在order-service服务的pom文件中引入feign的依赖：</p><figure class="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="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.cloud<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-cloud-starter-openfeign<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></pre></td></tr></table></figure><h3 id="2）添加注解"><a href="#2）添加注解" class="headerlink" title="2）添加注解"></a>2）添加注解</h3><p>在order-service的启动类添加注解开启Feign的功能：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714175102524.png" alt="image-20210714175102524"></p><h3 id="3）编写Feign的客户端"><a href="#3）编写Feign的客户端" class="headerlink" title="3）编写Feign的客户端"></a>3）编写Feign的客户端</h3><p>在order-service中新建一个接口，内容如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.client;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.User;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.openfeign.FeignClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line"></span><br><span class="line"><span class="meta">@FeignClient(&quot;userservice&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserClient</span> &#123;</span><br><span class="line">    <span class="meta">@GetMapping(&quot;/user/&#123;id&#125;&quot;)</span></span><br><span class="line">    User <span class="title function_">findById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个客户端主要是基于SpringMVC的注解来声明远程调用的信息，比如：</p><ul><li>服务名称：userservice</li><li>请求方式：GET</li><li>请求路径：/user/{id}</li><li>请求参数：Long id</li><li>返回值类型：User</li></ul><p>这样，Feign就可以帮助我们发送http请求，无需自己使用RestTemplate来发送了。</p><h3 id="4）测试"><a href="#4）测试" class="headerlink" title="4）测试"></a>4）测试</h3><p>修改order-service中的OrderService类中的queryOrderById方法，使用Feign客户端代替RestTemplate：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714175415087.png" alt="image-20210714175415087"></p><p>是不是看起来优雅多了。</p><h3 id="5）总结"><a href="#5）总结" class="headerlink" title="5）总结"></a>5）总结</h3><p>使用Feign的步骤：</p><p>① 引入依赖</p><p>② 添加@EnableFeignClients注解</p><p>③ 编写FeignClient接口</p><p>④ 使用FeignClient中定义的方法代替RestTemplate</p><h2 id="2-2-自定义配置"><a href="#2-2-自定义配置" class="headerlink" title="2.2.自定义配置"></a>2.2.自定义配置</h2><p>Feign可以支持很多的自定义配置，如下表所示：</p><div class="table-container"><table><thead><tr><th>类型</th><th>作用</th><th>说明</th></tr></thead><tbody><tr><td><strong>feign.Logger.Level</strong></td><td>修改日志级别</td><td>包含四种不同的级别：NONE、BASIC、HEADERS、FULL</td></tr><tr><td>feign.codec.Decoder</td><td>响应结果的解析器</td><td>http远程调用的结果做解析，例如解析json字符串为java对象</td></tr><tr><td>feign.codec.Encoder</td><td>请求参数编码</td><td>将请求参数编码，便于通过http请求发送</td></tr><tr><td>feign. Contract</td><td>支持的注解格式</td><td>默认是SpringMVC的注解</td></tr><tr><td>feign. Retryer</td><td>失败重试机制</td><td>请求失败的重试机制，默认是没有，不过会使用Ribbon的重试</td></tr></tbody></table></div><p>一般情况下，默认值就能满足我们使用，如果要自定义时，只需要创建自定义的@Bean覆盖默认Bean即可。</p><p>下面以日志为例来演示如何自定义配置。</p><h3 id="2-2-1-配置文件方式"><a href="#2-2-1-配置文件方式" class="headerlink" title="2.2.1.配置文件方式"></a>2.2.1.配置文件方式</h3><p>基于配置文件修改feign的日志级别可以针对单个服务：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span>  </span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span> </span><br><span class="line">      <span class="attr">userservice:</span> <span class="comment"># 针对某个微服务的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">FULL</span> <span class="comment">#  日志级别 </span></span><br></pre></td></tr></table></figure><p>也可以针对所有服务：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span>  </span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span> </span><br><span class="line">      <span class="attr">default:</span> <span class="comment"># 这里用default就是全局配置，如果是写服务名称，则是针对某个微服务的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">FULL</span> <span class="comment">#  日志级别 </span></span><br></pre></td></tr></table></figure><p>而日志的级别分为四种：</p><ul><li>NONE：不记录任何日志信息，这是默认值。</li><li>BASIC：仅记录请求的方法，URL以及响应状态码和执行时间</li><li>HEADERS：在BASIC的基础上，额外记录了请求和响应的头信息</li><li>FULL：记录所有请求和响应的明细，包括头信息、请求体、元数据。</li></ul><h3 id="2-2-2-Java代码方式"><a href="#2-2-2-Java代码方式" class="headerlink" title="2.2.2.Java代码方式"></a>2.2.2.Java代码方式</h3><p>也可以基于Java代码来修改日志级别，先声明一个类，然后声明一个Logger.Level的对象：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DefaultFeignConfiguration</span>  &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Logger.Level <span class="title function_">feignLogLevel</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Logger.Level.BASIC; <span class="comment">// 日志级别为BASIC</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果要<strong>全局生效</strong>，将其放到启动类的@EnableFeignClients这个注解中：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)</span> </span><br></pre></td></tr></table></figure><p>如果是<strong>局部生效</strong>，则把它放到对应的@FeignClient这个注解中：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@FeignClient(value = &quot;userservice&quot;, configuration = DefaultFeignConfiguration .class)</span> </span><br></pre></td></tr></table></figure><h2 id="2-3-Feign使用优化"><a href="#2-3-Feign使用优化" class="headerlink" title="2.3.Feign使用优化"></a>2.3.Feign使用优化</h2><p>Feign底层发起http请求，依赖于其它的框架。其底层客户端实现包括：</p><p>•URLConnection：默认实现，不支持连接池</p><p>•Apache HttpClient ：支持连接池</p><p>•OKHttp：支持连接池</p><p>因此提高Feign的性能主要手段就是使用<strong>连接池</strong>代替默认的URLConnection。</p><p>这里我们用Apache的HttpClient来演示。</p><p>1）引入依赖</p><p>在order-service的pom文件中引入Apache的HttpClient依赖：</p><figure class="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="comment">&lt;!--httpClient的依赖 --&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.github.openfeign<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>feign-httpclient<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></pre></td></tr></table></figure><p>2）配置连接池</p><p>在order-service的application.yml中添加配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">feign:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span></span><br><span class="line">      <span class="attr">default:</span> <span class="comment"># default全局的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">BASIC</span> <span class="comment"># 日志级别，BASIC就是基本的请求和响应信息</span></span><br><span class="line">  <span class="attr">httpclient:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span> <span class="comment"># 开启feign对HttpClient的支持</span></span><br><span class="line">    <span class="attr">max-connections:</span> <span class="number">200</span> <span class="comment"># 最大的连接数</span></span><br><span class="line">    <span class="attr">max-connections-per-route:</span> <span class="number">50</span> <span class="comment"># 每个路径的最大连接数</span></span><br></pre></td></tr></table></figure><p>接下来，在FeignClientFactoryBean中的loadBalance方法中打断点：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714185925910.png" alt="image-20210714185925910"></p><p>Debug方式启动order-service服务，可以看到这里的client，底层就是Apache HttpClient：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714190041542.png" alt="image-20210714190041542"></p><p>总结，Feign的优化：</p><p>1.日志级别尽量用basic</p><p>2.使用HttpClient或OKHttp代替URLConnection</p><p>①  引入feign-httpClient依赖</p><p>②  配置文件开启httpClient功能，设置连接池参数</p><h2 id="2-4-最佳实践"><a href="#2-4-最佳实践" class="headerlink" title="2.4.最佳实践"></a>2.4.最佳实践</h2><p>所谓最近实践，就是使用过程中总结的经验，最好的一种使用方式。</p><p>自习观察可以发现，Feign的客户端与服务提供者的controller代码非常相似：</p><p>feign客户端：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714190542730.png" alt="image-20210714190542730"></p><p>UserController：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714190528450.png" alt="image-20210714190528450"></p><p>有没有一种办法简化这种重复的代码编写呢？</p><h3 id="2-4-1-继承方式"><a href="#2-4-1-继承方式" class="headerlink" title="2.4.1.继承方式"></a>2.4.1.继承方式</h3><p>一样的代码可以通过继承来共享：</p><p>1）定义一个API接口，利用定义方法，并基于SpringMVC注解做声明。</p><p>2）Feign客户端和Controller都集成改接口</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714190640857.png" alt="image-20210714190640857"></p><p>优点：</p><ul><li>简单</li><li>实现了代码共享</li></ul><p>缺点：</p><ul><li><p>服务提供方、服务消费方紧耦合</p></li><li><p>参数列表中的注解映射并不会继承，因此Controller中必须再次声明方法、参数列表、注解</p></li></ul><h3 id="2-4-2-抽取方式"><a href="#2-4-2-抽取方式" class="headerlink" title="2.4.2.抽取方式"></a>2.4.2.抽取方式</h3><p>将Feign的Client抽取为独立模块，并且把接口有关的POJO、默认的Feign配置都放到这个模块中，提供给所有消费者使用。</p><p>例如，将UserClient、User、Feign的默认配置都抽取到一个feign-api包中，所有微服务引用该依赖包，即可直接使用。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714214041796.png" alt="image-20210714214041796"></p><h3 id="2-4-3-实现基于抽取的最佳实践"><a href="#2-4-3-实现基于抽取的最佳实践" class="headerlink" title="2.4.3.实现基于抽取的最佳实践"></a>2.4.3.实现基于抽取的最佳实践</h3><h4 id="1）抽取"><a href="#1）抽取" class="headerlink" title="1）抽取"></a>1）抽取</h4><p>首先创建一个module，命名为feign-api：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714204557771.png" alt="image-20210714204557771"></p><p>项目结构：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714204656214.png" alt="image-20210714204656214"></p><p>在feign-api中然后引入feign的starter依赖</p><figure class="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="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.cloud<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-cloud-starter-openfeign<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></pre></td></tr></table></figure><p>然后，order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714205221970.png" alt="image-20210714205221970"></p><h4 id="2）在order-service中使用feign-api"><a href="#2）在order-service中使用feign-api" class="headerlink" title="2）在order-service中使用feign-api"></a>2）在order-service中使用feign-api</h4><p>首先，删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。</p><p>在order-service的pom文件中中引入feign-api的依赖：</p><figure class="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>cn.itcast.demo<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>feign-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>1.0<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><p>修改order-service中的所有与上述三个组件有关的导包部分，改成导入feign-api中的包</p><h4 id="3）重启测试"><a href="#3）重启测试" class="headerlink" title="3）重启测试"></a>3）重启测试</h4><p>重启后，发现服务报错了：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714205623048.png" alt="image-20210714205623048"></p><p>这是因为UserClient现在在cn.itcast.feign.clients包下，</p><p>而order-service的@EnableFeignClients注解是在cn.itcast.order包下，不在同一个包，无法扫描到UserClient。</p><h4 id="4）解决扫描包问题"><a href="#4）解决扫描包问题" class="headerlink" title="4）解决扫描包问题"></a>4）解决扫描包问题</h4><p>方式一：</p><p>指定Feign应该扫描的包：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(basePackages = &quot;cn.itcast.feign.clients&quot;)</span></span><br></pre></td></tr></table></figure><p>方式二：</p><p>指定需要加载的Client接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(clients = &#123;UserClient.class&#125;)</span></span><br></pre></td></tr></table></figure><h1 id="3-Gateway服务网关"><a href="#3-Gateway服务网关" class="headerlink" title="3.Gateway服务网关"></a>3.Gateway服务网关</h1><p>Spring Cloud Gateway 是 Spring Cloud 的一个全新项目，该项目是基于 Spring 5.0，Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关，它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。</p><h2 id="3-1-为什么需要网关"><a href="#3-1-为什么需要网关" class="headerlink" title="3.1.为什么需要网关"></a>3.1.为什么需要网关</h2><p>Gateway网关是我们服务的守门神，所有微服务的统一入口。</p><p>网关的<strong>核心功能特性</strong>：</p><ul><li>请求路由</li><li>权限控制</li><li>限流</li></ul><p>架构图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714210131152.png" alt="image-20210714210131152"></p><p><strong>权限控制</strong>：网关作为微服务入口，需要校验用户是是否有请求资格，如果没有则进行拦截。</p><p><strong>路由和负载均衡</strong>：一切请求都必须先经过gateway，但网关不处理业务，而是根据某种规则，把请求转发到某个微服务，这个过程叫做路由。当然路由的目标服务有多个时，还需要做负载均衡。</p><p><strong>限流</strong>：当请求流量过高时，在网关中按照下流的微服务能够接受的速度来放行请求，避免服务压力过大。</p><p>在SpringCloud中网关的实现包括两种：</p><ul><li>gateway</li><li>zuul</li></ul><p>Zuul是基于Servlet的实现，属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux，属于响应式编程的实现，具备更好的性能。</p><h2 id="3-2-gateway快速入门"><a href="#3-2-gateway快速入门" class="headerlink" title="3.2.gateway快速入门"></a>3.2.gateway快速入门</h2><p>下面，我们就演示下网关的基本路由功能。基本步骤如下：</p><ol><li>创建SpringBoot工程gateway，引入网关依赖</li><li>编写启动类</li><li>编写基础配置和路由规则</li><li>启动网关服务进行测试</li></ol><h3 id="1）创建gateway服务，引入依赖"><a href="#1）创建gateway服务，引入依赖" class="headerlink" title="1）创建gateway服务，引入依赖"></a>1）创建gateway服务，引入依赖</h3><p>创建服务：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714210919458.png" alt="image-20210714210919458"></p><p>引入依赖：</p><figure class="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></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">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<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-cloud-starter-gateway<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="comment">&lt;!--nacos服务发现依赖--&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.alibaba.cloud<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-cloud-starter-alibaba-nacos-discovery<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></pre></td></tr></table></figure><h3 id="2）编写启动类"><a href="#2）编写启动类" class="headerlink" title="2）编写启动类"></a>2）编写启动类</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.gateway;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"></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_">GatewayApplication</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">SpringApplication.run(GatewayApplication.class, args);</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3）编写基础配置和路由规则"><a href="#3）编写基础配置和路由规则" class="headerlink" title="3）编写基础配置和路由规则"></a>3）编写基础配置和路由规则</h3><p>创建application.yml文件，内容如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">10010</span> <span class="comment"># 网关端口</span></span><br><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">gateway</span> <span class="comment"># 服务名称</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span> <span class="comment"># nacos地址</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span> <span class="comment"># 网关路由配置</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span> <span class="comment"># 路由id，自定义，只要唯一即可</span></span><br><span class="line">          <span class="comment"># uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://userservice</span> <span class="comment"># 路由的目标地址 lb就是负载均衡，后面跟服务名称</span></span><br><span class="line">          <span class="attr">predicates:</span> <span class="comment"># 路由断言，也就是判断请求是否符合路由规则的条件</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/user/**</span> <span class="comment"># 这个是按照路径匹配，只要以/user/开头就符合要求</span></span><br></pre></td></tr></table></figure><p>我们将符合<code>Path</code> 规则的一切请求，都代理到 <code>uri</code>参数指定的地址。</p><p>本例中，我们将 <code>/user/**</code>开头的请求，代理到<code>lb://userservice</code>，lb是负载均衡，根据服务名拉取服务列表，实现负载均衡。</p><h3 id="4）重启测试"><a href="#4）重启测试" class="headerlink" title="4）重启测试"></a>4）重启测试</h3><p>重启网关，访问<a href="http://localhost:10010/user/1时，符合`/user/**`规则，请求转发到uri：http://userservice/user/1，得到了结果：">http://localhost:10010/user/1时，符合`/user/**`规则，请求转发到uri：http://userservice/user/1，得到了结果：</a></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714211908341.png" alt="image-20210714211908341"></p><h3 id="5）网关路由的流程图"><a href="#5）网关路由的流程图" class="headerlink" title="5）网关路由的流程图"></a>5）网关路由的流程图</h3><p>整个访问的流程如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714211742956.png" alt="image-20210714211742956"></p><p>总结：</p><p>网关搭建步骤：</p><ol><li><p>创建项目，引入nacos服务发现和gateway依赖</p></li><li><p>配置application.yml，包括服务基本信息、nacos地址、路由</p></li></ol><p>路由配置包括：</p><ol><li><p>路由id：路由的唯一标示</p></li><li><p>路由目标（uri）：路由的目标地址，http代表固定地址，lb代表根据服务名负载均衡</p></li><li><p>路由断言（predicates）：判断路由的规则，</p></li><li><p>路由过滤器（filters）：对请求或响应做处理</p></li></ol><p>接下来，就重点来学习路由断言和路由过滤器的详细知识</p><h2 id="3-3-断言工厂"><a href="#3-3-断言工厂" class="headerlink" title="3.3.断言工厂"></a>3.3.断言工厂</h2><p>我们在配置文件中写的断言规则只是字符串，这些字符串会被Predicate Factory读取并处理，转变为路由判断的条件</p><p>例如Path=/user/**是按照路径匹配，这个规则是由</p><p><code>org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory</code>类来</p><p>处理的，像这样的断言工厂在SpringCloudGateway还有十几个:</p><div class="table-container"><table><thead><tr><th><strong>名称</strong></th><th><strong>说明</strong></th><th><strong>示例</strong></th></tr></thead><tbody><tr><td>After</td><td>是某个时间点后的请求</td><td>-  After=2037-01-20T17:42:47.789-07:00[America/Denver]</td></tr><tr><td>Before</td><td>是某个时间点之前的请求</td><td>-  Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]</td></tr><tr><td>Between</td><td>是某两个时间点之前的请求</td><td>-  Between=2037-01-20T17:42:47.789-07:00[America/Denver],  2037-01-21T17:42:47.789-07:00[America/Denver]</td></tr><tr><td>Cookie</td><td>请求必须包含某些cookie</td><td>- Cookie=chocolate, ch.p</td></tr><tr><td>Header</td><td>请求必须包含某些header</td><td>- Header=X-Request-Id, \d+</td></tr><tr><td>Host</td><td>请求必须是访问某个host（域名）</td><td>-  Host=<strong>.somehost.org,</strong>.anotherhost.org</td></tr><tr><td>Method</td><td>请求方式必须是指定方式</td><td>- Method=GET,POST</td></tr><tr><td>Path</td><td>请求路径必须符合指定规则</td><td>- Path=/red/{segment},/blue/**</td></tr><tr><td>Query</td><td>请求参数必须包含指定参数</td><td>- Query=name, Jack或者-  Query=name</td></tr><tr><td>RemoteAddr</td><td>请求者的ip必须是指定范围</td><td>- RemoteAddr=192.168.1.1/24</td></tr><tr><td>Weight</td><td>权重处理</td></tr></tbody></table></div><p>我们只需要掌握Path这种路由工程就可以了。</p><h2 id="3-4-过滤器工厂"><a href="#3-4-过滤器工厂" class="headerlink" title="3.4.过滤器工厂"></a>3.4.过滤器工厂</h2><p>GatewayFilter是网关中提供的一种过滤器，可以对进入网关的请求和微服务返回的响应做处理：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714212312871.png" alt="image-20210714212312871"></p><h3 id="3-4-1-路由过滤器的种类"><a href="#3-4-1-路由过滤器的种类" class="headerlink" title="3.4.1.路由过滤器的种类"></a>3.4.1.路由过滤器的种类</h3><p>Spring提供了31种不同的路由过滤器工厂。例如：</p><div class="table-container"><table><thead><tr><th><strong>名称</strong></th><th><strong>说明</strong></th></tr></thead><tbody><tr><td>AddRequestHeader</td><td>给当前请求添加一个请求头</td></tr><tr><td>RemoveRequestHeader</td><td>移除请求中的一个请求头</td></tr><tr><td>AddResponseHeader</td><td>给响应结果中添加一个响应头</td></tr><tr><td>RemoveResponseHeader</td><td>从响应结果中移除有一个响应头</td></tr><tr><td>RequestRateLimiter</td><td>限制请求的流量</td></tr></tbody></table></div><h3 id="3-4-2-请求头过滤器"><a href="#3-4-2-请求头过滤器" class="headerlink" title="3.4.2.请求头过滤器"></a>3.4.2.请求头过滤器</h3><p>下面我们以AddRequestHeader 为例来讲解。</p><blockquote><p><strong>需求</strong>：给所有进入userservice的请求添加一个请求头：Truth=itcast is freaking awesome!</p></blockquote><p>只需要修改gateway服务的application.yml文件，添加路由过滤即可：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span> </span><br><span class="line">        <span class="attr">uri:</span> <span class="string">lb://userservice</span> </span><br><span class="line">        <span class="attr">predicates:</span> </span><br><span class="line">        <span class="bullet">-</span> <span class="string">Path=/user/**</span> </span><br><span class="line">        <span class="attr">filters:</span> <span class="comment"># 过滤器</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">AddRequestHeader=Truth,</span> <span class="string">Itcast</span> <span class="string">is</span> <span class="string">freaking</span> <span class="string">awesome!</span> <span class="comment"># 添加请求头</span></span><br></pre></td></tr></table></figure><p>当前过滤器写在userservice路由下，因此仅仅对访问userservice的请求有效。</p><h3 id="3-4-3-默认过滤器"><a href="#3-4-3-默认过滤器" class="headerlink" title="3.4.3.默认过滤器"></a>3.4.3.默认过滤器</h3><p>如果要对所有的路由都生效，则可以将过滤器工厂写到default下。格式如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span> </span><br><span class="line">        <span class="attr">uri:</span> <span class="string">lb://userservice</span> </span><br><span class="line">        <span class="attr">predicates:</span> </span><br><span class="line">        <span class="bullet">-</span> <span class="string">Path=/user/**</span></span><br><span class="line">      <span class="attr">default-filters:</span> <span class="comment"># 默认过滤项</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">AddRequestHeader=Truth,</span> <span class="string">Itcast</span> <span class="string">is</span> <span class="string">freaking</span> <span class="string">awesome!</span> </span><br></pre></td></tr></table></figure><h3 id="3-4-4-总结"><a href="#3-4-4-总结" class="headerlink" title="3.4.4.总结"></a>3.4.4.总结</h3><p>过滤器的作用是什么？</p><p>① 对路由的请求或响应做加工处理，比如添加请求头</p><p>② 配置在路由下的过滤器只对当前路由的请求生效</p><p>defaultFilters的作用是什么？</p><p>① 对所有路由都生效的过滤器</p><h2 id="3-5-全局过滤器"><a href="#3-5-全局过滤器" class="headerlink" title="3.5.全局过滤器"></a>3.5.全局过滤器</h2><p>上一节学习的过滤器，网关提供了31种，但每一种过滤器的作用都是固定的。如果我们希望拦截请求，做自己的业务逻辑则没办法实现。</p><h3 id="3-5-1-全局过滤器作用"><a href="#3-5-1-全局过滤器作用" class="headerlink" title="3.5.1.全局过滤器作用"></a>3.5.1.全局过滤器作用</h3><p>全局过滤器的作用也是处理一切进入网关的请求和微服务响应，与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义，处理逻辑是固定的；而GlobalFilter的逻辑需要自己写代码实现。</p><p>定义方式是实现GlobalFilter接口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">GlobalFilter</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     *  处理当前请求，有必要的话通过&#123;<span class="doctag">@link</span> GatewayFilterChain&#125;将请求交给下一个过滤器处理</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> exchange 请求上下文，里面可以获取Request、Response等信息</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> chain 用来把请求委托给下一个过滤器 </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> &#123;<span class="doctag">@code</span> Mono&lt;Void&gt;&#125; 返回标示当前过滤器业务结束</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在filter中编写自定义逻辑，可以实现下列功能：</p><ul><li>登录状态判断</li><li>权限校验</li><li>请求限流等</li></ul><h3 id="3-5-2-自定义全局过滤器"><a href="#3-5-2-自定义全局过滤器" class="headerlink" title="3.5.2.自定义全局过滤器"></a>3.5.2.自定义全局过滤器</h3><p>需求：定义全局过滤器，拦截请求，判断请求的参数是否满足下面条件：</p><ul><li><p>参数中是否有authorization，</p></li><li><p>authorization参数值是否为admin</p></li></ul><p>如果同时满足则放行，否则拦截</p><p>实现：</p><p>在gateway中定义一个过滤器：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.gateway.filters;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.gateway.filter.GatewayFilterChain;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.gateway.filter.GlobalFilter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.core.annotation.Order;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.HttpStatus;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.server.ServerWebExchange;</span><br><span class="line"><span class="keyword">import</span> reactor.core.publisher.Mono;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Order(-1)</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_">AuthorizeFilter</span> <span class="keyword">implements</span> <span class="title class_">GlobalFilter</span> &#123;</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, GatewayFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取请求参数</span></span><br><span class="line">        MultiValueMap&lt;String, String&gt; params = exchange.getRequest().getQueryParams();</span><br><span class="line">        <span class="comment">// 2.获取authorization参数</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">auth</span> <span class="operator">=</span> params.getFirst(<span class="string">&quot;authorization&quot;</span>);</span><br><span class="line">        <span class="comment">// 3.校验</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;admin&quot;</span>.equals(auth)) &#123;</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">        <span class="comment">// 4.拦截</span></span><br><span class="line">        <span class="comment">// 4.1.禁止访问，设置状态码</span></span><br><span class="line">        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);</span><br><span class="line">        <span class="comment">// 4.2.结束处理</span></span><br><span class="line">        <span class="keyword">return</span> exchange.getResponse().setComplete();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-5-3-过滤器执行顺序"><a href="#3-5-3-过滤器执行顺序" class="headerlink" title="3.5.3.过滤器执行顺序"></a>3.5.3.过滤器执行顺序</h3><p>请求进入网关会碰到三类过滤器：当前路由的过滤器、DefaultFilter、GlobalFilter</p><p>请求路由后，会将当前路由过滤器和DefaultFilter、GlobalFilter，合并到一个过滤器链（集合）中，排序后依次执行每个过滤器：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714214228409.png" alt="image-20210714214228409"></p><p>排序的规则是什么呢？</p><ul><li>每一个过滤器都必须指定一个int类型的order值，<strong>order值越小，优先级越高，执行顺序越靠前</strong>。</li><li>GlobalFilter通过实现Ordered接口，或者添加@Order注解来指定order值，由我们自己指定</li><li>路由过滤器和defaultFilter的order由Spring指定，默认是按照声明顺序从1递增。</li><li>当过滤器的order值一样时，会按照 defaultFilter &gt; 路由过滤器 &gt; GlobalFilter的顺序执行。</li></ul><p>详细内容，可以查看源码：</p><p><code>org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()</code>方法是先加载defaultFilters，然后再加载某个route的filters，然后合并。</p><p><code>org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()</code>方法会加载全局过滤器，与前面的过滤器合并后根据order排序，组织过滤器链</p><h2 id="3-6-跨域问题"><a href="#3-6-跨域问题" class="headerlink" title="3.6.跨域问题"></a>3.6.跨域问题</h2><h3 id="3-6-1-什么是跨域问题"><a href="#3-6-1-什么是跨域问题" class="headerlink" title="3.6.1.什么是跨域问题"></a>3.6.1.什么是跨域问题</h3><p>跨域：域名不一致就是跨域，主要包括：</p><ul><li><p>域名不同： www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com</p></li><li><p>域名相同，端口不同：localhost:8080和localhost8081</p></li></ul><p>跨域问题：浏览器禁止请求的发起者与服务端发生跨域ajax请求，请求被浏览器拦截的问题</p><p>解决方案：CORS，这个以前应该学习过，这里不再赘述了。不知道的小伙伴可以查看<a href="https://www.ruanyifeng.com/blog/2016/04/cors.html">https://www.ruanyifeng.com/blog/2016/04/cors.html</a></p><h3 id="3-6-2-模拟跨域问题"><a href="#3-6-2-模拟跨域问题" class="headerlink" title="3.6.2.模拟跨域问题"></a>3.6.2.模拟跨域问题</h3><p>找到课前资料的页面文件：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714215713563.png" alt="image-20210714215713563"></p><p>放入tomcat或者nginx这样的web服务器中，启动并访问。</p><p>可以在浏览器控制台看到下面的错误：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2024/4/image-20210714215832675.png" alt="image-20210714215832675"></p><p>从localhost:8090访问localhost:10010，端口不同，显然是跨域的请求。</p><h3 id="3-6-3-解决跨域问题"><a href="#3-6-3-解决跨域问题" class="headerlink" title="3.6.3.解决跨域问题"></a>3.6.3.解决跨域问题</h3><p>在gateway服务的application.yml文件中，添加下面的配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="comment"># 。。。</span></span><br><span class="line">      <span class="attr">globalcors:</span> <span class="comment"># 全局的跨域处理</span></span><br><span class="line">        <span class="attr">add-to-simple-url-handler-mapping:</span> <span class="literal">true</span> <span class="comment"># 解决options请求被拦截问题</span></span><br><span class="line">        <span class="attr">corsConfigurations:</span></span><br><span class="line">          <span class="string">&#x27;[/**]&#x27;</span><span class="string">:</span></span><br><span class="line">            <span class="attr">allowedOrigins:</span> <span class="comment"># 允许哪些网站的跨域请求 </span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;http://localhost:8090&quot;</span></span><br><span class="line">            <span class="attr">allowedMethods:</span> <span class="comment"># 允许的跨域ajax的请求方式</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;GET&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;POST&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;DELETE&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;PUT&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;OPTIONS&quot;</span></span><br><span class="line">            <span class="attr">allowedHeaders:</span> <span class="string">&quot;*&quot;</span> <span class="comment"># 允许在请求中携带的头信息</span></span><br><span class="line">            <span class="attr">allowCredentials:</span> <span class="literal">true</span> <span class="comment"># 是否允许携带cookie</span></span><br><span class="line">            <span class="attr">maxAge:</span> <span class="number">360000</span> <span class="comment"># 这次跨域检测的有效期</span></span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;SpringCloud02&quot;&gt;&lt;a href=&quot;#SpringCloud02&quot; class=&quot;headerlink&quot; title=&quot;SpringCloud02&quot;&gt;&lt;/a&gt;SpringCloud02&lt;/h1&gt;&lt;h1 id=&quot;1-Nacos配置管理&quot;&gt;&lt;a href=</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    
    <category term="微服务" scheme="https://www.shallowrecall.top/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>SpringCloud01</title>
    <link href="https://www.shallowrecall.top/posts/d65dee17.html"/>
    <id>https://www.shallowrecall.top/posts/d65dee17.html</id>
    <published>2024-04-22T12:20:06.000Z</published>
    <updated>2026-10-27T18:22:13.998Z</updated>
    
    <content type="html"><![CDATA[<h1 id="SpringCloud01"><a href="#SpringCloud01" class="headerlink" title="SpringCloud01"></a>SpringCloud01</h1><h1 id="1-认识微服务"><a href="#1-认识微服务" class="headerlink" title="1.认识微服务"></a>1.认识微服务</h1><p>随着互联网行业的发展，对服务的要求也越来越高，服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢？</p><h2 id="1-0-学习目标"><a href="#1-0-学习目标" class="headerlink" title="1.0.学习目标"></a>1.0.学习目标</h2><p>了解微服务架构的优缺点</p><h2 id="1-1-单体架构"><a href="#1-1-单体架构" class="headerlink" title="1.1.单体架构"></a>1.1.单体架构</h2><p><strong>单体架构</strong>：将业务的所有功能集中在一个项目中开发，打成一个包部署。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713202807818.png" alt="image-20210713202807818"></p><p>单体架构的优缺点如下：</p><p><strong>优点：</strong></p><ul><li>架构简单</li><li>部署成本低</li></ul><p><strong>缺点：</strong></p><ul><li>耦合度高（维护困难、升级困难）</li></ul><h2 id="1-2-分布式架构"><a href="#1-2-分布式架构" class="headerlink" title="1.2.分布式架构"></a>1.2.分布式架构</h2><p><strong>分布式架构</strong>：根据业务功能对系统做拆分，每个业务功能模块作为独立项目开发，称为一个服务。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713203124797.png" alt="image-20210713203124797"></p><p>分布式架构的优缺点：</p><p><strong>优点：</strong></p><ul><li>降低服务耦合</li><li>有利于服务升级和拓展</li></ul><p><strong>缺点：</strong></p><ul><li>服务调用关系错综复杂</li></ul><p>分布式架构虽然降低了服务耦合，但是服务拆分时也有很多问题需要思考：</p><ul><li>服务拆分的粒度如何界定？</li><li>服务之间如何调用？</li><li>服务的调用关系如何管理？</li></ul><p>人们需要制定一套行之有效的标准来约束分布式架构。</p><h2 id="1-3-微服务"><a href="#1-3-微服务" class="headerlink" title="1.3.微服务"></a>1.3.微服务</h2><p>微服务的架构特征：</p><ul><li>单一职责：微服务拆分粒度更小，每一个服务都对应唯一的业务能力，做到单一职责</li><li>自治：团队独立、技术独立、数据独立，独立部署和交付</li><li>面向服务：服务提供统一标准的接口，与语言和技术无关</li><li>隔离性强：服务调用做好隔离、容错、降级，避免出现级联问题</li></ul><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713203753373.png" alt="image-20210713203753373"></p><p>微服务的上述特性其实是在给分布式架构制定一个标准，进一步降低服务之间的耦合度，提供服务的独立性和灵活性。做到高内聚，低耦合。</p><p>因此，可以认为<strong>微服务</strong>是一种经过良好架构设计的<strong>分布式架构方案</strong> 。</p><p>但方案该怎么落地？选用什么样的技术栈？全球的互联网公司都在积极尝试自己的微服务落地方案。</p><p>其中在Java领域最引人注目的就是SpringCloud提供的方案了。</p><h2 id="1-4-SpringCloud"><a href="#1-4-SpringCloud" class="headerlink" title="1.4.SpringCloud"></a>1.4.SpringCloud</h2><p>SpringCloud是目前国内使用最广泛的微服务框架。官网地址：<a href="https://spring.io/projects/spring-cloud。">https://spring.io/projects/spring-cloud。</a></p><p>SpringCloud集成了各种微服务功能组件，并基于SpringBoot实现了这些组件的自动装配，从而提供了良好的开箱即用体验。</p><p>其中常见的组件包括：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713204155887.png" alt="image-20210713204155887"></p><p>另外，SpringCloud底层是依赖于SpringBoot的，并且有版本的兼容关系，如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713205003790.png" alt="image-20210713205003790"></p><p>我们课堂学习的版本是 Hoxton.SR10，因此对应的SpringBoot版本是2.3.x版本。</p><h2 id="1-5-总结"><a href="#1-5-总结" class="headerlink" title="1.5.总结"></a>1.5.总结</h2><ul><li><p>单体架构：简单方便，高度耦合，扩展性差，适合小型项目。例如：学生管理系统</p></li><li><p>分布式架构：松耦合，扩展性好，但架构复杂，难度大。适合大型互联网项目，例如：京东、淘宝</p></li><li><p>微服务：一种良好的分布式架构方案</p><p>①优点：拆分粒度更小、服务更独立、耦合度更低</p><p>②缺点：架构非常复杂，运维、监控、部署难度提高</p></li><li><p>SpringCloud是微服务架构的一站式解决方案，集成了各种优秀微服务功能组件</p></li></ul><h1 id="2-服务拆分和远程调用"><a href="#2-服务拆分和远程调用" class="headerlink" title="2.服务拆分和远程调用"></a>2.服务拆分和远程调用</h1><p>任何分布式架构都离不开服务的拆分，微服务也是一样。</p><h2 id="2-1-服务拆分原则"><a href="#2-1-服务拆分原则" class="headerlink" title="2.1.服务拆分原则"></a>2.1.服务拆分原则</h2><p>这里我总结了微服务拆分时的几个原则：</p><ul><li>不同微服务，不要重复开发相同业务</li><li>微服务数据独立，不要访问其它微服务的数据库</li><li>微服务可以将自己的业务暴露为接口，供其它微服务调用</li></ul><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713210800950.png" alt="image-20210713210800950"></p><h2 id="2-2-服务拆分示例"><a href="#2-2-服务拆分示例" class="headerlink" title="2.2.服务拆分示例"></a>2.2.服务拆分示例</h2><p>以课前资料中的微服务cloud-demo为例，其结构如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713211009593.png" alt="image-20210713211009593"></p><p>cloud-demo：父工程，管理依赖</p><ul><li>order-service：订单微服务，负责订单相关业务</li><li>user-service：用户微服务，负责用户相关业务</li></ul><p>要求：</p><ul><li>订单微服务和用户微服务都必须有各自的数据库，相互独立</li><li>订单服务和用户服务都对外暴露Restful的接口</li><li>订单服务如果需要查询用户信息，只能调用用户服务的Restful接口，不能查询用户数据库</li></ul><h3 id="2-2-1-导入Sql语句"><a href="#2-2-1-导入Sql语句" class="headerlink" title="2.2.1.导入Sql语句"></a>2.2.1.导入Sql语句</h3><p>首先，将课前资料提供的<code>cloud-order.sql</code>和<code>cloud-user.sql</code>导入到mysql中：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713211417049.png" alt="image-20210713211417049"></p><p>cloud-user表中初始数据如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713211550169.png" alt="image-20210713211550169"></p><p>cloud-order表中初始数据如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713211657319.png" alt="image-20210713211657319"></p><p>cloud-order表中持有cloud-user表中的id字段。</p><h3 id="2-2-2-导入demo工程"><a href="#2-2-2-导入demo工程" class="headerlink" title="2.2.2.导入demo工程"></a>2.2.2.导入demo工程</h3><p>用IDEA导入课前资料提供的Demo：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713211814094.png" alt="image-20210713211814094"></p><p>项目结构如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212656887.png" alt="image-20210713212656887"></p><p>导入后，会在IDEA右下角出现弹窗：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212349272.png" alt="image-20210713212349272"></p><p>点击弹窗，然后按下图选择：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212336185.png" alt="image-20210713212336185"></p><p>会出现这样的菜单：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212513324.png" alt="image-20210713212513324"></p><p>配置下项目使用的JDK：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713220736408.png" alt="image-20210713220736408"></p><h2 id="2-3-实现远程调用案例"><a href="#2-3-实现远程调用案例" class="headerlink" title="2.3.实现远程调用案例"></a>2.3.实现远程调用案例</h2><p>在order-service服务中，有一个根据id查询订单的接口：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212749575.png" alt="image-20210713212749575"></p><p>根据id查询订单，返回值是Order对象，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713212901725.png" alt="image-20210713212901725"></p><p>其中的user为null</p><p>在user-service中有一个根据id查询用户的接口：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713213146089.png" alt="image-20210713213146089"></p><p>查询的结果如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713213213075.png" alt="image-20210713213213075"></p><h3 id="2-3-1-案例需求："><a href="#2-3-1-案例需求：" class="headerlink" title="2.3.1.案例需求："></a>2.3.1.案例需求：</h3><p>修改order-service中的根据id查询订单业务，要求在查询订单的同时，根据订单中包含的userId查询出用户信息，一起返回。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713213312278.png" alt="image-20210713213312278"></p><p>因此，我们需要在order-service中 向user-service发起一个http的请求，调用<a href="http://localhost:8081/user/{userId}这个接口。">http://localhost:8081/user/{userId}这个接口。</a></p><p>大概的步骤是这样的：</p><ul><li>注册一个RestTemplate的实例到Spring容器</li><li>修改order-service服务中的OrderService类中的queryOrderById方法，根据Order对象中的userId查询User</li><li>将查询的User填充到Order对象，一起返回</li></ul><h3 id="2-3-2-注册RestTemplate"><a href="#2-3-2-注册RestTemplate" class="headerlink" title="2.3.2.注册RestTemplate"></a>2.3.2.注册RestTemplate</h3><p>首先，我们在order-service服务中的OrderApplication启动类中，注册RestTemplate实例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.mybatis.spring.annotation.MapperScan;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@MapperScan(&quot;cn.itcast.order.mapper&quot;)</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_">OrderApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(OrderApplication.class, args);</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> RestTemplate <span class="title function_">restTemplate</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_">RestTemplate</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-3-3-实现远程调用"><a href="#2-3-3-实现远程调用" class="headerlink" title="2.3.3.实现远程调用"></a>2.3.3.实现远程调用</h3><p>修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713213959569.png" alt="image-20210713213959569"></p><h2 id="2-4-提供者与消费者"><a href="#2-4-提供者与消费者" class="headerlink" title="2.4.提供者与消费者"></a>2.4.提供者与消费者</h2><p>在服务调用关系中，会有两个不同的角色：</p><p><strong>服务提供者</strong>：一次业务中，被其它微服务调用的服务。（提供接口给其它微服务）</p><p><strong>服务消费者</strong>：一次业务中，调用其它微服务的服务。（调用其它微服务提供的接口）</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713214404481.png" alt="image-20210713214404481"></p><p>但是，服务提供者与服务消费者的角色并不是绝对的，而是相对于业务而言。</p><p>如果服务A调用了服务B，而服务B又调用了服务C，服务B的角色是什么？</p><ul><li>对于A调用B的业务而言：A是服务消费者，B是服务提供者</li><li>对于B调用C的业务而言：B是服务消费者，C是服务提供者</li></ul><p>因此，服务B既可以是服务提供者，也可以是服务消费者。</p><h1 id="3-Eureka注册中心"><a href="#3-Eureka注册中心" class="headerlink" title="3.Eureka注册中心"></a>3.Eureka注册中心</h1><p>假如我们的服务提供者user-service部署了多个实例，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713214925388.png" alt="image-20210713214925388"></p><p>大家思考几个问题：</p><ul><li>order-service在发起远程调用的时候，该如何得知user-service实例的ip地址和端口？</li><li>有多个user-service实例地址，order-service调用时该如何选择？</li><li>order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？</li></ul><h2 id="3-1-Eureka的结构和作用"><a href="#3-1-Eureka的结构和作用" class="headerlink" title="3.1.Eureka的结构和作用"></a>3.1.Eureka的结构和作用</h2><p>这些问题都需要利用SpringCloud中的注册中心来解决，其中最广为人知的注册中心就是Eureka，其结构如下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713220104956.png" alt="image-20210713220104956"></p><p>回答之前的各个问题。</p><p>问题1：order-service如何得知user-service实例地址？</p><p>获取地址信息的流程如下：</p><ul><li>user-service服务实例启动后，将自己的信息注册到eureka-server（Eureka服务端）。这个叫服务注册</li><li>eureka-server保存服务名称到服务实例地址列表的映射关系</li><li>order-service根据服务名称，拉取实例地址列表。这个叫服务发现或服务拉取</li></ul><p>问题2：order-service如何从多个user-service实例中选择具体的实例？</p><ul><li>order-service从实例列表中利用负载均衡算法选中一个实例地址</li><li>向该实例地址发起远程调用</li></ul><p>问题3：order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？</p><ul><li>user-service会每隔一段时间（默认30秒）向eureka-server发起请求，报告自己状态，称为心跳</li><li>当超过一定时间没有发送心跳时，eureka-server会认为微服务实例故障，将该实例从服务列表中剔除</li><li>order-service拉取服务时，就能将故障实例排除了</li></ul><blockquote><p>注意：一个微服务，既可以是服务提供者，又可以是服务消费者，因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端</p></blockquote><p>因此，接下来我们动手实践的步骤包括：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713220509769.png" alt="image-20210713220509769"></p><h2 id="3-2-搭建eureka-server"><a href="#3-2-搭建eureka-server" class="headerlink" title="3.2.搭建eureka-server"></a>3.2.搭建eureka-server</h2><p>首先大家注册中心服务端：eureka-server，这必须是一个独立的微服务</p><h3 id="3-2-1-创建eureka-server服务"><a href="#3-2-1-创建eureka-server服务" class="headerlink" title="3.2.1.创建eureka-server服务"></a>3.2.1.创建eureka-server服务</h3><p>在cloud-demo父工程下，创建一个子模块：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713220605881.png" alt="image-20210713220605881"></p><p>填写模块信息：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713220857396.png" alt="image-20210713220857396"></p><p>然后填写服务信息：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713221339022.png" alt="image-20210713221339022"></p><h3 id="3-2-2-引入eureka依赖"><a href="#3-2-2-引入eureka依赖" class="headerlink" title="3.2.2.引入eureka依赖"></a>3.2.2.引入eureka依赖</h3><p>引入SpringCloud为eureka提供的starter依赖：</p><figure class="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="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.cloud<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-cloud-starter-netflix-eureka-server<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></pre></td></tr></table></figure><h3 id="3-2-3-编写启动类"><a href="#3-2-3-编写启动类" class="headerlink" title="3.2.3.编写启动类"></a>3.2.3.编写启动类</h3><p>给eureka-server服务编写一个启动类，一定要添加一个@EnableEurekaServer注解，开启eureka的注册中心功能：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.eureka;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableEurekaServer</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EurekaApplication</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(EurekaApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-2-4-编写配置文件"><a href="#3-2-4-编写配置文件" class="headerlink" title="3.2.4.编写配置文件"></a>3.2.4.编写配置文件</h3><p>编写一个application.yml文件，内容如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">10086</span></span><br><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">eureka-server</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">service-url:</span> </span><br><span class="line">      <span class="attr">defaultZone:</span> <span class="string">http://127.0.0.1:10086/eureka</span></span><br></pre></td></tr></table></figure><h3 id="3-2-5-启动服务"><a href="#3-2-5-启动服务" class="headerlink" title="3.2.5.启动服务"></a>3.2.5.启动服务</h3><p>启动微服务，然后在浏览器访问：<a href="http://127.0.0.1:10086">http://127.0.0.1:10086</a></p><p>看到下面结果应该是成功了：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713222157190.png" alt="image-20210713222157190"></p><h2 id="3-3-服务注册"><a href="#3-3-服务注册" class="headerlink" title="3.3.服务注册"></a>3.3.服务注册</h2><p>下面，我们将user-service注册到eureka-server中去。</p><h3 id="1）引入依赖"><a href="#1）引入依赖" class="headerlink" title="1）引入依赖"></a>1）引入依赖</h3><p>在user-service的pom文件中，引入下面的eureka-client依赖：</p><figure class="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="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.cloud<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-cloud-starter-netflix-eureka-client<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></pre></td></tr></table></figure><h3 id="2）配置文件"><a href="#2）配置文件" class="headerlink" title="2）配置文件"></a>2）配置文件</h3><p>在user-service中，修改application.yml文件，添加服务名称、eureka地址：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">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">userservice</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">service-url:</span></span><br><span class="line">      <span class="attr">defaultZone:</span> <span class="string">http://127.0.0.1:10086/eureka</span></span><br></pre></td></tr></table></figure><h3 id="3）启动多个user-service实例"><a href="#3）启动多个user-service实例" class="headerlink" title="3）启动多个user-service实例"></a>3）启动多个user-service实例</h3><p>为了演示一个服务有多个实例的场景，我们添加一个SpringBoot的启动配置，再启动一个user-service。</p><p>首先，复制原来的user-service启动配置：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713222656562.png" alt="image-20210713222656562"></p><p>然后，在弹出的窗口中，填写信息：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713222757702.png" alt="image-20210713222757702"></p><p>现在，SpringBoot窗口会出现两个user-service启动配置：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713222841951.png" alt="image-20210713222841951"></p><p>不过，第一个是8081端口，第二个是8082端口。</p><p>启动两个user-service实例：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713223041491.png" alt="image-20210713223041491"></p><p>查看eureka-server管理页面：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713223150650.png" alt="image-20210713223150650"></p><h2 id="3-4-服务发现"><a href="#3-4-服务发现" class="headerlink" title="3.4.服务发现"></a>3.4.服务发现</h2><p>下面，我们将order-service的逻辑修改：向eureka-server拉取user-service的信息，实现服务发现。</p><h3 id="1）引入依赖-1"><a href="#1）引入依赖-1" class="headerlink" title="1）引入依赖"></a>1）引入依赖</h3><p>之前说过，服务发现、服务注册统一都封装在eureka-client依赖，因此这一步与服务注册时一致。</p><p>在order-service的pom文件中，引入下面的eureka-client依赖：</p><figure class="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="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.cloud<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-cloud-starter-netflix-eureka-client<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></pre></td></tr></table></figure><h3 id="2）配置文件-1"><a href="#2）配置文件-1" class="headerlink" title="2）配置文件"></a>2）配置文件</h3><p>服务发现也需要知道eureka地址，因此第二步与服务注册一致，都是配置eureka信息：</p><p>在order-service中，修改application.yml文件，添加服务名称、eureka地址：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">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">orderservice</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">service-url:</span></span><br><span class="line">      <span class="attr">defaultZone:</span> <span class="string">http://127.0.0.1:10086/eureka</span></span><br></pre></td></tr></table></figure><h3 id="3）服务拉取和负载均衡"><a href="#3）服务拉取和负载均衡" class="headerlink" title="3）服务拉取和负载均衡"></a>3）服务拉取和负载均衡</h3><p>最后，我们要去eureka-server中拉取user-service服务的实例列表，并且实现负载均衡。</p><p>不过这些动作不用我们去做，只需要添加一些注解即可。</p><p>在order-service的OrderApplication中，给RestTemplate这个Bean添加一个@LoadBalanced注解：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713224049419.png" alt="image-20210713224049419"></p><p>修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径，用服务名代替ip、端口：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713224245731.png" alt="image-20210713224245731"></p><p>spring会自动帮助我们从eureka-server端，根据userservice这个服务名称，获取实例列表，而后完成负载均衡。</p><h1 id="4-Ribbon负载均衡"><a href="#4-Ribbon负载均衡" class="headerlink" title="4.Ribbon负载均衡"></a>4.Ribbon负载均衡</h1><p>上一节中，我们添加了@LoadBalanced注解，即可实现负载均衡功能，这是什么原理呢？</p><h2 id="4-1-负载均衡原理"><a href="#4-1-负载均衡原理" class="headerlink" title="4.1.负载均衡原理"></a>4.1.负载均衡原理</h2><p>SpringCloud底层其实是利用了一个名为Ribbon的组件，来实现负载均衡功能的。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713224517686.png" alt="image-20210713224517686"></p><p>那么我们发出的请求明明是<a href="http://userservice/user/1，怎么变成了http://localhost:8081的呢？">http://userservice/user/1，怎么变成了http://localhost:8081的呢？</a></p><h2 id="4-2-源码跟踪"><a href="#4-2-源码跟踪" class="headerlink" title="4.2.源码跟踪"></a>4.2.源码跟踪</h2><p>为什么我们只输入了service名称就可以访问了呢？之前还要获取ip和端口。</p><p>显然有人帮我们根据service名称，获取到了服务实例的ip和端口。它就是<code>LoadBalancerInterceptor</code>，这个类会在对RestTemplate的请求进行拦截，然后从Eureka根据服务id获取服务列表，随后利用负载均衡算法得到真实的服务地址信息，替换服务id。</p><p>我们进行源码跟踪：</p><h3 id="1）LoadBalancerIntercepor"><a href="#1）LoadBalancerIntercepor" class="headerlink" title="1）LoadBalancerIntercepor"></a>1）LoadBalancerIntercepor</h3><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525620483637.png" alt="1525620483637"></p><p>可以看到这里的intercept方法，拦截了用户的HttpRequest请求，然后做了几件事：</p><ul><li><code>request.getURI()</code>：获取请求uri，本例中就是 <a href="http://user-service/user/8">http://user-service/user/8</a></li><li><code>originalUri.getHost()</code>：获取uri路径的主机名，其实就是服务id，<code>user-service</code></li><li><code>this.loadBalancer.execute()</code>：处理服务id，和用户请求。</li></ul><p>这里的<code>this.loadBalancer</code>是<code>LoadBalancerClient</code>类型，我们继续跟入。</p><h3 id="2）LoadBalancerClient"><a href="#2）LoadBalancerClient" class="headerlink" title="2）LoadBalancerClient"></a>2）LoadBalancerClient</h3><p>继续跟入execute方法：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525620787090.png" alt="1525620787090"></p><p>代码是这样的：</p><ul><li>getLoadBalancer(serviceId)：根据服务id获取ILoadBalancer，而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。</li><li>getServer(loadBalancer)：利用内置的负载均衡算法，从服务列表中选择一个。本例中，可以看到获取了8082端口的服务</li></ul><p>放行后，再次访问并跟踪，发现获取的是8081：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525620835911.png" alt="1525620835911"></p><p>果然实现了负载均衡。</p><h3 id="3）负载均衡策略IRule"><a href="#3）负载均衡策略IRule" class="headerlink" title="3）负载均衡策略IRule"></a>3）负载均衡策略IRule</h3><p>在刚才的代码中，可以看到获取服务使通过一个<code>getServer</code>方法来做负载均衡:</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525620835911.png" alt="1525620835911"></p><p>我们继续跟入：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1544361421671.png" alt="1544361421671"></p><p>继续跟踪源码chooseServer方法，发现这么一段代码：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525622652849.png" alt="1525622652849"></p><p>我们看看这个rule是谁：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525622699666.png" alt="1525622699666"></p><p>这里的rule默认值是一个<code>RoundRobinRule</code>，看类的介绍：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/1525622754316.png" alt="1525622754316"></p><p>这不就是轮询的意思嘛。</p><p>到这里，整个负载均衡的流程我们就清楚了。</p><h3 id="4）总结"><a href="#4）总结" class="headerlink" title="4）总结"></a>4）总结</h3><p>SpringCloudRibbon的底层采用了一个拦截器，拦截了RestTemplate发出的请求，对地址做了修改。用一幅图来总结一下：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713224724673.png" alt="image-20210713224724673"></p><p>基本流程如下：</p><ul><li>拦截我们的RestTemplate请求<a href="http://userservice/user/1">http://userservice/user/1</a></li><li>RibbonLoadBalancerClient会从请求url中获取服务名称，也就是user-service</li><li>DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表</li><li>eureka返回列表，localhost:8081、localhost:8082</li><li>IRule利用内置负载均衡规则，从列表中选择一个，例如localhost:8081</li><li>RibbonLoadBalancerClient修改请求地址，用localhost:8081替代userservice，得到<a href="http://localhost:8081/user/1，发起真实请求">http://localhost:8081/user/1，发起真实请求</a></li></ul><h2 id="4-3-负载均衡策略"><a href="#4-3-负载均衡策略" class="headerlink" title="4.3.负载均衡策略"></a>4.3.负载均衡策略</h2><h3 id="4-3-1-负载均衡策略"><a href="#4-3-1-负载均衡策略" class="headerlink" title="4.3.1.负载均衡策略"></a>4.3.1.负载均衡策略</h3><p>负载均衡的规则都定义在IRule接口中，而IRule有很多不同的实现类：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713225653000.png" alt="image-20210713225653000"></p><p>不同规则的含义如下：</p><div class="table-container"><table><thead><tr><th><strong>内置负载均衡规则类</strong></th><th><strong>规则描述</strong></th></tr></thead><tbody><tr><td>RoundRobinRule</td><td>简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。</td></tr><tr><td>AvailabilityFilteringRule</td><td>对以下两种服务器进行忽略：   （1）在默认情况下，这台服务器如果3次连接失败，这台服务器就会被设置为“短路”状态。短路状态将持续30秒，如果再次连接失败，短路的持续时间就会几何级地增加。  （2）并发数过高的服务器。如果一个服务器的并发连接数过高，配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限，可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。</td></tr><tr><td>WeightedResponseTimeRule</td><td>为每一个服务器赋予一个权重值。服务器响应时间越长，这个服务器的权重就越小。这个规则会随机选择服务器，这个权重值会影响服务器的选择。</td></tr><tr><td><strong>ZoneAvoidanceRule</strong></td><td>以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类，这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。</td></tr><tr><td>BestAvailableRule</td><td>忽略那些短路的服务器，并选择并发数较低的服务器。</td></tr><tr><td>RandomRule</td><td>随机选择一个可用的服务器。</td></tr><tr><td>RetryRule</td><td>重试机制的选择逻辑</td></tr></tbody></table></div><p>默认的实现就是ZoneAvoidanceRule，是一种轮询方案</p><h3 id="4-3-2-自定义负载均衡策略"><a href="#4-3-2-自定义负载均衡策略" class="headerlink" title="4.3.2.自定义负载均衡策略"></a>4.3.2.自定义负载均衡策略</h3><p>通过定义IRule实现可以修改负载均衡规则，有两种方式：</p><ol><li>代码方式：在order-service中的OrderApplication类中，定义一个新的IRule：</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> IRule <span class="title function_">randomRule</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_">RandomRule</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>配置文件方式：在order-service的application.yml文件中，添加新的配置也可以修改规则：</li></ol><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">userservice:</span> <span class="comment"># 给某个微服务配置负载均衡规则，这里是userservice服务</span></span><br><span class="line">  <span class="attr">ribbon:</span></span><br><span class="line">    <span class="attr">NFLoadBalancerRuleClassName:</span> <span class="string">com.netflix.loadbalancer.RandomRule</span> <span class="comment"># 负载均衡规则 </span></span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>，一般用默认的负载均衡规则，不做修改。</p></blockquote><h2 id="4-4-饥饿加载"><a href="#4-4-饥饿加载" class="headerlink" title="4.4.饥饿加载"></a>4.4.饥饿加载</h2><p>Ribbon默认是采用懒加载，即第一次访问时才会去创建LoadBalanceClient，请求时间会很长。</p><p>而饥饿加载则会在项目启动时创建，降低第一次访问的耗时，通过下面配置开启饥饿加载：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ribbon:</span></span><br><span class="line">  <span class="attr">eager-load:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">clients:</span> <span class="string">userservice</span></span><br></pre></td></tr></table></figure><h1 id="5-Nacos注册中心"><a href="#5-Nacos注册中心" class="headerlink" title="5.Nacos注册中心"></a>5.Nacos注册中心</h1><p>国内公司一般都推崇阿里巴巴的技术，比如注册中心，SpringCloudAlibaba也推出了一个名为Nacos的注册中心。</p><h2 id="5-1-认识和安装Nacos"><a href="#5-1-认识和安装Nacos" class="headerlink" title="5.1.认识和安装Nacos"></a>5.1.认识和安装Nacos</h2><p><a href="https://nacos.io/">Nacos</a>是阿里巴巴的产品，现在是<a href="https://spring.io/projects/spring-cloud">SpringCloud</a>中的一个组件。相比<a href="https://github.com/Netflix/eureka">Eureka</a>功能更加丰富，在国内受欢迎程度较高。</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713230444308.png" alt="image-20210713230444308"></p><p>安装方式可以参考课前资料《Nacos安装指南.md》</p><h2 id="5-2-服务注册到nacos"><a href="#5-2-服务注册到nacos" class="headerlink" title="5.2.服务注册到nacos"></a>5.2.服务注册到nacos</h2><p>Nacos是SpringCloudAlibaba的组件，而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说，并没有太大区别。</p><p>主要差异在于：</p><ul><li>依赖不同</li><li>服务地址不同</li></ul><h3 id="1）引入依赖-2"><a href="#1）引入依赖-2" class="headerlink" title="1）引入依赖"></a>1）引入依赖</h3><p>在cloud-demo父工程的pom文件中的<code>&lt;dependencyManagement&gt;</code>中引入SpringCloudAlibaba的依赖：</p><figure class="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></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.alibaba.cloud<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-cloud-alibaba-dependencies<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>2.2.6.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<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></pre></td></tr></table></figure><p>然后在user-service和order-service中的pom文件中引入nacos-discovery依赖：</p><figure class="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="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.alibaba.cloud<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-cloud-starter-alibaba-nacos-discovery<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></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：不要忘了注释掉eureka的依赖。</p></blockquote><h3 id="2）配置nacos地址"><a href="#2）配置nacos地址" class="headerlink" title="2）配置nacos地址"></a>2）配置nacos地址</h3><p>在user-service和order-service的application.yml中添加nacos地址：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>：不要忘了注释掉eureka的地址</p></blockquote><h3 id="3）重启"><a href="#3）重启" class="headerlink" title="3）重启"></a>3）重启</h3><p>重启微服务后，登录nacos管理页面，可以看到微服务信息：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713231439607.png" alt="image-20210713231439607"></p><h2 id="5-3-服务分级存储模型"><a href="#5-3-服务分级存储模型" class="headerlink" title="5.3.服务分级存储模型"></a>5.3.服务分级存储模型</h2><p>一个<strong>服务</strong>可以有多个<strong>实例</strong>，例如我们的user-service，可以有:</p><ul><li>127.0.0.1:8081</li><li>127.0.0.1:8082</li><li>127.0.0.1:8083</li></ul><p>假如这些实例分布于全国各地的不同机房，例如：</p><ul><li>127.0.0.1:8081，在上海机房</li><li>127.0.0.1:8082，在上海机房</li><li>127.0.0.1:8083，在杭州机房</li></ul><p>Nacos就将同一机房内的实例 划分为一个<strong>集群</strong>。</p><p>也就是说，user-service是服务，一个服务可以包含多个集群，如杭州、上海，每个集群下可以有多个实例，形成分级模型，如图：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713232522531.png" alt="image-20210713232522531"></p><p>微服务互相访问时，应该尽可能访问同集群实例，因为本地访问速度更快。当本集群内不可用时，才访问其它集群。例如：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713232658928.png" alt="image-20210713232658928"></p><p>杭州机房内的order-service应该优先访问同机房的user-service。</p><h3 id="5-3-1-给user-service配置集群"><a href="#5-3-1-给user-service配置集群" class="headerlink" title="5.3.1.给user-service配置集群"></a>5.3.1.给user-service配置集群</h3><p>修改user-service的application.yml文件，添加集群配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">cluster-name:</span> <span class="string">HZ</span> <span class="comment"># 集群名称</span></span><br></pre></td></tr></table></figure><p>重启两个user-service实例后，我们可以在nacos控制台看到下面结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713232916215.png" alt="image-20210713232916215"></p><p>我们再次复制一个user-service启动配置，添加属性：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH</span><br></pre></td></tr></table></figure><p>配置如图所示：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713233528982.png" alt="image-20210713233528982"></p><p>启动UserApplication3后再次查看nacos控制台：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713233727923.png" alt="image-20210713233727923"></p><h3 id="5-3-2-同集群优先的负载均衡"><a href="#5-3-2-同集群优先的负载均衡" class="headerlink" title="5.3.2.同集群优先的负载均衡"></a>5.3.2.同集群优先的负载均衡</h3><p>默认的<code>ZoneAvoidanceRule</code>并不能实现根据同集群优先来实现负载均衡。</p><p>因此Nacos中提供了一个<code>NacosRule</code>的实现，可以优先从同集群中挑选实例。</p><p>1）给order-service配置集群信息</p><p>修改order-service的application.yml文件，添加集群配置：</p><figure class="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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  cloud:</span><br><span class="line">    nacos:</span><br><span class="line">      server-addr: localhost:8848</span><br><span class="line">      discovery:</span><br><span class="line">        cluster-name: HZ <span class="comment"># 集群名称</span></span><br></pre></td></tr></table></figure><p>2）修改负载均衡规则</p><p>修改order-service的application.yml文件，修改负载均衡规则：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">userservice:</span></span><br><span class="line">  <span class="attr">ribbon:</span></span><br><span class="line">    <span class="attr">NFLoadBalancerRuleClassName:</span> <span class="string">com.alibaba.cloud.nacos.ribbon.NacosRule</span> <span class="comment"># 负载均衡规则 </span></span><br></pre></td></tr></table></figure><h2 id="5-4-权重配置"><a href="#5-4-权重配置" class="headerlink" title="5.4.权重配置"></a>5.4.权重配置</h2><p>实际部署中会出现这样的场景：</p><p>服务器设备性能有差异，部分实例所在机器性能较好，另一些较差，我们希望性能好的机器承担更多的用户请求。</p><p>但默认情况下NacosRule是同集群内随机挑选，不会考虑机器的性能问题。</p><p>因此，Nacos提供了权重配置来控制访问频率，权重越大则访问频率越高。</p><p>在nacos控制台，找到user-service的实例列表，点击编辑，即可修改权重：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713235133225.png" alt="image-20210713235133225"></p><p>在弹出的编辑窗口，修改权重：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210713235235219.png" alt="image-20210713235235219"></p><blockquote><p><strong>注意</strong>：如果权重修改为0，则该实例永远不会被访问</p></blockquote><h2 id="5-5-环境隔离"><a href="#5-5-环境隔离" class="headerlink" title="5.5.环境隔离"></a>5.5.环境隔离</h2><p>Nacos提供了namespace来实现环境隔离功能。</p><ul><li>nacos中可以有多个namespace</li><li>namespace下可以有group、service等</li><li>不同namespace之间相互隔离，例如不同namespace的服务互相不可见</li></ul><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000101516.png" alt="image-20210714000101516"></p><h3 id="5-5-1-创建namespace"><a href="#5-5-1-创建namespace" class="headerlink" title="5.5.1.创建namespace"></a>5.5.1.创建namespace</h3><p>默认情况下，所有service、data、group都在同一个namespace，名为public：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000414781.png" alt="image-20210714000414781"></p><p>我们可以点击页面新增按钮，添加一个namespace：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000440143.png" alt="image-20210714000440143"></p><p>然后，填写表单：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000505928.png" alt="image-20210714000505928"></p><p>就能在页面看到一个新的namespace：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000522913.png" alt="image-20210714000522913"></p><h3 id="5-5-2-给微服务配置namespace"><a href="#5-5-2-给微服务配置namespace" class="headerlink" title="5.5.2.给微服务配置namespace"></a>5.5.2.给微服务配置namespace</h3><p>给微服务配置namespace只能通过修改配置来实现。</p><p>例如，修改order-service的application.yml文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">cluster-name:</span> <span class="string">HZ</span></span><br><span class="line">        <span class="attr">namespace:</span> <span class="string">492a7d5d-237b-46a1-a99a-fa8e98e4b0f9</span> <span class="comment"># 命名空间，填ID</span></span><br></pre></td></tr></table></figure><p>重启order-service后，访问控制台，可以看到下面的结果：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000830703.png" alt="image-20210714000830703"></p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000837140.png" alt="image-20210714000837140"></p><p>此时访问order-service，因为namespace不同，会导致找不到userservice，控制台会报错：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714000941256.png" alt="image-20210714000941256"></p><h2 id="5-6-Nacos与Eureka的区别"><a href="#5-6-Nacos与Eureka的区别" class="headerlink" title="5.6.Nacos与Eureka的区别"></a>5.6.Nacos与Eureka的区别</h2><p>Nacos的服务实例分为两种l类型：</p><ul><li><p>临时实例：如果实例宕机超过一定时间，会从服务列表剔除，默认的类型。</p></li><li><p>非临时实例：如果实例宕机，不会从服务列表剔除，也可以叫永久实例。</p></li></ul><p>配置一个服务实例为永久实例：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">ephemeral:</span> <span class="literal">false</span> <span class="comment"># 设置为非临时实例</span></span><br></pre></td></tr></table></figure><p>Nacos和Eureka整体结构类似，服务注册、服务拉取、心跳等待，但是也存在一些差异：</p><p><img src="https://cdn.jsdelivr.net/gh/ShallowRecall/Images@main/images/2023/11/image-20210714001728017.png" alt="image-20210714001728017"></p><ul><li><p>Nacos与eureka的共同点</p><ul><li>都支持服务注册和服务拉取</li><li>都支持服务提供者心跳方式做健康检测</li></ul></li><li><p>Nacos与Eureka的区别</p><ul><li>Nacos支持服务端主动检测提供者状态：临时实例采用心跳模式，非临时实例采用主动检测模式</li><li>临时实例心跳不正常会被剔除，非临时实例则不会被剔除</li><li>Nacos支持服务列表变更的消息推送模式，服务列表更新更及时</li><li>Nacos集群默认采用AP方式，当集群中存在非临时实例时，采用CP模式；Eureka采用AP方式</li></ul></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;SpringCloud01&quot;&gt;&lt;a href=&quot;#SpringCloud01&quot; class=&quot;headerlink&quot; title=&quot;SpringCloud01&quot;&gt;&lt;/a&gt;SpringCloud01&lt;/h1&gt;&lt;h1 id=&quot;1-认识微服务&quot;&gt;&lt;a href=&quot;#1-</summary>
      
    
    
    
    <category term="编程学习" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Java" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/"/>
    
    <category term="SpringCloud" scheme="https://www.shallowrecall.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Java/SpringCloud/"/>
    
    
    <category term="微服务" scheme="https://www.shallowrecall.top/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>堆和栈的区别</title>
    <link href="https://www.shallowrecall.top/posts/322da11e.html"/>
    <id>https://www.shallowrecall.top/posts/322da11e.html</id>
    <published>2024-03-11T18:56:28.000Z</published>
    <updated>2026-10-27T18:22:13.996Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Stack-Vs-Heap"><a href="#Stack-Vs-Heap" class="headerlink" title="Stack Vs Heap"></a>Stack Vs Heap</h2><h3 id="C"><a href="#C" class="headerlink" title="C++"></a>C++</h3><ul><li>管理方式不同：栈是由编译器自动申请和释放空间，堆是需要程序员手动申请和释放；</li><li>空间大小不同：栈的空间是有限的，在32位平台下，VC6下默认为1M，堆最大可以到4G；</li><li>能否产生碎片：栈和数据结构中的栈原理相同，在弹出一个元素之前，上一个已经弹出了，不会产生碎片，如果不停地调用malloc、free堆造成内存碎片很多;</li><li>生长方向不同：堆生长方向是向上的，也就是向着地址增加的方向，栈刚好相反，向着内存减小的方向生长。</li><li>分配方式不同：堆都是动态分配的，没有静态分配的堆。栈有静态分配和动态分配。静态分配是编译器完成的，比如局部变量的分配。动态分配由 malloc 函数进行分配，但是栈的动态分配和堆是不同的，它的动态分配是由编译器进行释放，无需我们手工实现。</li><li>分配效率不同：栈的效率比堆高很多。栈是机器系统提供的数据结构，计算机在底层提供栈的支持，分配专门的寄存器来存放栈的地址，压栈出栈都有相应的指令，因此比较快。堆是由库函数提供的，机制很复杂，库函数会按照一定的算法进行搜索内存，因此比较慢。</li></ul><h3 id="Java"><a href="#Java" class="headerlink" title="Java"></a>Java</h3>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;Stack-Vs-Heap&quot;&gt;&lt;a href=&quot;#Stack-Vs-Heap&quot; class=&quot;headerlink&quot; title=&quot;Stack Vs Heap&quot;&gt;&lt;/a&gt;Stack Vs Heap&lt;/h2&gt;&lt;h3 id=&quot;C&quot;&gt;&lt;a href=&quot;#C&quot; class</summary>
      
    
    
    
    
  </entry>
  
</feed>
