<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>傅永德</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://fuyongde.github.io/</id>
  <link href="https://fuyongde.github.io/" rel="alternate"/>
  <link href="https://fuyongde.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 傅永德</rights>
  <subtitle>苟利国家生死以，岂因祸福避趋之</subtitle>
  <title>傅永德 | 个人博客</title>
  <updated>2026-11-13T03:38:00.391Z</updated>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="数据库" scheme="https://fuyongde.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="MySQL" scheme="https://fuyongde.github.io/tags/MySQL/"/>
    <category term="表设计" scheme="https://fuyongde.github.io/tags/%E8%A1%A8%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<h2 id="问题的由来"><a href="#问题的由来" class="headerlink" title="问题的由来"></a>问题的由来</h2><p>设计后台服务绕不开建表，表和表之间又互相关联：user、order，order 要引用 user。于是一个不起眼但天天要面对的问题冒出来：<strong>主键怎么命名？</strong></p><p>我倾向过 <code>user_id</code> 这种带表名的命名——user 表的主键就叫 <code>user_id</code>，order 表引用它的列也叫 <code>user_id</code>。理由很朴素：每个字段一眼可辨，<code>order.user_id</code> 指向谁不用翻表结构。</p><p>但”一眼可辨”这四个字经得起推敲吗？我把这个问题拆开来想了一遍。先把结论交底：<strong>表名冻结、术语稳定的系统——金融核心、内部管理系统这类——我会选带表名的主键；业务词汇三年一换的互联网产品，我会选 <code>id</code>。</strong>为什么，一个维度一个维度说。</p><h2 id="先把两种做法摆清楚"><a href="#先把两种做法摆清楚" class="headerlink" title="先把两种做法摆清楚"></a>先把两种做法摆清楚</h2><p>两种做法分别长这样：</p><ul><li><strong>id 命名</strong>：<code>user</code> 表主键叫 <code>id</code>，<code>order</code> 表引用列叫 <code>user_id</code>——两边不对称。Rails、Django、MyBatis-Plus 的约定生态都是这么默认的。</li><li><strong>表名命名</strong>：<code>user</code> 表主键就叫 <code>user_id</code>，<code>order</code> 表引用列也叫 <code>user_id</code>——同名对称。金融、电信这类传统企业系统里很常见。</li></ul><p>顺带消个歧：这是主键列的<strong>命名</strong>问题，不是”代理键 vs 自然键”（用自增 <code>id</code> 还是用身份证号当主键）之争——那是另一个话题。它比看上去微妙的地方在于，<code>user_id</code> 这个词在两种做法里含义不同：一边是 user 表的主键，一边是引用列的通用名字。很多人在这个话题里吵的其实是两件不同的事。</p><h2 id="同一张表引用两次的时候"><a href="#同一张表引用两次的时候" class="headerlink" title="同一张表引用两次的时候"></a>同一张表引用两次的时候</h2><p>想清楚的第一步，是撞上一个现实场景：同一张表引用同一张表两次。订单有买家和卖家，审批流有创建人和审批人，转账有付款方和收款方——全都指向 user 表。</p><p>这时候表名命名撞墙了：一张表里两列没法都叫 <code>user_id</code>，只能加角色前缀：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 表名命名，多角色场景：同名规则破例，被迫加角色前缀</span></span><br><span class="line"><span class="keyword">ON</span> o.buyer_user_id <span class="operator">=</span> u.user_id</span><br><span class="line"></span><br><span class="line"><span class="comment">-- id 命名，同一场景：本来就是这形态</span></span><br><span class="line"><span class="keyword">ON</span> o.buyer_user_id <span class="operator">=</span> u.id</span><br></pre></td></tr></table></figure><p>两条 JOIN 已经是同一种形态：引用列带角色名，和主键列名不再对称。Java 侧同理，<code>getBuyerUserId()</code> 谁来写都是这一个名字。</p><p>所以”永远不会有歧义”其实是一句说过头的话，准确的说法是”一条规则 + 一个例外条款”：一表一引用时用同名规则，一表多引用时改角色规则。而 id 命名从第一天起就只有一条规则：主键永远叫 <code>id</code>，外键叫 <code>[角色_]目标_id</code>，规则的简洁性上反而是后者占优。值得注意的是，这里丢的还不是自明性——<code>buyer_user_id</code> 照样一眼看出指向 user 表——丢的是规则本身的统一性。</p><h2 id="读别人的-SQL-的时候"><a href="#读别人的-SQL-的时候" class="headerlink" title="读别人的 SQL 的时候"></a>读别人的 SQL 的时候</h2><p>第二个维度是 JOIN 的可读性。读别人的长 SQL，JOIN 条件躺在第 30 行：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- id 命名</span></span><br><span class="line"><span class="keyword">SELECT</span> o.order_no, u.name <span class="keyword">FROM</span> `<span class="keyword">order</span>` o <span class="keyword">JOIN</span> <span class="keyword">user</span> u <span class="keyword">ON</span> o.user_id <span class="operator">=</span> u.id;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 表名命名</span></span><br><span class="line"><span class="keyword">SELECT</span> o.order_no, u.name <span class="keyword">FROM</span> `<span class="keyword">order</span>` o <span class="keyword">JOIN</span> <span class="keyword">user</span> u <span class="keyword">USING</span> (user_id);</span><br></pre></td></tr></table></figure><p><code>u.id</code> 要求你滚回顶部确认 <code>u</code> 是谁；<code>u.user_id</code> 自己就把话说完了，别名乱成 <code>t3</code> 也无妨。这在维护期最明显：写 SQL 的人不总是读 SQL 的人——半年后的 code review、线上排障、接手离职同事的报表，读者都要重建上下文，自包含的列名替他省一步。</p><p>但我后来意识到别高估这条：id 命名把语义信息存在”别名 → 表”那一层，而读写 SQL 本来就得背着这层映射——SELECT 列、WHERE 条件全靠它限定。表名命名做的事，本质是把这层语义冗余编码进每一个列名：<code>FROM user u ... u.user_id</code>，”user”在一条 SQL 里出现两遍。这条我承认表名命名更舒服，但舒服得有限。</p><h2 id="假如表要改名"><a href="#假如表要改名" class="headerlink" title="假如表要改名"></a>假如表要改名</h2><p>第三个维度让我纠结最久：业务语言演化，<code>user</code> 表要改名 <code>customer</code> 怎么办？表名命名立刻陷入两难：</p><ul><li><strong>改列名</strong>：<code>user_id</code> → <code>customer_id</code>，所有引用表的列、所有 JOIN、所有 Java 实体、所有日志埋点全要联动——一次大手术；</li><li><strong>不改</strong>：从此名实分离：</li></ul><figure class="highlight sql"><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">-- 十年后：业务早就叫客户，表还叫 user</span></span><br><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> <span class="keyword">WHERE</span> user_id <span class="operator">=</span> <span class="number">123</span>;</span><br></pre></td></tr></table></figure><p>十年后入职的人对着这段 schema，第一件事是问老人：为什么客户存在 user 表里？</p><p>暴露面也不一样：表名命名把表名编进了列名，改名时受牵连的是主键加全部外键；id 命名的主键不承诺任何表名，受牵连的只有外键。多表合并同理：user 和 member 并成一张表，表名命名的主键留谁的名字都是错，id 一个就收编两侧。</p><p>但我自己的经验里，表改名这种事从来没真正发生过——多数长命系统选择”沿用以前的命名”，表名是冻结的。这条顾虑在冻结的系统里不会兑现，在词汇漂移的系统里则是真实的债。</p><h2 id="当值离开表以后"><a href="#当值离开表以后" class="headerlink" title="当值离开表以后"></a>当值离开表以后</h2><p>第四个维度，是我想明白自己为什么那么在意”自明”的关键：值离开 schema 之后。导出的 CSV、同步进数仓的宽表、DBA 拿裸 SQL 排障、数据订正脚本——这些地方没有实体类帮你补语义，列名是唯一的语义来源：</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"># 排障时看到的数据（id 命名）</span><br><span class="line">order done. id=123</span><br><span class="line"></span><br><span class="line"># 排障时看到的数据（表名命名）</span><br><span class="line">order done. user_id=123</span><br></pre></td></tr></table></figure><p>半夜排障时，前者要先弄清 <code>id=123</code> 是哪张表的 id，后者一眼可辨。数仓场景尤其典型：宽表汇聚几十张表的列，全库主键同名 <code>id</code>，同名列冲突逼着数仓同学逐个起别名，而全局唯一列名在数仓里本来就是通行做法——带表名的命名天然合规。</p><p>有一个常见的反驳要处理掉：日志里打 <code>id=123</code>，其实在 log 语句层顺手写 <code>userId=</code> 就能补救，schema 不必出手。这话对——但 log 层救不了 CSV、数仓、DBA 的裸 SQL。这是表名命名真正占便宜的一个维度，也是它最该占便宜的。</p><h2 id="写通用代码的时候"><a href="#写通用代码的时候" class="headerlink" title="写通用代码的时候"></a>写通用代码的时候</h2><p>最后一个维度在 Java 这边。两种做法都有”一致性”，但不是同一种：表名命名是规则一致——主键 &#x3D; 表名_id，规则统一但名字各异；id 命名是名字一致——所有主键都叫 <code>id</code>，不光规则统一，名字本身也统一。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// id 命名：名字一致，一个字段通吃所有实体</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">BaseEntity</span> &#123; <span class="keyword">private</span> Long id; &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 表名命名：规则一致，名字各异，通用代码走注解元数据</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span>  &#123; <span class="keyword">private</span> Long userId; &#125;</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Order</span> &#123; <span class="keyword">private</span> Long orderId; &#125;</span><br></pre></td></tr></table></figure><p>差距要到自研基建才真正显出来：缓存组件按主键拼 key，数据同步按主键取值，通用订正工具按主键定位行——id 命名一句 <code>entity.getId()</code> 通吃所有实体；表名命名的每张表都要过一遍注解元数据。</p><p>但 MyBatis-Plus 的 <code>IService.getById()</code> 之所以能对所有实体工作，内部就是靠 <code>@TableId(value = &quot;user_id&quot;)</code> 这类注解把差异抹平的——框架替你把摩擦吃掉了大半，每个实体多一行注解而已。JPA、代码生成器同理。有差距，但被框架付掉了大半。</p><h2 id="我的权衡汇总"><a href="#我的权衡汇总" class="headerlink" title="我的权衡汇总"></a>我的权衡汇总</h2><p>五个维度想完，汇总成一张表：</p><table><thead><tr><th>维度</th><th>我的判断</th></tr></thead><tbody><tr><td>同表引用两次</td><td>打平——表名命名的同名规则在这里天然破例</td></tr><tr><td>JOIN 可读性</td><td>表名命名略占优——列名自包含，不依赖别名</td></tr><tr><td>假如表改名</td><td>看系统——表名冻结的库里这条不兑现</td></tr><tr><td>值离开表以后</td><td>表名命名胜——数仓、导出、裸 SQL 的唯一语义来源</td></tr><tr><td>写通用代码</td><td>接近——摩擦可被框架吸收</td></tr></tbody></table><p>表名冻结、术语稳定的系统里，带表名的主键自洽且略占优——这大概也是金融、电信核心系统常见 <code>表名_id</code>、互联网常见 <code>id</code> 的原因：不是谁更先进，是各自的域把账算对了。</p><h2 id="什么会让我改变主意"><a href="#什么会让我改变主意" class="headerlink" title="什么会让我改变主意"></a>什么会让我改变主意</h2><p>结论不是终点，有三个信号会让我重新掂量：</p><ol><li><strong>业务词汇开始漂移</strong>：产品三年换血、领域术语跟着业务跑，表名命名的”名字承诺”开始欠债；</li><li><strong>重度自研泛型基建</strong>：公司框架、缓存组件、数据同步工具全要面向主键编程，表名命名的摩擦从”每实体一行注解”放大到每个基建组件；</li><li><strong>多角色引用成为高频形态</strong>：此时两种做法趋同，选谁都一样，不必再为对称性付成本。</li></ol><p>新项目起手，我会先问自己三个问题再落键盘：表改名、领域术语换血发生过吗？数据会流到库外面去吗——数仓、报表、导出、别的团队直接查？同一张表引用同一目标两次常见吗？答案决定了主键叫什么——别让别人的结论替你的域做决定，包括我的。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2026/11/13/primary-key-naming/</id>
    <link href="https://fuyongde.github.io/2026/11/13/primary-key-naming/"/>
    <published>2026-11-13T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="问题的由来"><a href="#问题的由来" class="headerlink" title="问题的由来"></a>问题的由来</h2><p>设计后台服务绕不开建表，表和表之间又互相关联：user、order，order 要引用 user。于是一个不起眼但天天要面对的问题冒出来：<strong>主键怎么命名？</strong></p>
<p>我倾向过 <code>user_id</code> 这种带表名的命名——user 表的主键就叫 <code>user_id</code>，order 表引用它的列也叫 <code>user_id</code>。理由很朴素：每个字段一眼可辨，<code>order.user_id</code> 指向谁不用翻表结构。</p>
<p>但”一眼可辨”这四个字经得起推敲吗？我把这个问题拆开来想了一遍。先把结论交底：<strong>表名冻结、术语稳定的系统——金融核心、内部管理系统这类——我会选带表名的主键；业务词汇三年一换的互联网产品，我会选 <code>id</code>。</strong>为什么，一个维度一个维度说。</p>
<h2 id="先把两种做法摆清楚"><a href="#先把两种做法摆清楚" class="headerlink" title="先把两种做法摆清楚"></a>先把两种做法摆清楚</h2><p>两种做法分别长这样：</p>
<ul>
<li><strong>id 命名</strong>：<code>user</code> 表主键叫 <code>id</code>，<code>order</code> 表引用列叫 <code>user_id</code>——两边不对称。Rails、Django、MyBatis-Plus 的约定生态都是这么默认的。</li>
<li><strong>表名命名</strong>：<code>user</code> 表主键就叫 <code>user_id</code>，<code>order</code> 表引用列也叫 <code>user_id</code>——同名对称。金融、电信这类传统企业系统里很常见。</li>
</ul>
<p>顺带消个歧：这是主键列的<strong>命名</strong>问题，不是”代理键 vs 自然键”（用自增 <code>id</code> 还是用身份证号当主键）之争——那是另一个话题。它比看上去微妙的地方在于，<code>user_id</code> 这个词在两种做法里含义不同：一边是 user 表的主键，一边是引用列的通用名字。很多人在这个话题里吵的其实是两件不同的事。</p>
<h2 id="同一张表引用两次的时候"><a href="#同一张表引用两次的时候" class="headerlink" title="同一张表引用两次的时候"></a>同一张表引用两次的时候</h2><p>想清楚的第一步，是撞上一个现实场景：同一张表引用同一张表两次。订单有买家和卖家，审批流有创建人和审批人，转账有付款方和收款方——全都指向 user 表。</p>]]>
    </summary>
    <title>业务表主键：从纠结到想清楚</title>
    <updated>2026-11-13T03:38:00.391Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <content>
      <![CDATA[<h1 id="深圳暑假遛娃攻略"><a href="#深圳暑假遛娃攻略" class="headerlink" title="深圳暑假遛娃攻略"></a>深圳暑假遛娃攻略</h1><blockquote><p>深圳暑假亲子室内场馆打卡指南：旧地重游 + 全新探索并行，全程室内避暑，兼顾研学与遛娃。</p><p><strong>策略</strong>：大多免费免预约，少数按需提前预约；利用暑假慢慢走完所有点位。</p></blockquote><hr><h2 id="📋-全部点位速查表"><a href="#📋-全部点位速查表" class="headerlink" title="📋 全部点位速查表"></a>📋 全部点位速查表</h2><table><thead><tr><th>#</th><th>场馆</th><th>门票</th><th>预约</th><th>地铁</th><th>闭馆日</th></tr></thead><tbody><tr><td>1</td><td>深圳科学技术馆（光明新馆）</td><td>50&#x2F;成人，16岁以下32，1.2m以下免费</td><td>✅需预约</td><td>6号线光明站C&#x2F;D</td><td>周一</td></tr><tr><td>2</td><td>深圳市科学馆（福田）</td><td>免费</td><td>免预约</td><td>6号线科学馆站A</td><td>周一、周二</td></tr><tr><td>3</td><td>深圳市图书馆北馆少儿馆</td><td>免费</td><td>免预约</td><td>4号线红山站A</td><td>周一</td></tr><tr><td>4</td><td>深圳市美术馆新馆</td><td>免费</td><td>免预约</td><td>6号线红山站A</td><td>周一</td></tr><tr><td>5</td><td>深圳市当代艺术与城市规划馆</td><td>免费</td><td>免预约</td><td>4号线少年宫A2</td><td>—</td></tr><tr><td>6</td><td>南山安全教育体验馆</td><td>免费</td><td>i深圳预约</td><td>12号线中山公园C</td><td>周一</td></tr><tr><td>7</td><td>深圳红立方 + IDEA乐园</td><td>免费</td><td>公众号预约</td><td>3号线龙城广场D</td><td>周一</td></tr><tr><td>8</td><td>宝安科技馆</td><td>免费</td><td>公众号预约</td><td>12号线新安公园D</td><td>周一</td></tr><tr><td>9</td><td>蛇口海洋科普馆</td><td>免费</td><td>免预约</td><td>8号线东头角A</td><td>周一</td></tr><tr><td>10</td><td>何香凝美术馆</td><td>免费</td><td>免预约</td><td>1号线华侨城C</td><td>周一</td></tr><tr><td>⭐</td><td>深圳自然博物馆（新开）</td><td>免费</td><td>✅需实名预约</td><td>16号线沙壆站D</td><td>周一</td></tr></tbody></table><hr><h2 id="🏛️-各场馆详情"><a href="#🏛️-各场馆详情" class="headerlink" title="🏛️ 各场馆详情"></a>🏛️ 各场馆详情</h2><h3 id="1️⃣-深圳科学技术馆（光明新馆）"><a href="#1️⃣-深圳科学技术馆（光明新馆）" class="headerlink" title="1️⃣ 深圳科学技术馆（光明新馆）"></a>1️⃣ 深圳科学技术馆（光明新馆）</h3><ul><li>🎫 <strong>门票</strong>：50元&#x2F;成人，16岁以下32元，1.2m以下免费</li><li>📝 <strong>预约</strong>：需要预约</li><li>🚇 <strong>交通</strong>：6号线光明站C&#x2F;D出口</li><li>⏰ <strong>开放时间</strong>：<ul><li>周二至周五 10:00-17:00</li><li>周六至周日 09:30-18:30（夏季）</li><li>周一闭馆</li></ul></li></ul><h3 id="2️⃣-深圳市科学馆（福田）"><a href="#2️⃣-深圳市科学馆（福田）" class="headerlink" title="2️⃣ 深圳市科学馆（福田）"></a>2️⃣ 深圳市科学馆（福田）</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：6号线科学馆站A出口</li><li>⏰ <strong>开放时间</strong>：<ul><li>周三至周五 10:00-17:00</li><li>周六至周日 09:30-17:00</li><li>周一、周二闭馆</li></ul></li></ul><h3 id="3️⃣-深圳市图书馆北馆少儿馆"><a href="#3️⃣-深圳市图书馆北馆少儿馆" class="headerlink" title="3️⃣ 深圳市图书馆北馆少儿馆"></a>3️⃣ 深圳市图书馆北馆少儿馆</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：4号线红山站A口</li><li>⏰ <strong>开放时间</strong>：周二至周日 9:00-21:00（全馆开放），周一闭馆</li></ul><h3 id="4️⃣-深圳市美术馆新馆"><a href="#4️⃣-深圳市美术馆新馆" class="headerlink" title="4️⃣ 深圳市美术馆新馆"></a>4️⃣ 深圳市美术馆新馆</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：6号线红山站A出口</li><li>⏰ <strong>开放时间</strong>：周二至周日 10:00-18:00，周一闭馆</li></ul><h3 id="5️⃣-深圳市当代艺术与城市规划馆"><a href="#5️⃣-深圳市当代艺术与城市规划馆" class="headerlink" title="5️⃣ 深圳市当代艺术与城市规划馆"></a>5️⃣ 深圳市当代艺术与城市规划馆</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：地铁4号线少年宫A2口</li><li>⏰ <strong>开放时间</strong>：周二至周日 10:00-18:00</li></ul><h3 id="6️⃣-南山安全教育体验馆"><a href="#6️⃣-南山安全教育体验馆" class="headerlink" title="6️⃣ 南山安全教育体验馆"></a>6️⃣ 南山安全教育体验馆</h3><ul><li>🎫 <strong>门票</strong>：免费</li><li>📝 <strong>预约</strong>：提前”i深圳”预约</li><li>🚇 <strong>交通</strong>：12号线中山公园C出口</li><li>⏰ <strong>开放时间</strong>：周二至周日 9:30-11:30 &#x2F; 13:30-17:30，周一闭馆</li></ul><h3 id="7️⃣-深圳红立方-IDEA乐园"><a href="#7️⃣-深圳红立方-IDEA乐园" class="headerlink" title="7️⃣ 深圳红立方 + IDEA乐园"></a>7️⃣ 深圳红立方 + IDEA乐园</h3><ul><li>🎫 <strong>门票</strong>：免费</li><li>📝 <strong>预约</strong>：提前公众号预约</li><li>🚇 <strong>交通</strong>：3号线龙城广场D出口</li><li>⏰ <strong>开放时间</strong>：<ul><li>周一闭馆</li><li>周二至周四 9:30-18:00</li><li>周五至周日 9:30-21:30</li></ul></li></ul><h3 id="8️⃣-宝安科技馆"><a href="#8️⃣-宝安科技馆" class="headerlink" title="8️⃣ 宝安科技馆"></a>8️⃣ 宝安科技馆</h3><ul><li>🎫 <strong>门票</strong>：免费</li><li>📝 <strong>预约</strong>：提前公众号预约</li><li>🚇 <strong>交通</strong>：12号线新安公园站D出口</li><li>⏰ <strong>开放时间</strong>：<ul><li>周二至周五 10:00-17:00</li><li>周六至周日 9:30-17:30</li><li>周一闭馆</li></ul></li></ul><h3 id="9️⃣-蛇口海洋科普馆"><a href="#9️⃣-蛇口海洋科普馆" class="headerlink" title="9️⃣ 蛇口海洋科普馆"></a>9️⃣ 蛇口海洋科普馆</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：8号线东头角站A出口</li><li>⏰ <strong>开放时间</strong>：周二至周日 9:30-11:30 &#x2F; 14:30-17:30，周一闭馆</li></ul><h3 id="🔟-何香凝美术馆"><a href="#🔟-何香凝美术馆" class="headerlink" title="🔟 何香凝美术馆"></a>🔟 何香凝美术馆</h3><ul><li>🎫 <strong>门票</strong>：免费，免预约</li><li>🚇 <strong>交通</strong>：1号线华侨城站C出口</li><li>⏰ <strong>开放时间</strong>：周二至周日 9:30-17:00，周一闭馆</li></ul><hr><h2 id="⭐-特别推荐：深圳自然博物馆（全新开馆）"><a href="#⭐-特别推荐：深圳自然博物馆（全新开馆）" class="headerlink" title="⭐ 特别推荐：深圳自然博物馆（全新开馆）"></a>⭐ 特别推荐：深圳自然博物馆（全新开馆）</h2><blockquote><p>华南首座大型自然博物馆，<strong>2026年7月28日正式对外开放</strong>，以”宇宙→深圳”叙事完整演绎46亿年地球自然发展史。</p></blockquote><h3 id="基本信息"><a href="#基本信息" class="headerlink" title="基本信息"></a>基本信息</h3><ul><li>🎫 <strong>门票</strong>：免费，但<strong>必须实名预约</strong>，建议提前1-3天，分时段入馆</li><li>🚇 <strong>交通</strong>：<ul><li>地铁16号线沙壆站D口，出站步行可达</li><li>坪山云巴1号线”自然博物馆西站”</li><li>自驾导航”深圳自然博物馆”，配套停车场车位充足</li></ul></li><li>⏰ <strong>开放时间</strong>：周二至周日 9:00-17:00（16:00停止入馆），周一闭馆</li><li>⏱️ <strong>建议游玩时长</strong>：3-4小时（认真逛完八大展厅）</li></ul><h3 id="八大展厅（叙事主线：从宇宙到深圳）"><a href="#八大展厅（叙事主线：从宇宙到深圳）" class="headerlink" title="八大展厅（叙事主线：从宇宙到深圳）"></a>八大展厅（叙事主线：从宇宙到深圳）</h3><p>按顺序逛，体验完整叙事：</p><ol><li><strong>宇宙厅</strong> — 古代星图、天文望远镜、真实深空陨石样本</li><li><strong>地球厅</strong> — 46亿年地球变迁、岩石矿物、板块漂移</li><li><strong>演化厅</strong> — 38亿年生命演化，从单细胞到人类</li><li><strong>恐龙厅</strong> — 巨型恐龙骨架化石 + 场景复原，聚焦粤赣恐龙考古</li><li><strong>人类厅</strong> — 两广地区珍贵古人类化石，揭秘人类起源</li><li><strong>生物厅</strong> — 飞行、伪装、捕食等生物生存智慧</li><li><strong>生态厅</strong> — 森林、草原、海洋、极地等全球生态系统</li><li><strong>家园厅</strong> — 深圳本土物种、城市候鸟、本地生态</li></ol><h3 id="三大亮点"><a href="#三大亮点" class="headerlink" title="三大亮点"></a>三大亮点</h3><ol><li><strong>贝林珍稀动物标本</strong>：非洲象、长颈鹿、北极熊、尼罗鳄等明星标本全部搬入新馆</li><li><strong>沉浸式黑科技</strong>：场景复原 + 数字全息投影 + 互动装置，非静态展览，孩子玩到不想走</li><li><strong>建筑本身也是风景</strong>：造型借鉴坪山河水流形态，与山水融为一体，拍照绝美</li></ol><h3 id="馆内贴士"><a href="#馆内贴士" class="headerlink" title="馆内贴士"></a>馆内贴士</h3><ul><li>建议从宇宙厅开始按顺序逛，体验完整叙事</li><li>带娃家庭重点关注恐龙厅和生物厅，互动性强</li><li>可租借语音讲解器或 AR 导览设备，提升参观体验</li><li>馆内有饮水处和休息区，建议穿舒适平底鞋</li></ul><hr><h2 id="🎯-打卡策略建议"><a href="#🎯-打卡策略建议" class="headerlink" title="🎯 打卡策略建议"></a>🎯 打卡策略建议</h2><h3 id="按类型分类"><a href="#按类型分类" class="headerlink" title="按类型分类"></a>按类型分类</h3><table><thead><tr><th>类型</th><th>场馆</th><th>特点</th></tr></thead><tbody><tr><td>🔬 科技研学</td><td>科学技术馆(光明)、市科学馆(福田)、宝安科技馆、红立方</td><td>互动实验，激发科学兴趣</td></tr><tr><td>🎨 艺术人文</td><td>美术馆新馆、当代艺术馆、何香凝美术馆</td><td>审美熏陶，城市规划认知</td></tr><tr><td>📚 阅读启蒙</td><td>图书馆北馆少儿馆</td><td>阅读空间，安静研学</td></tr><tr><td>🌊 自然科普</td><td>蛇口海洋科普馆、<strong>深圳自然博物馆</strong></td><td>自然认知，生命演化</td></tr><tr><td>🛡️ 安全教育</td><td>南山安全教育体验馆</td><td>体验式安全知识</td></tr></tbody></table><h3 id="预约优先级提醒"><a href="#预约优先级提醒" class="headerlink" title="预约优先级提醒"></a>预约优先级提醒</h3><p>需提前预约的场馆（务必提前操作，避免跑空）：</p><ul><li>⭐ <strong>深圳自然博物馆</strong> — 7月28日新开，预计热度高，提前1-3天预约</li><li>深圳科学技术馆（光明新馆）</li><li>南山安全教育体验馆（i深圳）</li><li>深圳红立方 + IDEA乐园（公众号）</li><li>宝安科技馆（公众号）</li></ul><h3 id="同区连玩组合建议"><a href="#同区连玩组合建议" class="headerlink" title="同区连玩组合建议"></a>同区连玩组合建议</h3><ul><li><strong>红山站</strong>：图书馆北馆少儿馆 + 美术馆新馆（同站，半天搞定）</li><li><strong>少年宫片区</strong>：当代艺术与城市规划馆（周边配套完善）</li><li><strong>华侨城</strong>：何香凝美术馆（可与周边创意文化园连玩）</li><li><strong>坪山</strong>：深圳自然博物馆（独立一日游，配套停车场）</li></ul><h3 id="通用提醒"><a href="#通用提醒" class="headerlink" title="通用提醒"></a>通用提醒</h3><ul><li>几乎所有场馆<strong>周一闭馆</strong>，规划时避开周一</li><li>室内空调较足，给娃备一件薄外套</li><li>暑假周末人流大，优先选择<strong>工作日错峰</strong></li><li>部分场馆分上午&#x2F;下午场次，注意<strong>最晚入馆时间</strong></li><li>关注各场馆官方公众号，获取临时展览与活动信息</li></ul>]]>
    </content>
    <id>https://fuyongde.github.io/2026/07/10/2026-%E6%B7%B1%E5%9C%B3%E6%9A%91%E5%81%87%E6%BA%9C%E5%A8%83%E6%94%BB%E7%95%A5/</id>
    <link href="https://fuyongde.github.io/2026/07/10/2026-%E6%B7%B1%E5%9C%B3%E6%9A%91%E5%81%87%E6%BA%9C%E5%A8%83%E6%94%BB%E7%95%A5/"/>
    <published>2026-07-10T08:38:34.000Z</published>
    <summary>
      <![CDATA[<h1 id="深圳暑假遛娃攻略"><a href="#深圳暑假遛娃攻略" class="headerlink" title="深圳暑假遛娃攻略"></a>深圳暑假遛娃攻略</h1><blockquote>
<p>深圳暑假亲子室内场馆打卡指南：旧地重游 + 全新探索并行，全程室内避暑，兼顾研学与遛娃。</p>
<p><strong>策略</strong>：大多免费免预约，少数按需提前预约；利用暑假慢慢走完所有点位。</p>
</blockquote>
<hr>
<h2 id="📋-全部点位速查表"><a href="#📋-全部点位速查表" class="headerlink" title="📋 全部点位速查表"></a>📋 全部点位速查表</h2><table>
<thead>
<tr>
<th>#</th>
<th>场馆</th>
<th>门票</th>
<th>预约</th>
<th>地铁</th>
<th>闭馆日</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>深圳科学技术馆（光明新馆）</td>
<td>50&#x2F;成人，16岁以下32，1.2m以下免费</td>
<td>✅需预约</td>
<td>6号线光明站C&#x2F;D</td>
<td>周一</td>
</tr>
<tr>
<td>2</td>
<td>深圳市科学馆（福田）</td>
<td>免费</td>
<td>免预约</td>
<td>6号线科学馆站A</td>
<td>周一、周二</td>
</tr>
<tr>
<td>3</td>
<td>深圳市图书馆北馆少儿馆</td>
<td>免费</td>
<td>免预约</td>
<td>4号线红山站A</td>
<td>周一</td>
</tr>
<tr>
<td>4</td>
<td>深圳市美术馆新馆</td>
<td>免费</td>
<td>免预约</td>
<td>6号线红山站A</td>
<td>周一</td>
</tr>
<tr>
<td>5</td>
<td>深圳市当代艺术与城市规划馆</td>
<td>免费</td>
<td>免预约</td>
<td>4号线少年宫A2</td>
<td>—</td>
</tr>
<tr>
<td>6</td>
<td>南山安全教育体验馆</td>
<td>免费</td>
<td>i深圳预约</td>
<td>12号线中山公园C</td>
<td>周一</td>
</tr>
<tr>
<td>7</td>
<td>深圳红立方 + IDEA乐园</td>
<td>免费</td>
<td>公众号预约</td>
<td>3号线龙城广场D</td>
<td>周一</td>
</tr>
<tr>
<td>8</td>
<td>宝安科技馆</td>
<td>免费</td>
<td>公众号预约</td>
<td>12号线新安公园D</td>
<td>周一</td>
</tr>
<tr>
<td>9</td>
<td>蛇口海洋科普馆</td>
<td>免费</td>
<td>免预约</td>
<td>8号线东头角A</td>
<td>周一</td>
</tr>
<tr>
<td>10</td>
<td>何香凝美术馆</td>
<td>免费</td>
<td>免预约</td>
<td>1号线华侨城C</td>
<td>周一</td>
</tr>
<tr>
<td>⭐</td>
<td>深圳自然博物馆（新开）</td>
<td>免费</td>
<td>✅需实名预约</td>
<td>16号线沙壆站D</td>
<td>周一</td>
</tr>
</tbody></table>
<hr>
<h2 id="🏛️-各场馆详情"><a href="#🏛️-各场馆详情" class="headerlink" title="🏛️ 各场馆详情"></a>🏛️ 各场馆详情</h2><h3 id="1️⃣-深圳科学技术馆（光明新馆）"><a href="#1️⃣-深圳科学技术馆（光明新馆）" class="headerlink" title="1️⃣ 深圳科学技术馆（光明新馆）"></a>1️⃣ 深圳科学技术馆（光明新馆）</h3><ul>
<li>🎫 <strong>门票</strong>：50元&#x2F;成人，16岁以下32元，1.2m以下免费</li>
<li>📝 <strong>预约</strong>：需要预约</li>
<li>🚇 <strong>交通</strong>：6号线光明站C&#x2F;D出口</li>
<li>⏰ <strong>开放时间</strong>：<ul>
<li>周二至周五 10:00-17:00</li>
<li>周六至周日 09:30-18:30（夏季）</li>
<li>周一闭馆</li>
</ul>
</li>
</ul>
<h3 id="2️⃣-深圳市科学馆（福田）"><a href="#2️⃣-深圳市科学馆（福田）" class="headerlink" title="2️⃣ 深圳市科学馆（福田）"></a>2️⃣ 深圳市科学馆（福田）</h3>]]>
    </summary>
    <title>2026 深圳暑假溜娃攻略</title>
    <updated>2026-07-15T08:46:53.979Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="AI" scheme="https://fuyongde.github.io/categories/AI/"/>
    <category term="Claude Code" scheme="https://fuyongde.github.io/categories/AI/Claude-Code/"/>
    <category term="AI" scheme="https://fuyongde.github.io/tags/AI/"/>
    <category term="Agent Teams" scheme="https://fuyongde.github.io/tags/Agent-Teams/"/>
    <content>
      <![CDATA[<p>Agent Teams 让多个 Claude Code 实例并行工作并通过共享任务列表相互协调。与 subagents 不同，agent teams 中的队友可以直接相互通信、协作和分享发现，适合需要团队协作的复杂任务。</p><p>本文将详细介绍 Agent Teams 的启用方式、使用方法、最佳实践以及与 Subagents 的区别。</p><h2 id="Agent-Teams-概述"><a href="#Agent-Teams-概述" class="headerlink" title="Agent Teams 概述"></a>Agent Teams 概述</h2><h3 id="什么是-Agent-Teams"><a href="#什么是-Agent-Teams" class="headerlink" title="什么是 Agent Teams"></a>什么是 Agent Teams</h3><p>Agent Teams 是一组并行工作的 Claude Code 实例，通过共享任务列表协调工作。每个队友都是完整的、独立的 Claude Code 会话，拥有自己的 context window。</p><p><strong>核心组件：</strong></p><table><thead><tr><th>组件</th><th>角色</th></tr></thead><tbody><tr><td>Team Lead</td><td>创建团队、生成队友并协调工作的主 Claude Code 会话</td></tr><tr><td>Teammates</td><td>各自处理分配任务的独立 Claude Code 实例</td></tr><tr><td>Task List</td><td>队友认领和完成的共享工作项列表</td></tr><tr><td>Mailbox</td><td>代理之间通信的消息系统</td></tr></tbody></table><h3 id="与-Subagents-的区别"><a href="#与-Subagents-的区别" class="headerlink" title="与 Subagents 的区别"></a>与 Subagents 的区别</h3><p>Agent Teams 和 Subagents 都支持并行工作，但运作方式不同：</p><table><thead><tr><th>特性</th><th>Subagents</th><th>Agent Teams</th></tr></thead><tbody><tr><td>Context</td><td>自己的 context window；结果返回给调用者</td><td>自己的 context window；完全独立</td></tr><tr><td>通信</td><td>仅向主代理报告结果</td><td>队友直接相互发送消息</td></tr><tr><td>协调</td><td>主代理管理所有工作</td><td>共享任务列表，自我协调</td></tr><tr><td>最适合</td><td>只有结果重要的专注任务</td><td>需要讨论和协作的复杂工作</td></tr><tr><td>令牌成本</td><td>较低：结果汇总回主 context</td><td>较高：每个队友是独立的 Claude 实例</td></tr></tbody></table><p><strong>架构对比图：</strong></p><img src="/2026/03/22/Claude-Code-Agent-Teams-%E6%8C%87%E5%8D%97/subagents-vs-agent-teams-light.avif" class=""><h3 id="最佳用例"><a href="#最佳用例" class="headerlink" title="最佳用例"></a>最佳用例</h3><p>Agent Teams 最适合并行探索能增加真实价值的任务：</p><ul><li><strong>研究和审查</strong>：多个队友同时调查问题的不同方面，然后分享和质疑彼此的发现</li><li><strong>新模块或功能</strong>：队友各自拥有独立的部分，不会相互干扰</li><li><strong>使用竞争假设进行调试</strong>：队友并行测试不同的理论，更快收敛到答案</li><li><strong>跨层协调</strong>：跨越前端、后端和测试的更改，每个由不同的队友负责</li></ul><hr><h2 id="启用-Agent-Teams"><a href="#启用-Agent-Teams" class="headerlink" title="启用 Agent Teams"></a>启用 Agent Teams</h2><h3 id="启用实验性功能"><a href="#启用实验性功能" class="headerlink" title="启用实验性功能"></a>启用实验性功能</h3><p>Agent Teams 默认禁用，需要通过环境变量启用：</p><p><strong>方式一：设置环境变量</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux/macOS</span></span><br><span class="line"><span class="built_in">export</span> CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1</span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows PowerShell</span></span><br><span class="line"><span class="variable">$env</span>:CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1</span><br></pre></td></tr></table></figure><p><strong>方式二：在 settings.json 中配置</strong></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;env&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="启动与控制-Agent-Team"><a href="#启动与控制-Agent-Team" class="headerlink" title="启动与控制 Agent Team"></a>启动与控制 Agent Team</h2><h3 id="启动第一个-Agent-Team"><a href="#启动第一个-Agent-Team" class="headerlink" title="启动第一个 Agent Team"></a>启动第一个 Agent Team</h3><p>启用后，用自然语言告诉 Claude 创建团队：</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">Create an agent team to review this PR. One teammate should focus on</span><br><span class="line">security issues, another on performance, and a third on test coverage.</span><br></pre></td></tr></table></figure><p>这个例子效果很好，因为三个角色是独立的，可以并行探索问题。</p><h3 id="选择显示模式"><a href="#选择显示模式" class="headerlink" title="选择显示模式"></a>选择显示模式</h3><p>Agent Teams 支持两种显示模式：</p><table><thead><tr><th>模式</th><th>说明</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>In-process</strong></td><td>所有队友在主终端内运行</td><td>无需额外设置，任何终端都可用</td></tr><tr><td><strong>Split panes</strong></td><td>每个队友获得自己的窗格</td><td>需要同时看到所有人的输出</td></tr></tbody></table><p><strong>配置显示模式：</strong></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></pre></td><td class="code"><pre><span class="line"><span class="comment">// settings.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;teammateMode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;auto&quot;</span>    <span class="comment">// 自动选择：tmux 会话中用分割窗格，否则用 in-process</span></span><br><span class="line">  <span class="comment">// 或</span></span><br><span class="line">  <span class="attr">&quot;teammateMode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tmux&quot;</span>   <span class="comment">// 强制使用分割窗格（需要 tmux 或 iTerm2）</span></span><br><span class="line">  <span class="comment">// 或</span></span><br><span class="line">  <span class="attr">&quot;teammateMode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;in-process&quot;</span>  <span class="comment">// 强制使用 in-process 模式</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>In-process 模式操作：</strong></p><ul><li><code>Shift+Down</code>：循环浏览队友</li><li><code>Enter</code>：查看队友会话</li><li><code>Escape</code>：中断队友当前轮次</li><li><code>Ctrl+T</code>：切换任务列表</li></ul><p><strong>Split-pane 模式要求：</strong></p><ul><li><strong>tmux</strong>：通过系统包管理器安装</li><li><strong>iTerm2</strong>：安装 <code>it2</code> CLI，启用 <code>iTerm2 → Settings → General → Magic → Enable Python API</code></li></ul><h3 id="指定队友和模型"><a href="#指定队友和模型" class="headerlink" title="指定队友和模型"></a>指定队友和模型</h3><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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Create a team with 3 teammates:</span><br><span class="line">- A security reviewer using Opus</span><br><span class="line">- A performance analyzer using Sonnet</span><br><span class="line">- A test coverage checker using Haiku</span><br></pre></td></tr></table></figure><h3 id="要求计划批准"><a href="#要求计划批准" class="headerlink" title="要求计划批准"></a>要求计划批准</h3><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">Create a team to refactor the authentication system. Each teammate</span><br><span class="line">should plan their approach first and wait for approval before</span><br><span class="line">implementing changes.</span><br></pre></td></tr></table></figure><p>队友在只读计划模式下工作，直到负责人批准。</p><h3 id="直接与队友交谈"><a href="#直接与队友交谈" class="headerlink" title="直接与队友交谈"></a>直接与队友交谈</h3><p>每个队友都是完整的独立会话：</p><ul><li><strong>In-process 模式</strong>：用 <code>Shift+Down</code> 循环到队友，然后输入消息</li><li><strong>Split-pane 模式</strong>：点击队友窗格直接交互</li></ul><h3 id="任务分配与认领"><a href="#任务分配与认领" class="headerlink" title="任务分配与认领"></a>任务分配与认领</h3><p>共享任务列表协调整个团队：</p><p><strong>任务状态：</strong></p><ul><li>待处理（Pending）</li><li>进行中（In Progress）</li><li>已完成（Completed）</li></ul><p><strong>任务依赖：</strong><br>具有未解决依赖关系的待处理任务在这些依赖关系完成之前无法被认领。</p><p><strong>分配方式：</strong></p><ul><li><strong>负责人分配</strong>：告诉负责人将哪个任务分配给哪个队友</li><li><strong>自我认领</strong>：完成任务后，队友自己选择下一个未分配的任务</li></ul><h3 id="关闭队友与清理团队"><a href="#关闭队友与清理团队" class="headerlink" title="关闭队友与清理团队"></a>关闭队友与清理团队</h3><p><strong>关闭特定队友：</strong></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">Close the security-reviewer teammate</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></pre></td><td class="code"><pre><span class="line">Clean up the team</span><br></pre></td></tr></table></figure><h3 id="使用-Hooks-强制质量门"><a href="#使用-Hooks-强制质量门" class="headerlink" title="使用 Hooks 强制质量门"></a>使用 Hooks 强制质量门</h3><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><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="comment">// settings.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;TeammateIdle&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;matcher&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;command&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;scripts/check-work-quality.sh&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;TaskCompleted&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;matcher&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;command&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;scripts/validate-completion.sh&quot;</span> <span class="punctuation">&#125;</span><span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><ul><li><code>TeammateIdle</code>：队友即将空闲时运行。以代码 2 退出可发送反馈并保持队友工作</li><li><code>TaskCompleted</code>：任务被标记完成时运行。以代码 2 退出可阻止完成并发送反馈</li></ul><hr><h2 id="Agent-Teams-工作原理"><a href="#Agent-Teams-工作原理" class="headerlink" title="Agent Teams 工作原理"></a>Agent Teams 工作原理</h2><h3 id="启动方式"><a href="#启动方式" class="headerlink" title="启动方式"></a>启动方式</h3><p><strong>方式一：你请求一个团队</strong><br>给 Claude 一个受益于并行工作的任务，并明确要求创建 agent team。</p><p><strong>方式二：Claude 提议一个团队</strong><br>如果 Claude 确定你的任务将受益于并行工作，它可能会建议创建一个团队，你在继续之前确认。</p><h3 id="文件存储"><a href="#文件存储" class="headerlink" title="文件存储"></a>文件存储</h3><table><thead><tr><th>文件</th><th>位置</th></tr></thead><tbody><tr><td>Team config</td><td><code>~/.claude/teams/&#123;team-name&#125;/config.json</code></td></tr><tr><td>Task list</td><td><code>~/.claude/tasks/&#123;team-name&#125;/</code></td></tr></tbody></table><p>配置文件包含 <code>members</code> 数组，其中有每个队友的名称、代理 ID 和代理类型。队友可以读取此文件发现其他团队成员。</p><h3 id="权限"><a href="#权限" class="headerlink" title="权限"></a>权限</h3><p>队友从负责人的权限设置开始。如果负责人使用 <code>--dangerously-skip-permissions</code> 运行，所有队友也会这样做。</p><p>生成后可以更改个别队友模式，但在生成时无法设置每个队友的模式。</p><h3 id="Context-和通信"><a href="#Context-和通信" class="headerlink" title="Context 和通信"></a>Context 和通信</h3><p><strong>Context 加载：</strong><br>队友生成时加载与常规会话相同的项目 context：</p><ul><li>CLAUDE.md</li><li>MCP servers</li><li>Skills</li><li>来自负责人的生成提示</li></ul><p><strong>注意</strong>：负责人的对话历史不会继承。</p><p><strong>通信机制：</strong></p><ul><li><strong>自动消息传递</strong>：消息自动传递给收件人，负责人不需要轮询更新</li><li><strong>空闲通知</strong>：队友完成并停止时，自动通知负责人</li><li><strong>共享任务列表</strong>：所有代理可看到任务状态并认领工作</li></ul><p><strong>消息类型：</strong></p><ul><li><code>message</code>：向特定队友发送消息</li><li><code>broadcast</code>：同时发送给所有队友（谨慎使用，成本随团队规模增加）</li></ul><h3 id="令牌使用"><a href="#令牌使用" class="headerlink" title="令牌使用"></a>令牌使用</h3><p>Agent Teams 使用明显更多的令牌。每个队友有自己的 context window，令牌使用量随活跃队友数量增加。</p><p>对于研究、审查和新功能工作，额外的令牌通常是值得的。对于日常任务，单个会话更具成本效益。</p><hr><h2 id="用例示例"><a href="#用例示例" class="headerlink" title="用例示例"></a>用例示例</h2><h3 id="并行代码审查"><a href="#并行代码审查" class="headerlink" title="并行代码审查"></a>并行代码审查</h3><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><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">Create an agent team to review PR #42:</span><br><span class="line">- Teammate 1: Focus exclusively on security vulnerabilities</span><br><span class="line">- Teammate 2: Focus on performance issues and optimization opportunities</span><br><span class="line">- Teammate 3: Focus on test coverage and test quality</span><br><span class="line"></span><br><span class="line">Each teammate should review independently, then share findings</span><br><span class="line">and critique each other&#x27;s conclusions.</span><br></pre></td></tr></table></figure><h3 id="使用竞争假设进行调查"><a href="#使用竞争假设进行调查" class="headerlink" title="使用竞争假设进行调查"></a>使用竞争假设进行调查</h3><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><span class="line">4</span><br><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">Create an agent team to investigate why users are experiencing</span><br><span class="line">slow load times. Each teammate should pursue a different hypothesis:</span><br><span class="line"></span><br><span class="line">- Teammate 1: Investigate database query performance</span><br><span class="line">- Teammate 2: Investigate network latency and CDN issues</span><br><span class="line">- Teammate 3: Investigate client-side rendering performance</span><br><span class="line"></span><br><span class="line">Work in parallel, then share findings. Each teammate should also</span><br><span class="line">try to disprove the others&#x27; hypotheses.</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></pre></td><td class="code"><pre><span class="line">Create an agent team to implement the user profile feature:</span><br><span class="line">- Teammate 1: Backend API endpoints and database schema</span><br><span class="line">- Teammate 2: Frontend components and UI</span><br><span class="line">- Teammate 3: Integration tests and documentation</span><br><span class="line"></span><br><span class="line">Coordinate on the API contract first, then work in parallel.</span><br></pre></td></tr></table></figure><hr><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="给队友足够的-Context"><a href="#给队友足够的-Context" class="headerlink" title="给队友足够的 Context"></a>给队友足够的 Context</h3><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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Create a team to implement the payment flow.</span><br><span class="line">Context: We&#x27;re using Stripe API v2024, the checkout happens</span><br><span class="line">in /src/checkout/, and we need to support both card and</span><br><span class="line">bank transfer payments.</span><br></pre></td></tr></table></figure><h3 id="选择适当的团队规模"><a href="#选择适当的团队规模" class="headerlink" title="选择适当的团队规模"></a>选择适当的团队规模</h3><table><thead><tr><th>规模</th><th>适用场景</th></tr></thead><tbody><tr><td>2-3 队友</td><td>大多数任务的最佳规模</td></tr><tr><td>4-5 队友</td><td>大型、复杂的任务</td></tr><tr><td>6+ 队友</td><td>罕见，收益递减明显</td></tr></tbody></table><p><strong>约束因素：</strong></p><ul><li>令牌成本线性增加</li><li>协调开销增加</li><li>收益递减</li></ul><h3 id="适当调整任务大小"><a href="#适当调整任务大小" class="headerlink" title="适当调整任务大小"></a>适当调整任务大小</h3><table><thead><tr><th>大小</th><th>问题</th></tr></thead><tbody><tr><td>太小</td><td>协调开销超过收益</td></tr><tr><td>太大</td><td>队友工作太长时间不检查，增加浪费风险</td></tr><tr><td>恰到好处</td><td>自包含的单位，产生清晰的可交付成果</td></tr></tbody></table><h3 id="等待队友完成"><a href="#等待队友完成" class="headerlink" title="等待队友完成"></a>等待队友完成</h3><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">Wait for all teammates to finish their tasks before continuing.</span><br><span class="line">Don&#x27;t start implementing anything yourself.</span><br></pre></td></tr></table></figure><h3 id="从研究和审查开始"><a href="#从研究和审查开始" class="headerlink" title="从研究和审查开始"></a>从研究和审查开始</h3><p>新手建议从不需要编写代码的任务开始：</p><ul><li>审查 PR</li><li>研究库</li><li>调查错误</li></ul><p>这些任务展示并行探索的价值，没有并行实施的协调挑战。</p><h3 id="避免文件冲突"><a href="#避免文件冲突" class="headerlink" title="避免文件冲突"></a>避免文件冲突</h3><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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Assign distinct file sets to each teammate:</span><br><span class="line">- Teammate 1: src/api/* only</span><br><span class="line">- Teammate 2: src/components/* only</span><br><span class="line">- Teammate 3: tests/* only</span><br></pre></td></tr></table></figure><h3 id="监控和指导"><a href="#监控和指导" class="headerlink" title="监控和指导"></a>监控和指导</h3><p>检查队友进度，重定向不起作用的方法，综合发现。让团队无人值守运行太长时间增加浪费风险。</p><hr><h2 id="故障排除"><a href="#故障排除" class="headerlink" title="故障排除"></a>故障排除</h2><h3 id="队友未出现"><a href="#队友未出现" class="headerlink" title="队友未出现"></a>队友未出现</h3><p><strong>可能原因和解决方案：</strong></p><table><thead><tr><th>问题</th><th>解决方案</th></tr></thead><tbody><tr><td>In-process 模式中不可见</td><td>按 <code>Shift+Down</code> 循环浏览活跃队友</td></tr><tr><td>任务不够复杂</td><td>确保任务足够复杂以保证团队</td></tr><tr><td>tmux 未安装（分割模式）</td><td>安装 tmux 并确保在 PATH 中</td></tr><tr><td>iTerm2 Python API 未启用</td><td>启用 iTerm2 → Settings → General → Magic → Enable Python API</td></tr></tbody></table><h3 id="过多权限提示"><a href="#过多权限提示" class="headerlink" title="过多权限提示"></a>过多权限提示</h3><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><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// settings.json</span></span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;permissions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;allow&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;Read(**)&quot;</span><span class="punctuation">,</span> <span class="string">&quot;Bash(npm run:*)&quot;</span><span class="punctuation">,</span> <span class="string">&quot;Bash(git status:*)&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="队友在错误时停止"><a href="#队友在错误时停止" class="headerlink" title="队友在错误时停止"></a>队友在错误时停止</h3><p>使用 <code>Shift+Down</code>（in-process）或点击窗格（分割模式）检查输出：</p><ul><li>直接给予额外指示</li><li>生成替代队友继续工作</li></ul><h3 id="负责人过早关闭"><a href="#负责人过早关闭" class="headerlink" title="负责人过早关闭"></a>负责人过早关闭</h3><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">Continue. Wait for all teammates to complete before closing the team.</span><br></pre></td></tr></table></figure><h3 id="孤立的-tmux-会话"><a href="#孤立的-tmux-会话" class="headerlink" title="孤立的 tmux 会话"></a>孤立的 tmux 会话</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">tmux list-sessions</span><br><span class="line">tmux kill-session -t &lt;session-name&gt;</span><br></pre></td></tr></table></figure><hr><h2 id="当前限制"><a href="#当前限制" class="headerlink" title="当前限制"></a>当前限制</h2><p>Agent Teams 是实验性功能，存在以下限制：</p><table><thead><tr><th>限制</th><th>说明</th></tr></thead><tbody><tr><td>In-process 队友没有会话恢复</td><td><code>/resume</code> 和 <code>/rewind</code> 不会恢复 in-process 队友</td></tr><tr><td>任务状态可能滞后</td><td>队友有时无法将任务标记为已完成，需手动更新</td></tr><tr><td>关闭可能很慢</td><td>队友完成当前请求或工具调用后才关闭</td></tr><tr><td>每个会话一个团队</td><td>负责人一次只能管理一个团队</td></tr><tr><td>没有嵌套团队</td><td>队友无法生成自己的团队或队友</td></tr><tr><td>负责人是固定的</td><td>无法将队友提升为负责人或转移领导权</td></tr><tr><td>权限在生成时设置</td><td>无法在生成时为每个队友设置不同权限模式</td></tr><tr><td>分割窗格需要 tmux 或 iTerm2</td><td>VS Code 终端、Windows Terminal、Ghostty 不支持分割模式</td></tr></tbody></table><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Agent Teams 是 Claude Code 的高级特性，适合需要并行协作的复杂任务：</p><p><strong>核心优势：</strong></p><ul><li>队友可以直接相互通信和协作</li><li>通过共享任务列表自我协调</li><li>并行探索增加真实价值</li></ul><p><strong>与 Subagents 选择指南：</strong></p><table><thead><tr><th>场景</th><th>推荐方案</th></tr></thead><tbody><tr><td>只需要结果，不需要协作</td><td>Subagents</td></tr><tr><td>需要队友间讨论和协作</td><td>Agent Teams</td></tr><tr><td>成本敏感</td><td>Subagents</td></tr><tr><td>复杂研究、竞争假设调查</td><td>Agent Teams</td></tr><tr><td>简单的代码搜索</td><td>Subagents</td></tr><tr><td>跨层协调、多角度审查</td><td>Agent Teams</td></tr></tbody></table><p><strong>最佳实践总结：</strong></p><ol><li>从研究和审查任务开始熟悉 Agent Teams</li><li>控制团队规模（2-4 队友最佳）</li><li>给队友足够的任务 context</li><li>避免文件冲突，分配不同的文件集</li><li>监控进度，及时指导</li><li>使用 hooks 强制质量门</li></ol><p>通过合理使用 Agent Teams，可以让多个 Claude 实例像真正的团队一样协作，大幅提升复杂任务的效率。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2026/03/22/Claude-Code-Agent-Teams-%E6%8C%87%E5%8D%97/</id>
    <link href="https://fuyongde.github.io/2026/03/22/Claude-Code-Agent-Teams-%E6%8C%87%E5%8D%97/"/>
    <published>2026-03-22T03:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Agent Teams 让多个 Claude Code 实例并行工作并通过共享任务列表相互协调。与 subagents 不同，agent teams 中的队友可以直接相互通信、协作和分享发现，适合需要团队协作的复杂任务。</p>
<p>本文将详细介绍 Agent Teams 的启用方式、使用方法、最佳实践以及与 Subagents 的区别。</p>
<h2 id="Agent-Teams-概述"><a href="#Agent-Teams-概述" class="headerlink" title="Agent Teams 概述"></a>Agent Teams 概述</h2><h3 id="什么是-Agent-Teams"><a href="#什么是-Agent-Teams" class="headerlink" title="什么是 Agent Teams"></a>什么是 Agent Teams</h3><p>Agent Teams 是一组并行工作的 Claude Code 实例，通过共享任务列表协调工作。每个队友都是完整的、独立的 Claude Code 会话，拥有自己的 context window。</p>
<p><strong>核心组件：</strong></p>
<table>
<thead>
<tr>
<th>组件</th>
<th>角色</th>
</tr>
</thead>
<tbody><tr>
<td>Team Lead</td>
<td>创建团队、生成队友并协调工作的主 Claude Code 会话</td>
</tr>
<tr>
<td>Teammates</td>
<td>各自处理分配任务的独立 Claude Code 实例</td>
</tr>
<tr>
<td>Task List</td>
<td>队友认领和完成的共享工作项列表</td>
</tr>
<tr>
<td>Mailbox</td>
<td>代理之间通信的消息系统</td>
</tr>
</tbody></table>
<h3 id="与-Subagents-的区别"><a href="#与-Subagents-的区别" class="headerlink" title="与 Subagents 的区别"></a>与 Subagents 的区别</h3><p>Agent Teams 和 Subagents 都支持并行工作，但运作方式不同：</p>
<table>
<thead>
<tr>
<th>特性</th>
<th>Subagents</th>
<th>Agent Teams</th>
</tr>
</thead>
<tbody><tr>
<td>Context</td>
<td>自己的 context window；结果返回给调用者</td>
<td>自己的 context window；完全独立</td>
</tr>
<tr>
<td>通信</td>
<td>仅向主代理报告结果</td>
<td>队友直接相互发送消息</td>
</tr>
<tr>
<td>协调</td>
<td>主代理管理所有工作</td>
<td>共享任务列表，自我协调</td>
</tr>
<tr>
<td>最适合</td>
<td>只有结果重要的专注任务</td>
<td>需要讨论和协作的复杂工作</td>
</tr>
<tr>
<td>令牌成本</td>
<td>较低：结果汇总回主 context</td>
<td>较高：每个队友是独立的 Claude 实例</td>
</tr>
</tbody></table>]]>
    </summary>
    <title>Claude Code Agent Teams 指南</title>
    <updated>2026-03-24T02:31:03.349Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="AI" scheme="https://fuyongde.github.io/categories/AI/"/>
    <category term="Claude Code" scheme="https://fuyongde.github.io/categories/AI/Claude-Code/"/>
    <category term="AI" scheme="https://fuyongde.github.io/tags/AI/"/>
    <category term="Sub-Agent" scheme="https://fuyongde.github.io/tags/Sub-Agent/"/>
    <content>
      <![CDATA[<p>Subagents 是 Claude Code 中处理特定类型任务的专门 AI 助手。每个 subagent 在自己独立的 context window 中运行，具有自定义系统提示、特定的工具访问权限和独立的权限控制。当 Claude 遇到与 subagent 描述相匹配的任务时，它会委托给该 subagent，subagent 独立工作后返回结果。</p><p>本文将从创建方法、使用场景和最佳实践三个维度，全面介绍 Claude Code Sub-Agents 的使用。</p><h2 id="如何创建-Sub-Agent"><a href="#如何创建-Sub-Agent" class="headerlink" title="如何创建 Sub-Agent"></a>如何创建 Sub-Agent</h2><h3 id="使用-agents-命令（推荐方式）"><a href="#使用-agents-命令（推荐方式）" class="headerlink" title="使用 /agents 命令（推荐方式）"></a>使用 <code>/agents</code> 命令（推荐方式）</h3><p><code>/agents</code> 命令提供了交互式界面来管理 subagents，这是推荐的创建方式：</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">/agents</span><br></pre></td></tr></table></figure><p>在交互界面中可以：</p><ul><li>查看所有可用的 subagents（内置、用户、项目和插件）</li><li>使用引导式设置或 Claude 生成创建新的 subagents</li><li>编辑现有 subagent 配置和工具访问权限</li><li>删除自定义 subagents</li></ul><p>创建步骤示例：</p><ol><li><p>选择 <strong>Create new agent</strong></p></li><li><p>选择范围：<strong>User-level</strong>（用户级）或 <strong>Project-level</strong>（项目级）</p></li><li><p>选择 <strong>Generate with Claude</strong> 让 Claude 自动生成配置</p></li><li><p>描述你的 subagent，例如：</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">A code improvement agent that scans files and suggests improvements</span><br><span class="line">for readability, performance, and best practices.</span><br></pre></td></tr></table></figure></li><li><p>选择工具权限（只读审查者只需选择 Read-only tools）</p></li><li><p>选择模型（Sonnet 平衡能力和速度，Haiku 更快更便宜）</p></li><li><p>保存后立即可用，无需重启</p></li></ol><h3 id="手动创建-YAML-Frontmatter-文件"><a href="#手动创建-YAML-Frontmatter-文件" class="headerlink" title="手动创建 YAML Frontmatter 文件"></a>手动创建 YAML Frontmatter 文件</h3><p>Subagent 文件使用 YAML frontmatter 进行配置，后跟 Markdown 格式的系统提示：</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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: code-reviewer</span><br><span class="line">description: Expert code reviewer. Use proactively after code changes.</span><br><span class="line">model: sonnet</span><br><span class="line">tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Grep</span><br><span class="line"><span class="bullet">  -</span> Glob</span><br><span class="line"><span class="section">  - Bash</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">You are a senior code reviewer with expertise in software best practices.</span><br><span class="line"></span><br><span class="line">Your responsibilities:</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> Review code for quality, security, and maintainability</span><br><span class="line"><span class="bullet">2.</span> Identify potential bugs and performance issues</span><br><span class="line"><span class="bullet">3.</span> Suggest improvements with clear explanations</span><br><span class="line"></span><br><span class="line">When reviewing:</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> Focus on meaningful issues, not style nitpicks</span><br><span class="line"><span class="bullet">-</span> Explain WHY something is an issue</span><br><span class="line"><span class="bullet">-</span> Provide concrete suggestions for improvement</span><br><span class="line"><span class="bullet">-</span> Consider the context and purpose of the code</span><br><span class="line"></span><br><span class="line">Format your output as:</span><br><span class="line"></span><br><span class="line"><span class="section">## Summary</span></span><br><span class="line"></span><br><span class="line">[Brief overview of changes reviewed]</span><br><span class="line"></span><br><span class="line"><span class="section">## Issues Found</span></span><br><span class="line"></span><br><span class="line">[List of issues with severity and location]</span><br><span class="line"></span><br><span class="line"><span class="section">## Suggestions</span></span><br><span class="line"></span><br><span class="line">[Actionable improvement recommendations]</span><br></pre></td></tr></table></figure><h3 id="文件存储位置与优先级"><a href="#文件存储位置与优先级" class="headerlink" title="文件存储位置与优先级"></a>文件存储位置与优先级</h3><table><thead><tr><th>位置</th><th>范围</th><th>优先级</th><th>说明</th></tr></thead><tbody><tr><td><code>--agents</code> CLI 标志</td><td>当前会话</td><td>1（最高）</td><td>启动时传递 JSON</td></tr><tr><td><code>.claude/agents/</code></td><td>当前项目</td><td>2</td><td>提交到版本控制，团队共享</td></tr><tr><td><code>~/.claude/agents/</code></td><td>所有项目</td><td>3</td><td>个人 subagents，跨项目可用</td></tr><tr><td>插件的 <code>agents/</code> 目录</td><td>插件启用处</td><td>4（最低）</td><td>随插件安装</td></tr></tbody></table><p><strong>项目级 subagents</strong>（<code>.claude/agents/</code>）适合特定于代码库的 subagents，建议提交到版本控制让团队协作使用。</p><p><strong>用户级 subagents</strong>（<code>~/.claude/agents/</code>）适合个人在所有项目中复用的 subagents。</p><h3 id="完整配置字段说明"><a href="#完整配置字段说明" class="headerlink" title="完整配置字段说明"></a>完整配置字段说明</h3><p>以下是 YAML frontmatter 支持的所有字段：</p><table><thead><tr><th>字段</th><th>必需</th><th>说明</th></tr></thead><tbody><tr><td><code>name</code></td><td>✅</td><td>唯一标识符，使用小写字母和连字符</td></tr><tr><td><code>description</code></td><td>✅</td><td>描述何时应该委托给此 subagent</td></tr><tr><td><code>model</code></td><td>❌</td><td>使用的模型：<code>sonnet</code>、<code>opus</code>、<code>haiku</code>、完整模型 ID 或 <code>inherit</code>（默认）</td></tr><tr><td><code>tools</code></td><td>❌</td><td>允许使用的工具列表。省略则继承所有工具</td></tr><tr><td><code>disallowedTools</code></td><td>❌</td><td>禁用的工具列表</td></tr><tr><td><code>permissionMode</code></td><td>❌</td><td>权限模式：<code>default</code>、<code>acceptEdits</code>、<code>dontAsk</code>、<code>bypassPermissions</code>、<code>plan</code></td></tr><tr><td><code>maxTurns</code></td><td>❌</td><td>subagent 停止前最大执行轮数</td></tr><tr><td><code>skills</code></td><td>❌</td><td>启动时加载到上下文的技能</td></tr><tr><td><code>mcpServers</code></td><td>❌</td><td>此 subagent 可用的 MCP 服务器</td></tr><tr><td><code>hooks</code></td><td>❌</td><td>生命周期钩子</td></tr><tr><td><code>memory</code></td><td>❌</td><td>持久化内存范围：<code>user</code>、<code>project</code>、<code>local</code></td></tr><tr><td><code>background</code></td><td>❌</td><td>设为 <code>true</code> 总是作为后台任务运行</td></tr><tr><td><code>effort</code></td><td>❌</td><td>努力级别：<code>low</code>、<code>medium</code>、<code>high</code>、<code>max</code></td></tr><tr><td><code>isolation</code></td><td>❌</td><td>设为 <code>worktree</code> 在隔离的 git worktree 中运行</td></tr></tbody></table><h3 id="模型选择策略"><a href="#模型选择策略" class="headerlink" title="模型选择策略"></a>模型选择策略</h3><ul><li><strong>Haiku</strong>：快速、低延迟，适合只读探索、代码搜索</li><li><strong>Sonnet</strong>：平衡能力和速度，适合大多数分析任务</li><li><strong>Opus</strong>：最强能力，适合复杂推理任务</li><li><strong>inherit</strong>：继承主对话的模型（默认）</li></ul><h3 id="工具控制"><a href="#工具控制" class="headerlink" title="工具控制"></a>工具控制</h3><p><strong>使用 <code>tools</code> 字段（白名单）：</strong></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">tools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Read</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Grep</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Glob</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Bash</span></span><br></pre></td></tr></table></figure><p><strong>使用 <code>disallowedTools</code> 字段（黑名单）：</strong></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">disallowedTools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Write</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Edit</span></span><br></pre></td></tr></table></figure><p><strong>优先级</strong>：先应用 <code>disallowedTools</code>，再解析 <code>tools</code>。</p><h3 id="持久化内存"><a href="#持久化内存" class="headerlink" title="持久化内存"></a>持久化内存</h3><p>通过 <code>memory</code> 字段启用跨会话的知识积累：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">memory:</span> <span class="string">project</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>范围</th><th>位置</th><th>适用场景</th></tr></thead><tbody><tr><td><code>user</code></td><td><code>~/.claude/agent-memory/&lt;name&gt;/</code></td><td>跨项目的通用知识</td></tr><tr><td><code>project</code></td><td><code>.claude/agent-memory/&lt;name&gt;/</code></td><td>项目特定知识，可版本控制</td></tr><tr><td><code>local</code></td><td><code>.claude/agent-memory-local/&lt;name&gt;/</code></td><td>项目特定但不应提交的知识</td></tr></tbody></table><hr><h2 id="Sub-Agent-使用场景"><a href="#Sub-Agent-使用场景" class="headerlink" title="Sub-Agent 使用场景"></a>Sub-Agent 使用场景</h2><h3 id="内置-Subagents"><a href="#内置-Subagents" class="headerlink" title="内置 Subagents"></a>内置 Subagents</h3><p>Claude Code 包含以下内置 subagents：</p><p><strong>Explore Agent</strong></p><ul><li><strong>模型</strong>：Haiku（快速、低延迟）</li><li><strong>工具</strong>：只读工具（拒绝 Write 和 Edit）</li><li><strong>用途</strong>：文件发现、代码搜索、代码库探索</li><li><strong>调用方式</strong>：Claude 自动委托，支持三种彻底程度：<code>quick</code>、<code>medium</code>、<code>very thorough</code></li></ul><p><strong>Plan Agent</strong></p><ul><li><strong>模型</strong>：继承主对话</li><li><strong>工具</strong>：只读工具</li><li><strong>用途</strong>：Plan mode 下收集上下文的研究代理</li></ul><p><strong>General-purpose Agent</strong></p><ul><li><strong>模型</strong>：继承主对话</li><li><strong>工具</strong>：所有工具</li><li><strong>用途</strong>：复杂研究、多步骤操作、代码修改</li></ul><h3 id="典型使用场景"><a href="#典型使用场景" class="headerlink" title="典型使用场景"></a>典型使用场景</h3><h4 id="隔离大量输出的操作"><a href="#隔离大量输出的操作" class="headerlink" title="隔离大量输出的操作"></a>隔离大量输出的操作</h4><p>运行测试、获取文档、处理日志文件等操作会产生大量输出，消耗大量上下文。通过委托给 subagent，冗长的输出保留在 subagent 的上下文中，只有相关摘要返回到主对话。</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">Run the test suite in the background and report any failures</span><br></pre></td></tr></table></figure><h4 id="并行研究"><a href="#并行研究" class="headerlink" title="并行研究"></a>并行研究</h4><p>对于独立的调查任务，可以同时启动多个 subagents：</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">In parallel:</span><br><span class="line">1. Use the explore agent to find all API endpoint definitions</span><br><span class="line">2. Use the explore agent to find all database schema files</span><br><span class="line">3. Then compare them for consistency</span><br></pre></td></tr></table></figure><h4 id="链式工作流"><a href="#链式工作流" class="headerlink" title="链式工作流"></a>链式工作流</h4><p>对于多步骤工作流，可以按顺序使用 subagents：</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">1. Use code-reviewer to review the changes in src/auth/</span><br><span class="line">2. Based on the review, implement the suggested improvements</span><br><span class="line">3. Run tests to verify the fixes work</span><br></pre></td></tr></table></figure><h4 id="强制执行约束"><a href="#强制执行约束" class="headerlink" title="强制执行约束"></a>强制执行约束</h4><p>通过限制 subagent 可用的工具来强制执行特定约束：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">read-only-analyzer</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">Analyzes</span> <span class="string">code</span> <span class="string">without</span> <span class="string">making</span> <span class="string">any</span> <span class="string">changes</span></span><br><span class="line"><span class="attr">tools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Read</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Grep</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Glob</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><h4 id="成本控制"><a href="#成本控制" class="headerlink" title="成本控制"></a>成本控制</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">quick-search</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">Fast</span> <span class="string">code</span> <span class="string">search</span> <span class="string">using</span> <span class="string">Haiku</span></span><br><span class="line"><span class="attr">model:</span> <span class="string">haiku</span></span><br><span class="line"><span class="attr">tools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Grep</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Glob</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><h3 id="调用方式"><a href="#调用方式" class="headerlink" title="调用方式"></a>调用方式</h3><p><strong>自动委托</strong></p><p>Claude 根据任务描述和 subagent 的 <code>description</code> 字段自动决定是否委托。在描述中包含 “use proactively” 等短语可鼓励主动委托。</p><p><strong>自然语言调用</strong></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">Use the code-reviewer agent to review this PR</span><br></pre></td></tr></table></figure><p><strong>@-mention 调用</strong></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">@code-reviewer review the changes in src/</span><br></pre></td></tr></table></figure><p><strong>会话级启动</strong></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">claude --agent code-reviewer</span><br></pre></td></tr></table></figure><p>或在 <code>.claude/settings.json</code> 中设置默认：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;agent&quot;</span><span class="punctuation">:</span> <span class="string">&quot;code-reviewer&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="前台与后台运行"><a href="#前台与后台运行" class="headerlink" title="前台与后台运行"></a>前台与后台运行</h3><ul><li><strong>前台 subagents</strong>：阻塞主对话直到完成，权限提示和澄清问题会传递给你</li><li><strong>后台 subagents</strong>：并发运行，你可以继续工作</li></ul><p>启动后台任务的方式：</p><ul><li>请求 Claude “run this in the background”</li><li>按 <strong>Ctrl+B</strong> 将运行中的任务放入后台</li></ul><hr><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><h3 id="编写清晰的描述"><a href="#编写清晰的描述" class="headerlink" title="编写清晰的描述"></a>编写清晰的描述</h3><p>Claude 使用 <code>description</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 好的描述</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">Expert</span> <span class="string">code</span> <span class="string">reviewer</span> <span class="string">for</span> <span class="string">security</span> <span class="string">vulnerabilities.</span> <span class="string">Use</span> <span class="string">proactively</span> <span class="string">after</span> <span class="string">code</span> <span class="string">changes</span> <span class="string">to</span> <span class="string">check</span> <span class="string">for</span> <span class="string">SQL</span> <span class="string">injection,</span> <span class="string">XSS,</span> <span class="string">and</span> <span class="string">other</span> <span class="string">OWASP</span> <span class="string">Top</span> <span class="number">10</span> <span class="string">issues.</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 差的描述</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">Reviews</span> <span class="string">code</span></span><br></pre></td></tr></table></figure><h3 id="选择合适的范围"><a href="#选择合适的范围" class="headerlink" title="选择合适的范围"></a>选择合适的范围</h3><ul><li><strong>项目级</strong>：特定于代码库的 subagents，如了解项目架构的审查者</li><li><strong>用户级</strong>：跨项目复用的通用 subagents，如通用的代码风格审查者</li><li><strong>CLI 定义</strong>：快速测试或自动化脚本</li></ul><h3 id="限制工具访问"><a href="#限制工具访问" class="headerlink" title="限制工具访问"></a>限制工具访问</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 只读审查者</span></span><br><span class="line"><span class="attr">tools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Read</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Grep</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Glob</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 允许修复的调试者</span></span><br><span class="line"><span class="attr">tools:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Read</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Grep</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Glob</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Edit</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">Bash</span></span><br></pre></td></tr></table></figure><h3 id="利用持久化内存"><a href="#利用持久化内存" class="headerlink" title="利用持久化内存"></a>利用持久化内存</h3><p>在 subagent 的系统提示中包含内存维护指令：</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><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: project-expert</span><br><span class="line"><span class="section">memory: project</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">Before starting any task, check your memory in .claude/agent-memory/project-expert/</span><br><span class="line">for relevant patterns and architectural decisions.</span><br><span class="line"></span><br><span class="line">After completing work, save important learnings to MEMORY.md:</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> Code patterns discovered</span><br><span class="line"><span class="bullet">-</span> Common pitfalls identified</span><br><span class="line"><span class="bullet">-</span> Architectural decisions made</span><br></pre></td></tr></table></figure><h3 id="使用-Hooks-进行条件验证"><a href="#使用-Hooks-进行条件验证" class="headerlink" title="使用 Hooks 进行条件验证"></a>使用 Hooks 进行条件验证</h3><p>当需要比 <code>tools</code> 字段更细粒度的控制时，使用 <code>PreToolUse</code> hooks：</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="meta">---</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">db-query-validator</span></span><br><span class="line"><span class="attr">hooks:</span></span><br><span class="line">  <span class="attr">PreToolUse:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">matcher:</span> <span class="string">Bash</span></span><br><span class="line">      <span class="attr">hooks:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">type:</span> <span class="string">command</span></span><br><span class="line">          <span class="attr">command:</span> <span class="string">scripts/validate-sql-readonly.sh</span></span><br><span class="line"><span class="meta">---</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><span class="line">4</span><br><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">#!/bin/bash</span></span><br><span class="line"><span class="comment"># 只允许只读 SQL 查询</span></span><br><span class="line"></span><br><span class="line">COMMAND=<span class="string">&quot;<span class="variable">$TOOL_INPUT_COMMAND</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$COMMAND</span>&quot;</span> | grep -qiE <span class="string">&quot;(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE)&quot;</span>; <span class="keyword">then</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Error: Only read-only SQL queries are allowed&quot;</span></span><br><span class="line">  <span class="built_in">exit</span> 2  <span class="comment"># 阻止操作</span></span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">exit</span> 0  <span class="comment"># 允许操作</span></span><br></pre></td></tr></table></figure><h3 id="选择模型的经济性"><a href="#选择模型的经济性" class="headerlink" title="选择模型的经济性"></a>选择模型的经济性</h3><table><thead><tr><th>任务类型</th><th>推荐模型</th><th>原因</th></tr></thead><tbody><tr><td>代码搜索、文件发现</td><td>Haiku</td><td>快速、低成本</td></tr><tr><td>代码审查、分析</td><td>Sonnet</td><td>平衡能力和速度</td></tr><tr><td>复杂推理、架构设计</td><td>Opus</td><td>最强推理能力</td></tr></tbody></table><h3 id="何时使用-Subagent-vs-主对话"><a href="#何时使用-Subagent-vs-主对话" class="headerlink" title="何时使用 Subagent vs 主对话"></a>何时使用 Subagent vs 主对话</h3><p><strong>使用主对话</strong>：</p><ul><li>任务需要频繁的来回交互或迭代优化</li><li>多个阶段共享大量上下文（规划 → 实现 → 测试）</li><li>做快速、针对性的更改</li><li>延迟很重要（subagents 需要时间收集上下文）</li></ul><p><strong>使用 Subagent</strong>：</p><ul><li>任务产生大量你不需要在主上下文中的输出</li><li>想强制执行特定的工具限制或权限</li><li>工作是独立的，可以返回摘要</li></ul><h3 id="禁用特定-Subagents"><a href="#禁用特定-Subagents" class="headerlink" title="禁用特定 Subagents"></a>禁用特定 Subagents</h3><p>在 <code>settings.json</code> 中：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;permissions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;deny&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;Agent(explore-agent)&quot;</span><span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="示例-Subagents"><a href="#示例-Subagents" class="headerlink" title="示例 Subagents"></a>示例 Subagents</h2><h3 id="代码审查者"><a href="#代码审查者" class="headerlink" title="代码审查者"></a>代码审查者</h3><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><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></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: code-reviewer</span><br><span class="line">description: Expert code reviewer. Use proactively after code changes to review for quality, security, and best practices.</span><br><span class="line">model: sonnet</span><br><span class="line">tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Grep</span><br><span class="line"><span class="section">  - Glob</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">You are a senior code reviewer with 15 years of experience.</span><br><span class="line"></span><br><span class="line">Review code for:</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> <span class="strong">**Security**</span>: SQL injection, XSS, auth issues, secrets in code</span><br><span class="line"><span class="bullet">2.</span> <span class="strong">**Performance**</span>: N+1 queries, inefficient algorithms, memory leaks</span><br><span class="line"><span class="bullet">3.</span> <span class="strong">**Maintainability**</span>: Code smell, complexity, naming conventions</span><br><span class="line"><span class="bullet">4.</span> <span class="strong">**Best Practices**</span>: SOLID principles, DRY, error handling</span><br><span class="line"></span><br><span class="line">Output format:</span><br><span class="line"></span><br><span class="line"><span class="section">## Summary</span></span><br><span class="line"></span><br><span class="line">[Brief overview]</span><br><span class="line"></span><br><span class="line"><span class="section">## Critical Issues</span></span><br><span class="line"></span><br><span class="line">[Security vulnerabilities, bugs]</span><br><span class="line"></span><br><span class="line"><span class="section">## Suggestions</span></span><br><span class="line"></span><br><span class="line">[Improvement recommendations]</span><br><span class="line"></span><br><span class="line"><span class="section">## Nitpicks</span></span><br><span class="line"></span><br><span class="line">[Minor style suggestions]</span><br></pre></td></tr></table></figure><h3 id="调试者"><a href="#调试者" class="headerlink" title="调试者"></a>调试者</h3><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><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><br><span class="line">name: debugger</span><br><span class="line">description: Debug agent that analyzes and fixes issues. Use when encountering bugs or errors.</span><br><span class="line">model: sonnet</span><br><span class="line">tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Grep</span><br><span class="line"><span class="bullet">  -</span> Glob</span><br><span class="line"><span class="bullet">  -</span> Edit</span><br><span class="line"><span class="section">  - Bash</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">You are a debugging specialist.</span><br><span class="line"></span><br><span class="line">Workflow:</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> <span class="strong">**Reproduce**</span>: Understand the error and how to trigger it</span><br><span class="line"><span class="bullet">2.</span> <span class="strong">**Analyze**</span>: Use logs, stack traces, and code inspection to find root cause</span><br><span class="line"><span class="bullet">3.</span> <span class="strong">**Fix**</span>: Implement minimal, targeted fix</span><br><span class="line"><span class="bullet">4.</span> <span class="strong">**Verify**</span>: Run tests or reproduction steps to confirm fix</span><br><span class="line"></span><br><span class="line">Always explain your reasoning before making changes.</span><br></pre></td></tr></table></figure><h3 id="数据科学家"><a href="#数据科学家" class="headerlink" title="数据科学家"></a>数据科学家</h3><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><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><br><span class="line">name: data-scientist</span><br><span class="line">description: Data analysis specialist for CSV, JSON, and database queries. Use for data exploration, statistics, and visualization tasks.</span><br><span class="line">model: sonnet</span><br><span class="line">tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Write</span><br><span class="line"><span class="section">  - Bash</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">You are a data scientist with expertise in Python, SQL, and data analysis.</span><br><span class="line"></span><br><span class="line">Capabilities:</span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> Analyze CSV, JSON, and structured data</span><br><span class="line"><span class="bullet">-</span> Write SQL queries for data extraction</span><br><span class="line"><span class="bullet">-</span> Create statistical summaries and visualizations</span><br><span class="line"><span class="bullet">-</span> Identify patterns and anomalies in data</span><br><span class="line"></span><br><span class="line">Use Python with pandas, numpy, and matplotlib for analysis.</span><br><span class="line">Output both findings and the code used to generate them.</span><br></pre></td></tr></table></figure><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Subagents 是 Claude Code 的强大特性，通过以下方式提升开发效率：</p><ol><li><strong>上下文隔离</strong>：将探索和实现保持在主对话之外，保留宝贵的上下文空间</li><li><strong>约束执行</strong>：通过限制工具访问强制执行安全边界</li><li><strong>配置复用</strong>：通过用户级 subagents 跨项目共享配置</li><li><strong>行为专业化</strong>：为特定领域使用专注的系统提示</li><li><strong>成本控制</strong>：将任务路由到更便宜的模型如 Haiku</li></ol><p>创建 subagent 时，记住：</p><ul><li>编写清晰的 <code>description</code> 让 Claude 知道何时使用</li><li>根据任务选择合适的工具权限</li><li>利用 <code>memory</code> 字段实现跨会话学习</li><li>使用 hooks 实现细粒度的条件控制</li></ul><p>通过合理使用 subagents，你可以让 Claude Code 成为一个更加高效、安全的编程助手。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2026/03/22/Claude-Code-Sub-Agents-%E6%8C%87%E5%8D%97/</id>
    <link href="https://fuyongde.github.io/2026/03/22/Claude-Code-Sub-Agents-%E6%8C%87%E5%8D%97/"/>
    <published>2026-03-22T02:00:00.000Z</published>
    <summary>
      <![CDATA[<p>Subagents 是 Claude Code 中处理特定类型任务的专门 AI 助手。每个 subagent 在自己独立的 context window 中运行，具有自定义系统提示、特定的工具访问权限和独立的权限控制。当 Claude 遇到与 subagent 描述相匹配的任务时，它会委托给该 subagent，subagent 独立工作后返回结果。</p>
<p>本文将从创建方法、使用场景和最佳实践三个维度，全面介绍 Claude Code Sub-Agents 的使用。</p>
<h2 id="如何创建-Sub-Agent"><a href="#如何创建-Sub-Agent" class="headerlink" title="如何创建 Sub-Agent"></a>如何创建 Sub-Agent</h2><h3 id="使用-agents-命令（推荐方式）"><a href="#使用-agents-命令（推荐方式）" class="headerlink" title="使用 /agents 命令（推荐方式）"></a>使用 <code>/agents</code> 命令（推荐方式）</h3><p><code>/agents</code> 命令提供了交互式界面来管理 subagents，这是推荐的创建方式：</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">/agents</span><br></pre></td></tr></table></figure>

<p>在交互界面中可以：</p>
<ul>
<li>查看所有可用的 subagents（内置、用户、项目和插件）</li>
<li>使用引导式设置或 Claude 生成创建新的 subagents</li>
<li>编辑现有 subagent 配置和工具访问权限</li>
<li>删除自定义 subagents</li>
</ul>
<p>创建步骤示例：</p>
<ol>
<li><p>选择 <strong>Create new agent</strong></p>
</li>
<li><p>选择范围：<strong>User-level</strong>（用户级）或 <strong>Project-level</strong>（项目级）</p>
</li>
<li><p>选择 <strong>Generate with Claude</strong> 让 Claude 自动生成配置</p>
</li>
<li><p>描述你的 subagent，例如：</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">A code improvement agent that scans files and suggests improvements</span><br><span class="line">for readability, performance, and best practices.</span><br></pre></td></tr></table></figure>
</li>
<li><p>选择工具权限（只读审查者只需选择 Read-only tools）</p>
</li>
<li><p>选择模型（Sonnet 平衡能力和速度，Haiku 更快更便宜）</p>
</li>
<li><p>保存后立即可用，无需重启</p>
</li>
</ol>]]>
    </summary>
    <title>Claude Code Sub-Agents 指南</title>
    <updated>2026-03-24T02:34:02.401Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="AI" scheme="https://fuyongde.github.io/categories/AI/"/>
    <category term="Claude Code" scheme="https://fuyongde.github.io/categories/AI/Claude-Code/"/>
    <category term="AI" scheme="https://fuyongde.github.io/tags/AI/"/>
    <category term="SKILL" scheme="https://fuyongde.github.io/tags/SKILL/"/>
    <content>
      <![CDATA[<h2 id="基础概念"><a href="#基础概念" class="headerlink" title="基础概念"></a>基础概念</h2><p><strong>什么是 Skills？</strong><br>Skills 是 Claude Code 的扩展机制，让你可以创建、管理和分享自定义能力。每个 Skill 是一个包含 <code>SKILL.md</code> 文件的目录，Claude 会将其添加到工具箱中。</p><p><strong>核心价值：</strong></p><ul><li>自动化重复任务</li><li>添加领域专业知识</li><li>强制执行代码标准和最佳实践</li><li>加速项目脚手架和代码生成</li></ul><hr><h2 id="文件结构"><a href="#文件结构" class="headerlink" title="文件结构"></a>文件结构</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></pre></td><td class="code"><pre><span class="line">~/.claude/skills/&lt;skill-name&gt;/</span><br><span class="line">├── SKILL.md          # 必需，主指令文件</span><br><span class="line">├── templates/        # 可选，模板文件</span><br><span class="line">├── examples/         # 可选，示例输出</span><br><span class="line">├── scripts/          # 可选，脚本文件</span><br><span class="line">└── docs/             # 可选，详细参考文档</span><br></pre></td></tr></table></figure><p><strong>Skills 存储位置：</strong></p><table><thead><tr><th>位置</th><th>路径</th><th>适用范围</th></tr></thead><tbody><tr><td>Enterprise</td><td>托管设置</td><td>组织内所有用户</td></tr><tr><td>Personal</td><td><code>~/.claude/skills/</code></td><td>所有项目</td></tr><tr><td>Project</td><td><code>.claude/skills/</code></td><td>当前项目</td></tr><tr><td>Plugin</td><td><code>&lt;plugin&gt;/skills/</code></td><td>插件启用范围</td></tr></tbody></table><hr><h2 id="SKILL-md-文件格式"><a href="#SKILL-md-文件格式" class="headerlink" title="SKILL.md 文件格式"></a>SKILL.md 文件格式</h2><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><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><br><span class="line">name: my-skill</span><br><span class="line">description: 这个 skill 的用途说明</span><br><span class="line">argument-hint: [参数提示]</span><br><span class="line">disable-model-invocation: false</span><br><span class="line">user-invocable: true</span><br><span class="line">allowed-tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Glob</span><br><span class="line">model: claude-sonnet-4-20250514</span><br><span class="line">context: fork</span><br><span class="line"><span class="section">agent: Explore</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line"><span class="section"># Skill 内容</span></span><br><span class="line"></span><br><span class="line">这里是 Claude 执行的具体指令...</span><br></pre></td></tr></table></figure><p><strong>Frontmatter 字段说明：</strong></p><table><thead><tr><th>字段</th><th>必需</th><th>说明</th></tr></thead><tbody><tr><td><code>name</code></td><td>否</td><td>显示名称，默认用目录名</td></tr><tr><td><code>description</code></td><td>推荐</td><td>用途说明，Claude 用于判断何时加载</td></tr><tr><td><code>argument-hint</code></td><td>否</td><td>参数提示，如 <code>[issue-number]</code></td></tr><tr><td><code>disable-model-invocation</code></td><td>否</td><td><code>true</code> 禁止自动加载</td></tr><tr><td><code>user-invocable</code></td><td>否</td><td><code>false</code> 从 <code>/</code> 菜单隐藏</td></tr><tr><td><code>allowed-tools</code></td><td>否</td><td>授权工具列表，无需逐次确认</td></tr><tr><td><code>model</code></td><td>否</td><td>指定模型</td></tr><tr><td><code>context</code></td><td>否</td><td>设为 <code>fork</code> 在子代理中运行</td></tr><tr><td><code>agent</code></td><td>否</td><td>子代理类型：Explore、Plan、general-purpose</td></tr></tbody></table><hr><h2 id="Skill-类型"><a href="#Skill-类型" class="headerlink" title="Skill 类型"></a>Skill 类型</h2><p><strong>参考型（Reference Content）</strong></p><ul><li>添加知识供 Claude 应用</li><li>如：约定、模式、风格指南、领域知识</li><li>在对话上下文中内联运行</li></ul><p><strong>任务型（Task Content）</strong></p><ul><li>提供特定操作的步骤指令</li><li>如：部署、提交、代码生成</li><li>通常设置 <code>disable-model-invocation: true</code> 仅手动触发</li></ul><hr><h2 id="参数传递"><a href="#参数传递" class="headerlink" title="参数传递"></a>参数传递</h2><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></pre></td><td class="code"><pre><span class="line"><span class="section"># 使用 $ARGUMENTS 接收所有参数</span></span><br><span class="line"></span><br><span class="line">修复 GitHub issue $ARGUMENTS...</span><br><span class="line"></span><br><span class="line"><span class="section"># 使用索引访问特定参数</span></span><br><span class="line"></span><br><span class="line">修复组件 $ARGUMENTS[0]，从 $ARGUMENTS[1] 迁移到 $ARGUMENTS[2]</span><br><span class="line"></span><br><span class="line"><span class="section"># 简写形式</span></span><br><span class="line"></span><br><span class="line">修复组件 $0，从 $1 迁移到 $2</span><br></pre></td></tr></table></figure><p><strong>可用变量：</strong></p><ul><li><code>$ARGUMENTS</code> - 所有参数</li><li><code>$ARGUMENTS[N]</code> 或 <code>$N</code> - 第 N 个参数</li><li><code>${CLAUDE_SESSION_ID}</code> - 当前会话 ID</li><li><code>${CLAUDE_SKILL_DIR}</code> - Skill 目录路径</li></ul><hr><h2 id="进阶模式"><a href="#进阶模式" class="headerlink" title="进阶模式"></a>进阶模式</h2><h3 id="动态上下文注入"><a href="#动态上下文注入" class="headerlink" title="动态上下文注入"></a>动态上下文注入</h3><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></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line"><span class="section">name: pr-summary</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">!gh pr diff $ARGUMENTS</span><br><span class="line">!gh pr view $ARGUMENTS --json title,body</span><br><span class="line"></span><br><span class="line">请总结以上 PR 内容...</span><br></pre></td></tr></table></figure><p><code>!command</code> 语法先执行 shell 命令，输出替换占位符。</p><h3 id="子代理运行"><a href="#子代理运行" class="headerlink" title="子代理运行"></a>子代理运行</h3><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="meta">---</span></span><br><span class="line"><span class="attr">context:</span> <span class="string">fork</span></span><br><span class="line"><span class="attr">agent:</span> <span class="string">Explore</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><ul><li>创建隔离上下文</li><li>Skill 内容成为子代理的 prompt</li><li>适合大规模代码库探索</li></ul><h3 id="视觉输出生成"><a href="#视觉输出生成" class="headerlink" title="视觉输出生成"></a>视觉输出生成</h3><ul><li>Skill 可捆绑运行 Python 等脚本</li><li>生成交互式 HTML 文件</li><li>如：依赖图、测试覆盖率报告、代码库可视化</li></ul><hr><h2 id="内置-Bundled-Skills"><a href="#内置-Bundled-Skills" class="headerlink" title="内置 Bundled Skills"></a>内置 Bundled Skills</h2><table><thead><tr><th>命令</th><th>用途</th></tr></thead><tbody><tr><td><code>/simplify</code></td><td>代码简化，专注特定目标</td></tr><tr><td><code>/batch</code></td><td>并行执行 5-30 个独立任务单元</td></tr><tr><td><code>/debug</code></td><td>调试辅助</td></tr><tr><td><code>/loop</code></td><td>定时执行 prompt</td></tr><tr><td><code>/claude-api</code></td><td>Claude API 相关帮助</td></tr></tbody></table><hr><h2 id="权限控制"><a href="#权限控制" class="headerlink" title="权限控制"></a>权限控制</h2><p><strong>禁用所有 Skills：</strong></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">/permissions deny Skill</span><br></pre></td></tr></table></figure><p><strong>允许&#x2F;拒绝特定 Skills：</strong></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">/permissions allow Skill(review)</span><br><span class="line">/permissions deny Skill(deploy *)</span><br></pre></td></tr></table></figure><p><strong>隐藏单个 Skill：</strong></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">disable-model-invocation:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><hr><h2 id="常见问题排查"><a href="#常见问题排查" class="headerlink" title="常见问题排查"></a>常见问题排查</h2><table><thead><tr><th>问题</th><th>解决方案</th></tr></thead><tbody><tr><td>Skill 未触发</td><td>检查 description 关键词、验证 skill 是否在列表中、尝试直接调用</td></tr><tr><td>触发太频繁</td><td>使 description 更具体、设置 <code>disable-model-invocation: true</code></td></tr><tr><td>Claude 看不到所有 skills</td><td>Skills 描述可能超出字符预算（默认 16000 字符），运行 <code>/context</code> 检查</td></tr></tbody></table><hr><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><ol><li><strong>清晰命名</strong> - 使用描述性的 skill 名称</li><li><strong>精确描述</strong> - description 决定 Claude 何时自动加载</li><li><strong>分离关注点</strong> - 复杂 skill 用支持文件保持 SKILL.md 简洁</li><li><strong>版本控制</strong> - 项目级 skills 提交到 <code>.claude/skills/</code></li><li><strong>渐进增强</strong> - 从 5-10 个 skills 开始，按需添加</li></ol><hr><h2 id="实用示例"><a href="#实用示例" class="headerlink" title="实用示例"></a>实用示例</h2><h3 id="代码解释-Skill"><a href="#代码解释-Skill" class="headerlink" title="代码解释 Skill"></a>代码解释 Skill</h3><p>创建 <code>~/.claude/skills/explain-code/SKILL.md</code>：</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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: explain-code</span><br><span class="line"><span class="section">description: 使用可视化图表和类比解释代码工作原理</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">当用户询问某段代码如何工作时：</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> 首先用简单的语言解释代码的核心目的</span><br><span class="line"><span class="bullet">2.</span> 使用类比将概念与日常事物联系起来</span><br><span class="line"><span class="bullet">3.</span> 创建一个 ASCII 图表展示数据流和控制流</span><br><span class="line"><span class="bullet">4.</span> 标注关键的决策点和数据转换</span><br><span class="line"><span class="bullet">5.</span> 提供一个具体的使用场景示例</span><br><span class="line"></span><br><span class="line">格式：</span><br><span class="line"></span><br><span class="line"><span class="section">## 概述</span></span><br><span class="line"></span><br><span class="line">[一句话描述]</span><br><span class="line"></span><br><span class="line"><span class="section">## 类比</span></span><br><span class="line"></span><br><span class="line">[将代码比作日常事物]</span><br><span class="line"></span><br><span class="line"><span class="section">## 流程图</span></span><br><span class="line"></span><br><span class="line">[ASCII 图表]</span><br><span class="line"></span><br><span class="line"><span class="section">## 关键点</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> [要点1]</span><br><span class="line"><span class="bullet">-</span> [要点2]</span><br><span class="line"></span><br><span class="line"><span class="section">## 示例场景</span></span><br><span class="line"></span><br><span class="line">[具体场景描述]</span><br></pre></td></tr></table></figure><h3 id="Git-提交-Skill"><a href="#Git-提交-Skill" class="headerlink" title="Git 提交 Skill"></a>Git 提交 Skill</h3><p>创建 <code>~/.claude/skills/git-commit/SKILL.md</code>：</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><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><br><span class="line">name: commit</span><br><span class="line">description: 生成符合规范的 Git 提交信息</span><br><span class="line">argument-hint: [可选的提交类型]</span><br><span class="line"><span class="section">disable-model-invocation: true</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">根据当前暂存的更改生成提交信息：</span><br><span class="line"></span><br><span class="line"><span class="bullet">1.</span> 运行 <span class="code">`git diff --staged`</span> 查看更改</span><br><span class="line"><span class="bullet">2.</span> 分析更改类型（feat/fix/docs/style/refactor/test/chore）</span><br><span class="line"><span class="bullet">3.</span> 生成符合 Conventional Commits 规范的提交信息</span><br><span class="line"></span><br><span class="line">提交信息格式：</span><br><span class="line"></span><br><span class="line"><span class="code">```</span></span><br><span class="line"><span class="code">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">&lt;body&gt;</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">&lt;footer&gt;</span></span><br><span class="line"><span class="code">```</span></span><br><span class="line"></span><br><span class="line">$ARGUMENTS</span><br></pre></td></tr></table></figure><h3 id="代码审查-Skill"><a href="#代码审查-Skill" class="headerlink" title="代码审查 Skill"></a>代码审查 Skill</h3><p>创建 <code>~/.claude/skills/code-review/SKILL.md</code>：</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><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></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: review</span><br><span class="line">description: 对代码进行全面审查，检查安全、性能和最佳实践</span><br><span class="line">allowed-tools:</span><br><span class="line"><span class="bullet">  -</span> Read</span><br><span class="line"><span class="bullet">  -</span> Glob</span><br><span class="line"><span class="section">  - Grep</span></span><br><span class="line"><span class="section">---</span></span><br><span class="line"></span><br><span class="line">对指定的代码进行审查：</span><br><span class="line"></span><br><span class="line"><span class="section">## 审查维度</span></span><br><span class="line"></span><br><span class="line"><span class="section">### 安全性</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> SQL 注入风险</span><br><span class="line"><span class="bullet">-</span> XSS 漏洞</span><br><span class="line"><span class="bullet">-</span> 敏感信息泄露</span><br><span class="line"><span class="bullet">-</span> 输入验证</span><br><span class="line"></span><br><span class="line"><span class="section">### 性能</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 不必要的循环</span><br><span class="line"><span class="bullet">-</span> 内存泄漏风险</span><br><span class="line"><span class="bullet">-</span> N+1 查询问题</span><br><span class="line"><span class="bullet">-</span> 缓存机会</span><br><span class="line"></span><br><span class="line"><span class="section">### 代码质量</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 命名规范</span><br><span class="line"><span class="bullet">-</span> 代码复杂度</span><br><span class="line"><span class="bullet">-</span> 重复代码</span><br><span class="line"><span class="bullet">-</span> 注释完整性</span><br><span class="line"></span><br><span class="line"><span class="section">### 最佳实践</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> 设计模式应用</span><br><span class="line"><span class="bullet">-</span> 错误处理</span><br><span class="line"><span class="bullet">-</span> 测试覆盖</span><br><span class="line"></span><br><span class="line"><span class="section">## 输出格式</span></span><br><span class="line"></span><br><span class="line"><span class="code">```</span></span><br><span class="line"><span class="code">## 审查报告：[文件名]</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">### 发现的问题</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">| 级别  | 位置    | 问题 | 建议 |</span></span><br><span class="line"><span class="code">| ----- | ------- | ---- | ---- |</span></span><br><span class="line"><span class="code">| ERROR | line:42 | ...  | ...  |</span></span><br><span class="line"><span class="code">| WARN  | line:58 | ...  | ...  |</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">### 优点</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">- ...</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">### 总体评分：X/10</span></span><br><span class="line"><span class="code"></span></span><br><span class="line"><span class="code">```</span></span><br><span class="line"></span><br><span class="line">$ARGUMENTS</span><br></pre></td></tr></table></figure><hr><h2 id="资源链接"><a href="#资源链接" class="headerlink" title="资源链接"></a>资源链接</h2><p><strong>官方资源：</strong></p><ul><li>官方文档：<a href="https://code.claude.com/docs/en/skills">https://code.claude.com/docs/en/skills</a></li><li>最佳实践：<a href="https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices">https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices</a></li></ul><p><strong>社区资源：</strong></p><ul><li>Claude Code Tresor：<a href="https://github.com/alirezarezvani/claude-code-tresor%EF%BC%88%E5%BC%80%E7%AE%B1%E5%8D%B3%E7%94%A8%EF%BC%89">https://github.com/alirezarezvani/claude-code-tresor（开箱即用）</a></li><li>Claude Skills Library：<a href="https://github.com/alirezarezvani/claude-skills%EF%BC%88%E9%A2%86%E5%9F%9F%E4%B8%93%E5%AE%B6%E5%8C%85%EF%BC%89">https://github.com/alirezarezvani/claude-skills（领域专家包）</a></li><li>Skill Factory：<a href="https://github.com/alirezarezvani/claude-code-skill-factory%EF%BC%88%E8%87%AA%E5%AE%9A%E4%B9%89%E6%9E%84%E5%BB%BA%EF%BC%89">https://github.com/alirezarezvani/claude-code-skill-factory（自定义构建）</a></li></ul>]]>
    </content>
    <id>https://fuyongde.github.io/2026/03/12/Claude-Code-Skills-%E6%8C%87%E5%8D%97/</id>
    <link href="https://fuyongde.github.io/2026/03/12/Claude-Code-Skills-%E6%8C%87%E5%8D%97/"/>
    <published>2026-03-12T14:27:52.000Z</published>
    <summary>
      <![CDATA[<h2 id="基础概念"><a href="#基础概念" class="headerlink" title="基础概念"></a>基础概念</h2><p><strong>什么是 Skills？</strong><br>Skills 是 Claude Code 的扩展机制，让你可以创建、管理和分享自定义能力。每个 Skill 是一个包含 <code>SKILL.md</code> 文件的目录，Claude 会将其添加到工具箱中。</p>
<p><strong>核心价值：</strong></p>
<ul>
<li>自动化重复任务</li>
<li>添加领域专业知识</li>
<li>强制执行代码标准和最佳实践</li>
<li>加速项目脚手架和代码生成</li>
</ul>
<hr>
<h2 id="文件结构"><a href="#文件结构" class="headerlink" title="文件结构"></a>文件结构</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></pre></td><td class="code"><pre><span class="line">~/.claude/skills/&lt;skill-name&gt;/</span><br><span class="line">├── SKILL.md          # 必需，主指令文件</span><br><span class="line">├── templates/        # 可选，模板文件</span><br><span class="line">├── examples/         # 可选，示例输出</span><br><span class="line">├── scripts/          # 可选，脚本文件</span><br><span class="line">└── docs/             # 可选，详细参考文档</span><br></pre></td></tr></table></figure>

<p><strong>Skills 存储位置：</strong></p>
<table>
<thead>
<tr>
<th>位置</th>
<th>路径</th>
<th>适用范围</th>
</tr>
</thead>
<tbody><tr>
<td>Enterprise</td>
<td>托管设置</td>
<td>组织内所有用户</td>
</tr>
<tr>
<td>Personal</td>
<td><code>~/.claude/skills/</code></td>
<td>所有项目</td>
</tr>
<tr>
<td>Project</td>
<td><code>.claude/skills/</code></td>
<td>当前项目</td>
</tr>
<tr>
<td>Plugin</td>
<td><code>&lt;plugin&gt;/skills/</code></td>
<td>插件启用范围</td>
</tr>
</tbody></table>
<hr>]]>
    </summary>
    <title>Claude Code Skills 指南</title>
    <updated>2026-03-24T02:32:23.209Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="解决方案" scheme="https://fuyongde.github.io/categories/%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    <category term="JaCoCo" scheme="https://fuyongde.github.io/tags/JaCoCo/"/>
    <content>
      <![CDATA[<p>本文为基于 JaCoCo 源码二次开发的全流程方案，旨在实现类局部修改后保留未改动方法历史覆盖率，仅失效改动方法覆盖率的核心诉求，兼顾 JaCoCo 原生正确性原则与业务实用性。</p><h2 id="一、核心目标与不可突破原则"><a href="#一、核心目标与不可突破原则" class="headerlink" title="一、核心目标与不可突破原则"></a>一、核心目标与不可突破原则</h2><h3 id="1-1-核心开发目标"><a href="#1-1-核心开发目标" class="headerlink" title="1.1 核心开发目标"></a>1.1 核心开发目标</h3><ul><li>类局部修改（方法 &#x2F; 代码块修改）时，未改动方法的历史覆盖率完全复用，改动方法覆盖率清零并重新统计；</li><li>完全兼容 JaCoCo 原生能力：插桩方式（动态 &#x2F; 离线）、覆盖率维度、dump 文件格式、构建插件（Maven&#x2F;Gradle）；</li><li>坚守正确性底线：仅当方法字节码逻辑完全未变时才复用覆盖率，杜绝错误数据。</li></ul><h3 id="1-2-不可突破原则"><a href="#1-2-不可突破原则" class="headerlink" title="1.2 不可突破原则"></a>1.2 不可突破原则</h3><ul><li>正确性原则：复用数据必须基于方法逻辑一致性，方法体改动则覆盖率强制失效；</li><li>向下兼容原则：改造后 JaCoCo 可解析原生 dump 文件，原生 JaCoCo 可解析改造后文件（降级兼容）；</li><li>无侵入原则：业务侧无感知，使用方式与原生 JaCoCo 一致，新增开关控制功能。</li></ul><h3 id="1-3-核心改造思想"><a href="#1-3-核心改造思想" class="headerlink" title="1.3 核心改造思想"></a>1.3 核心改造思想</h3><p>原生 JaCoCo 以 类级别 ClassID 绑定覆盖率数据，类变更则全量失效；改造后下沉为 方法级别 MethodID 绑定，实现方法级粒度的失效与复用，核心逻辑如下：</p><ul><li>覆盖率数据存储结构从 ClassID + 全局探针ID → ClassID + MethodID + 方法内局部探针ID</li></ul><hr><h2 id="二、前置准备"><a href="#二、前置准备" class="headerlink" title="二、前置准备"></a>二、前置准备</h2><h3 id="2-1-源码与环境准备"><a href="#2-1-源码与环境准备" class="headerlink" title="2.1 源码与环境准备"></a>2.1 源码与环境准备</h3><ul><li>源码仓库：克隆 JaCoCo 官方仓库 <a href="https://github.com/jacoco/jacoco">https://github.com/jacoco/jacoco</a>，基于 0.8.12&#x2F;0.8.13 稳定版 开发（避免主干版本不稳定风险）；</li><li>构建环境<ul><li>构建工具：Maven 3.9.11+</li><li>JDK 版本：编译用 JDK 17，测试兼容 JDK 8+</li><li>IDE：IntelliJ IDEA&#x2F;Eclipse（需安装 ASM 插件辅助字节码分析）；</li><li>核心依赖：重点掌握 ASM 9.x 框架（JaCoCo 插桩 &#x2F; 字节码解析的底层依赖）；</li></ul></li><li>改造涉及模块（仅需修改 3 个核心模块，其余无改动）</li></ul><table><thead><tr><th>模块名称</th><th>改造比重</th><th>核心作用</th></tr></thead><tbody><tr><td>org.jacoco.core</td><td>80%</td><td>插桩、探针管理、ClassID&#x2F;MethodID 生成、覆盖率数据存储</td></tr><tr><td>org.jacoco.report</td><td>20%</td><td>覆盖率报告渲染、数据聚合逻辑调整</td></tr><tr><td>org.jacoco.agent</td><td>&lt;5%</td><td>动态插桩入口适配，无核心逻辑修改</td></tr></tbody></table><h3 id="2-2-原生-JaCoCo-核心机制回顾"><a href="#2-2-原生-JaCoCo-核心机制回顾" class="headerlink" title="2.2 原生 JaCoCo 核心机制回顾"></a>2.2 原生 JaCoCo 核心机制回顾</h3><p>二次开发前必须吃透的 3 个底层逻辑：</p><ul><li>ClassID 生成：由 org.jacoco.core.data.CRC64 类实现，对 class 完整字节码做 CRC64 哈希，字节码变动则 ClassID 改变；</li><li>探针机制：JaCoCo 在字节码指令级插入探针（boolean 数组），行 &#x2F; 分支 &#x2F; 方法覆盖率均基于探针状态聚合；</li><li>覆盖率存储：核心载体为 org.jacoco.core.data.ExecutionData，存储 ClassID + 全局探针数组，最终序列化到 .exec 文件。</li></ul><hr><h2 id="三、核心改造点（优先级排序）"><a href="#三、核心改造点（优先级排序）" class="headerlink" title="三、核心改造点（优先级排序）"></a>三、核心改造点（优先级排序）</h2><p>所有改造均为增量改造，保留原生逻辑，通过开关控制新功能，降低开发风险。</p><h3 id="3-1-核心改造一：方法级唯一标识-MethodID-生成与管理"><a href="#3-1-核心改造一：方法级唯一标识-MethodID-生成与管理" class="headerlink" title="3.1 核心改造一：方法级唯一标识 MethodID 生成与管理"></a>3.1 核心改造一：方法级唯一标识 MethodID 生成与管理</h3><ul><li>改造意义：为每个方法生成稳定唯一标识，作为覆盖率复用的核心依据，是整个方案的基石。</li></ul><h4 id="3-1-1-MethodID-设计要求"><a href="#3-1-1-MethodID-设计要求" class="headerlink" title="3.1.1 MethodID 设计要求"></a>3.1.1 MethodID 设计要求</h4><ul><li>稳定性：方法逻辑不变 → MethodID 不变（忽略注释、格式、行号表等无关内容）；</li><li>唯一性：方法逻辑变动（哪怕一行代码）→ MethodID 立即改变。</li></ul><h4 id="3-1-2-源码实现步骤"><a href="#3-1-2-源码实现步骤" class="headerlink" title="3.1.2 源码实现步骤"></a>3.1.2 源码实现步骤</h4><ul><li>新增工具类：org.jacoco.core.data.MethodCRC64</li><li>参考原生 CRC64 类，专门用于生成 MethodID，核心逻辑如下：</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><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">public</span> <span class="keyword">class</span> <span class="title class_">MethodCRC64</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">CRC64</span> <span class="variable">crc</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CRC64</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 生成 MethodID：基于方法核心特征字节码</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> access 方法访问修饰符</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> name 方法名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> desc 方法签名</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> methodBytes 方法体核心字节码（排除调试信息）</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 8字节 MethodID</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">generateId</span><span class="params">(<span class="type">int</span> access, String name, String desc, <span class="type">byte</span>[] methodBytes)</span> &#123;</span><br><span class="line">        crc.reset();</span><br><span class="line">        crc.update(access);</span><br><span class="line">        crc.update(name.getBytes(StandardCharsets.UTF_8));</span><br><span class="line">        crc.update(desc.getBytes(StandardCharsets.UTF_8));</span><br><span class="line">        crc.update(methodBytes);</span><br><span class="line">        <span class="keyword">return</span> crc.getValue();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>MethodID 提取逻辑<ul><li>在 org.jacoco.core.internal.flow.ClassProbesVisitor 中，通过 ASM 解析 class 文件时：</li><li>遍历每个方法，使用 ClassReader.SKIP_DEBUG 模式跳过调试信息；</li><li>提取方法的 访问修饰符 + 方法名 + 方法签名 + 方法体字节码，调用 MethodCRC64 生成 MethodID；</li><li>缓存 方法名+签名 → MethodID 的映射关系。</li></ul></li></ul><h3 id="3-2-核心改造二：重构探针编号与管理机制"><a href="#3-2-核心改造二：重构探针编号与管理机制" class="headerlink" title="3.2 核心改造二：重构探针编号与管理机制"></a>3.2 核心改造二：重构探针编号与管理机制</h3><ul><li>改造意义：将原生类级全局探针改为方法级局部探针，实现探针与方法的精准绑定。</li></ul><h4 id="3-2-1-原生探针机制痛点"><a href="#3-2-1-原生探针机制痛点" class="headerlink" title="3.2.1 原生探针机制痛点"></a>3.2.1 原生探针机制痛点</h4><p>原生探针为类内全局连续编号（如方法 A 探针 0-1，方法 B 探针 2-4），类变更则全局探针数组全部失效，无法区分探针归属。</p><h4 id="3-2-2-改造后探针机制"><a href="#3-2-2-改造后探针机制" class="headerlink" title="3.2.2 改造后探针机制"></a>3.2.2 改造后探针机制</h4><ul><li>探针编号规则<ul><li>每个方法内部探针独立编号（从 0 开始），例如方法 A 探针 0-1、方法 B 探针 0-2；</li><li>类级别维护 全局探针ID → (MethodID, 方法内局部探针ID) 映射表，兼容原生全局探针结构。</li></ul></li><li>源码改造位置<ul><li>org.jacoco.core.internal.flow.MethodProbesVisitor：修改方法内探针编号逻辑为局部编号；</li><li>org.jacoco.core.internal.instr.Instrumenter：适配插桩时的局部探针编号生成；</li><li>新增工具类 org.jacoco.core.data.ProbeMapping：维护全局探针与方法探针的映射关系，并序列化到 dump 文件。</li></ul></li></ul><h3 id="3-3-核心改造三：扩展覆盖率数据载体与-dump-文件存储"><a href="#3-3-核心改造三：扩展覆盖率数据载体与-dump-文件存储" class="headerlink" title="3.3 核心改造三：扩展覆盖率数据载体与 dump 文件存储"></a>3.3 核心改造三：扩展覆盖率数据载体与 dump 文件存储</h3><ul><li>改造意义：实现方法级覆盖率数据的持久化存储，兼容原生 dump 文件格式。</li></ul><h4 id="3-3-1-原生-ExecutionData-痛点"><a href="#3-3-1-原生-ExecutionData-痛点" class="headerlink" title="3.3.1 原生 ExecutionData 痛点"></a>3.3.1 原生 ExecutionData 痛点</h4><p>原生 ExecutionData 仅存储类级数据，无方法维度信息，无法支撑方法级复用：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span> classId; <span class="comment">// 类ID</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> String name; <span class="comment">// 类名</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="type">boolean</span>[] probes; <span class="comment">// 全局探针执行状态</span></span><br></pre></td></tr></table></figure><h4 id="3-3-2-扩展-ExecutionData（增量改造）"><a href="#3-3-2-扩展-ExecutionData（增量改造）" class="headerlink" title="3.3.2 扩展 ExecutionData（增量改造）"></a>3.3.2 扩展 ExecutionData（增量改造）</h4><p>通过继承扩展实现，不修改原生核心字段，避免破坏兼容性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.jacoco.core.data;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 扩展类：支持方法级覆盖率数据存储</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ExtendedExecutionData</span> <span class="keyword">extends</span> <span class="title class_">ExecutionData</span> &#123;</span><br><span class="line">    <span class="comment">// MethodID -&gt; 方法内局部探针执行状态</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;Long, <span class="type">boolean</span>[]&gt; methodProbes;</span><br><span class="line">    <span class="comment">// MethodID -&gt; 方法名+签名（用于报告展示）</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;Long, String&gt; methodNames;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">ExtendedExecutionData</span><span class="params">(<span class="type">long</span> classId, String name, <span class="type">int</span> probeCount)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>(classId, name, probeCount);</span><br><span class="line">        <span class="built_in">this</span>.methodProbes = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="built_in">this</span>.methodNames = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 省略 getter/setter 方法</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="3-3-3-序列化-反序列化逻辑"><a href="#3-3-3-序列化-反序列化逻辑" class="headerlink" title="3.3.3 序列化 &#x2F; 反序列化逻辑"></a>3.3.3 序列化 &#x2F; 反序列化逻辑</h4><ul><li>序列化：原生 3 个字段正常写入，新增的 methodProbes 和 methodNames 追加写入 dump 文件末尾；</li><li>反序列化：改造后 JaCoCo 可解析原生 &#x2F; 改造后 dump 文件，原生 JaCoCo 解析改造后文件时自动忽略新增字段，实现降级兼容。</li></ul><h3 id="3-4-核心改造四：跨版本覆盖率数据复用核心算法"><a href="#3-4-核心改造四：跨版本覆盖率数据复用核心算法" class="headerlink" title="3.4 核心改造四：跨版本覆盖率数据复用核心算法"></a>3.4 核心改造四：跨版本覆盖率数据复用核心算法</h3><ul><li>改造意义：实现历史覆盖率与新版本类数据的智能合并，是方案的核心业务逻辑。</li></ul><h4 id="3-4-1-算法核心场景"><a href="#3-4-1-算法核心场景" class="headerlink" title="3.4.1 算法核心场景"></a>3.4.1 算法核心场景</h4><p>合并旧版本类历史 dump 数据与新版本类本次测试数据，核心逻辑为 MethodID 精准匹配。</p><h4 id="3-4-2-核心算法伪代码"><a href="#3-4-2-核心算法伪代码" class="headerlink" title="3.4.2 核心算法伪代码"></a>3.4.2 核心算法伪代码</h4><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="comment">/**</span></span><br><span class="line"><span class="comment"> * 方法级覆盖率复用核心算法</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> oldData 旧版本类的方法级覆盖率数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> newData 新版本类的方法级覆盖率数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 合并后数据：未改方法复用历史，改动方法用新数据</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> ExtendedExecutionData <span class="title function_">mergeCoverage</span><span class="params">(ExtendedExecutionData oldData, ExtendedExecutionData newData)</span> &#123;</span><br><span class="line">    <span class="type">ExtendedExecutionData</span> <span class="variable">mergedData</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ExtendedExecutionData</span>(</span><br><span class="line">        newData.getClassId(), newData.getName(), newData.getProbes().length</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="keyword">for</span> (Map.Entry&lt;Long, <span class="type">boolean</span>[]&gt; entry : newData.getMethodProbes().entrySet()) &#123;</span><br><span class="line">        <span class="type">Long</span> <span class="variable">newMethodId</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line">        <span class="type">boolean</span>[] newMethodProbes = entry.getValue();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 核心判断：MethodID 一致则复用历史数据</span></span><br><span class="line">        <span class="keyword">if</span> (oldData.getMethodProbes().containsKey(newMethodId)) &#123;</span><br><span class="line">            mergedData.getMethodProbes().put(newMethodId, oldData.getMethodProbes().get(newMethodId));</span><br><span class="line">            mergedData.getMethodNames().put(newMethodId, oldData.getMethodNames().get(newMethodId));</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// 方法改动/新增：使用本次测试数据</span></span><br><span class="line">            mergedData.getMethodProbes().put(newMethodId, newMethodProbes);</span><br><span class="line">            mergedData.getMethodNames().put(newMethodId, newData.getMethodNames().get(newMethodId));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 同步更新全局探针数组，兼容原生报告</span></span><br><span class="line">    mergedData.updateGlobalProbes();</span><br><span class="line">    <span class="keyword">return</span> mergedData;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-5-核心改造五：报告生成模块改造"><a href="#3-5-核心改造五：报告生成模块改造" class="headerlink" title="3.5 核心改造五：报告生成模块改造"></a>3.5 核心改造五：报告生成模块改造</h3><ul><li>改造意义：实现方法级覆盖率数据的可视化展示，完成最后一公里落地。</li></ul><h4 id="3-5-1-核心改造点"><a href="#3-5-1-核心改造点" class="headerlink" title="3.5.1 核心改造点"></a>3.5.1 核心改造点</h4><ul><li>数据聚合逻辑：在报告生成入口调用 mergeCoverage 算法，合并新旧版本数据；</li><li>报告可视化增强（可选）<ul><li>在 HTML&#x2F;XML 报告中新增方法变更状态标识：未改动方法标注「复用历史覆盖率」，改动方法标注「新增覆盖率」；</li><li>保留原生覆盖率统计维度（行 &#x2F; 分支 &#x2F; 方法覆盖率），仅调整数据来源为方法级聚合。</li></ul></li><li>源码改造位置：org.jacoco.report.internal.html.HTMLFormatter、org.jacoco.report.xml.XMLFormatter</li></ul><hr><h2 id="四、非核心改造点（适配性改造）"><a href="#四、非核心改造点（适配性改造）" class="headerlink" title="四、非核心改造点（适配性改造）"></a>四、非核心改造点（适配性改造）</h2><p>完成核心改造后，需进行少量适配工作，确保功能完整可用：</p><ul><li>Agent 模块适配：修改 org.jacoco.agent.rt.internal_*.Agent，调用 ExtendedExecutionData 替代原生 ExecutionData，无核心逻辑改动；</li><li>构建插件适配：在 Maven&#x2F;Gradle 插件中新增开关配置，控制是否开启方法级复用：</li></ul><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;!-- Maven 插件示例配置 --&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.jacoco<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>jacoco-maven-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.8.12-custom<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 新增开关：默认关闭，开启后启用方法级覆盖率复用 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">enableMethodLevelCoverage</span>&gt;</span>true<span class="tag">&lt;/<span class="name">enableMethodLevelCoverage</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li>CLI 工具适配：为 jacococli.jar 的 merge&#x2F;report 命令新增 –method-level 参数，支持方法级数据处理。</li></ul><hr><h2 id="五、技术难点与避坑指南（Q-A）"><a href="#五、技术难点与避坑指南（Q-A）" class="headerlink" title="五、技术难点与避坑指南（Q&amp;A）"></a>五、技术难点与避坑指南（Q&amp;A）</h2><h3 id="Q1：ASM-字节码解析时如何保证-MethodID-的准确性？"><a href="#Q1：ASM-字节码解析时如何保证-MethodID-的准确性？" class="headerlink" title="Q1：ASM 字节码解析时如何保证 MethodID 的准确性？"></a>Q1：ASM 字节码解析时如何保证 MethodID 的准确性？</h3><p><strong>问题描述</strong>：在生成 MethodID 时，容易把调试信息、行号表等无关信息算进 MethodID，导致方法逻辑没有变化但 MethodID 却改变了。</p><p><strong>解决方案</strong>：生成 MethodID 时，使用 ASM 的 ClassReader.SKIP_DEBUG 模式解析 class 文件，这样可以自动跳过调试信息，只处理核心字节码，确保 MethodID 的稳定性。</p><h3 id="Q2：如何处理-Lambda-表达式和内部类的覆盖率统计？"><a href="#Q2：如何处理-Lambda-表达式和内部类的覆盖率统计？" class="headerlink" title="Q2：如何处理 Lambda 表达式和内部类的覆盖率统计？"></a>Q2：如何处理 Lambda 表达式和内部类的覆盖率统计？</h3><p><strong>问题描述</strong>：Java 的 Lambda 表达式、匿名内部类在 ASM 解析时会生成「合成方法（synthetic method）」，这些方法的 MethodID 也需要生成，否则会导致 Lambda 内的覆盖率失效。</p><p><strong>解决方案</strong>：对合成方法一视同仁，正常生成 MethodID，探针插桩逻辑不变。JaCoCo 原生已经兼容了内部类，只需继承该逻辑即可。</p><h3 id="Q3：改造后新增的功能是否会影响-JaCoCo-的性能？"><a href="#Q3：改造后新增的功能是否会影响-JaCoCo-的性能？" class="headerlink" title="Q3：改造后新增的功能是否会影响 JaCoCo 的性能？"></a>Q3：改造后新增的功能是否会影响 JaCoCo 的性能？</h3><p><strong>问题描述</strong>：改造后新增了 MethodID 生成、方法探针映射等功能，是否会显著影响 JaCoCo 的性能？</p><p><strong>解答</strong>：实际上影响很小，几乎可以忽略：</p><ul><li>MethodID 生成是在「编译期 &#x2F; 插桩期」执行一次，运行时无任何开销；</li><li>方法探针映射是内存中的 Map 操作，时间复杂度 O(1)；</li></ul><p>实测结果显示：改造后的 JaCoCo，插桩耗时增加 &lt; 5%，运行时覆盖率采集耗时增加 &lt; 2%，性能影响完全可以忽略。</p><h3 id="Q4：如何确保与第三方工具（如-IDEA、Jenkins、SonarQube）的兼容性？"><a href="#Q4：如何确保与第三方工具（如-IDEA、Jenkins、SonarQube）的兼容性？" class="headerlink" title="Q4：如何确保与第三方工具（如 IDEA、Jenkins、SonarQube）的兼容性？"></a>Q4：如何确保与第三方工具（如 IDEA、Jenkins、SonarQube）的兼容性？</h3><p><strong>问题描述</strong>：改造后可能会导致与第三方工具的兼容性失效，这是绝对要避免的坑。</p><p><strong>解决方案</strong>：遵循核心原则：永远不要修改 JaCoCo 原生的核心类和核心字段，所有改造都应用「继承、扩展、新增」的方式实现。保留原生全局探针数组，第三方工具可正常解析，方法级数据作为增强维度。</p><h3 id="Q5：如何实现降级兼容，使改造后的-JaCoCo-能够解析原生-dump-文件？"><a href="#Q5：如何实现降级兼容，使改造后的-JaCoCo-能够解析原生-dump-文件？" class="headerlink" title="Q5：如何实现降级兼容，使改造后的 JaCoCo 能够解析原生 dump 文件？"></a>Q5：如何实现降级兼容，使改造后的 JaCoCo 能够解析原生 dump 文件？</h3><p><strong>问题描述</strong>：改造后需要确保能够向后兼容，既能解析原生 dump 文件，也要让原生 JaCoCo 能解析改造后的文件。</p><p><strong>解决方案</strong>：</p><ul><li>在序列化时，原生的 3 个字段正常写入，新增的 methodProbes 和 methodNames 追加写入 dump 文件末尾；</li><li>在反序列化时，改造后的 JaCoCo 可解析原生及改造后的 dump 文件；</li><li>原生 JaCoCo 解析改造后的文件时自动忽略新增字段，实现降级兼容。</li></ul><hr><h2 id="六、效果验证（核心场景测试）"><a href="#六、效果验证（核心场景测试）" class="headerlink" title="六、效果验证（核心场景测试）"></a>六、效果验证（核心场景测试）</h2><p>开发完成后，需验证以下 3 个核心场景，确保功能达标：</p><ul><li><p>场景 1：类局部方法修改</p><ul><li>初始状态：类 A 含方法 A1&#x2F;A2&#x2F;A3，测试后覆盖率 100%，生成 dump1.exec；</li><li>修改操作：仅修改方法 A2 的分支逻辑，A1&#x2F;A3 保持不变；</li><li>新测试：仅执行方法 A2 的测试用例，生成 dump2.exec；</li><li>预期结果：合并后报告中 A1&#x2F;A3 覆盖率 100%（复用历史），A2 覆盖率为本次测试结果。</li></ul></li><li><p>场景 2：类新增方法</p><ul><li>初始状态：类 A 含方法 A1&#x2F;A2，覆盖率 100%，生成 dump1.exec；</li><li>修改操作：新增方法 A3；</li><li>新测试：仅执行 A3 的测试用例，生成 dump2.exec；</li><li>预期结果：A1&#x2F;A2 复用 100% 覆盖率，A3 覆盖率为本次测试结果。</li></ul></li><li><p>场景 3：方法完全重写</p><ul><li>初始状态：方法 A1 覆盖率 100%，生成 dump1.exec；</li><li>修改操作：完全重写 A1 的方法体逻辑，MethodID 改变；</li><li>新测试：未执行 A1 的测试用例，生成 dump2.exec；</li><li>预期结果：A1 覆盖率为 0，其余方法复用历史数据。</li></ul></li></ul><hr><h2 id="七、工作量评估与开发周期"><a href="#七、工作量评估与开发周期" class="headerlink" title="七、工作量评估与开发周期"></a>七、工作量评估与开发周期</h2><table><thead><tr><th>工作内容</th><th>代码量</th><th>预估时间</th></tr></thead><tbody><tr><td>核心改造（MethodID + 探针 + 数据结构 + 算法）</td><td>1500-2000 行</td><td>10-14 天</td></tr><tr><td>非核心适配（Agent + 插件 + CLI）</td><td>300-500 行</td><td>3-5 天</td></tr><tr><td>功能测试与问题修复</td><td>-</td><td>5-7 天</td></tr><tr><td>总计</td><td>1800-2500 行</td><td>18-26 天</td></tr></tbody></table><p>备注：以上时间基于「熟悉 JaCoCo 源码与 ASM 框架」的前提，若需先学习相关技术，建议增加 1 周学习时间。</p><hr><h2 id="八、总结与进阶建议"><a href="#八、总结与进阶建议" class="headerlink" title="八、总结与进阶建议"></a>八、总结与进阶建议</h2><h3 id="8-1-方案核心收益"><a href="#8-1-方案核心收益" class="headerlink" title="8.1 方案核心收益"></a>8.1 方案核心收益</h3><ul><li>完美解决类局部修改导致的全量覆盖率失效问题，平衡「正确性」与「实用性」；</li><li>完全兼容 JaCoCo 原生生态，无业务侵入性，可平滑上线；</li><li>改造逻辑清晰，代码增量可控，便于后续维护与版本升级。</li></ul>]]>
    </content>
    <id>https://fuyongde.github.io/2026/01/18/JaCoCo-%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91%E8%A7%A3%E5%86%B3%E3%80%8C%E7%B1%BB%E5%B1%80%E9%83%A8%E4%BF%AE%E6%94%B9%E4%B8%A2%E5%A4%B1%E5%8E%86%E5%8F%B2%E8%A6%86%E7%9B%96%E7%8E%87%E3%80%8D%E5%AE%8C%E6%95%B4%E6%96%B9%E6%A1%88/</id>
    <link href="https://fuyongde.github.io/2026/01/18/JaCoCo-%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91%E8%A7%A3%E5%86%B3%E3%80%8C%E7%B1%BB%E5%B1%80%E9%83%A8%E4%BF%AE%E6%94%B9%E4%B8%A2%E5%A4%B1%E5%8E%86%E5%8F%B2%E8%A6%86%E7%9B%96%E7%8E%87%E3%80%8D%E5%AE%8C%E6%95%B4%E6%96%B9%E6%A1%88/"/>
    <published>2026-01-18T13:48:10.000Z</published>
    <summary>
      <![CDATA[<p>本文为基于 JaCoCo 源码二次开发的全流程方案，旨在实现类局部修改后保留未改动方法历史覆盖率，仅失效改动方法覆盖率的核心诉求，兼顾 JaCoCo 原生正确性原则与业务实用性。</p>
<h2 id="一、核心目标与不可突破原则"><a href="#一、核心目标与不可突破原则" class="headerlink" title="一、核心目标与不可突破原则"></a>一、核心目标与不可突破原则</h2><h3 id="1-1-核心开发目标"><a href="#1-1-核心开发目标" class="headerlink" title="1.1 核心开发目标"></a>1.1 核心开发目标</h3><ul>
<li>类局部修改（方法 &#x2F; 代码块修改）时，未改动方法的历史覆盖率完全复用，改动方法覆盖率清零并重新统计；</li>
<li>完全兼容 JaCoCo 原生能力：插桩方式（动态 &#x2F; 离线）、覆盖率维度、dump 文件格式、构建插件（Maven&#x2F;Gradle）；</li>
<li>坚守正确性底线：仅当方法字节码逻辑完全未变时才复用覆盖率，杜绝错误数据。</li>
</ul>
<h3 id="1-2-不可突破原则"><a href="#1-2-不可突破原则" class="headerlink" title="1.2 不可突破原则"></a>1.2 不可突破原则</h3><ul>
<li>正确性原则：复用数据必须基于方法逻辑一致性，方法体改动则覆盖率强制失效；</li>
<li>向下兼容原则：改造后 JaCoCo 可解析原生 dump 文件，原生 JaCoCo 可解析改造后文件（降级兼容）；</li>
<li>无侵入原则：业务侧无感知，使用方式与原生 JaCoCo 一致，新增开关控制功能。</li>
</ul>
<h3 id="1-3-核心改造思想"><a href="#1-3-核心改造思想" class="headerlink" title="1.3 核心改造思想"></a>1.3 核心改造思想</h3><p>原生 JaCoCo 以 类级别 ClassID 绑定覆盖率数据，类变更则全量失效；改造后下沉为 方法级别 MethodID 绑定，实现方法级粒度的失效与复用，核心逻辑如下：</p>
<ul>
<li>覆盖率数据存储结构从 ClassID + 全局探针ID → ClassID + MethodID + 方法内局部探针ID</li>
</ul>
<hr>]]>
    </summary>
    <title>JaCoCo 二次开发解决「类局部修改丢失历史覆盖率」完整方案</title>
    <updated>2026-03-18T09:09:08.033Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="Guava" scheme="https://fuyongde.github.io/categories/Guava/"/>
    <category term="Guava" scheme="https://fuyongde.github.io/tags/Guava/"/>
    <category term="源码" scheme="https://fuyongde.github.io/tags/%E6%BA%90%E7%A0%81/"/>
    <category term="限流" scheme="https://fuyongde.github.io/tags/%E9%99%90%E6%B5%81/"/>
    <content>
      <![CDATA[<p>最近使用 Guava 中 RateLimiter.getRate() 方法时遇到了一个反直觉的现象，这里贴出来给大家分享一下。</p><h3 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">10.0D</span>);</span><br><span class="line">    <span class="type">double</span> <span class="variable">rate</span> <span class="operator">=</span> rateLimiter.getRate();</span><br><span class="line">    System.out.println(rate);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>直觉上最终会输出 <code>10.0</code>，实际最终也是输出了 <code>10.0</code>。</p><p>可是如果我们将上述代码中的 <code>10.0D</code> 变为 <code>15.0D</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="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">15.0D</span>);</span><br><span class="line">    <span class="type">double</span> <span class="variable">rate</span> <span class="operator">=</span> rateLimiter.getRate();</span><br><span class="line">    System.out.println(rate);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行后输出的值竟然是 <code>14.999999999999998</code>。看到这个结果我觉得非常的奇怪，毕竟直觉告诉我应该输出 <code>15.0</code> 才对。</p><h3 id="刨根问底"><a href="#刨根问底" class="headerlink" title="刨根问底"></a>刨根问底</h3><p>看到这样的现象，我们首先就要从源码上分析一下，看一下 Guava 内部是如何处理的。</p><p>打开 RateLimiter.getRate() 方法，发现其返回的是抽象方法 doGetRate() 的值。</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="keyword">public</span> <span class="keyword">final</span> <span class="type">double</span> <span class="title function_">getRate</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (mutex()) &#123;</span><br><span class="line">        <span class="keyword">return</span> doGetRate();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们创建 RateLimiter 的实例时默认创建的是类型为 SmoothBursty 的对象，该类的 doGetRate() 方法实现如下：</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">@Override</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">double</span> <span class="title function_">doGetRate</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> SECONDS.toMicros(<span class="number">1L</span>) / stableIntervalMicros;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里竟然不是直接返回我们创建 RateLimiter 时设置的值，而是通过除法运算得来的。</p><p>继续分析 stableIntervalMicros 是怎么设值的。</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="comment">/**</span></span><br><span class="line"><span class="comment"> * 该方法是创建 RateLimiter 时调用的方法</span></span><br><span class="line"><span class="comment">**/</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">void</span> <span class="title function_">doSetRate</span><span class="params">(<span class="type">double</span> permitsPerSecond, <span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">    resync(nowMicros);</span><br><span class="line">    <span class="type">double</span> <span class="variable">stableIntervalMicros</span> <span class="operator">=</span> SECONDS.toMicros(<span class="number">1L</span>) / permitsPerSecond;</span><br><span class="line">    <span class="built_in">this</span>.stableIntervalMicros = stableIntervalMicros;</span><br><span class="line">    doSetRate(permitsPerSecond, stableIntervalMicros);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此结论已经非常清晰了：由于在创建 RateLimiter 时成员变量 stableIntervalMicros 便是通过简单的除法运算得来的，当 1000000 微秒（1 秒）不能够被传入的限流值除尽时 stableIntervalMicros 就已经丢失了精度，从而导致在这种情况下调用 doGetRate() 是不能够得到我们创建 RateLimiter 时传入的原始值的。</p><h3 id="发散"><a href="#发散" class="headerlink" title="发散"></a>发散</h3><p>Guava 为什么不在创建 RateLimiter 时把我们传入的原始值保存在一个成员变量里，调用 doGetRate() 方法时直接返回该成员变量的值呢？</p><p>我个人的理解可以从变量和方法命名上去分析，RateLimiter.create(double permitsPerSecond) 方法的变量名 permitsPerSecond 其含义是每秒产生多少令牌。而 RateLimiter.getRate() 从方法名以及 Guava 的注释上来看该方法的含义更倾向于提供产生令牌的实际速率。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>在使用一些三方的库甚至是 Java 本身提供的一些方法时要弄清楚其使用场景，尽量多验证一下各种边界值、特殊值的情况，不能盲目相信自己的直觉。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2022/07/20/%E4%B8%8D%E8%A6%81%E8%BF%87%E5%88%86%E7%9B%B8%E4%BF%A1%E8%87%AA%E5%B7%B1%E7%9A%84%E7%9B%B4%E8%A7%89%E4%B9%8BRateLimiter-getRate/</id>
    <link href="https://fuyongde.github.io/2022/07/20/%E4%B8%8D%E8%A6%81%E8%BF%87%E5%88%86%E7%9B%B8%E4%BF%A1%E8%87%AA%E5%B7%B1%E7%9A%84%E7%9B%B4%E8%A7%89%E4%B9%8BRateLimiter-getRate/"/>
    <published>2022-07-20T12:40:33.000Z</published>
    <summary>
      <![CDATA[<p>最近使用 Guava 中 RateLimiter.getRate() 方法时遇到了一个反直觉的现象，这里贴出来给大家分享一下。</p>
<h3 id="现象"><a href="#现象" class="headerlink" title="现象"></a>现象</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">10.0D</span>);</span><br><span class="line">    <span class="type">double</span> <span class="variable">rate</span> <span class="operator">=</span> rateLimiter.getRate();</span><br><span class="line">    System.out.println(rate);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>直觉上最终会输出 <code>10.0</code>，实际最终也是输出了 <code>10.0</code>。</p>
<p>可是如果我们将上述代码中的 <code>10.0D</code> 变为 <code>15.0D</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="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">15.0D</span>);</span><br><span class="line">    <span class="type">double</span> <span class="variable">rate</span> <span class="operator">=</span> rateLimiter.getRate();</span><br><span class="line">    System.out.println(rate);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>执行后输出的值竟然是 <code>14.999999999999998</code>。看到这个结果我觉得非常的奇怪，毕竟直觉告诉我应该输出 <code>15.0</code> 才对。</p>
<h3 id="刨根问底"><a href="#刨根问底" class="headerlink" title="刨根问底"></a>刨根问底</h3><p>看到这样的现象，我们首先就要从源码上分析一下，看一下 Guava 内部是如何处理的。</p>]]>
    </summary>
    <title>不要过分相信自己的直觉之 RateLimiter.getRate()</title>
    <updated>2026-11-03T08:17:12.615Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <content>
      <![CDATA[<h2 id="JaCoCo-简介"><a href="#JaCoCo-简介" class="headerlink" title="JaCoCo 简介"></a>JaCoCo 简介</h2><blockquote><p>JaCoCo should provide the standard technology for code coverage analysis in Java VM based environments. The focus is providing a lightweight, flexible and well documented library for integration with various build and development tools.</p></blockquote><p>JaCoCo 要为基于 Java VM 的环境中的代码提供覆盖率分析标准技术，重点是提供一个轻量、灵活且文档齐全的库，以便与各种构建和开发工具集成。</p><h3 id="功能特性"><a href="#功能特性" class="headerlink" title="功能特性"></a>功能特性</h3><ul><li>指令（C0）、分支（C1）、行、方法、类型、圈复杂度的覆盖率分析；</li><li>基于 Java 字节码，即使没有源码依然可以运行；</li><li>支持 on-the-fly 模式，可以通过 javaagent 便捷地集成，也可以通过 API 来自定义类装载器进行集成；</li><li>与框架无关，可以对基于 Java VM 的应用程序顺利的集成；</li><li>与所有已发布的 Java 版本兼容；</li><li>支持不同的 JVM 语言；</li><li>可以生成多种格式的报告（HTML、XML、CSV）；</li><li>支持远程协议和 JMX 控制，可以在任何时间点来进行覆盖率数据的存储；</li></ul><h3 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h3><p>下载完 JaCoCo 之后会得到几个 jar 包，其中 jacocoagent.jar 和 jacococli.jar 是我们比较关注的。</p><ul><li>jacocoagent.jar 是 JVM 应用程序启动时的代理</li><li>jacococli.jar 是生成覆盖率报告、合并 dump 文件的命令行工具</li></ul><h4 id="jacocoagent"><a href="#jacocoagent" class="headerlink" title="jacocoagent"></a>jacocoagent</h4><p>Java Agent 的原理可以阅读 <code>java.lang.instrument.Instrumentation</code> 这个类，这里先介绍 jacocoagent 的使用方法。</p><p>在 Java 应用的启动命令上加入如下的参数：</p><p><code>-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]</code></p><p>即可对启动的 Java 应用程序中指定的类进行代码覆盖率的采集，其中 option 的选项有：</p><table><thead><tr><th>option</th><th>缺省值</th><th>描述</th></tr></thead><tbody><tr><td>destfile</td><td>jacoco.exec</td><td>保存 jacoco 采集的数据文件的路径</td></tr><tr><td>append</td><td>true</td><td>数据文件保存路径的文件存在的情况下，是否追加覆盖率数据</td></tr><tr><td>includes</td><td>*</td><td>采集哪些类（采集类过多会导致性能下降严重）</td></tr><tr><td>excludes</td><td>*</td><td>排除哪些类</td></tr><tr><td>inclbootstrapclasses</td><td>false</td><td>是否需要采集 bootstrap classLoader 加载的类</td></tr><tr><td>inclnolocationclasses</td><td>false</td><td>是否需要采集没有原位置的类（即程序运行时生成的类）</td></tr><tr><td>sessionid</td><td>auto-generated</td><td>会话标识符，如果没有配置该参数，jacocoagent 会自动生成</td></tr><tr><td>dumponexit</td><td>true</td><td>是否在 VM 关闭时写入数据</td></tr><tr><td>output</td><td>file</td><td>存储数据的方式，有效的选项：file、tcpserver、tcpclient、none</td></tr><tr><td>address</td><td>loopback interface</td><td>当 output 为 tcpserver 时启用，缺省值为 127.0.0.1</td></tr><tr><td>port</td><td>6300</td><td>当 output 为 tcpserver 时启用</td></tr><tr><td>classdumpdir</td><td>no dumps</td><td>保存插桩之后的 class 的路径</td></tr><tr><td>jmx</td><td>false</td><td>是否开启 JMX 功能</td></tr></tbody></table><h4 id="jacococli"><a href="#jacococli" class="headerlink" title="jacococli"></a>jacococli</h4><p>jacoco 的命令行工具，可以用于连接 jacocoagent 采集并存储执行数据文件、对指定的类进行插桩、合并 dump 文件、生成报告等功能，具体使用方法不再一一列举，可以查看<a href="https://www.jacoco.org/jacoco/trunk/doc/cli.html">官方文档</a></p>]]>
    </content>
    <id>https://fuyongde.github.io/2022/01/26/JaCoCo%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2022/01/26/JaCoCo%E6%89%8B%E5%86%8C/"/>
    <published>2022-01-26T15:09:52.000Z</published>
    <summary>
      <![CDATA[<h2 id="JaCoCo-简介"><a href="#JaCoCo-简介" class="headerlink" title="JaCoCo 简介"></a>JaCoCo 简介</h2><blockquote>
<p>JaCoCo should provide the standard technology for code coverage analysis in Java VM based environments. The focus is providing a lightweight, flexible and well documented library for integration with various build and development tools.</p>
</blockquote>
<p>JaCoCo 要为基于 Java VM 的环境中的代码提供覆盖率分析标准技术，重点是提供一个轻量、灵活且文档齐全的库，以便与各种构建和开发工具集成。</p>
<h3 id="功能特性"><a href="#功能特性" class="headerlink" title="功能特性"></a>功能特性</h3><ul>
<li>指令（C0）、分支（C1）、行、方法、类型、圈复杂度的覆盖率分析；</li>
<li>基于 Java 字节码，即使没有源码依然可以运行；</li>
<li>支持 on-the-fly 模式，可以通过 javaagent 便捷地集成，也可以通过 API 来自定义类装载器进行集成；</li>
<li>与框架无关，可以对基于 Java VM 的应用程序顺利的集成；</li>
<li>与所有已发布的 Java 版本兼容；</li>
<li>支持不同的 JVM 语言；</li>
<li>可以生成多种格式的报告（HTML、XML、CSV）；</li>
<li>支持远程协议和 JMX 控制，可以在任何时间点来进行覆盖率数据的存储；</li>
</ul>
<h3 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h3><p>下载完 JaCoCo 之后会得到几个 jar 包，其中 jacocoagent.jar 和 jacococli.jar 是我们比较关注的。</p>
<ul>
<li>jacocoagent.jar 是 JVM 应用程序启动时的代理</li>
<li>jacococli.jar 是生成覆盖率报告、合并 dump 文件的命令行工具</li>
</ul>
<h4 id="jacocoagent"><a href="#jacocoagent" class="headerlink" title="jacocoagent"></a>jacocoagent</h4><p>Java Agent 的原理可以阅读 <code>java.lang.instrument.Instrumentation</code> 这个类，这里先介绍 jacocoagent 的使用方法。</p>]]>
    </summary>
    <title>JaCoCo手册</title>
    <updated>2026-11-03T08:18:10.559Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="Java" scheme="https://fuyongde.github.io/tags/Java/"/>
    <content>
      <![CDATA[<p>最近在做基于 JaCoCo 的代码覆盖率工具，了解到了 javaagent 相关的一些知识点。</p><h2 id="javaagent-是什么？"><a href="#javaagent-是什么？" class="headerlink" title="javaagent 是什么？"></a>javaagent 是什么？</h2><p>javaagent 是 java 命令的一个参数选项，可以用于加载 Java 语言的代理。</p><p>那么这个参数加载的 Java 语言的代理需要满足什么样的规范呢？</p><ol><li>代理 jar 包中的 MANIFEST.MF 文件需指定 Premain-Class；</li><li>Premain-Class 指定的类中必须实现 premain 方法。</li></ol>]]>
    </content>
    <id>https://fuyongde.github.io/2022/01/08/javaagent-%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/</id>
    <link href="https://fuyongde.github.io/2022/01/08/javaagent-%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97/"/>
    <published>2022-01-08T13:03:22.000Z</published>
    <summary>
      <![CDATA[<p>最近在做基于 JaCoCo 的代码覆盖率工具，了解到了 javaagent 相关的一些知识点。</p>
<h2 id="javaagent-是什么？"><a href="#javaagent-是什么？" class="headerlink" title="javaagent]]>
    </summary>
    <title>javaagent 使用指南</title>
    <updated>2026-11-03T08:17:39.109Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="手册" scheme="https://fuyongde.github.io/categories/%E6%89%8B%E5%86%8C/"/>
    <category term="shell" scheme="https://fuyongde.github.io/tags/shell/"/>
    <category term="bash" scheme="https://fuyongde.github.io/tags/bash/"/>
    <category term="zsh" scheme="https://fuyongde.github.io/tags/zsh/"/>
    <content>
      <![CDATA[<p>zsh 可能是目前最好用的终端。</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h3 id="Mac-OS"><a href="#Mac-OS" class="headerlink" title="Mac OS"></a>Mac OS</h3><p>macOS 自带 zsh，并且当前最新版本 macOS 默认终端即为 zsh</p><h3 id="Ubuntu"><a href="#Ubuntu" class="headerlink" title="Ubuntu"></a>Ubuntu</h3><p>使用 <code>sudo apt install zsh</code> 命令即可安装 zsh</p><h3 id="CentOS"><a href="#CentOS" class="headerlink" title="CentOS"></a>CentOS</h3><p>使用 <code>sudo yum install zsh</code> 命令即可安装 zsh</p><p>在安装好 zsh 之后，第一次使用的时候，会有如下的提示：</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><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">This is the Z Shell configuration function for new users,</span><br><span class="line">zsh-newuser-install.</span><br><span class="line">You are seeing this message because you have no zsh startup files</span><br><span class="line">(the files .zshenv, .zprofile, .zshrc, .zlogin in the directory</span><br><span class="line">~).  This function can help you with a few settings that should</span><br><span class="line">make your use of the shell easier.</span><br><span class="line"></span><br><span class="line">You can:</span><br><span class="line"></span><br><span class="line">(q)  Quit and do nothing.  The function will be run again next time.</span><br><span class="line"></span><br><span class="line">(0)  Exit, creating the file ~/.zshrc containing just a comment.</span><br><span class="line">     That will prevent this function being run again.</span><br><span class="line"></span><br><span class="line">(1)  Continue to the main menu.</span><br><span class="line"></span><br><span class="line">(2)  Populate your ~/.zshrc with the configuration recommended</span><br><span class="line">     by the system administrator and exit (you will need to edit</span><br><span class="line">     the file by hand, if so desired).</span><br><span class="line"></span><br><span class="line">--- Type one of the keys in parentheses ---</span><br></pre></td></tr></table></figure><p>这里我推荐直接使用选项 (2)，通过 <code>~/.zshrc</code> 文件来配置我们的 zsh。</p><h3 id="查看-zsh-版本"><a href="#查看-zsh-版本" class="headerlink" title="查看 zsh 版本"></a>查看 zsh 版本</h3><p>使用 <code>zsh --version</code> 命令即可查看 zsh 的版本。</p><h3 id="将-zsh-设置为默认的-shell"><a href="#将-zsh-设置为默认的-shell" class="headerlink" title="将 zsh 设置为默认的 shell"></a>将 zsh 设置为默认的 shell</h3><p>使用 <code>chsh -s /bin/zsh root</code> 即可将 root 用户的默认 shell 设置为 zsh，如需为其他用户设置默认的 shell，将 <code>root</code> 替换为其他用户名即可。</p><h3 id="查看默认的-shell"><a href="#查看默认的-shell" class="headerlink" title="查看默认的 shell"></a>查看默认的 shell</h3><p>使用 <code>echo $SHELL</code> 命令即可查看默认的 shell</p><h3 id="查看当前正在使用的-shell"><a href="#查看当前正在使用的-shell" class="headerlink" title="查看当前正在使用的 shell"></a>查看当前正在使用的 shell</h3><p>使用 <code>echo $0</code> 命令即可查看当前正在使用的 shell</p><hr><h2 id="使用-oh-my-zsh-美化-zsh"><a href="#使用-oh-my-zsh-美化-zsh" class="headerlink" title="使用 oh-my-zsh 美化 zsh"></a>使用 oh-my-zsh 美化 zsh</h2><h3 id="安装-oh-my-zsh"><a href="#安装-oh-my-zsh" class="headerlink" title="安装 oh-my-zsh"></a>安装 oh-my-zsh</h3><p>打开 oh-my-zsh 官网，直接使用官网提供的命令，即可安装 oh-my-zsh，命令如下（任选其一即可）：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">使用 curl 下载</span></span><br><span class="line">sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">或者使用 wget</span></span><br><span class="line">sh -c &quot;$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)&quot;</span><br></pre></td></tr></table></figure><p>对于国内的用户，上面命令下载 install.sh 可能存在连接超时的问题，我们可以替换为 gitee 的镜像来进行下载，命令如下：</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">wget https://gitee.com/mirrors/oh-my-zsh/raw/master/tools/install.sh</span><br></pre></td></tr></table></figure><p>下载之后，使用 <code>sh install.sh</code> 来安装 oh-my-zsh，如果在 clone oh-my-zsh 过程仍然连接超时的情况下，可以通过替换 install.sh 脚本中的仓库地址来进行加速。</p><p>将脚本中的如下内容</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">REPO=$&#123;REPO:-ohmyzsh/ohmyzsh&#125;</span><br><span class="line">REMOTE=$&#123;REMOTE:-https://github.com/$&#123;REPO&#125;.git&#125;</span><br></pre></td></tr></table></figure><p>替换为</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">REPO=$&#123;REPO:-mirrors/oh-my-zsh&#125;</span><br><span class="line">REMOTE=$&#123;REMOTE:-https://gitee.com/$&#123;REPO&#125;.git&#125;</span><br></pre></td></tr></table></figure><h3 id="配置-oh-my-zsh-主题"><a href="#配置-oh-my-zsh-主题" class="headerlink" title="配置 oh-my-zsh 主题"></a>配置 oh-my-zsh 主题</h3><p>修改 <code>~/.zshrc</code> 文件，找到 ZSH_THEME 的选项，修改为自己喜欢的，笔者喜欢使用 ys 主题，其配置如下：</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">ZSH_THEME=&quot;ys&quot;</span><br></pre></td></tr></table></figure><p>如果你比较喜欢每次打开终端都体验不一样的主题的话，可以使用 random 主题。</p><h3 id="安装常用的插件"><a href="#安装常用的插件" class="headerlink" title="安装常用的插件"></a>安装常用的插件</h3><p>下面列举一些非常实用的插件</p><ul><li><p>zsh-syntax-highlighting 有时候记不住一些命令，等输入完了才知道命令输错了，这个插件直接在输入过程中就会提示你，当前命令是否正确，错误红色，正确绿色</p></li><li><p>zsh-autosuggestions 该插件会记录你之前输入过的所有命令，并且自动匹配你可能想要输入命令，然后按 → 补全</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装 zsh-syntax-highlighting</span></span><br><span class="line">git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $&#123;ZSH_CUSTOM:-~/.oh-my-zsh/custom&#125;/plugins/zsh-syntax-highlighting</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装 zsh-autosuggestions</span></span><br><span class="line">git clone https://github.com/zsh-users/zsh-autosuggestions $&#123;ZSH_CUSTOM:-~/.oh-my-zsh/custom&#125;/plugins/zsh-autosuggestions</span><br></pre></td></tr></table></figure><p>修改 ~&#x2F;.zshrc 中插件的配置</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">plugins=(git zsh-syntax-highlighting zsh-autosuggestions)</span><br></pre></td></tr></table></figure><p>然后使用 <code>source ~/.zshrc</code> 来使配置生效</p>]]>
    </content>
    <id>https://fuyongde.github.io/2021/08/24/zsh-%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2021/08/24/zsh-%E6%89%8B%E5%86%8C/"/>
    <published>2021-08-24T14:01:14.000Z</published>
    <summary>
      <![CDATA[<p>zsh 可能是目前最好用的终端。</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h3 id="Mac-OS"><a href="#Mac-OS" class="headerlink" title="Mac OS"></a>Mac OS</h3><p>macOS 自带 zsh，并且当前最新版本 macOS 默认终端即为 zsh</p>
<h3 id="Ubuntu"><a href="#Ubuntu" class="headerlink" title="Ubuntu"></a>Ubuntu</h3><p>使用 <code>sudo apt install zsh</code> 命令即可安装 zsh</p>
<h3 id="CentOS"><a href="#CentOS" class="headerlink" title="CentOS"></a>CentOS</h3><p>使用 <code>sudo yum install zsh</code> 命令即可安装 zsh</p>
<p>在安装好 zsh 之后，第一次使用的时候，会有如下的提示：</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><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">This is the Z Shell configuration function for new users,</span><br><span class="line">zsh-newuser-install.</span><br><span class="line">You are seeing this message because you have no zsh startup files</span><br><span class="line">(the files .zshenv, .zprofile, .zshrc, .zlogin in the directory</span><br><span class="line">~).  This function can help you with a few settings that should</span><br><span class="line">make your use of the shell easier.</span><br><span class="line"></span><br><span class="line">You can:</span><br><span class="line"></span><br><span class="line">(q)  Quit and do nothing.  The function will be run again next time.</span><br><span class="line"></span><br><span class="line">(0)  Exit, creating the file ~/.zshrc containing just a comment.</span><br><span class="line">     That will prevent this function being run again.</span><br><span class="line"></span><br><span class="line">(1)  Continue to the main menu.</span><br><span class="line"></span><br><span class="line">(2)  Populate your ~/.zshrc with the configuration recommended</span><br><span class="line">     by the system administrator and exit (you will need to edit</span><br><span class="line">     the file by hand, if so desired).</span><br><span class="line"></span><br><span class="line">--- Type one of the keys in parentheses ---</span><br></pre></td></tr></table></figure>]]>
    </summary>
    <title>zsh 手册</title>
    <updated>2026-03-18T09:09:08.159Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="Spring" scheme="https://fuyongde.github.io/categories/Spring/"/>
    <category term="源码" scheme="https://fuyongde.github.io/tags/%E6%BA%90%E7%A0%81/"/>
    <category term="Spring" scheme="https://fuyongde.github.io/tags/Spring/"/>
    <content>
      <![CDATA[<p>使用 Spring 做项目时先思考一个问题，一个接口有多个实现类的情况下，在成员变量的声明上如果没有指定注入哪个别名的 Bean 的时候 Spring 会如何选择对应的 Bean 来进行注入？</p><h2 id="问题模拟"><a href="#问题模拟" class="headerlink" title="问题模拟"></a>问题模拟</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>针对上面定义的接口编写两个实现类，并分别定义其 beanName 为 testService 和 testService2</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">@Service(&quot;testService&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;testService&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service(&quot;testService2&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestServiceImpl2</span> <span class="keyword">implements</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;testService2&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编写 Handler，并注入 TestService</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="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestHandler</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> TestService testService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        testService.printBeanName();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编写单元测试，并观察输出</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">@SpringBootTest</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">TestHandlerTest</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> TestHandler testHandler;</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_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        testHandler.test();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行该单元测试，发现其打印的 beanName 为 testService。从输出结果可以看出在这种情况下最终注入的是 beanName 为 testService 的 bean，与组合交易子系统的现象一致。</p><hr><h2 id="源码分析与验证"><a href="#源码分析与验证" class="headerlink" title="源码分析与验证"></a>源码分析与验证</h2><p>根据 Spring 的文档，使用 Autowired 注解在自动装配时会通过 <code>org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties</code> 方法来对属性进行赋值操作。我们可以在该方法入口打上断点，并对断点指定条件，如下图：</p><img src="/2021/08/08/%E4%B8%80%E4%B8%AA-Spring-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98%E6%B5%85%E6%9E%90/debug.png" class="" title="debug"><p>由于方法深度较深，调试的过程这里不再赘述，我们直接分析影响我们最终结果的方法 <code>org.springframework.beans.factory.support.DefaultListableBeanFactory#determineAutowireCandidate</code>，其源码及分析如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> candidates 为我们要注入的属性对应的所有 Bean 组成的 map</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> descriptor 描述了我们要注入的属性的一些信息，包含了属性对应的类、方法、fieldName 等信息</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">protected</span> String <span class="title function_">determineAutowireCandidate</span><span class="params">(Map&lt;String, Object&gt; candidates, DependencyDescriptor descriptor)</span> &#123;</span><br><span class="line">    <span class="comment">// requiredType 为我们要注入的属性的类型，针对本例子 requiredType 的值为 TestService 这个类</span></span><br><span class="line">    Class&lt;?&gt; requiredType = descriptor.getDependencyType();</span><br><span class="line">    <span class="comment">// 如果 candidates 中有 Bean 添加了 @Primary 注解，则会返回该 Bean 对应的 beanName</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">primaryCandidate</span> <span class="operator">=</span> determinePrimaryCandidate(candidates, requiredType);</span><br><span class="line">    <span class="keyword">if</span> (primaryCandidate != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> primaryCandidate;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 如果 candidates 中的 Bean 没有 @Primary 注解，但是添加了 @Priority 注解，会解析 Bean 的优先级，返回优先级最高的 Bean 对应的 beanName</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">priorityCandidate</span> <span class="operator">=</span> determineHighestPriorityCandidate(candidates, requiredType);</span><br><span class="line">    <span class="keyword">if</span> (priorityCandidate != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> priorityCandidate;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 降级逻辑</span></span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;String, Object&gt; entry : candidates.entrySet()) &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">candidateName</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line">        <span class="type">Object</span> <span class="variable">beanInstance</span> <span class="operator">=</span> entry.getValue();</span><br><span class="line">        <span class="comment">// 此时的 this.resolvableDependencies 是不包含本例子中的 TestService 对应的任一 bean 的</span></span><br><span class="line">        <span class="comment">// descriptor.getDependencyName() 方法的处理中会返回要注入的属性所对应的 fieldName，本例子中的 fieldName 为 testService</span></span><br><span class="line">        <span class="comment">// 最终如果要注入属性名跟 candidates 的 Bean 的 beanName 相同，则会返回对应的 beanName</span></span><br><span class="line">        <span class="keyword">if</span> ((beanInstance != <span class="literal">null</span> &amp;&amp; <span class="built_in">this</span>.resolvableDependencies.containsValue(beanInstance)) ||</span><br><span class="line">                matchesBeanName(candidateName, descriptor.getDependencyName())) &#123;</span><br><span class="line">            <span class="keyword">return</span> candidateName;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>下面我们来验证一下源码中的我们未验证到的情况</p><h3 id="使用-Primary-注解来标识主要的类"><a href="#使用-Primary-注解来标识主要的类" class="headerlink" title="使用 Primary 注解来标识主要的类"></a>使用 Primary 注解来标识主要的类</h3><p>将 beanName 为 testService2 的 Bean 加上 Primary 注解并运行单元测试，其打印的 beanName 为 testService2。</p><h3 id="使用-Priority-来标识-Bean-的优先级"><a href="#使用-Priority-来标识-Bean-的优先级" class="headerlink" title="使用 Priority 来标识 Bean 的优先级"></a>使用 Priority 来标识 Bean 的优先级</h3><p>去掉上面操作所添加的 Primary 注解，来验证使用 Priority 时的情况。</p><ul><li>将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试，其打印的 beanName 为 testService2；</li><li>将 beanName 为 testService 的 bean 加上 @Priority(1) 注解，将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试，其打印的 beanName 为 testService；</li><li>将 beanName 为 testService 的 bean 加上 @Priority(3) 注解，将 beanName 为 testService2 的 bean 加上 @Priority(2) 注解并运行单元测试，其打印的 beanName 为 testService2。</li></ul><h3 id="修改-TestHandler-中的属性名"><a href="#修改-TestHandler-中的属性名" class="headerlink" title="修改 TestHandler 中的属性名"></a>修改 TestHandler 中的属性名</h3><p>去掉上面操作所添加的 Primary 和 Priority 注解，来验证根据属性名注入时的情况。</p><ul><li>将 TestHandler 中的属性名修改为 testService2 并运行单元测试，其打印的 beanName 为 testService2；</li><li>将 TestHandler 中的属性名修改为 testService3 并运行单元测试，程序抛出 NoUniqueBeanDefinitionException 异常。</li></ul><hr><h2 id="最佳实践建议"><a href="#最佳实践建议" class="headerlink" title="最佳实践建议"></a>最佳实践建议</h2><p>在实际开发中，当遇到同一接口有多个实现类的情况时，推荐使用以下方式明确指定要注入的Bean：</p><h3 id="使用-Qualifier-注解"><a href="#使用-Qualifier-注解" class="headerlink" title="使用 @Qualifier 注解"></a>使用 @Qualifier 注解</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestHandler</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="meta">@Qualifier(&quot;testService2&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> TestService testService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        testService.printBeanName();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用字段名匹配"><a href="#使用字段名匹配" class="headerlink" title="使用字段名匹配"></a>使用字段名匹配</h3><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="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestHandler</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> TestService testService2; <span class="comment">// 字段名与Bean名匹配</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        testService2.printBeanName();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="使用-Primary-注解"><a href="#使用-Primary-注解" class="headerlink" title="使用 @Primary 注解"></a>使用 @Primary 注解</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">@Service(&quot;testService&quot;)</span></span><br><span class="line"><span class="meta">@Primary</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>根据上面源码的分析与实践，同一类型存在多个 Bean 的情况下，在使用 Autowired 注解时 Spring 会采用以下装配顺序来选择要装配的 Bean：</p><ol><li>使用了 Primary 注解的 Bean;</li><li>查找使用了 Priority 注解的 Bean，并选择优先级最高的；</li><li>要注入的属性的属性名称与 Bean 名称相同的；</li></ol><p>在我们平时写代码时，为了代码的可读性和明确性，建议使用 Qualifier 注解来明确指明要注入的Bean，避免依赖默认的装配规则。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2021/08/08/%E4%B8%80%E4%B8%AA-Spring-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98%E6%B5%85%E6%9E%90/</id>
    <link href="https://fuyongde.github.io/2021/08/08/%E4%B8%80%E4%B8%AA-Spring-%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98%E6%B5%85%E6%9E%90/"/>
    <published>2021-08-08T06:00:14.000Z</published>
    <summary>
      <![CDATA[<p>使用 Spring 做项目时先思考一个问题，一个接口有多个实现类的情况下，在成员变量的声明上如果没有指定注入哪个别名的 Bean 的时候 Spring 会如何选择对应的 Bean 来进行注入？</p>
<h2 id="问题模拟"><a href="#问题模拟" class="headerlink" title="问题模拟"></a>问题模拟</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>针对上面定义的接口编写两个实现类，并分别定义其 beanName 为 testService 和 testService2</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">@Service(&quot;testService&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;testService&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service(&quot;testService2&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestServiceImpl2</span> <span class="keyword">implements</span> <span class="title class_">TestService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">printBeanName</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;testService2&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>编写 Handler，并注入 TestService</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="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestHandler</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> TestService testService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">test</span><span class="params">()</span> &#123;</span><br><span class="line">        testService.printBeanName();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>编写单元测试，并观察输出</p>]]>
    </summary>
    <title>一个 Spring 依赖注入的问题浅析</title>
    <updated>2026-11-03T08:17:12.921Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="Guava" scheme="https://fuyongde.github.io/categories/Guava/"/>
    <category term="Guava" scheme="https://fuyongde.github.io/tags/Guava/"/>
    <category term="源码" scheme="https://fuyongde.github.io/tags/%E6%BA%90%E7%A0%81/"/>
    <category term="限流" scheme="https://fuyongde.github.io/tags/%E9%99%90%E6%B5%81/"/>
    <content>
      <![CDATA[<p>线上业务使用了 Guava 中的 RateLimiter 来做单机的限流。最近运维同事反馈，在修改了限流的配置之后，请求频率没有超过限流的配置却触发了限流逻辑，在此对该问题进行一下分析与总结。</p><h2 id="问题模拟"><a href="#问题模拟" class="headerlink" title="问题模拟"></a>问题模拟</h2><p>根据上述问题的现象，初步推测问题可能跟 Guava 中 RateLimiter 的限流算法实现有关，为了方便排查问题，参考线上业务的限流的逻辑写了一个 demo，其核心代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/hello&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HelloController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** 用于存储不同的接口，对应的 RateLimiter **/</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, RateLimiter&gt; RATE_LIMITER_MAP = Maps.newConcurrentMap();</span><br><span class="line">    <span class="comment">/** 用于存储不同接口限流配置的参数 **/</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, String&gt; RATE_MAP = Maps.newConcurrentMap();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="comment">// 初始化一个接口默认的限流配置</span></span><br><span class="line">        RATE_MAP.put(<span class="string">&quot;loan&quot;</span>, <span class="string">&quot;100000&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 测试限流的接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 限流配置的 key</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/testLimit&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">testLimit</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> RATE_MAP.get(key);</span><br><span class="line">        logger.info(<span class="string">&quot;自定义限流配置=&#123;&#125;&quot;</span>, value);</span><br><span class="line">        <span class="type">Double</span> <span class="variable">currentRate</span> <span class="operator">=</span> Double.parseDouble(value);</span><br><span class="line"></span><br><span class="line">        <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RATE_LIMITER_MAP.get(key);</span><br><span class="line">        <span class="keyword">if</span> (rateLimiter == <span class="literal">null</span> || !isSameRate(currentRate, rateLimiter.getRate())) &#123;</span><br><span class="line">            logger.info(<span class="string">&quot;创建新的RateLimiter|速率=&#123;&#125;&quot;</span>, currentRate.toString());</span><br><span class="line">            rateLimiter = RateLimiter.create(currentRate);</span><br><span class="line">            RATE_LIMITER_MAP.putIfAbsent(key, rateLimiter);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">allowed</span> <span class="operator">=</span> rateLimiter.tryAcquire();</span><br><span class="line">        logger.info(<span class="string">&quot;是否允许当前请求通过=&#123;&#125;|rate=&#123;&#125;&quot;</span>, allowed, rateLimiter.getRate());</span><br><span class="line">        <span class="keyword">return</span> allowed ? <span class="string">&quot;SUCCESS&quot;</span> : <span class="string">&quot;FAIL&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 模拟修改限流配置</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 限流配置的key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> rate 限流的值</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/setRate&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">setRate</span><span class="params">(String key, String rate)</span> &#123;</span><br><span class="line">        RATE_MAP.put(key, rate);</span><br><span class="line">        <span class="type">String</span> <span class="variable">currentValue</span> <span class="operator">=</span> RATE_MAP.get(key);</span><br><span class="line">        logger.info(<span class="string">&quot;设置完之后的值=&#123;&#125;&quot;</span>, currentValue);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;SUCCESS&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>启动工程，我们模拟一下出现问题时的操作。</p><ol><li>构造 HTTP 请求调用 <code>/setRate</code> 接口，将 loan 对应的限流的值修改为 5；</li><li>构造 HTTP 请求快速调用 <code>/testLimit</code> 接口 2 次，发现第一次调用接口返回 SUCCESS，第二次接口返回 FAIL，说明第 2 次请求触发了限流；</li><li>构造 HTTP 请求调用 <code>/testLimit</code> 5 次，保证 5 次请求在 1 秒内接收，发现这 5 次请求接口均返回 SUCCESS，说明这 5 次请求均未触发限流；</li><li>构造 HTTP 请求调用 <code>/testLimit</code> 6 次，保证 6 次请求均在 1 秒内接收，发现前 6 次请求接口均返回 SUCCESS，说明这 6 次请求均未触发限流；</li><li>构造 HTTP 请求调用 <code>/testLimit</code> 7 次，保证 7 次请求均在 1 秒内接收，发现前 6 次请求接口均返回 SUCCESS，第 7 次请求接口返回 FAIL，说明前 6 次请求均未触发限流，第 7 次请求触发限流。</li></ol><p>至此，我们模拟的现象与线上一致。步骤 3 是我们预期的结果，步骤 2 中在未超过限流配置的情况下触发了限流，步骤 4、5 中的第 6 次请求在超过了限流配置的情况下，仍然允许通过，我们直观上认为这些是不合理的。</p><hr><h2 id="排查与分析"><a href="#排查与分析" class="headerlink" title="排查与分析"></a>排查与分析</h2><p>Guava 中 RateLimiter 提供了两种令牌桶的算法实现：</p><ul><li>平滑突发限流（SmoothBursty）</li><li>平滑预热限流（SmoothWarmingUp）</li></ul><p>我们项目中使用了默认的平滑突发限流（SmoothBursty）算法，这里我们只针对该算法进行分析。</p><h3 id="tryAcquire-方法"><a href="#tryAcquire-方法" class="headerlink" title="tryAcquire 方法"></a>tryAcquire 方法</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><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">tryAcquire</span><span class="params">(<span class="type">int</span> <span class="keyword">permits</span>, <span class="type">long</span> timeout, TimeUnit unit)</span> &#123;</span><br><span class="line">  <span class="type">long</span> <span class="variable">timeoutMicros</span> <span class="operator">=</span> max(unit.toMicros(timeout), <span class="number">0</span>);</span><br><span class="line">  checkPermits(<span class="keyword">permits</span>);</span><br><span class="line">  <span class="type">long</span> microsToWait;</span><br><span class="line">  <span class="keyword">synchronized</span> (mutex()) &#123;</span><br><span class="line">    <span class="type">long</span> <span class="variable">nowMicros</span> <span class="operator">=</span> stopwatch.readMicros();</span><br><span class="line">    <span class="keyword">if</span> (!canAcquire(nowMicros, timeoutMicros)) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      microsToWait = reserveAndGetWaitLength(<span class="keyword">permits</span>, nowMicros);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  stopwatch.sleepMicrosUninterruptibly(microsToWait);</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以观察到只有在 canAcquire 方法返回 false 的情况下，才会返回 false，通过阅读 canAcquire 方法可知该方法跟 SmoothBursty 中的成员变量 nextFreeTicketMicros 有关，该成员变量的释义如下：</p><blockquote><p>The time when the next request (no matter its size) will be granted. After granting a request, this is pushed further in the future. Large requests push this further than small requests.</p></blockquote><p>该成员变量标识下一次请求获得令牌的最早时间，在创建 SmoothBursty 时初始值为 0，并且每次授权一个请求之后，这个值会向后推移。查找该成员变量引用的地方，发现 SmoothBursty 类中 reserveEarliestAvailable、resync 方法会触发该成员变量的更新，并且观察该方法的调用栈，其中就有 <code>RateLimiter#tryAcquire()</code> 方法。</p><h4 id="reserveEarliestAvailable、resync-方法"><a href="#reserveEarliestAvailable、resync-方法" class="headerlink" title="reserveEarliestAvailable、resync 方法"></a>reserveEarliestAvailable、resync 方法</h4><p>其源码</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> requiredPermits 表示我们该次请求所取得令牌数，我们这里值为 1</span></span><br><span class="line"><span class="comment">  * <span class="doctag">@param</span> nowMicros 距创建 RateLimiter 时经过了多少时长，单位为微秒</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">long</span> <span class="title function_">reserveEarliestAvailable</span><span class="params">(<span class="type">int</span> requiredPermits, <span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">  <span class="comment">// 刷新令牌数</span></span><br><span class="line">  resync(nowMicros);</span><br><span class="line">  <span class="type">long</span> <span class="variable">returnValue</span> <span class="operator">=</span> nextFreeTicketMicros;</span><br><span class="line">  <span class="comment">// storedPermits 是创建 RateLimiter 时的一个成员变量，表示当前存储的令牌数，在 resync 方法和本方法中会更新所存储的令牌数</span></span><br><span class="line">  <span class="comment">// 当前存储的令牌数跟需要获取的令牌数进行比较，取两者的最小值，即为当前可以得到令牌数</span></span><br><span class="line">  <span class="type">double</span> <span class="variable">storedPermitsToSpend</span> <span class="operator">=</span> min(requiredPermits, <span class="built_in">this</span>.storedPermits);</span><br><span class="line">  <span class="comment">// 需要预先支付的令牌</span></span><br><span class="line">  <span class="type">double</span> <span class="variable">freshPermits</span> <span class="operator">=</span> requiredPermits - storedPermitsToSpend;</span><br><span class="line">  <span class="comment">// 在突然涌入大量请求，而现有的令牌数又不够用的情况下，会预先支付一定的令牌数</span></span><br><span class="line">  <span class="comment">// stableIntervalMicros 为创建 RateLimiter 时计算出的稳定时间间隔，其计算公式为 周期 / 令牌数，在我们的例子中，该值即为 200000 微秒（即 200ms）</span></span><br><span class="line">  <span class="comment">// waitMicros 表示预先支付令牌的时间</span></span><br><span class="line">  <span class="type">long</span> <span class="variable">waitMicros</span> <span class="operator">=</span></span><br><span class="line">      storedPermitsToWaitTime(<span class="built_in">this</span>.storedPermits, storedPermitsToSpend)</span><br><span class="line">          + (<span class="type">long</span>) (freshPermits * stableIntervalMicros);</span><br><span class="line">  <span class="comment">// 预先支付令牌的时间+原请求获取令牌的最早时间，即为下一次获取令牌的最早时间</span></span><br><span class="line">  <span class="built_in">this</span>.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);</span><br><span class="line">  <span class="comment">// 所存储的令牌数需要减去本次消耗的令牌</span></span><br><span class="line">  <span class="built_in">this</span>.storedPermits -= storedPermitsToSpend;</span><br><span class="line">  <span class="comment">// 这里返回旧的获取令牌最早的时间，即本次请求无需为预支付的令牌付出额外的等待时间</span></span><br><span class="line">  <span class="keyword">return</span> returnValue;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">resync</span><span class="params">(<span class="type">long</span> nowMicros)</span> &#123;</span><br><span class="line">  <span class="comment">// if nextFreeTicket is in the past, resync to now</span></span><br><span class="line">  <span class="keyword">if</span> (nowMicros &gt; nextFreeTicketMicros) &#123;</span><br><span class="line">    <span class="comment">// 在 SmoothBursty 类中 coolDownIntervalMicros() 方法返回的是成员变量 stableIntervalMicros，在我们的例子中，该值即为 200000 微秒（即 200ms）</span></span><br><span class="line">    <span class="type">double</span> <span class="variable">newPermits</span> <span class="operator">=</span> (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();</span><br><span class="line">    <span class="comment">// maxPermits 在 创建 RateLimiter 时的一个成员变量，表示所能存储的最大令牌数，在我们的例子中，该值为 5</span></span><br><span class="line">    <span class="comment">// 更新当前所存储的令牌数，取值为 原存储的令牌数与新产生的令牌数之和 与 最大存储令牌数 两个数值中较小的值，保证可用的令牌数不会大于所允许存储的最大令牌数</span></span><br><span class="line">    storedPermits = min(maxPermits, storedPermits + newPermits);</span><br><span class="line">    nextFreeTicketMicros = nowMicros;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 reserveEarliestAvailable 方法结尾处打断点，来观察一下修改配置之后，第一次调用该方法时对 nextFreeTicketMicros 成员变量的设值情况。如下图:</p><img src="/2021/06/26/%E4%BD%BF%E7%94%A8Guava%E4%B8%ADRateLimiter%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/debug.png" class="" title="运行中各个变量的赋值情况"><p>通过上图中的执行情况可知，在限流配置修改为 5TPS 的情况下，第一次请求进来之后，令牌桶内所存储的令牌数还不足 1，发生了预支付令牌的情况，此时 nextFreeTicketMicros 成员变量会被赋值为约 200000 微秒（即 200ms），如果第二个请求是在第一个请求之后 200ms 内请求进来的，一定会触发限流逻辑，如下图：</p><img src="/2021/06/26/%E4%BD%BF%E7%94%A8Guava%E4%B8%ADRateLimiter%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/limit1.png" class="" title="第二次调用该方法时触发的限流的情况"><p>同样的，在上面的操作中，1 秒内的连续 6 次请求，第 6 次仍未触发限流，这种现象也是由于预支付令牌产生的。在一段时间内没有请求进来的情况下，令牌桶内累计了 5 个令牌，此时前 5 个令牌会被前 5 个请求消费掉，当第 6 个请求进来的时候，时间仍然在向前推进，只是此时桶内累计的令牌数不足 1 个，会触发预先支付令牌，故这种情况下第 6 个请求，仍然是能够通过的。而当第 7 个请求进来的时候，由于下一次请求获得令牌的最早时间已经大于当前时间了，故取令牌失败，触发限流，如下图：</p><img src="/2021/06/26/%E4%BD%BF%E7%94%A8Guava%E4%B8%ADRateLimiter%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/limit2.png" class="" title="第七次调用该方法时触发的限流的情况"><hr><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p>通过以上源码的分析，Guava 中 RateLimiter 的平滑突发限流（SmoothBursty）是通过以固定的速率向桶内放置令牌，从而达到平滑输出的效果。由于桶内会进行令牌的积累，可以应对一定程度的流量突发。</p><p>对于我们线上遇到的修改配置之后第二个请求就触发了限流的问题，我们在切换配置时应该使用 RateLimiter 的 setRate 方法直接修改其限流配置，这样可以避免重新创建一个新的 RateLimiter 时会创建一个新的跑表（Stopwatch），同时也不会将 nextFreeTicketMicros 重新置为 0，使得在取令牌时对桶内令牌数的计算不会像重新创建一个那么极端。</p><hr><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>大家也可以看一下 Guava 中平滑预热限流（SmoothWarmingUp）在实现上与平滑突发限流（SmoothBursty）有什么区别？此外常见的其他限流算法如漏桶算法、计数法分别是怎么实现的？</p>]]>
    </content>
    <id>https://fuyongde.github.io/2021/06/26/%E4%BD%BF%E7%94%A8Guava%E4%B8%ADRateLimiter%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/</id>
    <link href="https://fuyongde.github.io/2021/06/26/%E4%BD%BF%E7%94%A8Guava%E4%B8%ADRateLimiter%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/"/>
    <published>2021-06-26T11:44:43.000Z</published>
    <summary>
      <![CDATA[<p>线上业务使用了 Guava 中的 RateLimiter 来做单机的限流。最近运维同事反馈，在修改了限流的配置之后，请求频率没有超过限流的配置却触发了限流逻辑，在此对该问题进行一下分析与总结。</p>
<h2 id="问题模拟"><a href="#问题模拟" class="headerlink" title="问题模拟"></a>问题模拟</h2><p>根据上述问题的现象，初步推测问题可能跟 Guava 中 RateLimiter 的限流算法实现有关，为了方便排查问题，参考线上业务的限流的逻辑写了一个 demo，其核心代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/hello&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HelloController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/** 用于存储不同的接口，对应的 RateLimiter **/</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, RateLimiter&gt; RATE_LIMITER_MAP = Maps.newConcurrentMap();</span><br><span class="line">    <span class="comment">/** 用于存储不同接口限流配置的参数 **/</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, String&gt; RATE_MAP = Maps.newConcurrentMap();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="comment">// 初始化一个接口默认的限流配置</span></span><br><span class="line">        RATE_MAP.put(<span class="string">&quot;loan&quot;</span>, <span class="string">&quot;100000&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 测试限流的接口</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 限流配置的 key</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/testLimit&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">testLimit</span><span class="params">(String key)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> RATE_MAP.get(key);</span><br><span class="line">        logger.info(<span class="string">&quot;自定义限流配置=&#123;&#125;&quot;</span>, value);</span><br><span class="line">        <span class="type">Double</span> <span class="variable">currentRate</span> <span class="operator">=</span> Double.parseDouble(value);</span><br><span class="line"></span><br><span class="line">        <span class="type">RateLimiter</span> <span class="variable">rateLimiter</span> <span class="operator">=</span> RATE_LIMITER_MAP.get(key);</span><br><span class="line">        <span class="keyword">if</span> (rateLimiter == <span class="literal">null</span> || !isSameRate(currentRate, rateLimiter.getRate())) &#123;</span><br><span class="line">            logger.info(<span class="string">&quot;创建新的RateLimiter|速率=&#123;&#125;&quot;</span>, currentRate.toString());</span><br><span class="line">            rateLimiter = RateLimiter.create(currentRate);</span><br><span class="line">            RATE_LIMITER_MAP.putIfAbsent(key, rateLimiter);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">allowed</span> <span class="operator">=</span> rateLimiter.tryAcquire();</span><br><span class="line">        logger.info(<span class="string">&quot;是否允许当前请求通过=&#123;&#125;|rate=&#123;&#125;&quot;</span>, allowed, rateLimiter.getRate());</span><br><span class="line">        <span class="keyword">return</span> allowed ? <span class="string">&quot;SUCCESS&quot;</span> : <span class="string">&quot;FAIL&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 模拟修改限流配置</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> key 限流配置的key</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> rate 限流的值</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/setRate&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">setRate</span><span class="params">(String key, String rate)</span> &#123;</span><br><span class="line">        RATE_MAP.put(key, rate);</span><br><span class="line">        <span class="type">String</span> <span class="variable">currentValue</span> <span class="operator">=</span> RATE_MAP.get(key);</span><br><span class="line">        logger.info(<span class="string">&quot;设置完之后的值=&#123;&#125;&quot;</span>, currentValue);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;SUCCESS&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>启动工程，我们模拟一下出现问题时的操作。</p>
<ol>
<li>构造 HTTP 请求调用 <code>/setRate</code> 接口，将 loan 对应的限流的值修改为 5；</li>
<li>构造 HTTP 请求快速调用 <code>/testLimit</code> 接口 2 次，发现第一次调用接口返回 SUCCESS，第二次接口返回 FAIL，说明第 2 次请求触发了限流；</li>
<li>构造 HTTP 请求调用 <code>/testLimit</code> 5 次，保证 5 次请求在 1 秒内接收，发现这 5 次请求接口均返回 SUCCESS，说明这 5 次请求均未触发限流；</li>
<li>构造 HTTP 请求调用 <code>/testLimit</code> 6 次，保证 6 次请求均在 1 秒内接收，发现前 6 次请求接口均返回 SUCCESS，说明这 6 次请求均未触发限流；</li>
<li>构造 HTTP 请求调用 <code>/testLimit</code> 7 次，保证 7 次请求均在 1 秒内接收，发现前 6 次请求接口均返回 SUCCESS，第 7 次请求接口返回 FAIL，说明前 6 次请求均未触发限流，第 7 次请求触发限流。</li>
</ol>
<p>至此，我们模拟的现象与线上一致。步骤 3 是我们预期的结果，步骤 2 中在未超过限流配置的情况下触发了限流，步骤 4、5 中的第 6 次请求在超过了限流配置的情况下，仍然允许通过，我们直观上认为这些是不合理的。</p>
<hr>
<h2 id="排查与分析"><a href="#排查与分析" class="headerlink" title="排查与分析"></a>排查与分析</h2><p>Guava 中 RateLimiter 提供了两种令牌桶的算法实现：</p>]]>
    </summary>
    <title>使用 Guava 中 RateLimiter 遇到的一个问题</title>
    <updated>2026-11-03T08:17:12.486Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="https" scheme="https://fuyongde.github.io/tags/https/"/>
    <category term="okhttp" scheme="https://fuyongde.github.io/tags/okhttp/"/>
    <content>
      <![CDATA[<p>近期收到一些小伙伴问 Java 中如何调用 https 中双向认证的接口的问题，本文采用 okhttp 以及 Java11 中的 httpclient 来介绍如何使用 Java 代码调用 https 双向认证的接口。</p><h2 id="前置工作"><a href="#前置工作" class="headerlink" title="前置工作"></a>前置工作</h2><ol><li>生成服务端、客户端证书，并将各自的公钥加入到对方的信任链中以供后续的使用；</li><li>搭建一个 https 协议的服务以供客户端调用。</li></ol><h3 id="制作证书"><a href="#制作证书" class="headerlink" title="制作证书"></a>制作证书</h3><p>这里我们采用 jdk 提供的 keytool 命令来生成我们所需要的服务端、客户端证书，并且可以使用 keytool 命令来将各自的公钥加入到对方的信任链中。</p><h4 id="生成服务端、客户端密钥库以及证书"><a href="#生成服务端、客户端密钥库以及证书" class="headerlink" title="生成服务端、客户端密钥库以及证书"></a>生成服务端、客户端密钥库以及证书</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">生成服务端keystore，这里加拓展项的目的是为了说明域名验证的问题</span></span><br><span class="line">keytool -genkey -validity 3650 -alias server -keyalg RSA -keystore server.keystore -ext san=dns:www.goku.edu,ip:127.0.0.1</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">生成客户端keystore，这里针对客户端的密钥库我们采用 PKCS12 的格式</span></span><br><span class="line">keytool -genkey -validity 3650 -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12</span><br></pre></td></tr></table></figure><h4 id="导出服务端、客户端证书中的公钥"><a href="#导出服务端、客户端证书中的公钥" class="headerlink" title="导出服务端、客户端证书中的公钥"></a>导出服务端、客户端证书中的公钥</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">导出服务端证书的公钥</span></span><br><span class="line">keytool -export -keystore server.keystore -alias server -file server.cer -rfc</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">导出客户端证书的公钥</span></span><br><span class="line">keytool -export -keystore client.p12 -storetype PKCS12 -alias client -file client.cer</span><br></pre></td></tr></table></figure><h4 id="将各自的公钥加入到对方的密钥库中"><a href="#将各自的公钥加入到对方的密钥库中" class="headerlink" title="将各自的公钥加入到对方的密钥库中"></a>将各自的公钥加入到对方的密钥库中</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将服务端证书导入到客户端的信任库中</span></span><br><span class="line">keytool -import -alias server -file server.cer -keystore client.p12</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将客户端证书导入到服务端的信任库中</span></span><br><span class="line">keytool -import -alias client -file client.cer -keystore server.keystore</span><br></pre></td></tr></table></figure><h4 id="查看密钥库"><a href="#查看密钥库" class="headerlink" title="查看密钥库"></a>查看密钥库</h4><p>可以通过如下命令，来查看公钥是否加入到各自的密钥库中</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">keytool -list -v -keystore server.keystore</span><br><span class="line">keytool -list -rfc -keystore client.p12</span><br></pre></td></tr></table></figure><h3 id="搭建-HTTPS-服务"><a href="#搭建-HTTPS-服务" class="headerlink" title="搭建 HTTPS 服务"></a>搭建 HTTPS 服务</h3><p>由于搭建服务不是本文的重点，建议使用 SpringBoot 脚手架快速搭建一个服务，下面只列关键点。</p><h4 id="编写一个简单的-HTTP-接口"><a href="#编写一个简单的-HTTP-接口" class="headerlink" title="编写一个简单的 HTTP 接口"></a>编写一个简单的 HTTP 接口</h4><p>代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/index&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">IndexRestController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/hello&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">hello</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello World!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>完成之后，启动服务，使用浏览器打开对应的接口地址，如果正确输出<code>Hello World!</code>则说明接口正常。</p><h4 id="开启-SpringBoot-的-SSL-双向认证"><a href="#开启-SpringBoot-的-SSL-双向认证" class="headerlink" title="开启 SpringBoot 的 SSL 双向认证"></a>开启 SpringBoot 的 SSL 双向认证</h4><p>在 <code>application.properties</code> 中新增如下配置，即可开启 SSL 的双向认证</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><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.ssl.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">server.ssl.protocol</span>=<span class="string">TLS</span></span><br><span class="line"><span class="attr">server.ssl.key-store</span>=<span class="string">classpath:certs/server.keystore</span></span><br><span class="line"><span class="attr">server.ssl.key-store-password</span>=<span class="string">111111</span></span><br><span class="line"><span class="attr">server.ssl.key-password</span>=<span class="string">111111</span></span><br><span class="line"><span class="attr">server.ssl.key-alias</span>=<span class="string">server</span></span><br><span class="line"><span class="attr">server.ssl.trust-store</span>=<span class="string">classpath:certs/server.keystore</span></span><br><span class="line"><span class="attr">server.ssl.trust-store-password</span>=<span class="string">111111</span></span><br><span class="line"><span class="attr">server.ssl.client-auth</span>=<span class="string">NEED</span></span><br></pre></td></tr></table></figure><p>配完之后，启动服务，再次使用浏览器打开上一步的接口地址，发现接口并不能正常使用，并且浏览器会提示”此站点的连接不安全”，如下图所示</p><img src="/2020/12/10/how-use-httpclient-over-https/certificate_not_trusted.png" class=""><hr><h2 id="使用-OkHttp"><a href="#使用-OkHttp" class="headerlink" title="使用 OkHttp"></a>使用 OkHttp</h2><h3 id="构建-OkHttpClient"><a href="#构建-OkHttpClient" class="headerlink" title="构建 OkHttpClient"></a>构建 OkHttpClient</h3><p>构建 SSL 的双向认证，有两个要点</p><ol><li>创建 SSLSocket</li><li>创建 HostnameVerifier</li></ol><h4 id="创建-SSLSocketFactory"><a href="#创建-SSLSocketFactory" class="headerlink" title="创建 SSLSocketFactory"></a>创建 SSLSocketFactory</h4><p>在进行 HTTPS 的请求时，最重要的就是创建 SSLSocket ，在 Java 中可以通过 SSLSocketFactory 来创建 SSLSocket，故在构建 OkHttpClient 时成员变量 SSLSocketFactory 的创建就显得非常关键。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> SSLSocketFactory <span class="title function_">sslSocketFactory</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> HttpsUtils.class.getResourceAsStream(<span class="string">&quot;/certs/client.p12&quot;</span>)) &#123;</span><br><span class="line">        <span class="comment">// 加载 keyStore</span></span><br><span class="line">        <span class="type">KeyStore</span> <span class="variable">keyStore</span> <span class="operator">=</span> KeyStore.getInstance(<span class="string">&quot;PKCS12&quot;</span>);</span><br><span class="line">        keyStore.load(inputStream, <span class="string">&quot;111111&quot;</span>.toCharArray());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 创建密钥管理器</span></span><br><span class="line">        <span class="type">KeyManagerFactory</span> <span class="variable">kmf</span> <span class="operator">=</span> KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());</span><br><span class="line">        kmf.init(keyStore, <span class="string">&quot;111111&quot;</span>.toCharArray());</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">defaultAlgorithm</span> <span class="operator">=</span> TrustManagerFactory.getDefaultAlgorithm();</span><br><span class="line">        <span class="type">TrustManagerFactory</span> <span class="variable">tmf</span> <span class="operator">=</span> TrustManagerFactory.getInstance(defaultAlgorithm);</span><br><span class="line">        tmf.init(keyStore);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 初始化 SSLContext</span></span><br><span class="line">        <span class="type">SSLContext</span> <span class="variable">sslContext</span> <span class="operator">=</span> SSLContext.getInstance(<span class="string">&quot;TLS&quot;</span>);</span><br><span class="line">        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), <span class="keyword">new</span> <span class="title class_">SecureRandom</span>());</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> sslContext.getSocketFactory();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (NoSuchAlgorithmException | KeyManagementException | KeyStoreException | UnrecoverableKeyException | IOException | CertificateException e) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于第二个参数所需的 X509TrustManager，我们采用 OkHttp 提供的默认实现<code>okhttp3.internal.Util#platformTrustManager</code>即可。</p><h4 id="创建-HostnameVerifier"><a href="#创建-HostnameVerifier" class="headerlink" title="创建 HostnameVerifier"></a>创建 HostnameVerifier</h4><p>HostnameVerifier 是为了帮助我们验证我们请求的域名以及对应的 IP 是否一致。这里可以回顾一下创建证书时我们添加的拓展项<code>-ext san=dns:www.goku.edu,ip:127.0.0.1</code>。由于我们在本机测试，可以信任对应的域名，也可以通过修改 hosts 文件的方式来指定域名，也可以自定义 HostnameVerifier 的实现。这里以不对域名做校验为例子，代码如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> HostnameVerifier <span class="title function_">hostnameVerifier</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> (hostname, session) -&gt; <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在完成以上所有步骤之后，就可以构建我们的 OkHttpClient，并发送我们的 https 请求。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="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 &#123;</span><br><span class="line">    <span class="comment">// 创建 OkHttpClient</span></span><br><span class="line">    <span class="type">OkHttpClient</span> <span class="variable">httpClient</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">OkHttpClient</span>.Builder()</span><br><span class="line">        .hostnameVerifier(hostnameVerifier())</span><br><span class="line">        .sslSocketFactory(sslSocketFactory(), Util.platformTrustManager())</span><br><span class="line">        .build();</span><br><span class="line">    <span class="comment">// 构造请求</span></span><br><span class="line">    <span class="type">Request</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Request</span>.Builder()</span><br><span class="line">            .url(<span class="string">&quot;https://www.goku.edu:8443/index/hello&quot;</span>)</span><br><span class="line">            .build();</span><br><span class="line">    <span class="comment">// 发送请求</span></span><br><span class="line">    <span class="type">Response</span> <span class="variable">response</span> <span class="operator">=</span> httpClient.newCall(request).execute();</span><br><span class="line">    System.out.println(response.body().string());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行代码，发现控制台正确打印<code>Hello World!</code>，说明我们对于 SSL 双向认证的 Https 请求已经发送成功，并成功的接收到响应了。</p><hr><h2 id="使用-Java11-的-HttpClient"><a href="#使用-Java11-的-HttpClient" class="headerlink" title="使用 Java11 的 HttpClient"></a>使用 Java11 的 HttpClient</h2><p>熟悉 Java11 HttpClient 的开发者都知道 Java11 中的 HttpClient 跟 OkHttpClient 中的用法几乎一致。回归到 SSL 的双向认证，有以下两个不同点</p><ol><li>Java11 的 HttpClient 所需传的参数为 SSLContext，这个跟我们在使用 OkHttp 时创建 SSLSocketFactory 时初始化的 SSLContext 完全一致；</li><li>Java11 的 HttpClient 并不支持自定义的 hostnameVerifier，只能通过配置项来关闭或打开域名验证。</li></ol><p>其代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="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, InterruptedException, KeyStoreException,</span><br><span class="line">        CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException &#123;</span><br><span class="line">    <span class="comment">// 如需禁用 HostnameVerification</span></span><br><span class="line">    <span class="comment">// System.setProperty(&quot;jdk.internal.httpclient.disableHostnameVerification&quot;, &quot;true&quot;);</span></span><br><span class="line"></span><br><span class="line">    <span class="type">InputStream</span> <span class="variable">inputStream</span> <span class="operator">=</span> HttpsUtils.class.getResourceAsStream(<span class="string">&quot;/certs/client.p12&quot;</span>);</span><br><span class="line">    <span class="comment">// 创建KeyStore，用来存储信任证书</span></span><br><span class="line">    <span class="type">KeyStore</span> <span class="variable">keyStore</span> <span class="operator">=</span> KeyStore.getInstance(<span class="string">&quot;PKCS12&quot;</span>);</span><br><span class="line">    keyStore.load(inputStream, <span class="string">&quot;111111&quot;</span>.toCharArray());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建密钥管理器</span></span><br><span class="line">    <span class="type">KeyManagerFactory</span> <span class="variable">kmf</span> <span class="operator">=</span> KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());</span><br><span class="line">    kmf.init(keyStore, <span class="string">&quot;111111&quot;</span>.toCharArray());</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">defaultAlgorithm</span> <span class="operator">=</span> TrustManagerFactory.getDefaultAlgorithm();</span><br><span class="line">    <span class="type">TrustManagerFactory</span> <span class="variable">tmf</span> <span class="operator">=</span> TrustManagerFactory.getInstance(defaultAlgorithm);</span><br><span class="line">    tmf.init(keyStore);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化 SSLContext</span></span><br><span class="line">    <span class="type">SSLContext</span> <span class="variable">sslContext</span> <span class="operator">=</span> SSLContext.getInstance(<span class="string">&quot;TLS&quot;</span>);</span><br><span class="line">    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), <span class="keyword">new</span> <span class="title class_">SecureRandom</span>());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建 HttpClient</span></span><br><span class="line">    <span class="type">HttpClient</span> <span class="variable">httpClient</span> <span class="operator">=</span> HttpClient.newBuilder()</span><br><span class="line">            .sslContext(sslContext)</span><br><span class="line">            .build();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建一个自定义的HTTP请求对象</span></span><br><span class="line">    <span class="type">HttpRequest</span> <span class="variable">request</span> <span class="operator">=</span> HttpRequest.newBuilder()</span><br><span class="line">            .GET()</span><br><span class="line">            .uri(URI.create(<span class="string">&quot;https://www.goku.edu:8443/index/hello&quot;</span>))</span><br><span class="line">            .header(<span class="string">&quot;Accept-Language&quot;</span>, <span class="string">&quot;zh-CN&quot;</span>)</span><br><span class="line">            .timeout(Duration.ofMillis(<span class="number">5000</span>))</span><br><span class="line">            .build();</span><br><span class="line">    <span class="comment">// 客户端传递请求信息，且返回字符串形式的应答报文</span></span><br><span class="line">    HttpResponse&lt;String&gt; response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());</span><br><span class="line">    System.out.println(response.body());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://fuyongde.github.io/2020/12/10/how-use-httpclient-over-https/</id>
    <link href="https://fuyongde.github.io/2020/12/10/how-use-httpclient-over-https/"/>
    <published>2020-12-10T14:04:59.000Z</published>
    <summary>
      <![CDATA[<p>近期收到一些小伙伴问 Java 中如何调用 https 中双向认证的接口的问题，本文采用 okhttp 以及 Java11 中的 httpclient 来介绍如何使用 Java 代码调用 https 双向认证的接口。</p>
<h2 id="前置工作"><a href="#前置工作" class="headerlink" title="前置工作"></a>前置工作</h2><ol>
<li>生成服务端、客户端证书，并将各自的公钥加入到对方的信任链中以供后续的使用；</li>
<li>搭建一个 https 协议的服务以供客户端调用。</li>
</ol>
<h3 id="制作证书"><a href="#制作证书" class="headerlink" title="制作证书"></a>制作证书</h3><p>这里我们采用 jdk 提供的 keytool 命令来生成我们所需要的服务端、客户端证书，并且可以使用 keytool 命令来将各自的公钥加入到对方的信任链中。</p>
<h4 id="生成服务端、客户端密钥库以及证书"><a href="#生成服务端、客户端密钥库以及证书" class="headerlink" title="生成服务端、客户端密钥库以及证书"></a>生成服务端、客户端密钥库以及证书</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">生成服务端keystore，这里加拓展项的目的是为了说明域名验证的问题</span></span><br><span class="line">keytool -genkey -validity 3650 -alias server -keyalg RSA -keystore server.keystore -ext san=dns:www.goku.edu,ip:127.0.0.1</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">生成客户端keystore，这里针对客户端的密钥库我们采用 PKCS12 的格式</span></span><br><span class="line">keytool -genkey -validity 3650 -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12</span><br></pre></td></tr></table></figure>

<h4 id="导出服务端、客户端证书中的公钥"><a href="#导出服务端、客户端证书中的公钥" class="headerlink" title="导出服务端、客户端证书中的公钥"></a>导出服务端、客户端证书中的公钥</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">导出服务端证书的公钥</span></span><br><span class="line">keytool -export -keystore server.keystore -alias server -file server.cer -rfc</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">导出客户端证书的公钥</span></span><br><span class="line">keytool -export -keystore client.p12 -storetype PKCS12 -alias client -file client.cer</span><br></pre></td></tr></table></figure>

<h4 id="将各自的公钥加入到对方的密钥库中"><a href="#将各自的公钥加入到对方的密钥库中" class="headerlink" title="将各自的公钥加入到对方的密钥库中"></a>将各自的公钥加入到对方的密钥库中</h4>]]>
    </summary>
    <title>如何使用 Java 的 HttpClient 请求 Https 服务</title>
    <updated>2026-03-18T09:09:08.026Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="手册" scheme="https://fuyongde.github.io/categories/%E6%89%8B%E5%86%8C/"/>
    <category term="未完待续" scheme="https://fuyongde.github.io/tags/%E6%9C%AA%E5%AE%8C%E5%BE%85%E7%BB%AD/"/>
    <category term="npm" scheme="https://fuyongde.github.io/tags/npm/"/>
    <content>
      <![CDATA[<h2 id="配置-npmmirror-镜像"><a href="#配置-npmmirror-镜像" class="headerlink" title="配置 npmmirror 镜像"></a>配置 npmmirror 镜像</h2><h3 id="1-备份旧镜像"><a href="#1-备份旧镜像" class="headerlink" title="1. 备份旧镜像"></a>1. 备份旧镜像</h3><p>在替换 npmmirror 镜像之前，我们先查看一下原有的镜像</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">npm get registry</span><br></pre></td></tr></table></figure><p>结果为 <code>https://registry.npmjs.org/</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn get registry</span><br></pre></td></tr></table></figure><p>结果为 <code>https://registry.yarnpkg.com</code></p><h3 id="2-替换为-npmmirror-镜像"><a href="#2-替换为-npmmirror-镜像" class="headerlink" title="2. 替换为 npmmirror 镜像"></a>2. 替换为 npmmirror 镜像</h3><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置 npm 的源为 npmmirror</span></span><br><span class="line">npm config set registry https://registry.npmmirror.com</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置 yarn 的源为 npmmirror</span></span><br><span class="line">yarn config set registry https://registry.npmmirror.com</span><br></pre></td></tr></table></figure><hr><h2 id="升级版本"><a href="#升级版本" class="headerlink" title="升级版本"></a>升级版本</h2><h3 id="查看-npm-版本"><a href="#查看-npm-版本" class="headerlink" title="查看 npm 版本"></a>查看 npm 版本</h3><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">npm -v</span><br></pre></td></tr></table></figure><h3 id="升级到最新的-npm-版本"><a href="#升级到最新的-npm-版本" class="headerlink" title="升级到最新的 npm 版本"></a>升级到最新的 npm 版本</h3><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">npm install -g npm</span><br></pre></td></tr></table></figure><h3 id="安装指定版本的-npm"><a href="#安装指定版本的-npm" class="headerlink" title="安装指定版本的 npm"></a>安装指定版本的 npm</h3><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">npm install -g npm@版本号</span><br></pre></td></tr></table></figure><p>例如安装 npm 6.x 的最新版本：</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">npm install -g npm@6</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">npm install -g npm@6.14.6</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://fuyongde.github.io/2020/07/19/npm-%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2020/07/19/npm-%E6%89%8B%E5%86%8C/"/>
    <published>2020-07-19T10:46:48.000Z</published>
    <summary>
      <![CDATA[<h2 id="配置-npmmirror-镜像"><a href="#配置-npmmirror-镜像" class="headerlink" title="配置 npmmirror 镜像"></a>配置 npmmirror 镜像</h2><h3 id="1-备份旧镜像"><a href="#1-备份旧镜像" class="headerlink" title="1. 备份旧镜像"></a>1. 备份旧镜像</h3><p>在替换 npmmirror 镜像之前，我们先查看一下原有的镜像</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">npm get registry</span><br></pre></td></tr></table></figure>

<p>结果为 <code>https://registry.npmjs.org/</code></p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yarn get registry</span><br></pre></td></tr></table></figure>

<p>结果为 <code>https://registry.yarnpkg.com</code></p>
<h3 id="2-替换为-npmmirror-镜像"><a href="#2-替换为-npmmirror-镜像" class="headerlink" title="2. 替换为 npmmirror 镜像"></a>2. 替换为 npmmirror 镜像</h3><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置 npm 的源为 npmmirror</span></span><br><span class="line">npm config set registry https://registry.npmmirror.com</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">设置 yarn 的源为 npmmirror</span></span><br><span class="line">yarn config set registry https://registry.npmmirror.com</span><br></pre></td></tr></table></figure>

<hr>]]>
    </summary>
    <title>npm 手册</title>
    <updated>2026-03-18T09:09:08.119Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="git" scheme="https://fuyongde.github.io/tags/git/"/>
    <category term="骚操作" scheme="https://fuyongde.github.io/tags/%E9%AA%9A%E6%93%8D%E4%BD%9C/"/>
    <content>
      <![CDATA[<p>在日常开发中，如果我们一不小心删除了某个分支，又想重新使用这个分支，该怎么找回该分支上的代码呢？</p><h2 id="场景还原"><a href="#场景还原" class="headerlink" title="场景还原"></a>场景还原</h2><ol><li>有一个 git 仓库，从 <code>master</code> 分支新建 <code>feature/delete_branch</code> 分支，并进行一次提交，如下：</li></ol><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><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="meta prompt_"># </span><span class="language-bash">新建一个 feature/delete_branch 分支</span></span><br><span class="line">git branch feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">切换到 feature/delete_branch 分支</span></span><br><span class="line">git checkout feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">新建一个文件，进行一次提交并推送至远程分支</span></span><br><span class="line">git add delete-file.md</span><br><span class="line">git commit -m &#x27;:sparkles: add a delete-file.md file&#x27;</span><br><span class="line">git push origin feature/delete_branch:feature/delete_branch</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看当前分支图</span></span><br><span class="line">git log --all --decorate --oneline --graph</span><br><span class="line">* 88ac246 (HEAD -&gt; feature/delete_branch, origin/feature/delete_branch) :sparkles: add a delete-file.md file</span><br><span class="line">* 8693480 (origin/master, master) :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure><ol start="2"><li>切换到 <code>master</code> 分支，并删除 <code>feature/delete_branch</code> 分支</li></ol><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><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">切换到 master 分支</span></span><br><span class="line">git checkout master</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">删除 feature/delete_branch 分支</span></span><br><span class="line">git branch -d feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将删除操作同步至远程</span></span><br><span class="line">git push origin --delete feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">此时无论是本地还是远程都已经删除了 feature/delete_branch</span></span><br><span class="line">git log --all --decorate --oneline --graph</span><br><span class="line">* 8693480 (HEAD -&gt; master, origin/master) :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure><hr><h2 id="还原删除的分支"><a href="#还原删除的分支" class="headerlink" title="还原删除的分支"></a>还原删除的分支</h2><ol><li>通过 <code>git reflog</code> 或 <code>git log -g</code> 命令找到我们需要恢复的 <code>commit id</code></li></ol><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><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="meta prompt_"># </span><span class="language-bash">这里演示 git reflog 命令展示出来的提交</span></span><br><span class="line">git reflog</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;0&#125;: checkout: moving from feature/delete_branch to master</span><br><span class="line">88ac246 (feature/delete_branch) HEAD@&#123;1&#125;: commit: :sparkles: add a delete-file.md file</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;2&#125;: checkout: moving from master to feature/delete_branch</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;3&#125;: commit (initial): :sparkles: add a hello-git.md file</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">这里演示 git <span class="built_in">log</span> -g 命令展示出来的提交</span></span><br><span class="line">git log -g</span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;0&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: checkout: moving from feature/delete_branch to master</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br><span class="line"></span><br><span class="line">commit 88ac246020a0cc25071fb7fd3ac88363f4bf2beb (feature/delete_branch)</span><br><span class="line">Reflog: HEAD@&#123;1&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: commit: :sparkles: add a delete-file.md file</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:44:01 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a delete-file.md file</span><br><span class="line"></span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;2&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: checkout: moving from master to feature/delete_branch</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br><span class="line"></span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;3&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: commit (initial): :sparkles: add a hello-git.md file</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure><ol start="2"><li>通过 <code>git branch [branch name] [commit id]</code> 命令来恢复删除的分支</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">恢复 feature/delete_branch 分支到 88ac246 版本</span></span><br><span class="line">git branch feature/delete_branch 88ac246</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">检出 feature/delete_branch 分支</span></span><br><span class="line">git checkout feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">此时可以看到已经删除的分支都找回来了，将分支推送到远程</span></span><br><span class="line">git push origin feature/delete_branch:feature/delete_branch</span><br></pre></td></tr></table></figure><hr><h2 id="原因"><a href="#原因" class="headerlink" title="原因"></a>原因</h2><p>当我们删除一个分支时，git 只是删除了指向相关提交的指针，但该提交对象依然会留在版本库中，因此，如果我们知道删除分支时的 <code>commit id</code>，就可以将某个删除的分支恢复过来。</p><hr><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ol><li>通过这种方式恢复分支有时间限制，Git会定期清理无法访问的对象</li><li>无论是使用 <code>git branch -d</code> 还是 <code>git branch -D</code> 删除的分支，都可以通过这种方式恢复</li><li>如果远程分支被删除，需要有相应的权限才能重新推送分支到远程仓库</li></ol>]]>
    </content>
    <id>https://fuyongde.github.io/2020/03/26/git-%E6%81%A2%E5%A4%8D%E5%B7%B2%E5%88%A0%E9%99%A4%E7%9A%84%E5%88%86%E6%94%AF/</id>
    <link href="https://fuyongde.github.io/2020/03/26/git-%E6%81%A2%E5%A4%8D%E5%B7%B2%E5%88%A0%E9%99%A4%E7%9A%84%E5%88%86%E6%94%AF/"/>
    <published>2020-03-26T09:52:01.000Z</published>
    <summary>
      <![CDATA[<p>在日常开发中，如果我们一不小心删除了某个分支，又想重新使用这个分支，该怎么找回该分支上的代码呢？</p>
<h2 id="场景还原"><a href="#场景还原" class="headerlink" title="场景还原"></a>场景还原</h2><ol>
<li>有一个 git 仓库，从 <code>master</code> 分支新建 <code>feature/delete_branch</code> 分支，并进行一次提交，如下：</li>
</ol>
<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><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="meta prompt_"># </span><span class="language-bash">新建一个 feature/delete_branch 分支</span></span><br><span class="line">git branch feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">切换到 feature/delete_branch 分支</span></span><br><span class="line">git checkout feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">新建一个文件，进行一次提交并推送至远程分支</span></span><br><span class="line">git add delete-file.md</span><br><span class="line">git commit -m &#x27;:sparkles: add a delete-file.md file&#x27;</span><br><span class="line">git push origin feature/delete_branch:feature/delete_branch</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看当前分支图</span></span><br><span class="line">git log --all --decorate --oneline --graph</span><br><span class="line">* 88ac246 (HEAD -&gt; feature/delete_branch, origin/feature/delete_branch) :sparkles: add a delete-file.md file</span><br><span class="line">* 8693480 (origin/master, master) :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>切换到 <code>master</code> 分支，并删除 <code>feature/delete_branch</code> 分支</li>
</ol>
<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><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">切换到 master 分支</span></span><br><span class="line">git checkout master</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">删除 feature/delete_branch 分支</span></span><br><span class="line">git branch -d feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">将删除操作同步至远程</span></span><br><span class="line">git push origin --delete feature/delete_branch</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">此时无论是本地还是远程都已经删除了 feature/delete_branch</span></span><br><span class="line">git log --all --decorate --oneline --graph</span><br><span class="line">* 8693480 (HEAD -&gt; master, origin/master) :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure>

<hr>
<h2 id="还原删除的分支"><a href="#还原删除的分支" class="headerlink" title="还原删除的分支"></a>还原删除的分支</h2><ol>
<li>通过 <code>git reflog</code> 或 <code>git log -g</code> 命令找到我们需要恢复的 <code>commit id</code></li>
</ol>
<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><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="meta prompt_"># </span><span class="language-bash">这里演示 git reflog 命令展示出来的提交</span></span><br><span class="line">git reflog</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;0&#125;: checkout: moving from feature/delete_branch to master</span><br><span class="line">88ac246 (feature/delete_branch) HEAD@&#123;1&#125;: commit: :sparkles: add a delete-file.md file</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;2&#125;: checkout: moving from master to feature/delete_branch</span><br><span class="line">8693480 (HEAD -&gt; master, origin/master) HEAD@&#123;3&#125;: commit (initial): :sparkles: add a hello-git.md file</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">这里演示 git <span class="built_in">log</span> -g 命令展示出来的提交</span></span><br><span class="line">git log -g</span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;0&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: checkout: moving from feature/delete_branch to master</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br><span class="line"></span><br><span class="line">commit 88ac246020a0cc25071fb7fd3ac88363f4bf2beb (feature/delete_branch)</span><br><span class="line">Reflog: HEAD@&#123;1&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: commit: :sparkles: add a delete-file.md file</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:44:01 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a delete-file.md file</span><br><span class="line"></span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;2&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: checkout: moving from master to feature/delete_branch</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br><span class="line"></span><br><span class="line">commit 86934803092ae7ae7266f9ebff3429437e423b80 (HEAD -&gt; master, origin/master)</span><br><span class="line">Reflog: HEAD@&#123;3&#125; (fuyongde &lt;fuyongde@foxmail.com&gt;)</span><br><span class="line">Reflog message: commit (initial): :sparkles: add a hello-git.md file</span><br><span class="line">Author: fuyongde &lt;fuyongde@foxmail.com&gt;</span><br><span class="line">Date:   Thu Mar 26 18:36:35 2020 +0800</span><br><span class="line"></span><br><span class="line">    :sparkles: add a hello-git.md file</span><br></pre></td></tr></table></figure>]]>
    </summary>
    <title>git 恢复已删除的分支</title>
    <updated>2026-03-18T09:09:08.006Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <content>
      <![CDATA[<p>很久没有搞过 Android 开发了，Android 的命令行安装方式相比之前发生了一些变化，这里针对安装最新的 Android 命令行时遇到的一些问题，记录一下解决方法。</p><p><em>问题：下载最新的 commandlinetools 在自己指定的目录安装之后，无法使用</em></p><p><strong>原因：最新版本的 commandlinetools 对安装的目录是有要求的</strong></p><ol><li>Download latest Command line tools from android i.e. commandlinetools-win-6200805_latest.zip</li><li>Unzip the downloaded file</li><li>Create directory for storing commandline tools somewhere on your disk, with following path included: android&#x2F;cmdline-tools&#x2F;latest Basically when You unzip this Cmd line tools, just rename tools directory to latest and make sure You put this latest folder in android&#x2F;cmdline-tools directory somewhere on your disk</li><li>Create ANDROID_HOME environment variable for directory that stores the cmdline tools directory location like: <code>C:\YourLocationWhereYouStoreTheDirectory\android\cmdline-tools\latest</code></li><li>Create new entry in Path environment variable as %ANDROID_HOME%\bin</li></ol>]]>
    </content>
    <id>https://fuyongde.github.io/2020/03/24/android-command-line-%E5%AE%89%E8%A3%85%E6%97%B6%E7%9A%84%E5%9D%91/</id>
    <link href="https://fuyongde.github.io/2020/03/24/android-command-line-%E5%AE%89%E8%A3%85%E6%97%B6%E7%9A%84%E5%9D%91/"/>
    <published>2020-03-24T03:38:28.000Z</published>
    <summary>
      <![CDATA[<p>很久没有搞过 Android 开发了，Android 的命令行安装方式相比之前发生了一些变化，这里针对安装最新的 Android 命令行时遇到的一些问题，记录一下解决方法。</p>
<p><em>问题：下载最新的 commandlinetools 在自己指定的目录安装之后，]]>
    </summary>
    <title>android command line 安装时的坑</title>
    <updated>2026-03-18T09:09:07.934Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="手册" scheme="https://fuyongde.github.io/categories/%E6%89%8B%E5%86%8C/"/>
    <category term="pip" scheme="https://fuyongde.github.io/tags/pip/"/>
    <category term="Python" scheme="https://fuyongde.github.io/tags/Python/"/>
    <content>
      <![CDATA[<p>PyPI (Python Package Index) 是 <code>Python</code> 编程语言的软件存储库。开发者可以通过 PyPI 查找和安装由 <code>Python</code> 社区开发和共享的软件，也可以将自己开发的库上传至 PyPI 。</p><p>由于国外访问比较慢，我们可以通过配置国内的镜像来进行加速访问。</p><h2 id="配置-Aliyun-的镜像"><a href="#配置-Aliyun-的镜像" class="headerlink" title="配置 Aliyun 的镜像"></a>配置 Aliyun 的镜像</h2><h3 id="Linux-平台"><a href="#Linux-平台" class="headerlink" title="Linux 平台"></a>Linux 平台</h3><ol><li>找到下列文件</li></ol><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">~/.pip/pip.conf</span><br></pre></td></tr></table></figure><p>如果该文件不存在，需要创建相应的目录和文件：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mkdir ~/.pip</span><br><span class="line">touch ~/.pip/pip.conf</span><br></pre></td></tr></table></figure><ol start="2"><li>在上述文件中添加或修改:</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">index-url</span> = https://mirrors.aliyun.com/pypi/simple/</span><br><span class="line"></span><br><span class="line"><span class="section">[install]</span></span><br><span class="line"><span class="attr">trusted-host</span> = mirrors.aliyun.com</span><br></pre></td></tr></table></figure><h3 id="Windows-平台"><a href="#Windows-平台" class="headerlink" title="Windows 平台"></a>Windows 平台</h3><p>在 <code>explorer</code> 地址栏输入 <code>%AppData%</code>，进入文件夹后，在 <code>pip</code> 文件夹下新建 <code>pip.ini</code> 文件并写入以下配置。</p><p>如果 <code>pip</code> 文件夹不存在，需要先创建该文件夹。</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></pre></td><td class="code"><pre><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">index-url</span> = https://mirrors.aliyun.com/pypi/simple/</span><br><span class="line"><span class="attr">trusted-host</span> = mirrors.aliyun.com</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://fuyongde.github.io/2020/03/20/pip-%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2020/03/20/pip-%E6%89%8B%E5%86%8C/"/>
    <published>2020-03-20T15:42:33.000Z</published>
    <summary>
      <![CDATA[<p>PyPI (Python Package Index) 是 <code>Python</code> 编程语言的软件存储库。开发者可以通过 PyPI 查找和安装由 <code>Python</code> 社区开发和共享的软件，也可以将自己开发的库上传至 PyPI 。</p>
<p>由于国外访问比较慢，我们可以通过配置国内的镜像来进行加速访问。</p>
<h2 id="配置-Aliyun-的镜像"><a href="#配置-Aliyun-的镜像" class="headerlink" title="配置 Aliyun 的镜像"></a>配置 Aliyun 的镜像</h2><h3 id="Linux-平台"><a href="#Linux-平台" class="headerlink" title="Linux 平台"></a>Linux 平台</h3><ol>
<li>找到下列文件</li>
</ol>
<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">~/.pip/pip.conf</span><br></pre></td></tr></table></figure>

<p>如果该文件不存在，需要创建相应的目录和文件：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mkdir ~/.pip</span><br><span class="line">touch ~/.pip/pip.conf</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>在上述文件中添加或修改:</li>
</ol>
<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></pre></td><td class="code"><pre><span class="line"><span class="section">[global]</span></span><br><span class="line"><span class="attr">index-url</span> = https://mirrors.aliyun.com/pypi/simple/</span><br><span class="line"></span><br><span class="line"><span class="section">[install]</span></span><br><span class="line"><span class="attr">trusted-host</span> = mirrors.aliyun.com</span><br></pre></td></tr></table></figure>]]>
    </summary>
    <title>pip 手册</title>
    <updated>2026-03-18T09:09:08.125Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <content>
      <![CDATA[<h1 id="Windows-系统如何安装-Python-绿色版"><a href="#Windows-系统如何安装-Python-绿色版" class="headerlink" title="Windows 系统如何安装 Python 绿色版"></a>Windows 系统如何安装 Python 绿色版</h1><p>以下为适配 Windows 系统（以 Windows11 X64 为例）的 Python 3.13.9 绿色版安装步骤，安装路径统一为 C:\devtools\python\Python313，可实现多 Python 版本共存，适合需要灵活管理多个 Python 环境的场景。</p><h2 id="一、下载并解压-Python-3-13-9-绿色版"><a href="#一、下载并解压-Python-3-13-9-绿色版" class="headerlink" title="一、下载并解压 Python 3.13.9 绿色版"></a>一、下载并解压 Python 3.13.9 绿色版</h2><h3 id="获取绿色版解压包"><a href="#获取绿色版解压包" class="headerlink" title="获取绿色版解压包"></a>获取绿色版解压包</h3><p>访问 Python 官方下载页面，定位到 Python 3.13.9 版本：</p><ul><li>官方入口：打开 Python 下载首页，在 “Looking for a specific release?” 栏目下找到 “Python 3.13.9” 并点击进入版本详情页</li><li>直接下载：若已知链接，可访问 Python 3.13.9 专属下载页，在 “Files” 区域选择 Windows x86-64 zip file（64 位系统绿色版，文件格式为 .zip，无需安装，解压即可使用）</li></ul><h3 id="解压到指定路径"><a href="#解压到指定路径" class="headerlink" title="解压到指定路径"></a>解压到指定路径</h3><ul><li>找到下载完成的 .zip 压缩包，右键选择 “解压到…”</li><li>在解压路径选择窗口中，手动输入或浏览定位到 <code>C:\devtools\python\Python313</code>（若路径中 “devtools””python” 文件夹不存在，需提前在对应目录手动新建，确保路径无中文、无空格，避免后续出现命令执行错误）</li><li>等待解压完成，打开 <code>C:\devtools\python\Python313</code> 目录，确认包含 <code>python.exe</code> 等核心文件，即解压成功</li></ul><hr><h2 id="二、安装-pip-包管理器（绿色版默认不含-pip）"><a href="#二、安装-pip-包管理器（绿色版默认不含-pip）" class="headerlink" title="二、安装 pip 包管理器（绿色版默认不含 pip）"></a>二、安装 pip 包管理器（绿色版默认不含 pip）</h2><p>pip 是 Python 官方包管理工具，用于安装、卸载第三方库（如 numpy、pandas 等），绿色版 Python 默认不预装，需手动安装：</p><h3 id="下载-get-pip-py-脚本"><a href="#下载-get-pip-py-脚本" class="headerlink" title="下载 get-pip.py 脚本"></a>下载 get-pip.py 脚本</h3><ul><li>访问 pip 官方安装指南页面：<a href="https://pip.pypa.io/en/stable/installation/">https://pip.pypa.io/en/stable/installation/</a></li><li>在页面中找到 “Download get-pip.py” 链接，右键点击该链接，选择 “链接另存为”</li><li>在保存窗口中，将文件路径定位到 <code>C:\devtools\python\Python313</code>（即 Python 根目录），确保文件名保留为 <code>get-pip.py</code>，点击 “保存”</li></ul><h3 id="执行-pip-安装命令"><a href="#执行-pip-安装命令" class="headerlink" title="执行 pip 安装命令"></a>执行 pip 安装命令</h3><ul><li>按下 Win + R 组合键打开 “运行” 窗口，输入 <code>cmd</code> 后按下回车，打开 Windows 命令提示符（CMD）</li><li>在 CMD 中，通过 cd 命令切换到 Python 根目录，执行命令：</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /d C:\devtools\python\Python313</span><br></pre></td></tr></table></figure><blockquote><p><code>/d</code> 参数用于跨盘符切换路径，若当前 CMD 已在 C 盘，可省略 <code>/d</code>，直接执行 <code>cd C:\devtools\python\Python313</code></p></blockquote><ul><li>切换路径后，执行 pip 安装命令：</li></ul><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">py get-pip.py</span><br></pre></td></tr></table></figure><ul><li>等待命令执行完成（过程中会自动下载并安装 pip 及依赖组件），若 Python 根目录新增 <code>Lib</code> 和 <code>Scripts</code> 两个文件夹，且无报错提示，即 pip 安装成功</li></ul><hr><h2 id="三、解决-“找不到模块”-问题（关键步骤）"><a href="#三、解决-“找不到模块”-问题（关键步骤）" class="headerlink" title="三、解决 “找不到模块” 问题（关键步骤）"></a>三、解决 “找不到模块” 问题（关键步骤）</h2><p>绿色版 Python 为避免多版本冲突，默认不自动加载模块路径，导致执行 pip 命令时可能报错（如 “ModuleNotFoundError”），需通过修改配置文件解决：</p><h3 id="初始验证（确认问题）"><a href="#初始验证（确认问题）" class="headerlink" title="初始验证（确认问题）"></a>初始验证（确认问题）</h3><p>在已切换到 Python 根目录的 CMD 窗口中，执行以下两条命令：</p><ul><li>验证 Python 版本：<code>python -V</code><br>（若输出 “Python 3.13.9”，说明 Python 核心程序正常）</li><li>验证 pip 状态：<code>Scripts\pip -V</code><br>（若出现 “找不到模块” 或 “命令无法识别” 等报错，需继续执行后续步骤）</li></ul><h3 id="修改-python313-pth-配置文件"><a href="#修改-python313-pth-配置文件" class="headerlink" title="修改 python313._pth 配置文件"></a>修改 python313._pth 配置文件</h3><ul><li>打开 <code>C:\devtools\python\Python313</code> 目录，找到名为 <code>python313._pth</code> 的文件（文件名格式为 “python [版本号]._pth”，Python 3.13.9 对应此名称）</li><li>右键选择该文件，点击 “打开方式”→”记事本”（或其他文本编辑器）</li><li>在文件内容中找到 <code>#import site</code> 这一行（<code>#</code> 表示注释，该行未生效），删除开头的注释符 <code>#</code>，修改后为 <code>import site</code></li><li>按下 Ctrl + S 保存文件，关闭记事本</li></ul><h3 id="再次验证（确认问题解决）"><a href="#再次验证（确认问题解决）" class="headerlink" title="再次验证（确认问题解决）"></a>再次验证（确认问题解决）</h3><p>在 CMD 窗口中重新执行 <code>Scripts\pip -V</code>，若输出类似以下信息，说明问题已解决，pip 可正常使用：</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">pip X.X.X from C:\devtools\python\Python313\Lib\site-packages\pip (python 3.13)</span><br></pre></td></tr></table></figure><blockquote><p>“X.X.X” 为 pip 实际版本号，无需关注具体数值，只要无报错即可</p></blockquote><hr><h2 id="四、可选操作：添加环境变量（实现全局便捷调用）"><a href="#四、可选操作：添加环境变量（实现全局便捷调用）" class="headerlink" title="四、可选操作：添加环境变量（实现全局便捷调用）"></a>四、可选操作：添加环境变量（实现全局便捷调用）</h2><p>默认情况下，需在 Python 根目录执行 python 或 pip 命令，若希望在任意目录的 CMD 窗口中直接调用，需将 Python 路径添加到系统环境变量 Path 中：</p><h3 id="打开环境变量配置窗口"><a href="#打开环境变量配置窗口" class="headerlink" title="打开环境变量配置窗口"></a>打开环境变量配置窗口</h3><ul><li>右键点击 “此电脑”→ 选择 “属性”→ 在弹出的 “系统” 窗口中，点击左侧 “高级系统设置”</li><li>在 “系统属性” 窗口的 “高级” 选项卡下，点击右下角 “环境变量”</li><li>在 “环境变量” 窗口中，找到 “系统变量” 区域的 <code>Path</code> 变量，点击 “编辑”</li></ul><h3 id="添加-Python-相关路径"><a href="#添加-Python-相关路径" class="headerlink" title="添加 Python 相关路径"></a>添加 Python 相关路径</h3><p>在 “编辑环境变量” 窗口中，点击 “新建”，分别添加以下两个路径：</p><ul><li><code>C:\devtools\python\Python313</code>（Python 核心程序路径，用于调用 python 命令）</li><li><code>C:\devtools\python\Python313\Scripts</code>（pip 工具路径，用于调用 pip 命令）</li></ul><p>添加完成后，点击 “确定” 关闭所有窗口（需依次关闭 “编辑环境变量””环境变量””系统属性” 窗口，确保配置生效）</p><h3 id="验证环境变量是否生效"><a href="#验证环境变量是否生效" class="headerlink" title="验证环境变量是否生效"></a>验证环境变量是否生效</h3><ul><li>关闭已打开的所有 CMD 窗口（环境变量修改需重启 CMD 生效）</li><li>重新打开一个新的 CMD 窗口，在任意目录下执行以下命令：<ul><li><code>python -V</code>（输出 Python 3.13.9 版本）</li><li><code>pip -V</code>（输出 pip 版本及路径）</li></ul></li><li>若两条命令均无报错且正常输出，说明环境变量配置成功</li></ul><hr><h2 id="五、安装完成后的验证操作"><a href="#五、安装完成后的验证操作" class="headerlink" title="五、安装完成后的验证操作"></a>五、安装完成后的验证操作</h2><p>为确保 Python 及 pip 完全可用，可执行简单的测试命令：</p><ul><li>在 CMD 中执行 <code>python</code>，进入 Python 交互环境（显示 “&gt;&gt;&gt;” 提示符）</li><li>输入 <code>print(&quot;Python 3.13.9 绿色版安装成功！&quot;)</code>，按下回车，若输出对应字符串，说明 Python 交互环境正常</li><li>输入 <code>exit()</code> 退出交互环境，执行 <code>pip list</code>，若显示已安装的包列表（如 pip、setuptools 等），说明 pip 可正常使用</li></ul><p>至此，Python 3.13.9 绿色版在 Windows 系统中的安装全部完成，可根据需求安装第三方库或运行 Python 脚本。</p>]]>
    </content>
    <id>https://fuyongde.github.io/2020/03/19/How-Install-Embed-Python-On-Windows/</id>
    <link href="https://fuyongde.github.io/2020/03/19/How-Install-Embed-Python-On-Windows/"/>
    <published>2020-03-19T06:42:33.000Z</published>
    <summary>
      <![CDATA[<h1 id="Windows-系统如何安装-Python-绿色版"><a href="#Windows-系统如何安装-Python-绿色版" class="headerlink" title="Windows 系统如何安装 Python 绿色版"></a>Windows 系统如何安装 Python 绿色版</h1><p>以下为适配 Windows 系统（以 Windows11 X64 为例）的 Python 3.13.9 绿色版安装步骤，安装路径统一为 C:\devtools\python\Python313，可实现多 Python 版本共存，适合需要灵活管理多个 Python 环境的场景。</p>
<h2 id="一、下载并解压-Python-3-13-9-绿色版"><a href="#一、下载并解压-Python-3-13-9-绿色版" class="headerlink" title="一、下载并解压 Python 3.13.9 绿色版"></a>一、下载并解压 Python 3.13.9 绿色版</h2><h3 id="获取绿色版解压包"><a href="#获取绿色版解压包" class="headerlink" title="获取绿色版解压包"></a>获取绿色版解压包</h3><p>访问 Python 官方下载页面，定位到 Python 3.13.9 版本：</p>
<ul>
<li>官方入口：打开 Python 下载首页，在 “Looking for a specific release?” 栏目下找到 “Python 3.13.9” 并点击进入版本详情页</li>
<li>直接下载：若已知链接，可访问 Python 3.13.9 专属下载页，在 “Files” 区域选择 Windows x86-64 zip file（64 位系统绿色版，文件格式为 .zip，无需安装，解压即可使用）</li>
</ul>
<h3 id="解压到指定路径"><a href="#解压到指定路径" class="headerlink" title="解压到指定路径"></a>解压到指定路径</h3><ul>
<li>找到下载完成的 .zip 压缩包，右键选择 “解压到…”</li>
<li>在解压路径选择窗口中，手动输入或浏览定位到 <code>C:\devtools\python\Python313</code>（若路径中 “devtools””python” 文件夹不存在，需提前在对应目录手动新建，确保路径无中文、无空格，避免后续出现命令执行错误）</li>
<li>等待解压完成，打开 <code>C:\devtools\python\Python313</code> 目录，确认包含 <code>python.exe</code> 等核心文件，即解压成功</li>
</ul>
<hr>
<h2 id="二、安装-pip-包管理器（绿色版默认不含-pip）"><a href="#二、安装-pip-包管理器（绿色版默认不含-pip）" class="headerlink" title="二、安装 pip 包管理器（绿色版默认不含 pip）"></a>二、安装 pip 包管理器（绿色版默认不含 pip）</h2>]]>
    </summary>
    <title>How Install Embed Python On Windows</title>
    <updated>2026-03-18T09:09:08.021Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="手册" scheme="https://fuyongde.github.io/categories/%E6%89%8B%E5%86%8C/"/>
    <category term="未完待续" scheme="https://fuyongde.github.io/tags/%E6%9C%AA%E5%AE%8C%E5%BE%85%E7%BB%AD/"/>
    <category term="Gradle" scheme="https://fuyongde.github.io/tags/Gradle/"/>
    <category term="构建工具" scheme="https://fuyongde.github.io/tags/%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<h2 id="Gradle-是什么"><a href="#Gradle-是什么" class="headerlink" title="Gradle 是什么"></a>Gradle 是什么</h2><blockquote><p>Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL. Read about Gradle features to learn what is possible with Gradle.</p></blockquote><p>Gradle 是一个专注于灵活性和性能的开源自动化构建工具，其脚本由 <code>Groovy</code> 或 <code>Kotlin DSL</code> 编写。Gradle 有以下特性：</p><ul><li>高度可定制：Gradle 采用可以高度自定义的方式进行建模。</li><li>快速：Gradle 通过重用旧的输出、仅变更的输入以及并行的方式来快速完成任务。</li><li>功能强大：Gradle 是 <code>Android</code> 的官方构建工具，并附带对许多流行语言和技术的支持。</li></ul><hr><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>安装之前，请确保已经正确安装 <code>JDK8</code> 或更高版本</p><h3 id="使用包管理器安装"><a href="#使用包管理器安装" class="headerlink" title="使用包管理器安装"></a>使用包管理器安装</h3><p>针对类 <code>Unix</code> 系统（<code>macOS</code>、<code>Linux</code>、<code>Cygwin</code>、<code>Solaris</code> 和 <code>FreeBSD</code>）可以采用以下两种包管理的方式安装。</p><ul><li>SDKMAN 包管理器</li></ul><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">sdk install gradle</span><br></pre></td></tr></table></figure><ul><li>Homebrew 包管理器</li></ul><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">brew install gradle</span><br></pre></td></tr></table></figure><h3 id="手动安装"><a href="#手动安装" class="headerlink" title="手动安装"></a>手动安装</h3><ol><li>下载指定的 Gradle 发行版，下载地址可参考<a href="https://gradle.org/releases/">发布页</a></li><li>解压缩到指定的目录，如 Windows 系统中的 <code>C:\gradle</code> 或 Linux 系统中的 <code>/opt/gradle</code> 等</li><li>配置环境变量</li></ol><ul><li>针对 <code>Windows</code> 系统，打开 <code>控制面板</code> &gt; <code>系统和安全</code> &gt; <code>系统</code> &gt; <code>高级系统设置</code> &gt; <code>环境变量</code> &gt; <code>系统变量</code>，新增 <code>GRADLE_HOME</code> 变量，该变量指向 Gradle 的安装路径，然后选中<code>系统变量</code>中的 <code>Path</code>，并新增 <code>%GRADLE_HOME%\bin\</code></li><li>针对 <code>Linux</code> 系统，在 <code>~/.bashrc</code> 或 <code>~/.profile</code> 文件中添加以下内容：<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">export GRADLE_HOME=/opt/gradle/gradle-[version]</span><br><span class="line">export PATH=$PATH:$GRADLE_HOME/bin</span><br></pre></td></tr></table></figure></li></ul><p>除了配置环境变量之外，还可以配置 <code>GRADLE_USER_HOME</code> 变量来指定 Gradle 的库文件的目录，该变量缺省值为当前系统用户目录下的 <code>.gradle</code> 目录。</p><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>在命令行中执行 <code>gradle -v</code> 命令，若输出为你安装的版本信息，则说明安装成功了。</p><hr><h2 id="使用-Gradle-构建一个项目"><a href="#使用-Gradle-构建一个项目" class="headerlink" title="使用 Gradle 构建一个项目"></a>使用 Gradle 构建一个项目</h2><ol><li>新建一个目录 <code>mkdir hello-world</code></li><li>进入该目录 <code>cd hello-world</code></li><li>使用 Gradle 初始化该目录 <code>gradle init</code>。执行完该命令之后按照指引选择要初始化工程的类型等。之后可以得到一个至少包含以下路径的目录结构。</li></ol><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></pre></td><td class="code"><pre><span class="line">├── build.gradle</span><br><span class="line">├── gradle</span><br><span class="line">│   └── wrapper</span><br><span class="line">│       ├── gradle-wrapper.jar</span><br><span class="line">│       └── gradle-wrapper.properties</span><br><span class="line">├── gradlew</span><br><span class="line">├── gradlew.bat</span><br><span class="line">└── settings.gradle</span><br></pre></td></tr></table></figure><p>看到 <code>gradlew</code>、<code>gradlew.bat</code> 以及 <code>wrapper</code> 目录是不是有点熟悉？没错，这个和 Maven Wrapper 的思想是一样的，可参考 <a href="https://fuyongde.github.io/2019/06/23/maven-wapper-%E6%89%8B%E5%86%8C/">Maven Wrapper 手册</a>这篇文章。</p><hr><h2 id="Gradle-Wrapper"><a href="#Gradle-Wrapper" class="headerlink" title="Gradle Wrapper"></a>Gradle Wrapper</h2><p>Gradle Wrapper 的作用是让团队成员无需预先安装 Gradle 环境即可构建项目，保证所有成员使用相同版本的 Gradle。其核心是以下文件：</p><ul><li><code>gradlew</code>：Unix 系统的 shell 脚本</li><li><code>gradlew.bat</code>：Windows 系统的批处理脚本</li><li><code>gradle/wrapper/gradle-wrapper.jar</code>：Wrapper 的可执行 JAR 文件</li><li><code>gradle/wrapper/gradle-wrapper.properties</code>：配置文件，指定使用的 Gradle 版本</li></ul><p>使用 Gradle Wrapper 运行构建时，使用 <code>./gradlew</code>（Unix）或 <code>gradlew.bat</code>（Windows）代替直接使用 <code>gradle</code> 命令。</p><p>未完待续…</p>]]>
    </content>
    <id>https://fuyongde.github.io/2020/03/08/gradle-%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2020/03/08/gradle-%E6%89%8B%E5%86%8C/"/>
    <published>2020-03-08T11:46:14.000Z</published>
    <summary>
      <![CDATA[<h2 id="Gradle-是什么"><a href="#Gradle-是什么" class="headerlink" title="Gradle 是什么"></a>Gradle 是什么</h2><blockquote>
<p>Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL. Read about Gradle features to learn what is possible with Gradle.</p>
</blockquote>
<p>Gradle 是一个专注于灵活性和性能的开源自动化构建工具，其脚本由 <code>Groovy</code> 或 <code>Kotlin DSL</code> 编写。Gradle 有以下特性：</p>
<ul>
<li>高度可定制：Gradle 采用可以高度自定义的方式进行建模。</li>
<li>快速：Gradle 通过重用旧的输出、仅变更的输入以及并行的方式来快速完成任务。</li>
<li>功能强大：Gradle 是 <code>Android</code> 的官方构建工具，并附带对许多流行语言和技术的支持。</li>
</ul>
<hr>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>安装之前，请确保已经正确安装 <code>JDK8</code> 或更高版本</p>
<h3 id="使用包管理器安装"><a href="#使用包管理器安装" class="headerlink" title="使用包管理器安装"></a>使用包管理器安装</h3><p>针对类 <code>Unix</code> 系统（<code>macOS</code>、<code>Linux</code>、<code>Cygwin</code>、<code>Solaris</code> 和 <code>FreeBSD</code>）可以采用以下两种包管理的方式安装。</p>
<ul>
<li>SDKMAN 包管理器</li>
</ul>]]>
    </summary>
    <title>Gradle 手册</title>
    <updated>2026-11-03T08:18:46.531Z</updated>
  </entry>
  <entry>
    <author>
      <name>傅永德</name>
    </author>
    <category term="手册" scheme="https://fuyongde.github.io/categories/%E6%89%8B%E5%86%8C/"/>
    <category term="未完待续" scheme="https://fuyongde.github.io/tags/%E6%9C%AA%E5%AE%8C%E5%BE%85%E7%BB%AD/"/>
    <category term="Jenkins" scheme="https://fuyongde.github.io/tags/Jenkins/"/>
    <category term="CI" scheme="https://fuyongde.github.io/tags/CI/"/>
    <category term="持续集成" scheme="https://fuyongde.github.io/tags/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/"/>
    <content>
      <![CDATA[<p>Jenkins 是常见的 CI 平台</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h3 id="Windows-平台"><a href="#Windows-平台" class="headerlink" title="Windows 平台"></a>Windows 平台</h3><p>略</p><h3 id="CentOS-平台"><a href="#CentOS-平台" class="headerlink" title="CentOS 平台"></a>CentOS 平台</h3><p>更为详细的步骤及说明可查看<a href="https://pkg.jenkins.io/redhat-stable/">官网</a></p><h4 id="添加-jenkins-源"><a href="#添加-jenkins-源" class="headerlink" title="添加 jenkins 源"></a>添加 jenkins 源</h4><p>命令如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo</span><br><span class="line">sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key</span><br></pre></td></tr></table></figure><h4 id="yum-安装"><a href="#yum-安装" class="headerlink" title="yum 安装"></a>yum 安装</h4><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></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">在线安装的方式</span></span><br><span class="line">yum install jenkins</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">离线安装的方式，由于国内连接 jenkins 网站较慢，可先行下载 rpm 文件格式的离线安装包，采用离线的方式安装</span></span><br><span class="line">yum install jenkins-2.204.2-1.1.noarch.rpm</span><br></pre></td></tr></table></figure><h4 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h4><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">systemctl start jenkins</span><br></pre></td></tr></table></figure><h4 id="解决插件中心安装慢的问题"><a href="#解决插件中心安装慢的问题" class="headerlink" title="解决插件中心安装慢的问题"></a>解决插件中心安装慢的问题</h4><p>第一次登录 Jenkins 会出现初始化缓慢的问题，产生这个问题的原因有两个，一个是因为首次打开 Jenkins 会尝试连接 Google，另外一个原因是因为连接插件中心缓慢。</p><h5 id="将插件中心修改为清华的源"><a href="#将插件中心修改为清华的源" class="headerlink" title="将插件中心修改为清华的源"></a>将插件中心修改为清华的源</h5><p>修改文件 <code>$JENKINS_HOME/hudson.model.UpdateCenter.xml</code>，将 url 从 <code>https://updates.jenkins.io/update-center.json</code> 修改为 <code>https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json</code></p><h5 id="将-Google-的地址修改为-Baidu"><a href="#将-Google-的地址修改为-Baidu" class="headerlink" title="将 Google 的地址修改为 Baidu"></a>将 Google 的地址修改为 Baidu</h5><p>修改文件 <code>$JENKINS_HOME/updates/default.json</code></p><ul><li>将 <code>connectionCheckUrl</code> 的值修改为 <code>http://www.baidu.com/</code></li><li>替换文件中 <code>updates.jenkins-ci.org/download</code> 为 <code>mirrors.tuna.tsinghua.edu.cn/jenkins</code></li></ul><p>未完待续…</p>]]>
    </content>
    <id>https://fuyongde.github.io/2020/02/18/jenkins-%E6%89%8B%E5%86%8C/</id>
    <link href="https://fuyongde.github.io/2020/02/18/jenkins-%E6%89%8B%E5%86%8C/"/>
    <published>2020-02-18T08:54:55.000Z</published>
    <summary>
      <![CDATA[<p>Jenkins 是常见的 CI 平台</p>
<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><h3 id="Windows-平台"><a href="#Windows-平台" class="headerlink" title="Windows 平台"></a>Windows 平台</h3><p>略</p>
<h3 id="CentOS-平台"><a href="#CentOS-平台" class="headerlink" title="CentOS 平台"></a>CentOS 平台</h3><p>更为详细的步骤及说明可查看<a href="https://pkg.jenkins.io/redhat-stable/">官网</a></p>
<h4 id="添加-jenkins-源"><a href="#添加-jenkins-源" class="headerlink" title="添加 jenkins 源"></a>添加 jenkins 源</h4><p>命令如下：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo</span><br><span class="line">sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key</span><br></pre></td></tr></table></figure>

<h4 id="yum-安装"><a href="#yum-安装" class="headerlink" title="yum 安装"></a>yum 安装</h4>]]>
    </summary>
    <title>Jenkins 手册</title>
    <updated>2026-03-18T09:09:08.066Z</updated>
  </entry>
</feed>
