<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>上下文网</title>
    <description>上下文网</description>
    <link>http://43.128.224.135:3003</link>
    <generator>ReactPress</generator>
    <lastBuildDate>Sun, 16 Aug 2026 19:30:15 GMT</lastBuildDate>
    <category>前端</category><category>后端</category><category>py</category><category>其他</category>
    
    <item>
      <title>微信读书APP签名逆向libencrypt.so</title>
      <description><![CDATA[<p>最近在分析微信读书接口时，发现请求中存在signature参数。</p>
<p>最开始尝试：</p>
<pre><code class="text language-text">SHA256(timestamp+deviceId+random)
</code></pre>
<p>但是生成结果和真实signature不一致。</p>
<p>因此判断真实签名逻辑并不在Java层，而是在Native层的libencrypt.so中实现。</p>
<p>本文记录一次完整的逆向过程，目标是定位</p>
<pre><code class="text language-text">weread.encrypt.EncryptUtils
</code></pre>
<p>中的</p>
<pre><code class="text language-text">nativeGetSignatures
</code></pre>
<p>方法，并还原签名生成流程。</p>
<hr />
<h1 id="一、hookjava层定位入口">一、HookJava层定位入口</h1>
<p>首先定位Java层：</p>
<pre><code class="text language-text">weread.encrypt.EncryptUtils
</code></pre>
<p>找到：</p>
<pre><code class="java language-java">nativeGetSignatures(String[]strArr)
</code></pre>
<p>方法。</p>
<p>通过FridaHook调试：</p>
<pre><code class="javascript language-javascript">Java.perform(function () {

    const EncryptUtils =
        Java.use(
            "weread.encrypt.EncryptUtils"
        );


    EncryptUtils.nativeGetSignatures
        .implementation = function(arr) {


            console.log(
                "参数数量:"
                + arr.length
            );


            for(let i = 0; i &lt; arr.length; i++){

                console.log(
                    "arg[" + i + "]="
                    + arr[i]
                );

            }


            let result =
                this.nativeGetSignatures(arr);


            console.log(
                "返回:"
                + result
            );


            return result;

        };

});
</code></pre>
<p>运行后：</p>
<p>发现返回值长度：</p>
<pre><code class="text language-text">64位：
dc3ea89d77e43ce790a0b9dfbddd02a2a8d3525abd5f35e4ecd7c822d1f32cfb
</code></pre>
<p>64位十六进制字符串符合SHA256输出特征。</p>
<p>因此初步判断：</p>
<pre><code class="text language-text">signature=SHA256(...)
</code></pre>
<hr />
<h1 id="二、定位libencryptso">二、定位libencrypt.so</h1>
<p>Java层最终进入Native。</p>
<p>找到：</p>
<pre><code class="text language-text">libencrypt.so
</code></pre>
<p>其中核心JNI函数：</p>
<pre><code class="text language-text">Java_weread_encrypt_EncryptUtils_nativeGetSignatures
</code></pre>
<p>继续分析，发现主要调用：</p>
<pre><code class="text language-text">weread::GenSignature(n)
</code></pre>
<p>真正的签名生成逻辑在：weread::GenSignature中。</p>
<hr />
<h1 id="三、分析native层参数处理">三、分析Native层参数处理</h1>
<p>主动调用调试：</p>
<p>发现Java层提交的数组进入Native后，并不是直接拼接。</p>
<p>首先进行了sort()排序。</p>
<p>也就是说，Java层传入：</p>
<pre><code class="text language-text">[
参数1,
参数2,
参数3
]
</code></pre>
<p>进入Native后：</p>
<pre><code class="text language-text">排序后的参数
</code></pre>
<p>才参与后续签名计算。</p>
<p>因此：</p>
<p>参数顺序非常重要。</p>
<p>不能直接按照调用处看到的顺序进行拼接。</p>
<hr />
<h1 id="四、发现隐藏salt">四、发现隐藏salt</h1>
<p>继续分析：</p>
<p>在：</p>
<pre><code class="text language-text">weread::GenSignature
</code></pre>
<p>中发现最终参与签名的数据比传入数组多一个字段。</p>
<p>原本：</p>
<pre><code class="text language-text">3个参数
</code></pre>
<p>经过处理后：</p>
<pre><code class="text language-text">4个参数
</code></pre>
<p>多出来的就是：</p>
<pre><code class="text language-text">salt
</code></pre>
<p>最终类似：</p>
<pre><code class="text language-text">参数1
+
参数2
+
参数3
+
salt
</code></pre>
<hr />
<h1 id="五、分析salt生成方式">五、分析salt生成方式</h1>
<p>继续跟踪，发现调用：</p>
<pre><code class="text language-text">weread::RemapString()
</code></pre>
<p>进行字符串重映射。</p>
<p>Native中保存了一张：</p>
<pre><code class="text language-text">256字节查找表
</code></pre>
<p>转换逻辑：</p>
<pre><code class="text language-text">输入字符

↓

ASCII值作为索引

↓

查询byte数组

↓

生成新的字符串
</code></pre>
<hr />
<p>Python还原：</p>
<pre><code class="python language-python">def transform_string(input_str):

    byte_Index = [
        0x34,
        0xCA,
        0x55,
        0x40,
        0x1D,
        0xB6,
        0x93,
        0xC6,
        # ...
        # 完整256字节表
        0xB7,
        0x15
    ]


    result = []


    for char in input_str:

        index = ord(char)


        if index &lt; 0 or index &gt;= len(byte_Index):

            raise ValueError(
                f"{char} 超出范围"
            )


        result.append(
            byte_Index[index]
        )


    return bytes(result)
</code></pre>
<p>将IDA中找到的256字节表复制出来，即可还原真实salt。</p>
<hr />
<h1 id="六、分析hash流程">六、分析Hash流程</h1>
<p>继续跟踪：</p>
<p>发现签名并不是简单一次SHA256。</p>
<p>实际流程：</p>
<pre><code class="text language-text">两次SHA256
</code></pre>
<p>并且每次Hash前都会进行一次数据重排序。</p>
<p>完整流程：</p>
<pre><code class="text language-text">输入4个参数

↓

sort排序

↓

追加salt

↓

第一次数据重排序

↓

SHA256

↓

第二次数据重排序

↓

SHA256

↓

输出signature
</code></pre>
<hr />
<h1 id="七、自定义数据重排序算法">七、自定义数据重排序算法</h1>
<p>通过IDA分析发现：</p>
<p>每次SHA256前都会计算一个校验值。</p>
<h2 id="1计算校验值">1.计算校验值</h2>
<p>通过异或：</p>
<pre><code class="text language-text">checksum=byte1^byte2^byte3...
</code></pre>
<p>得到：</p>
<pre><code class="text language-text">0-10
</code></pre>
<p>范围内的值。</p>
<hr />
<h2 id="2根据校验值循环位移">2.根据校验值循环位移</h2>
<p>根据checksum：</p>
<p>对输入数据进行循环移动。</p>
<p>类似：</p>
<pre><code class="python language-python">def reorder(data):

    checksum = 0


    for b in data:

        checksum ^= b


    offset = checksum % 10


    return (
        data[offset:]
        +
        data[:offset]
    )
</code></pre>
<hr />
<h1 id="八、最终签名流程">八、最终签名流程</h1>
<p>最终还原：</p>
<pre><code class="text language-text">Java层参数

↓

nativeGetSignatures

↓

weread::GenSignature

↓

参数sort排序

↓

RemapString生成salt

↓

追加salt

↓

第一次数据重排序

↓

SHA256

↓

第二次数据重排序

↓

SHA256

↓

signature
</code></pre>
<h2 id="拿到signature后那不就随便造了么">拿到signature后那不就随便造了么</h2>]]></description>
      <link>http://43.128.224.135:3003/article/1f1cffb7-51fc-4878-86b1-6872b0c60580</link>
      <guid>http://43.128.224.135:3003/article/1f1cffb7-51fc-4878-86b1-6872b0c60580</guid>
      <pubDate>Mon, 10 Aug 2026 07:41:05 GMT</pubDate>
      <category>py</category>
    </item>
    <item>
      <title>MySQL之EXPLAIN</title>
      <description><![CDATA[<h2 id="一、btree-与磁盘-io">一、B+Tree 与磁盘 I/O</h2>
<ul>
<li>B+Tree 每次向下查找一层，都会产生一次 <strong>磁盘 I/O</strong></li>
<li>树的高度越低，I/O 次数越少，查询越快</li>
</ul>
<hr />
<h2 id="二、聚簇索引（clustered-index）">二、聚簇索引（Clustered Index）</h2>
<blockquote>
  <p><strong>InnoDB 中，主键索引就是聚簇索引</strong></p>
</blockquote>
<h3 id="聚簇索引结构">聚簇索引结构</h3>
<pre><code>id
 ↓
完整的一行数据
</code></pre>
<ul>
<li>叶子节点直接存储 <strong>整行数据</strong></li>
<li>一张表 <strong>只有一个</strong> 聚簇索引（通常是主键）</li>
</ul>
<hr />
<h2 id="三、二级索引（secondary-index）">三、二级索引（Secondary Index）</h2>
<ul>
<li>叶子节点存储的是：<strong>主键值</strong>，而非整行数据</li>
</ul>
<h3 id="回表（bookmark-lookup）">回表（Bookmark Lookup）</h3>
<pre><code>二级索引 → 主键 id → 聚簇索引 → 整行数据
</code></pre>
<ul>
<li>使用二级索引查非索引列时，会发生 <strong>回表</strong></li>
<li>如果只查询 <strong>二级索引列 + 主键</strong>，则 <strong>不需要回表</strong></li>
</ul>
<h3 id="覆盖索引（covering-index）">覆盖索引（Covering Index）</h3>
<ul>
<li>查询的列 <strong>全部包含在索引中</strong></li>
<li>Extra 中显示：<code>Using index</code></li>
<li>✅ 性能最好，避免回表</li>
</ul>
<hr />
<h2 id="四、explain-解读">四、EXPLAIN 解读</h2>
<table>
<thead>
<tr>
<th>字段</th>
<th>含义</th>
<th>如何判断优劣</th>
</tr>
</thead>
<tbody>
<tr>
<td>type</td>
<td>访问类型（查询方式）</td>
<td>越靠近 <code>const</code> 越好<br><code>ref</code>：普通索引<br><code>range</code>：索引范围扫描（如 <code>BETWEEN</code>、<code>&gt;</code>）<br>❌ 避免 <code>ALL</code>（全表扫描）</td>
</tr>
<tr>
<td>key</td>
<td>实际使用的索引</td>
<td>不能为 <code>NULL</code></td>
</tr>
<tr>
<td>rows</td>
<td>预估扫描行数</td>
<td>越小越好</td>
</tr>
<tr>
<td>Extra</td>
<td>额外信息</td>
<td>✅ <code>Using index</code>（覆盖索引）<br>⚠️ <code>Using filesort</code><br>⚠️ <code>Using temporary</code></td>
</tr>
</tbody>
</table>
<h3 id="explain-阅读顺序（口诀）">EXPLAIN 阅读顺序（口诀）</h3>
<ol>
<li>看 <code>type</code>：有没有全表扫描</li>
<li>看 <code>key</code>：是否真正用到索引</li>
<li>看 <code>rows</code>：扫描了多少行</li>
<li>看 <code>Extra</code>：是否有排序、临时表、覆盖索引</li>
</ol>
<hr />
<h2 id="五、sql-慢查询排查流程">五、SQL 慢查询排查流程</h2>
<pre><code>SQL 很慢
   │
   ▼
① 有没有走索引？
   │
   ▼
② 为什么没走索引？
   │
   ▼
③ 扫描了多少行？
   │
   ▼
④ 有没有排序？
   │
   ▼
⑤ 有没有回表？
</code></pre>
<hr />
<h2 id="六、常见导致索引失效的原因">六、常见导致索引失效的原因</h2>
<ul>
<li>隐式类型转换</li>
<li>联合索引违反 <strong>最左匹配原则</strong></li>
<li>对索引列做函数操作</li>
<li><code>OR</code> 条件不当</li>
<li><code>LIKE '%xxx'</code>（前模糊）</li>
</ul>
<hr />
<h2 id="七、order-by-为什么会慢？">七、ORDER BY 为什么会慢？</h2>
<ul>
<li><code>ORDER BY age</code> 且 <code>age</code> 无索引</li>
<li>MySQL 无法利用索引排序</li>
<li>结果：<code>Using filesort</code></li>
<li>本质：<strong>把数据查出来后再在内存 / 磁盘中排序</strong></li>
</ul>
<hr />
<h2 id="八、事务隔离级别与-mvcc（快照）">八、事务隔离级别与 MVCC（快照）</h2>
<ul>
<li><strong>RC（Read Committed）</strong>：每次 SELECT 都生成新快照</li>
<li><strong>RR（Repeatable Read）</strong>：事务内使用同一快照（MySQL InnoDB 默认）</li>
</ul>
<blockquote>
  <p>本质：<strong>MVCC（多版本并发控制）</strong></p>
</blockquote>
<hr />
<h2 id="九、深分页问题（limit-优化）">九、深分页问题（LIMIT 优化）</h2>
<pre><code class="sql language-sql">SELECT * FROM t LIMIT 1000000, 10; -- ❌ 很慢
</code></pre>
<h3 id="原因">原因</h3>
<ul>
<li>MySQL 需要先扫描前 1000000 条记录</li>
<li>再丢弃，只取 10 条</li>
</ul>
<h3 id="优化方案：游标分页（keyset-pagination）">优化方案：游标分页（Keyset Pagination）</h3>
<pre><code class="sql language-sql">SELECT * FROM t WHERE id &gt; 1000000 ORDER BY id LIMIT 10;
</code></pre>
<p>✅ 利用索引顺序定位  <br />
✅ 避免 OFFSET 扫描  <br />
✅ 性能稳定，适合大数据量翻页</p>
<hr />
<blockquote>
  <p><strong>索引决定 I/O，EXPLAIN 决定方向，回表和排序决定代价，分页和隔离级别决定边界。</strong></p>
</blockquote>]]></description>
      <link>http://43.128.224.135:3003/article/cd8f61af-868d-4465-8ce0-56dd17a8d27e</link>
      <guid>http://43.128.224.135:3003/article/cd8f61af-868d-4465-8ce0-56dd17a8d27e</guid>
      <pubDate>Thu, 06 Aug 2026 10:10:38 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>Java 开发常用笔记</title>
      <description><![CDATA[<h1 id="一、spring-boot-接口三种入参请求和接收方式">一、Spring Boot 接口三种入参请求和接收方式</h1>
<p>Spring Boot 接口常见的入参方式主要有三种：</p>
<pre><code class="text language-text">路径参数
查询参数
JSON 请求体
</code></pre>
<h2 id="1-路径参数：pathvariable">1. 路径参数：@PathVariable</h2>
<p>请求地址：</p>
<pre><code class="text language-text">GET /users/1001
</code></pre>
<p>Controller：</p>
<pre><code class="java language-java">@GetMapping("/users/{id}")
public UserVO getUser(
        @PathVariable Long id
) {
    return userService.getUser(id);
}
</code></pre>
<p>也可以明确指定参数名：</p>
<pre><code class="java language-java">@GetMapping("/users/{userId}")
public UserVO getUser(
        @PathVariable("userId") Long id
) {
    return userService.getUser(id);
}
</code></pre>
<p>适合场景：</p>
<pre><code class="text language-text">根据 ID 查询
根据编号删除
访问明确的单个资源
</code></pre>
<hr />
<h2 id="2-查询参数：requestparam">2. 查询参数：@RequestParam</h2>
<p>请求地址：</p>
<pre><code class="text language-text">GET /users?page=1&amp;size=10&amp;status=1
</code></pre>
<p>Controller：</p>
<pre><code class="java language-java">@GetMapping("/users")
public List&lt;UserVO&gt; list(
        @RequestParam Integer page,
        @RequestParam Integer size,
        @RequestParam(required = false) Integer status
) {
    return userService.list(page, size, status);
}
</code></pre>
<p>设置默认值：</p>
<pre><code class="java language-java">@GetMapping("/users")
public List&lt;UserVO&gt; list(
        @RequestParam(defaultValue = "1") Integer page,
        @RequestParam(defaultValue = "10") Integer size
) {
    return userService.list(page, size);
}
</code></pre>
<p>参数较多时，可以直接使用对象接收：</p>
<pre><code class="java language-java">@Data
public class UserQuery {

    private Integer page;

    private Integer size;

    private Integer status;
}
</code></pre>
<pre><code class="java language-java">@GetMapping("/users")
public List&lt;UserVO&gt; list(UserQuery query) {
    return userService.list(query);
}
</code></pre>
<p>适合场景：</p>
<pre><code class="text language-text">分页查询
条件筛选
简单表单参数
</code></pre>
<hr />
<h2 id="3-json-请求体：requestbody">3. JSON 请求体：@RequestBody</h2>
<p>请求：</p>
<pre><code class="json language-json">{
  "username": "zhangsan",
  "age": 30
}
</code></pre>
<p>DTO：</p>
<pre><code class="java language-java">@Data
public class UserCreateDTO {

    private String username;

    private Integer age;
}
</code></pre>
<p>Controller：</p>
<pre><code class="java language-java">@PostMapping("/users")
public Long create(
        @RequestBody UserCreateDTO dto
) {
    return userService.create(dto);
}
</code></pre>
<p>适合场景：</p>
<pre><code class="text language-text">新增数据
修改数据
复杂对象
嵌套对象
数组和集合
</code></pre>
<hr />
<h2 id="4-三种方式怎么选">4. 三种方式怎么选</h2>
<pre><code class="text language-text">@PathVariable：资源标识
@RequestParam：查询条件
@RequestBody：复杂业务数据
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">@PutMapping("/users/{id}")
public void update(
        @PathVariable Long id,
        @RequestBody UserUpdateDTO dto
) {
}
</code></pre>
<p>这里：</p>
<pre><code class="text language-text">id 表示修改哪个用户
dto 表示修改哪些内容
</code></pre>
<hr />
<h1 id="二、comparable-和-comparator-实现排序">二、Comparable 和 Comparator 实现排序</h1>
<p>Java 中对象排序常见有两种方式：</p>
<pre><code class="text language-text">Comparable：对象自己定义排序规则
Comparator：外部定义排序规则
</code></pre>
<hr />
<h2 id="1-comparable">1. Comparable</h2>
<p>实体类实现 <code>Comparable</code>：</p>
<pre><code class="java language-java">@Data
@AllArgsConstructor
public class User implements Comparable&lt;User&gt; {

    private Long id;

    private String username;

    private Integer age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(this.age, other.age);
    }
}
</code></pre>
<p>排序：</p>
<pre><code class="java language-java">List&lt;User&gt; users = new ArrayList&lt;&gt;();

users.add(new User(1L, "zhangsan", 30));
users.add(new User(2L, "lisi", 20));
users.add(new User(3L, "wangwu", 25));

Collections.sort(users);
</code></pre>
<p>按照年龄升序排列。</p>
<p>降序：</p>
<pre><code class="java language-java">@Override
public int compareTo(User other) {
    return Integer.compare(other.age, this.age);
}
</code></pre>
<hr />
<h2 id="2-comparator">2. Comparator</h2>
<p>不修改实体类，在外部指定排序规则：</p>
<pre><code class="java language-java">users.sort(
        Comparator.comparing(User::getAge)
);
</code></pre>
<p>降序：</p>
<pre><code class="java language-java">users.sort(
        Comparator.comparing(User::getAge)
                .reversed()
);
</code></pre>
<p>多字段排序：</p>
<pre><code class="java language-java">users.sort(
        Comparator.comparing(User::getAge)
                .thenComparing(User::getUsername)
);
</code></pre>
<p>处理 <code>null</code>：</p>
<pre><code class="java language-java">users.sort(
        Comparator.comparing(
                User::getAge,
                Comparator.nullsLast(
                        Integer::compareTo
                )
        )
);
</code></pre>
<hr />
<h2 id="3-两者区别">3. 两者区别</h2>
<pre><code class="text language-text">Comparable：类本身只有一种主要排序规则
Comparator：可以定义多种排序规则
</code></pre>
<p>实际开发中，<code>Comparator</code> 使用更加灵活。</p>
<hr />
<h1 id="三、遍历集合时-remove-或-add-的注意事项">三、遍历集合时 remove 或 add 的注意事项</h1>
<p>遍历集合时直接执行 <code>remove()</code> 或 <code>add()</code>，很容易出现：</p>
<pre><code class="text language-text">ConcurrentModificationException
</code></pre>
<hr />
<h2 id="1-错误示例">1. 错误示例</h2>
<pre><code class="java language-java">List&lt;String&gt; list =
        new ArrayList&lt;&gt;(
                List.of("A", "B", "C")
        );

for (String item : list) {

    if ("B".equals(item)) {
        list.remove(item);
    }
}
</code></pre>
<p>增强 <code>for</code> 底层使用迭代器。</p>
<p>遍历过程中直接修改原集合，会破坏迭代器状态。</p>
<hr />
<h2 id="2-使用-iterator-删除">2. 使用 Iterator 删除</h2>
<pre><code class="java language-java">Iterator&lt;String&gt; iterator =
        list.iterator();

while (iterator.hasNext()) {

    String item = iterator.next();

    if ("B".equals(item)) {
        iterator.remove();
    }
}
</code></pre>
<p>这是遍历时删除元素的标准写法。</p>
<hr />
<h2 id="3-使用-removeif">3. 使用 removeIf</h2>
<p>条件删除可以直接写：</p>
<pre><code class="java language-java">list.removeIf(
        item -&gt; "B".equals(item)
);
</code></pre>
<p>这种写法更简洁。</p>
<hr />
<h2 id="4-普通-for-循环倒序删除">4. 普通 for 循环倒序删除</h2>
<pre><code class="java language-java">for (int i = list.size() - 1; i &gt;= 0; i--) {

    if ("B".equals(list.get(i))) {
        list.remove(i);
    }
}
</code></pre>
<p>倒序删除不会影响前面还没有遍历的下标。</p>
<hr />
<h2 id="5-遍历时新增">5. 遍历时新增</h2>
<p>不建议在增强 <code>for</code> 中直接新增：</p>
<pre><code class="java language-java">for (String item : list) {
    list.add("D");
}
</code></pre>
<p>可以先保存待新增数据：</p>
<pre><code class="java language-java">List&lt;String&gt; newItems =
        new ArrayList&lt;&gt;();

for (String item : list) {

    if ("A".equals(item)) {
        newItems.add("D");
    }
}

list.addAll(newItems);
</code></pre>
<hr />
<h1 id="四、beanutilscopyproperties-复制对象避坑">四、BeanUtils.copyProperties 复制对象避坑</h1>
<p>Spring 提供的：</p>
<pre><code class="java language-java">BeanUtils.copyProperties(source, target);
</code></pre>
<p>可以复制两个对象中名称和类型相同的属性。</p>
<hr />
<h2 id="1-基本使用">1. 基本使用</h2>
<pre><code class="java language-java">UserEntity entity = new UserEntity();

entity.setId(1001L);
entity.setUsername("zhangsan");

UserVO vo = new UserVO();

BeanUtils.copyProperties(entity, vo);
</code></pre>
<p>注意参数顺序：</p>
<pre><code class="java language-java">BeanUtils.copyProperties(源对象, 目标对象);
</code></pre>
<p>很容易写反。</p>
<hr />
<h2 id="2-只会浅拷贝">2. 只会浅拷贝</h2>
<p>假设对象中有一个集合：</p>
<pre><code class="java language-java">private List&lt;String&gt; roles;
</code></pre>
<p>复制以后：</p>
<pre><code class="java language-java">source.getRoles() == target.getRoles()
</code></pre>
<p>结果可能是：</p>
<pre><code class="text language-text">true
</code></pre>
<p>也就是说两个对象引用的是同一个集合。</p>
<p>修改其中一个，另一个也可能受到影响。</p>
<hr />
<h2 id="3-属性名称相同但类型不同，不会复制">3. 属性名称相同但类型不同，不会复制</h2>
<p>例如：</p>
<pre><code class="java language-java">UserEntity：

private Long status;
</code></pre>
<pre><code class="java language-java">UserVO：

private String status;
</code></pre>
<p>虽然字段名一样，但类型不同，不会自动转换。</p>
<p>需要手动处理：</p>
<pre><code class="java language-java">vo.setStatus(
        String.valueOf(entity.getStatus())
);
</code></pre>
<hr />
<h2 id="4-null-会覆盖原值">4. null 会覆盖原值</h2>
<p>例如更新对象：</p>
<pre><code class="java language-java">UserUpdateDTO dto = new UserUpdateDTO();

dto.setUsername(null);
</code></pre>
<p>执行：</p>
<pre><code class="java language-java">BeanUtils.copyProperties(dto, entity);
</code></pre>
<p>会把原来的 <code>username</code> 覆盖成 <code>null</code>。</p>
<p>可以忽略指定字段：</p>
<pre><code class="java language-java">BeanUtils.copyProperties(
        dto,
        entity,
        "username"
);
</code></pre>
<p>但如果需要自动忽略所有 <code>null</code>，通常要自己封装工具方法，或者使用 MapStruct。</p>
<hr />
<h2 id="5-不要过度依赖-beanutils">5. 不要过度依赖 BeanUtils</h2>
<p>字段少时，手动赋值更加清晰：</p>
<pre><code class="java language-java">UserVO vo = new UserVO();

vo.setId(entity.getId());
vo.setUsername(entity.getUsername());
</code></pre>
<p>字段很多、转换复杂时，可以考虑：</p>
<pre><code class="text language-text">MapStruct
</code></pre>
<hr />
<h1 id="五、optional-常用方法实践">五、Optional 常用方法实践</h1>
<p><code>Optional</code> 主要用于处理可能为 <code>null</code> 的返回值，减少简单的空判断。</p>
<p>但它不能完全替代 <code>if-else</code>。</p>
<hr />
<h2 id="1-of">1. of</h2>
<p>对象不能为空：</p>
<pre><code class="java language-java">Optional&lt;String&gt; optional =
        Optional.of("hello");
</code></pre>
<p>如果传入 <code>null</code>，会抛出：</p>
<pre><code class="text language-text">NullPointerException
</code></pre>
<hr />
<h2 id="2-ofnullable">2. ofNullable</h2>
<p>允许传入 <code>null</code>：</p>
<pre><code class="java language-java">Optional&lt;String&gt; optional =
        Optional.ofNullable(username);
</code></pre>
<p>这是最常用的创建方式。</p>
<hr />
<h2 id="3-ispresent">3. isPresent</h2>
<p>判断是否有值：</p>
<pre><code class="java language-java">if (optional.isPresent()) {
    System.out.println(optional.get());
}
</code></pre>
<p>不过这种写法和普通 <code>if</code> 区别不大。</p>
<hr />
<h2 id="4-ifpresent">4. ifPresent</h2>
<p>有值时执行：</p>
<pre><code class="java language-java">optional.ifPresent(
        value -&gt; System.out.println(value)
);
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">Optional.ofNullable(user)
        .ifPresent(this::saveLog);
</code></pre>
<hr />
<h2 id="5-orelse">5. orElse</h2>
<p>为空时返回默认值：</p>
<pre><code class="java language-java">String username =
        optional.orElse("默认用户");
</code></pre>
<p>注意：</p>
<pre><code class="java language-java">optional.orElse(createDefaultUser());
</code></pre>
<p>即使 <code>optional</code> 有值，<code>createDefaultUser()</code> 也可能被执行。</p>
<hr />
<h2 id="6-orelseget">6. orElseGet</h2>
<p>为空时才执行：</p>
<pre><code class="java language-java">String username =
        optional.orElseGet(
                () -&gt; createDefaultUsername()
        );
</code></pre>
<p>如果默认值需要计算或查询，优先使用：</p>
<pre><code class="java language-java">orElseGet()
</code></pre>
<hr />
<h2 id="7-orelsethrow">7. orElseThrow</h2>
<p>为空时抛异常：</p>
<pre><code class="java language-java">User user =
        Optional.ofNullable(
                userMapper.selectById(id)
        ).orElseThrow(
                () -&gt; new RuntimeException(
                        "用户不存在"
                )
        );
</code></pre>
<p>这是业务查询中很常见的写法。</p>
<hr />
<h2 id="8-map">8. map</h2>
<p>对内部值进行转换：</p>
<pre><code class="java language-java">String username =
        Optional.ofNullable(user)
                .map(User::getUsername)
                .orElse("未知用户");
</code></pre>
<p>避免写成：</p>
<pre><code class="java language-java">if (user != null) {
    if (user.getUsername() != null) {
    }
}
</code></pre>
<hr />
<h2 id="9-filter">9. filter</h2>
<p>根据条件过滤：</p>
<pre><code class="java language-java">User user =
        Optional.ofNullable(
                userMapper.selectById(id)
        ).filter(
                item -&gt; item.getStatus() == 1
        ).orElseThrow(
                () -&gt; new RuntimeException(
                        "用户不可用"
                )
        );
</code></pre>
<hr />
<h2 id="10-flatmap">10. flatMap</h2>
<p>当转换结果本身也是 <code>Optional</code> 时使用：</p>
<pre><code class="java language-java">Optional&lt;String&gt; result =
        optionalUser.flatMap(
                this::getUsernameOptional
        );
</code></pre>
<p>避免出现：</p>
<pre><code class="java language-java">Optional&lt;Optional&lt;String&gt;&gt;
</code></pre>
<hr />
<h2 id="11-optional-不适合滥用">11. Optional 不适合滥用</h2>
<p>不建议用于实体字段：</p>
<pre><code class="java language-java">private Optional&lt;String&gt; username;
</code></pre>
<p>也不建议方法参数大量使用：</p>
<pre><code class="java language-java">public void save(Optional&lt;User&gt; user)
</code></pre>
<p>更适合作为：</p>
<pre><code class="text language-text">方法返回值
</code></pre>
<hr />
<h1 id="六、serialversionuid-为什么要手动赋值">六、serialVersionUID 为什么要手动赋值</h1>
<p>实现 <code>Serializable</code> 后，通常会看到：</p>
<pre><code class="java language-java">private static final long serialVersionUID = 1L;
</code></pre>
<p>完整示例：</p>
<pre><code class="java language-java">@Data
public class User
        implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;
}
</code></pre>
<hr />
<h2 id="1-serialversionuid-的作用">1. serialVersionUID 的作用</h2>
<p>它相当于序列化类的版本号。</p>
<p>序列化时会记录版本号。</p>
<p>反序列化时会检查：</p>
<pre><code class="text language-text">当前类版本号
是否和序列化数据中的版本号一致
</code></pre>
<p>不一致可能抛出：</p>
<pre><code class="text language-text">InvalidClassException
</code></pre>
<hr />
<h2 id="2-不手动赋值的问题">2. 不手动赋值的问题</h2>
<p>如果没有手动定义，Java 会根据类结构自动生成一个值。</p>
<p>当类发生变化时，例如：</p>
<pre><code class="text language-text">新增字段
删除字段
修改方法
调整结构
</code></pre>
<p>自动生成的版本号可能变化。</p>
<p>之前序列化的数据就可能无法反序列化。</p>
<hr />
<h2 id="3-手动定义的意义">3. 手动定义的意义</h2>
<pre><code class="java language-java">private static final long serialVersionUID = 1L;
</code></pre>
<p>表示开发者主动控制版本。</p>
<p>普通新增字段时，可以继续保持：</p>
<pre><code class="java language-java">1L
</code></pre>
<p>如果类结构发生重大变化，并且不希望兼容旧数据，可以改成：</p>
<pre><code class="java language-java">2L
</code></pre>
<hr />
<h2 id="4-常见使用场景">4. 常见使用场景</h2>
<pre><code class="text language-text">Java 原生序列化
Session 持久化
对象写入文件
RPC
缓存对象
</code></pre>
<p>如果只是普通 DTO，并且项目完全不使用 Java 原生序列化，实际影响可能不大。</p>
<hr />
<h1 id="七、引用拷贝、浅拷贝和深拷贝">七、引用拷贝、浅拷贝和深拷贝</h1>
<p>Java 对象复制时，需要区分：</p>
<pre><code class="text language-text">引用拷贝
浅拷贝
深拷贝
</code></pre>
<hr />
<h2 id="1-引用拷贝">1. 引用拷贝</h2>
<pre><code class="java language-java">User user1 = new User();

User user2 = user1;
</code></pre>
<p>这里没有创建新对象。</p>
<pre><code class="java language-java">user1 == user2
</code></pre>
<p>结果是：</p>
<pre><code class="text language-text">true
</code></pre>
<p>两个变量指向同一个对象。</p>
<p>修改：</p>
<pre><code class="java language-java">user2.setUsername("lisi");
</code></pre>
<p><code>user1</code> 也会变化。</p>
<hr />
<h2 id="2-浅拷贝">2. 浅拷贝</h2>
<p>浅拷贝会创建一个新对象，但对象内部的引用类型字段仍然共享。</p>
<p>例如：</p>
<pre><code class="java language-java">@Data
public class User implements Cloneable {

    private String username;

    private Address address;

    @Override
    protected User clone()
            throws CloneNotSupportedException {

        return (User) super.clone();
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">User user1 = new User();

user1.setUsername("zhangsan");
user1.setAddress(new Address("昆明"));

User user2 = user1.clone();
</code></pre>
<p>此时：</p>
<pre><code class="java language-java">user1 == user2
</code></pre>
<p>是：</p>
<pre><code class="text language-text">false
</code></pre>
<p>但：</p>
<pre><code class="java language-java">user1.getAddress() == user2.getAddress()
</code></pre>
<p>可能是：</p>
<pre><code class="text language-text">true
</code></pre>
<p>修改 <code>user2</code> 的地址，会影响 <code>user1</code>。</p>
<hr />
<h2 id="3-深拷贝">3. 深拷贝</h2>
<p>深拷贝不仅创建外层对象，也会复制内部引用对象。</p>
<p>手动实现：</p>
<pre><code class="java language-java">@Override
protected User clone()
        throws CloneNotSupportedException {

    User user = (User) super.clone();

    user.setAddress(
            this.address.clone()
    );

    return user;
}
</code></pre>
<p>这样：</p>
<pre><code class="java language-java">user1.getAddress()
        == user2.getAddress()
</code></pre>
<p>结果是：</p>
<pre><code class="text language-text">false
</code></pre>
<hr />
<h2 id="4-beanutils-属于哪种">4. BeanUtils 属于哪种</h2>
<pre><code class="java language-java">BeanUtils.copyProperties(
        source,
        target
);
</code></pre>
<p>通常属于浅拷贝。</p>
<p>基本类型和不可变对象看不出问题，但集合、自定义对象仍可能共享引用。</p>
<hr />
<h2 id="5-常见深拷贝方式">5. 常见深拷贝方式</h2>
<pre><code class="text language-text">手动创建新对象
实现 clone
JSON 序列化再反序列化
MapStruct 自定义转换
复制构造方法
</code></pre>
<p>实际业务中，推荐使用：</p>
<pre><code class="text language-text">手动转换
复制构造方法
MapStruct
</code></pre>
<p>逻辑更加清晰。</p>
<hr />
<h1 id="八、async-使用避坑">八、@Async 使用避坑</h1>
<p>Spring 的 <code>@Async</code> 可以让方法在线程池中异步执行。</p>
<hr />
<h2 id="1-开启异步">1. 开启异步</h2>
<p>启动类或配置类添加：</p>
<pre><code class="java language-java">@EnableAsync
@Configuration
public class AsyncConfig {
}
</code></pre>
<hr />
<h2 id="2-基本使用">2. 基本使用</h2>
<pre><code class="java language-java">@Service
public class MessageService {

    @Async
    public void sendMessage() {

        System.out.println(
                Thread.currentThread().getName()
        );
    }
}
</code></pre>
<p>调用：</p>
<pre><code class="java language-java">messageService.sendMessage();
</code></pre>
<p>调用线程不会等待方法执行结束。</p>
<hr />
<h2 id="3-建议指定线程池">3. 建议指定线程池</h2>
<p>不要长期依赖默认线程池。</p>
<pre><code class="java language-java">@Bean("messageExecutor")
public Executor messageExecutor() {

    ThreadPoolTaskExecutor executor =
            new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(5);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("message-");
    executor.initialize();

    return executor;
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">@Async("messageExecutor")
public void sendMessage() {
}
</code></pre>
<hr />
<h2 id="4-同类内部调用会失效">4. 同类内部调用会失效</h2>
<p>错误示例：</p>
<pre><code class="java language-java">@Service
public class OrderService {

    public void createOrder() {
        this.sendMessage();
    }

    @Async
    public void sendMessage() {
    }
}
</code></pre>
<p><code>this.sendMessage()</code> 没有经过 Spring 代理对象，异步不会生效。</p>
<p>推荐拆分到另一个 Bean：</p>
<pre><code class="java language-java">@Service
@RequiredArgsConstructor
public class OrderService {

    private final MessageService messageService;

    public void createOrder() {
        messageService.sendMessage();
    }
}
</code></pre>
<hr />
<h2 id="5-private-方法不会生效">5. private 方法不会生效</h2>
<p>错误写法：</p>
<pre><code class="java language-java">@Async
private void sendMessage() {
}
</code></pre>
<p>代理对象无法正常代理私有方法。</p>
<p>一般应该使用：</p>
<pre><code class="java language-java">public
</code></pre>
<hr />
<h2 id="6-异常不容易被调用者捕获">6. 异常不容易被调用者捕获</h2>
<p>返回 <code>void</code> 时：</p>
<pre><code class="java language-java">@Async
public void execute() {
    throw new RuntimeException("异步异常");
}
</code></pre>
<p>调用方无法通过普通 <code>try-catch</code> 捕获。</p>
<p>可以返回：</p>
<pre><code class="java language-java">CompletableFuture&lt;T&gt;
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">@Async("messageExecutor")
public CompletableFuture&lt;String&gt; execute() {

    return CompletableFuture.completedFuture(
            "success"
    );
}
</code></pre>
<hr />
<h2 id="7-事务上下文不会自动传递">7. 事务上下文不会自动传递</h2>
<p>异步方法运行在另一个线程。</p>
<p>原线程中的：</p>
<pre><code class="text language-text">事务
ThreadLocal
请求上下文
部分安全上下文
</code></pre>
<p>不一定自动传递。</p>
<p>因此不要默认认为异步方法和调用方处于同一个事务。</p>
<hr />
<h2 id="8-异步任务可能丢失">8. 异步任务可能丢失</h2>
<p>如果应用刚提交异步任务就关闭，任务可能没有完成。</p>
<p>重要业务不要只依赖内存异步线程。</p>
<p>例如：</p>
<pre><code class="text language-text">支付通知
订单状态同步
重要消息发送
</code></pre>
<p>更适合使用消息队列。</p>
<hr />
<h1 id="九、mybatis-一级缓存避坑">九、MyBatis 一级缓存避坑</h1>
<p>MyBatis 一级缓存默认存在，作用范围是：</p>
<pre><code class="text language-text">同一个 SqlSession
</code></pre>
<p>在 Spring 项目中，通常可以理解为：</p>
<pre><code class="text language-text">同一个事务中的同一个 SqlSession
</code></pre>
<hr />
<h2 id="1-基本效果">1. 基本效果</h2>
<pre><code class="java language-java">User user1 =
        userMapper.selectById(1001L);

User user2 =
        userMapper.selectById(1001L);
</code></pre>
<p>如果处于同一个 <code>SqlSession</code>，第二次查询可能直接从一级缓存获取，不再执行 SQL。</p>
<hr />
<h2 id="2-增删改会清空缓存">2. 增删改会清空缓存</h2>
<p>例如：</p>
<pre><code class="java language-java">User user1 =
        userMapper.selectById(1001L);

userMapper.updateById(user);

User user2 =
        userMapper.selectById(1001L);
</code></pre>
<p>执行更新后，一级缓存通常会被清空。</p>
<p>第二次查询会重新访问数据库。</p>
<hr />
<h2 id="3-不同-sqlsession-不共享">3. 不同 SqlSession 不共享</h2>
<p>两个不同请求、两个不同事务，通常不共享一级缓存。</p>
<p>因此一级缓存不能当成全局缓存使用。</p>
<p>它和 Redis 完全不是一个概念。</p>
<hr />
<h2 id="4-同一个对象引用可能带来误解">4. 同一个对象引用可能带来误解</h2>
<p>第一次查询：</p>
<pre><code class="java language-java">User user1 =
        userMapper.selectById(1001L);
</code></pre>
<p>修改对象但没有执行更新：</p>
<pre><code class="java language-java">user1.setUsername("lisi");
</code></pre>
<p>再次查询：</p>
<pre><code class="java language-java">User user2 =
        userMapper.selectById(1001L);
</code></pre>
<p>在某些情况下，可能直接从一级缓存返回对象。</p>
<p>此时：</p>
<pre><code class="java language-java">user1 == user2
</code></pre>
<p>可能是：</p>
<pre><code class="text language-text">true
</code></pre>
<p>看起来像数据库已经变化，实际上可能只是内存中的缓存对象被修改。</p>
<hr />
<h2 id="5-批量操作和循环查询要注意">5. 批量操作和循环查询要注意</h2>
<p>例如：</p>
<pre><code class="java language-java">for (Long id : ids) {
    userMapper.selectById(id);
}
</code></pre>
<p>如果数据量很大，会产生大量查询，也可能让本地缓存占用更多内存。</p>
<p>更合理的方式是批量查询：</p>
<pre><code class="java language-java">userMapper.selectBatchIds(ids);
</code></pre>
<hr />
<h2 id="6-手动清理一级缓存">6. 手动清理一级缓存</h2>
<p>可以调用：</p>
<pre><code class="java language-java">sqlSession.clearCache();
</code></pre>
<p>但一般不需要手动清理。</p>
<p>如果业务频繁依赖手动清缓存，通常说明事务边界或查询设计需要调整。</p>
<hr />
<h2 id="7-一级缓存的核心注意点">7. 一级缓存的核心注意点</h2>
<p>可以简单记住：</p>
<pre><code class="text language-text">作用域是 SqlSession
不能跨请求共享
增删改后通常清空
返回的可能是同一个对象引用
不能替代 Redis
</code></pre>
<hr />
<p>MyBatis 一级缓只在当前 SqlSession 内生效，不能当成业务缓存<br />
```</p>
<p>这些知识本身不复杂，但如果不了解边界，就很容易在实际项目中出现一些比较隐蔽的问题。</p>]]></description>
      <link>http://43.128.224.135:3003/article/a9c10321-cf2a-401d-ab04-fd73b0231386</link>
      <guid>http://43.128.224.135:3003/article/a9c10321-cf2a-401d-ab04-fd73b0231386</guid>
      <pubDate>Tue, 04 Aug 2026 02:24:19 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>JDK 动态代理和 CGLIB 动态代理</title>
      <description><![CDATA[<p>在业务开发中，我们经常会遇到这样的问题：</p>
<p>一个业务方法已经写好了，但现在还需要在它执行前后增加一些通用功能，例如：</p>
<ul>
<li>记录接口日志</li>
<li>校验用户权限</li>
<li>统计方法耗时</li>
<li>开启和提交事务</li>
<li>查询缓存</li>
<li>接口额度校验</li>
<li>异常统一处理</li>
</ul>
<p>最直接的写法，是把这些代码全部写进业务方法中：</p>
<pre><code class="java language-java">public void createOrder() {

    System.out.println("记录请求日志");

    long start = System.currentTimeMillis();

    System.out.println("创建订单");

    long cost = System.currentTimeMillis() - start;

    System.out.println("方法耗时：" + cost + "ms");
}
</code></pre>
<p>这种代码虽然能运行，但存在明显问题：</p>
<pre><code class="text language-text">业务代码和通用代码混在一起
多个方法需要重复编写相同逻辑
后续修改日志或权限逻辑时，需要修改大量业务代码
</code></pre>
<p>代理模式就是用来解决这类问题的。</p>
<p>代理对象挡在调用者和目标对象之间，在不修改原业务代码的前提下，对目标方法进行增强。</p>
<pre><code class="text language-text">调用者
  ↓
代理对象
  ↓
目标对象
</code></pre>
<p>动态代理则是在程序运行过程中，自动创建代理对象。</p>
<p>Java 中常见的动态代理有两种：</p>
<pre><code class="text language-text">JDK 动态代理
CGLIB 动态代理
</code></pre>
<p>Spring AOP 的底层也主要依赖这两种代理方式。</p>
<hr />
<h2 id="一、什么是代理模式">一、什么是代理模式</h2>
<p>假设我们有一个订单服务：</p>
<pre><code class="java language-java">public class OrderService {

    public void createOrder() {
        System.out.println("创建订单");
    }
}
</code></pre>
<p>调用者直接调用：</p>
<pre><code class="java language-java">OrderService orderService = new OrderService();
orderService.createOrder();
</code></pre>
<p>调用关系是：</p>
<pre><code class="text language-text">调用者 → OrderService
</code></pre>
<p>使用代理后，调用关系变成：</p>
<pre><code class="text language-text">调用者 → OrderServiceProxy → OrderService
</code></pre>
<p>代理对象可以在调用目标方法之前和之后增加逻辑：</p>
<pre><code class="text language-text">代理前置处理
目标方法执行
代理后置处理
</code></pre>
<p>例如：</p>
<pre><code class="text language-text">记录开始时间
创建订单
计算执行耗时
</code></pre>
<p>代理模式的核心不是替代目标对象，而是：</p>
<blockquote>
  <p>在不修改目标类代码的情况下，对目标对象的方法进行增强。</p>
</blockquote>
<hr />
<h2 id="二、静态代理">二、静态代理</h2>
<p>理解动态代理之前，可以先看静态代理。</p>
<p>定义一个接口：</p>
<pre><code class="java language-java">public interface OrderService {

    void createOrder();
}
</code></pre>
<p>目标类：</p>
<pre><code class="java language-java">public class OrderServiceImpl implements OrderService {

    @Override
    public void createOrder() {
        System.out.println("创建订单");
    }
}
</code></pre>
<p>代理类：</p>
<pre><code class="java language-java">public class OrderServiceProxy implements OrderService {

    private final OrderService target;

    public OrderServiceProxy(OrderService target) {
        this.target = target;
    }

    @Override
    public void createOrder() {

        long start = System.currentTimeMillis();

        System.out.println("开始创建订单");

        target.createOrder();

        long cost = System.currentTimeMillis() - start;

        System.out.println("方法耗时：" + cost + "ms");
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">public class Main {

    public static void main(String[] args) {

        OrderService target = new OrderServiceImpl();

        OrderService proxy = new OrderServiceProxy(target);

        proxy.createOrder();
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">开始创建订单
创建订单
方法耗时：1ms
</code></pre>
<p>静态代理本身没有问题，但它有一个明显缺点：</p>
<blockquote>
  <p>每代理一个接口，通常都需要手动写一个代理类。</p>
</blockquote>
<p>假如项目中有：</p>
<pre><code class="text language-text">OrderService
UserService
ProductService
PaymentService
</code></pre>
<p>就需要分别创建：</p>
<pre><code class="text language-text">OrderServiceProxy
UserServiceProxy
ProductServiceProxy
PaymentServiceProxy
</code></pre>
<p>这些代理类的逻辑可能都一样，只是目标对象不同。</p>
<p>动态代理解决的就是这个问题：</p>
<blockquote>
  <p>不再手动为每个目标对象编写代理类，而是在程序运行时自动生成代理对象。</p>
</blockquote>
<hr />
<h1 id="三、jdk-动态代理">三、JDK 动态代理</h1>
<p>JDK 动态代理由 Java 标准库提供，核心类是：</p>
<pre><code class="java language-java">java.lang.reflect.Proxy
java.lang.reflect.InvocationHandler
</code></pre>
<p>它有一个重要限制：</p>
<blockquote>
  <p>被代理的目标类必须实现接口。</p>
</blockquote>
<hr />
<h2 id="四、jdk-动态代理完整示例">四、JDK 动态代理完整示例</h2>
<p>先定义业务接口：</p>
<pre><code class="java language-java">public interface OrderService {

    void createOrder(String productName);

    String queryOrder(Long orderId);
}
</code></pre>
<p>实现类：</p>
<pre><code class="java language-java">public class OrderServiceImpl implements OrderService {

    @Override
    public void createOrder(String productName) {
        System.out.println("创建订单，商品：" + productName);
    }

    @Override
    public String queryOrder(Long orderId) {
        System.out.println("查询订单：" + orderId);
        return "ORDER-" + orderId;
    }
}
</code></pre>
<p>创建动态代理处理器：</p>
<pre><code class="java language-java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class LogInvocationHandler implements InvocationHandler {

    private final Object target;

    public LogInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(
            Object proxy,
            Method method,
            Object[] args
    ) throws Throwable {

        long start = System.currentTimeMillis();

        System.out.println(
                "开始执行方法：" + method.getName()
        );

        try {

            Object result = method.invoke(target, args);

            System.out.println(
                    "方法执行成功：" + method.getName()
            );

            return result;

        } catch (Exception e) {

            System.out.println(
                    "方法执行失败：" + method.getName()
            );

            throw e;

        } finally {

            long cost =
                    System.currentTimeMillis() - start;

            System.out.println(
                    "方法耗时：" + cost + "ms"
            );
        }
    }
}
</code></pre>
<p>创建代理对象：</p>
<pre><code class="java language-java">import java.lang.reflect.Proxy;

public class Main {

    public static void main(String[] args) {

        OrderService target =
                new OrderServiceImpl();

        OrderService proxy =
                (OrderService) Proxy.newProxyInstance(
                        target.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),
                        new LogInvocationHandler(target)
                );

        proxy.createOrder("Java 并发编程");

        String orderNo = proxy.queryOrder(1001L);

        System.out.println("查询结果：" + orderNo);
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">开始执行方法：createOrder
创建订单，商品：Java 并发编程
方法执行成功：createOrder
方法耗时：1ms

开始执行方法：queryOrder
查询订单：1001
方法执行成功：queryOrder
方法耗时：0ms
查询结果：ORDER-1001
</code></pre>
<hr />
<h2 id="五、proxynewproxyinstance-的三个参数">五、Proxy.newProxyInstance 的三个参数</h2>
<p>创建代理对象的核心代码是：</p>
<pre><code class="java language-java">Proxy.newProxyInstance(
        classLoader,
        interfaces,
        invocationHandler
);
</code></pre>
<p>完整写法：</p>
<pre><code class="java language-java">OrderService proxy =
        (OrderService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
</code></pre>
<p>三个参数分别表示什么？</p>
<h3 id="1-classloader">1. ClassLoader</h3>
<pre><code class="java language-java">target.getClass().getClassLoader()
</code></pre>
<p>代理类也是一个 Java 类。</p>
<p>既然是类，就需要类加载器把它加载进 JVM。</p>
<p>一般直接使用目标类的类加载器即可。</p>
<hr />
<h3 id="2-interfaces">2. interfaces</h3>
<pre><code class="java language-java">target.getClass().getInterfaces()
</code></pre>
<p>表示代理对象需要实现哪些接口。</p>
<p>目标类：</p>
<pre><code class="java language-java">public class OrderServiceImpl
        implements OrderService
</code></pre>
<p>所以生成的代理对象也会实现：</p>
<pre><code class="java language-java">OrderService
</code></pre>
<p>调用者最终拿到的是一个实现了 <code>OrderService</code> 接口的代理对象。</p>
<hr />
<h3 id="3-invocationhandler">3. InvocationHandler</h3>
<pre><code class="java language-java">new LogInvocationHandler(target)
</code></pre>
<p>它负责处理所有被代理方法的调用。</p>
<p>当执行：</p>
<pre><code class="java language-java">proxy.createOrder("Java");
</code></pre>
<p>实际上会进入：</p>
<pre><code class="java language-java">InvocationHandler.invoke(...)
</code></pre>
<p>因此可以把 <code>InvocationHandler</code> 理解为：</p>
<blockquote>
  <p>代理对象所有方法调用的统一入口。</p>
</blockquote>
<hr />
<h2 id="六、invocationhandler-的三个参数">六、InvocationHandler 的三个参数</h2>
<pre><code class="java language-java">public Object invoke(
        Object proxy,
        Method method,
        Object[] args
)
</code></pre>
<h3 id="proxy">proxy</h3>
<p>当前生成的代理对象。</p>
<p>一般不需要直接使用它。</p>
<p>特别注意，不要随便在 <code>invoke()</code> 中调用：</p>
<pre><code class="java language-java">proxy.toString();
</code></pre>
<p>因为它可能再次进入 <code>invoke()</code>，造成递归调用。</p>
<hr />
<h3 id="method">method</h3>
<p>当前被调用的方法对象。</p>
<p>例如调用：</p>
<pre><code class="java language-java">proxy.createOrder("Java");
</code></pre>
<p>那么：</p>
<pre><code class="java language-java">method.getName()
</code></pre>
<p>得到：</p>
<pre><code class="text language-text">createOrder
</code></pre>
<p>还可以获取注解、返回值类型和参数信息：</p>
<pre><code class="java language-java">method.getAnnotations();
method.getReturnType();
method.getParameterTypes();
</code></pre>
<hr />
<h3 id="args">args</h3>
<p>当前方法的参数。</p>
<p>例如：</p>
<pre><code class="java language-java">proxy.createOrder("Java");
</code></pre>
<p>那么：</p>
<pre><code class="java language-java">args[0]
</code></pre>
<p>就是：</p>
<pre><code class="text language-text">Java
</code></pre>
<hr />
<h2 id="七、methodinvoketarget-args-做了什么">七、method.invoke(target, args) 做了什么</h2>
<p>这行代码是 JDK 动态代理中真正执行目标方法的地方：</p>
<pre><code class="java language-java">Object result = method.invoke(target, args);
</code></pre>
<p>它表示：</p>
<blockquote>
  <p>使用反射，在目标对象 target 上调用当前 method，并把 args 作为参数传进去。</p>
</blockquote>
<p>例如当前调用的是：</p>
<pre><code class="java language-java">proxy.createOrder("Java");
</code></pre>
<p>最终相当于执行：</p>
<pre><code class="java language-java">target.createOrder("Java");
</code></pre>
<p>只不过这里通过反射完成。</p>
<p>完整流程是：</p>
<pre><code class="text language-text">调用 proxy.createOrder()
        ↓
进入 InvocationHandler.invoke()
        ↓
执行前置增强
        ↓
method.invoke(target, args)
        ↓
执行 OrderServiceImpl.createOrder()
        ↓
执行后置增强
        ↓
返回结果
</code></pre>
<hr />
<h2 id="八、封装通用的-jdk-代理工厂">八、封装通用的 JDK 代理工厂</h2>
<p>为了避免每次都写 <code>Proxy.newProxyInstance()</code>，可以封装一个代理工厂。</p>
<pre><code class="java language-java">import java.lang.reflect.Proxy;

public class JdkProxyFactory {

    private final Object target;

    public JdkProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxy() {

        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">public class Main {

    public static void main(String[] args) {

        OrderService target =
                new OrderServiceImpl();

        OrderService proxy =
                (OrderService) new JdkProxyFactory(target)
                        .getProxy();

        proxy.createOrder("MySQL 实战");
    }
}
</code></pre>
<p>使用泛型进一步优化：</p>
<pre><code class="java language-java">import java.lang.reflect.Proxy;

public class JdkProxyFactory {

    @SuppressWarnings("unchecked")
    public static &lt;T&gt; T createProxy(T target) {

        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
    }
}
</code></pre>
<p>调用：</p>
<pre><code class="java language-java">OrderService proxy =
        JdkProxyFactory.createProxy(
                new OrderServiceImpl()
        );

proxy.createOrder("Spring Boot");
</code></pre>
<hr />
<h1 id="九、cglib-动态代理">九、CGLIB 动态代理</h1>
<p>JDK 动态代理要求目标类必须实现接口。</p>
<p>但有些类没有接口：</p>
<pre><code class="java language-java">public class ProductService {

    public void queryProduct() {
        System.out.println("查询商品");
    }
}
</code></pre>
<p>这时无法直接使用 JDK 动态代理。</p>
<p>CGLIB 的解决方案是：</p>
<blockquote>
  <p>动态创建目标类的子类，通过重写父类方法实现代理。</p>
</blockquote>
<p>原来的类：</p>
<pre><code class="text language-text">ProductService
</code></pre>
<p>CGLIB 运行时大致生成：</p>
<pre><code class="java language-java">public class ProductServiceProxy
        extends ProductService {

    @Override
    public void queryProduct() {
        // 代理逻辑
    }
}
</code></pre>
<p>所以 CGLIB 不要求目标类实现接口。</p>
<hr />
<h2 id="十、cglib-依赖">十、CGLIB 依赖</h2>
<p>现代 Spring 项目中一般已经包含 CGLIB 相关能力。</p>
<p>单独演示时，可以使用 Spring 提供的 CGLIB 包：</p>
<pre><code class="xml language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework&lt;/groupId&gt;
    &lt;artifactId&gt;spring-core&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>使用的类通常位于：</p>
<pre><code class="java language-java">org.springframework.cglib.proxy.Enhancer
org.springframework.cglib.proxy.MethodInterceptor
org.springframework.cglib.proxy.MethodProxy
</code></pre>
<hr />
<h2 id="十一、cglib-动态代理完整示例">十一、CGLIB 动态代理完整示例</h2>
<p>目标类不实现任何接口：</p>
<pre><code class="java language-java">public class ProductService {

    public void createProduct(String productName) {
        System.out.println("创建商品：" + productName);
    }

    public String queryProduct(Long productId) {
        System.out.println("查询商品：" + productId);
        return "PRODUCT-" + productId;
    }
}
</code></pre>
<p>创建 CGLIB 方法拦截器：</p>
<pre><code class="java language-java">import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class LogMethodInterceptor
        implements MethodInterceptor {

    @Override
    public Object intercept(
            Object proxy,
            Method method,
            Object[] args,
            MethodProxy methodProxy
    ) throws Throwable {

        long start = System.currentTimeMillis();

        System.out.println(
                "开始执行方法：" + method.getName()
        );

        try {

            Object result =
                    methodProxy.invokeSuper(proxy, args);

            System.out.println(
                    "方法执行成功：" + method.getName()
            );

            return result;

        } catch (Exception e) {

            System.out.println(
                    "方法执行失败：" + method.getName()
            );

            throw e;

        } finally {

            long cost =
                    System.currentTimeMillis() - start;

            System.out.println(
                    "方法耗时：" + cost + "ms"
            );
        }
    }
}
</code></pre>
<p>创建代理对象：</p>
<pre><code class="java language-java">import org.springframework.cglib.proxy.Enhancer;

public class CglibProxyFactory {

    @SuppressWarnings("unchecked")
    public static &lt;T&gt; T createProxy(
            Class&lt;T&gt; targetClass
    ) {

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(targetClass);

        enhancer.setCallback(
                new LogMethodInterceptor()
        );

        return (T) enhancer.create();
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">public class Main {

    public static void main(String[] args) {

        ProductService proxy =
                CglibProxyFactory.createProxy(
                        ProductService.class
                );

        proxy.createProduct("机械键盘");

        String productNo =
                proxy.queryProduct(1001L);

        System.out.println(productNo);
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">开始执行方法：createProduct
创建商品：机械键盘
方法执行成功：createProduct
方法耗时：1ms

开始执行方法：queryProduct
查询商品：1001
方法执行成功：queryProduct
方法耗时：0ms
PRODUCT-1001
</code></pre>
<hr />
<h2 id="十二、cglib-的执行流程">十二、CGLIB 的执行流程</h2>
<p>当调用：</p>
<pre><code class="java language-java">proxy.queryProduct(1001L);
</code></pre>
<p>执行流程大致是：</p>
<pre><code class="text language-text">调用 CGLIB 生成的子类对象
        ↓
进入 MethodInterceptor.intercept()
        ↓
执行前置增强
        ↓
调用父类原方法
        ↓
执行后置增强
        ↓
返回结果
</code></pre>
<p>核心代码：</p>
<pre><code class="java language-java">methodProxy.invokeSuper(proxy, args);
</code></pre>
<p>它表示：</p>
<blockquote>
  <p>调用代理对象父类中的原始方法。</p>
</blockquote>
<p>因为代理对象本身就是目标类的子类，所以可以调用父类实现。</p>
<hr />
<h2 id="十三、为什么使用-invokesuper，而不是-methodinvoke">十三、为什么使用 invokeSuper，而不是 method.invoke</h2>
<p>CGLIB 中推荐使用：</p>
<pre><code class="java language-java">methodProxy.invokeSuper(proxy, args);
</code></pre>
<p>而不是：</p>
<pre><code class="java language-java">method.invoke(proxy, args);
</code></pre>
<p>因为 <code>proxy</code> 本身是代理对象。</p>
<p>如果执行：</p>
<pre><code class="java language-java">method.invoke(proxy, args);
</code></pre>
<p>可能再次调用代理对象的方法，再次进入 <code>intercept()</code>，最终形成递归调用。</p>
<p>正确方式是：</p>
<pre><code class="java language-java">methodProxy.invokeSuper(proxy, args);
</code></pre>
<p>直接执行父类原始方法。</p>
<hr />
<h2 id="十四、带构造参数的-cglib-代理">十四、带构造参数的 CGLIB 代理</h2>
<p>如果目标类没有无参构造方法：</p>
<pre><code class="java language-java">public class ProductService {

    private final String tenantId;

    public ProductService(String tenantId) {
        this.tenantId = tenantId;
    }

    public void queryProduct() {
        System.out.println(
                tenantId + " 查询商品"
        );
    }
}
</code></pre>
<p>直接调用：</p>
<pre><code class="java language-java">enhancer.create();
</code></pre>
<p>会失败，因为 CGLIB 创建子类对象时需要调用父类构造方法。</p>
<p>需要传入构造参数：</p>
<pre><code class="java language-java">ProductService proxy =
        (ProductService) enhancer.create(
                new Class[]{String.class},
                new Object[]{"tenant-1001"}
        );
</code></pre>
<p>完整代码：</p>
<pre><code class="java language-java">Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(ProductService.class);

enhancer.setCallback(
        new LogMethodInterceptor()
);

ProductService proxy =
        (ProductService) enhancer.create(
                new Class[]{String.class},
                new Object[]{"tenant-1001"}
        );
</code></pre>
<hr />
<h1 id="十五、jdk-动态代理和-cglib-的核心区别">十五、JDK 动态代理和 CGLIB 的核心区别</h1>
<table>
<thead>
<tr>
<th>对比项</th>
<th>JDK 动态代理</th>
<th>CGLIB 动态代理</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>InvocationHandler</td>
<td>MethodInterceptor</td>
</tr>
<tr>
<td>目标方法调用</td>
<td>Method.invoke</td>
<td>MethodProxy.invokeSuper</td>
</tr>
<tr>
<td>final 类</td>
<td>可以代理接口，但不能代理类本身</td>
<td>不能代理</td>
</tr>
<tr>
<td>final 方法</td>
<td>接口方式通常不受影响</td>
<td>不能重写，因此无法增强</td>
</tr>
<tr>
<td>private 方法</td>
<td>不能通过接口代理</td>
<td>不能被子类重写</td>
</tr>
<tr>
<td>常见应用</td>
<td>面向接口开发</td>
<td>无接口类代理</td>
</tr>
</tbody>
</table>
<p>可以这样记：</p>
<blockquote>
  <p>JDK 动态代理是“实现接口”，CGLIB 动态代理是“继承目标类”。</p>
</blockquote>
<hr />
<h2 id="十六、为什么-cglib-不能代理-final-类">十六、为什么 CGLIB 不能代理 final 类</h2>
<p>CGLIB 的原理是继承目标类：</p>
<pre><code class="java language-java">class ProxyClass extends TargetClass {
}
</code></pre>
<p>如果目标类是：</p>
<pre><code class="java language-java">public final class ProductService {
}
</code></pre>
<p>Java 不允许其他类继承它。</p>
<p>因此 CGLIB 无法创建：</p>
<pre><code class="java language-java">class ProductServiceProxy
        extends ProductService
</code></pre>
<p>同理，如果方法是：</p>
<pre><code class="java language-java">public final void queryProduct() {
}
</code></pre>
<p>子类不能重写这个方法，所以 CGLIB 也无法对它进行增强。</p>
<hr />
<h2 id="十七、业务示例：统一记录接口耗时">十七、业务示例：统一记录接口耗时</h2>
<p>假设系统中有多个业务服务：</p>
<pre><code class="java language-java">public interface UserService {

    void createUser(String username);

    String getUser(Long userId);
}
</code></pre>
<p>实现类：</p>
<pre><code class="java language-java">public class UserServiceImpl
        implements UserService {

    @Override
    public void createUser(String username) {
        System.out.println("创建用户：" + username);
    }

    @Override
    public String getUser(Long userId) {
        System.out.println("查询用户：" + userId);
        return "user-" + userId;
    }
}
</code></pre>
<p>定义通用耗时代理：</p>
<pre><code class="java language-java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PerformanceInvocationHandler
        implements InvocationHandler {

    private final Object target;

    public PerformanceInvocationHandler(
            Object target
    ) {
        this.target = target;
    }

    @Override
    public Object invoke(
            Object proxy,
            Method method,
            Object[] args
    ) throws Throwable {

        long start = System.nanoTime();

        try {

            return method.invoke(target, args);

        } finally {

            long cost =
                    System.nanoTime() - start;

            System.out.printf(
                    "%s.%s 执行耗时：%.3f ms%n",
                    target.getClass().getSimpleName(),
                    method.getName(),
                    cost / 1_000_000.0
            );
        }
    }
}
</code></pre>
<p>代理工厂：</p>
<pre><code class="java language-java">import java.lang.reflect.Proxy;

public class PerformanceProxyFactory {

    @SuppressWarnings("unchecked")
    public static &lt;T&gt; T create(T target) {

        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new PerformanceInvocationHandler(target)
        );
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">UserService userService =
        PerformanceProxyFactory.create(
                new UserServiceImpl()
        );

userService.createUser("zhangsan");

userService.getUser(1001L);
</code></pre>
<p>以后其他实现了接口的服务，也可以直接使用同一个代理工厂：</p>
<pre><code class="java language-java">OrderService orderService =
        PerformanceProxyFactory.create(
                new OrderServiceImpl()
        );
</code></pre>
<p>这就是动态代理相对静态代理的价值：</p>
<blockquote>
  <p>一套代理逻辑可以复用到多个不同的目标对象上。</p>
</blockquote>
<hr />
<h1 id="十八、业务示例：接口额度校验">十八、业务示例：接口额度校验</h1>
<p>假设查询接口执行前，需要检查用户剩余额度。</p>
<p>业务接口：</p>
<pre><code class="java language-java">public interface ReportService {

    String generateReport(Long userId);
}
</code></pre>
<p>实现类：</p>
<pre><code class="java language-java">public class ReportServiceImpl
        implements ReportService {

    @Override
    public String generateReport(Long userId) {

        System.out.println("生成数据报告");

        return "report-success";
    }
}
</code></pre>
<p>额度服务：</p>
<pre><code class="java language-java">public class QuotaService {

    public boolean hasQuota(Long userId) {
        return userId != null &amp;&amp; userId &gt; 0;
    }

    public void consume(Long userId) {
        System.out.println(
                "用户 " + userId + " 扣减一次额度"
        );
    }
}
</code></pre>
<p>代理处理器：</p>
<pre><code class="java language-java">import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class QuotaInvocationHandler
        implements InvocationHandler {

    private final Object target;

    private final QuotaService quotaService;

    public QuotaInvocationHandler(
            Object target,
            QuotaService quotaService
    ) {
        this.target = target;
        this.quotaService = quotaService;
    }

    @Override
    public Object invoke(
            Object proxy,
            Method method,
            Object[] args
    ) throws Throwable {

        Long userId = (Long) args[0];

        if (!quotaService.hasQuota(userId)) {
            throw new RuntimeException(
                    "用户额度不足"
            );
        }

        Object result =
                method.invoke(target, args);

        quotaService.consume(userId);

        return result;
    }
}
</code></pre>
<p>创建代理：</p>
<pre><code class="java language-java">ReportService target =
        new ReportServiceImpl();

QuotaService quotaService =
        new QuotaService();

ReportService proxy =
        (ReportService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new QuotaInvocationHandler(
                        target,
                        quotaService
                )
        );
</code></pre>
<p>调用：</p>
<pre><code class="java language-java">String result =
        proxy.generateReport(1001L);

System.out.println(result);
</code></pre>
<p>执行流程：</p>
<pre><code class="text language-text">检查额度
    ↓
执行报告查询
    ↓
查询成功
    ↓
扣减额度
</code></pre>
<p>这和我们在 Spring AOP 中编写的 <code>@Around</code> 切面非常相似。</p>
<hr />
<h1 id="十九、动态代理与-spring-aop-的关系">十九、动态代理与 Spring AOP 的关系</h1>
<p>Spring AOP 本质上就是基于代理实现的。</p>
<p>假设我们写了一个切面：</p>
<pre><code class="java language-java">@Around("@annotation(apiLog)")
public Object around(
        ProceedingJoinPoint joinPoint,
        ApiLog apiLog
) throws Throwable {

    System.out.println("执行前");

    Object result = joinPoint.proceed();

    System.out.println("执行后");

    return result;
}
</code></pre>
<p>表面上看，是 Spring 自动拦截了业务方法。</p>
<p>底层实际上可以理解为：</p>
<pre><code class="text language-text">Spring 创建业务 Bean
        ↓
Spring 为业务 Bean 创建代理对象
        ↓
容器中保存的是代理对象
        ↓
调用代理对象的方法
        ↓
进入切面
        ↓
调用目标方法
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">@Service
public class UserServiceImpl
        implements UserService {
}
</code></pre>
<p>注入：</p>
<pre><code class="java language-java">@Resource
private UserService userService;
</code></pre>
<p>这里注入的 <code>userService</code> 很可能不是原始的 <code>UserServiceImpl</code>，而是 Spring 创建的代理对象。</p>
<hr />
<h2 id="二十、spring-如何选择-jdk-和-cglib">二十、Spring 如何选择 JDK 和 CGLIB</h2>
<p>通常可以这样理解：</p>
<h3 id="目标类实现了接口">目标类实现了接口</h3>
<p>Spring 可以使用 JDK 动态代理：</p>
<pre><code class="text language-text">代理对象实现目标接口
</code></pre>
<h3 id="目标类没有实现接口">目标类没有实现接口</h3>
<p>Spring 使用 CGLIB：</p>
<pre><code class="text language-text">代理对象继承目标类
</code></pre>
<p>在 Spring Boot 中，也可以通过配置影响代理方式：</p>
<pre><code class="yaml language-yaml">spring:
  aop:
    proxy-target-class: true
</code></pre>
<p><code>true</code> 表示优先使用基于类的代理，也就是 CGLIB 代理。</p>
<p>不过具体行为还会受到 Spring 版本、代理配置和目标类特征等因素影响。</p>
<hr />
<h2 id="二十一、为什么同类内部调用会导致-aop-失效">二十一、为什么同类内部调用会导致 AOP 失效</h2>
<p>这是 Spring AOP 中非常常见的问题。</p>
<p>例如：</p>
<pre><code class="java language-java">@Service
public class OrderService {

    public void createOrder() {
        this.saveOrder();
    }

    @Transactional
    public void saveOrder() {
        System.out.println("保存订单");
    }
}
</code></pre>
<p>外部调用：</p>
<pre><code class="java language-java">orderService.createOrder();
</code></pre>
<p>执行 <code>createOrder()</code> 时，会进入代理对象。</p>
<p>但是在 <code>createOrder()</code> 内部：</p>
<pre><code class="java language-java">this.saveOrder();
</code></pre>
<p>这里的 <code>this</code> 指向的是目标对象本身，不是代理对象。</p>
<p>调用关系变成：</p>
<pre><code class="text language-text">代理对象.createOrder()
        ↓
目标对象.createOrder()
        ↓
目标对象.this.saveOrder()
</code></pre>
<p><code>saveOrder()</code> 没有再次经过代理对象，所以事务、日志、缓存等切面可能不会生效。</p>
<p>正确的代理调用流程应该是：</p>
<pre><code class="text language-text">调用者
  ↓
代理对象
  ↓
切面逻辑
  ↓
目标方法
</code></pre>
<p>而 <code>this.xxx()</code> 绕过了代理对象。</p>
<p>常见解决方法有：</p>
<pre><code class="text language-text">把被调用方法拆到另一个 Bean
通过代理对象调用
重新设计业务边界
</code></pre>
<p>最推荐的是拆分到另一个 Bean：</p>
<pre><code class="java language-java">@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderPersistenceService
            orderPersistenceService;

    public void createOrder() {
        orderPersistenceService.saveOrder();
    }
}
</code></pre>
<pre><code class="java language-java">@Service
public class OrderPersistenceService {

    @Transactional
    public void saveOrder() {
        System.out.println("保存订单");
    }
}
</code></pre>
<hr />
<h2 id="二十二、异常处理中的一个细节">二十二、异常处理中的一个细节</h2>
<p>JDK 动态代理中常见写法：</p>
<pre><code class="java language-java">method.invoke(target, args);
</code></pre>
<p>如果目标方法抛出异常，反射会把原异常包装成：</p>
<pre><code class="java language-java">InvocationTargetException
</code></pre>
<p>可以把真实异常取出来：</p>
<pre><code class="java language-java">try {

    return method.invoke(target, args);

} catch (InvocationTargetException e) {

    throw e.getTargetException();
}
</code></pre>
<p>完整写法：</p>
<pre><code class="java language-java">@Override
public Object invoke(
        Object proxy,
        Method method,
        Object[] args
) throws Throwable {

    try {

        return method.invoke(target, args);

    } catch (InvocationTargetException e) {

        throw e.getTargetException();
    }
}
</code></pre>
<p>这样调用者拿到的是业务方法本身抛出的异常，而不是反射包装异常。</p>
<hr />
<h1 id="二十三、动态代理适合解决什么问题">二十三、动态代理适合解决什么问题</h1>
<p>动态代理适合处理横跨多个业务模块的通用逻辑。</p>
<p>例如：</p>
<pre><code class="text language-text">日志记录
权限校验
事务管理
接口耗时统计
缓存处理
接口额度校验
统一异常处理
审计记录
重试机制
数据权限
</code></pre>
<p>这些逻辑的共同特点是：</p>
<blockquote>
  <p>它们不是某一个具体业务的核心，但很多业务方法都需要使用。</p>
</blockquote>
<p>例如订单创建方法的核心应该是：</p>
<pre><code class="text language-text">创建订单
</code></pre>
<p>而不是：</p>
<pre><code class="text language-text">检查权限
记录日志
开启事务
统计耗时
提交事务
发送监控指标
</code></pre>
<p>动态代理可以把这些通用逻辑从业务代码中抽离出来。</p>
<hr />
<h1 id="二十四、动态代理不适合解决什么问题">二十四、动态代理不适合解决什么问题</h1>
<p>代理不是所有问题都应该使用。</p>
<p>如果逻辑只属于某个具体方法，例如：</p>
<pre><code class="text language-text">订单价格计算
商品库存判断
用户等级计算
报告数据组装
</code></pre>
<p>这些属于业务本身，不应该为了“解耦”而全部放进代理。</p>
<p>代理更适合：</p>
<pre><code class="text language-text">通用、重复、横跨多个模块的逻辑
</code></pre>
<p>不适合：</p>
<pre><code class="text language-text">和具体业务强相关的核心逻辑
</code></pre>
<hr />
<h1 id="二十五、jdk-动态代理和-cglib-怎么选择">二十五、JDK 动态代理和 CGLIB 怎么选择</h1>
<p>如果自己编写动态代理，可以按照下面的方式选择。</p>
<h3 id="有接口，且面向接口编程">有接口，且面向接口编程</h3>
<p>优先考虑 JDK 动态代理：</p>
<pre><code class="java language-java">public class UserServiceImpl
        implements UserService
</code></pre>
<p>优点是结构清晰，调用者依赖接口。</p>
<hr />
<h3 id="没有接口">没有接口</h3>
<p>使用 CGLIB：</p>
<pre><code class="java language-java">public class ProductService {
}
</code></pre>
<p>CGLIB 可以直接创建目标类的子类。</p>
<hr />
<h3 id="使用-spring-aop">使用 Spring AOP</h3>
<p>大部分情况下不需要手动选择。</p>
<p>交给 Spring 创建代理即可。</p>
<p>开发者更需要关注的是：</p>
<pre><code class="text language-text">方法是否被 Spring Bean 管理
调用是否经过代理对象
方法是否可以被代理
是否存在同类内部调用
final 方法是否阻止 CGLIB 增强
</code></pre>
<hr />
<h1 id="二十六、总结">二十六、总结</h1>
<p>代理模式的核心是：</p>
<blockquote>
  <p>在不修改目标类代码的前提下，通过代理对象对目标方法进行增强。</p>
</blockquote>
<p>静态代理需要手动编写代理类：</p>
<pre><code class="text language-text">一个目标类型对应一个代理类
</code></pre>
<p>动态代理则是在运行时自动创建代理对象：</p>
<pre><code class="text language-text">一套代理逻辑可以复用到多个目标对象
</code></pre>
<p>JDK 动态代理的核心：</p>
<pre><code class="text language-text">目标类实现接口
Proxy.newProxyInstance()
InvocationHandler.invoke()
Method.invoke()
</code></pre>
<p>CGLIB 动态代理的核心：</p>
<pre><code class="text language-text">创建目标类的子类
Enhancer
MethodInterceptor.intercept()
MethodProxy.invokeSuper()
</code></pre>
<p>两者最核心的区别可以记成一句话：</p>
<blockquote>
  <p>JDK 动态代理通过实现接口创建代理，CGLIB 通过继承目标类创建代理。</p>
</blockquote>
<p>而 Spring AOP 可以理解为：</p>
<pre><code class="text language-text">Spring 自动帮我们创建代理对象
        ↓
把日志、事务、权限、缓存等通用逻辑
织入目标方法的执行过程
</code></pre>
<p>当我们真正理解 JDK 动态代理和 CGLIB 后，再看 Spring AOP、<code>@Transactional</code>、<code>@Async</code>、缓存注解和自定义切面，就不会只停留在“加一个注解就能生效”的层面，而是能够理解它们背后的代理调用过程。</p>]]></description>
      <link>http://43.128.224.135:3003/article/fc9ac188-07b9-496f-8a5b-c22f679861fe</link>
      <guid>http://43.128.224.135:3003/article/fc9ac188-07b9-496f-8a5b-c22f679861fe</guid>
      <pubDate>Tue, 04 Aug 2026 02:19:11 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>ConcurrentHashMap 和 LongAdder 的配合使用</title>
      <description><![CDATA[<p>在并发编程中，我们经常会遇到这样的需求：</p>
<blockquote>
  <p>统计某个用户、接口、商品或任务被访问了多少次。</p>
</blockquote>
<p>例如：</p>
<pre><code class="text language-text">接口 A 被访问了 100 次
接口 B 被访问了 35 次
接口 C 被访问了 8 次
</code></pre>
<p>如果是单线程程序，直接使用 <code>HashMap&lt;String, Integer&gt;</code> 就可以。</p>
<p>但在多线程环境下，这种写法会有线程安全问题：</p>
<pre><code class="java language-java">Map&lt;String, Integer&gt; counter = new HashMap&lt;&gt;();

counter.put("api:user:list",
        counter.getOrDefault("api:user:list", 0) + 1);
</code></pre>
<p>原因是：</p>
<pre><code class="java language-java">读取旧值
加 1
写回新值
</code></pre>
<p>这三个步骤并不是原子操作。</p>
<p>多个线程同时执行时，可能发生计数丢失。</p>
<p>这时就可以使用：</p>
<pre><code class="java language-java">ConcurrentHashMap + LongAdder
</code></pre>
<hr />
<h2 id="一、为什么使用-concurrenthashmap">一、为什么使用 ConcurrentHashMap</h2>
<p><code>ConcurrentHashMap</code> 是线程安全的 Map。</p>
<p>它适合多个线程同时进行：</p>
<pre><code class="java language-java">put
get
remove
computeIfAbsent
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">ConcurrentHashMap&lt;String, LongAdder&gt; counter =
        new ConcurrentHashMap&lt;&gt;();
</code></pre>
<p>这里：</p>
<pre><code class="text language-text">Key：业务标识
Value：计数器
</code></pre>
<p>例如：</p>
<pre><code class="text language-text">api:user:list      -&gt; 计数器
api:order:create   -&gt; 计数器
api:product:detail -&gt; 计数器
</code></pre>
<p><code>ConcurrentHashMap</code> 负责保证多个线程操作 Map 时的安全。</p>
<p>但它只解决了 Map 本身的并发问题，并不代表 Value 的累加操作天然安全。</p>
<hr />
<h2 id="二、为什么使用-longadder">二、为什么使用 LongAdder</h2>
<p><code>LongAdder</code> 是 Java 提供的并发计数器。</p>
<p>基本用法：</p>
<pre><code class="java language-java">LongAdder adder = new LongAdder();

adder.increment();
adder.increment();

System.out.println(adder.sum());
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">2
</code></pre>
<p>常用方法：</p>
<pre><code class="java language-java">adder.increment();      // 加 1
adder.decrement();      // 减 1
adder.add(10);          // 加 10
adder.sum();            // 获取当前值
adder.reset();          // 重置为 0
adder.sumThenReset();   // 获取值并重置
</code></pre>
<hr />
<h2 id="三、concurrenthashmap-和-longadder-怎么配合">三、ConcurrentHashMap 和 LongAdder 怎么配合</h2>
<p>最常见的写法是：</p>
<pre><code class="java language-java">ConcurrentHashMap&lt;String, LongAdder&gt; counter =
        new ConcurrentHashMap&lt;&gt;();

counter
        .computeIfAbsent("api:user:list", key -&gt; new LongAdder())
        .increment();
</code></pre>
<p>这段代码可以拆成两步理解。</p>
<p>第一步：</p>
<pre><code class="java language-java">counter.computeIfAbsent(
        "api:user:list",
        key -&gt; new LongAdder()
);
</code></pre>
<p>表示：</p>
<blockquote>
  <p>如果 Map 中已经有这个 Key，就返回原来的 LongAdder；如果没有，就创建一个新的 LongAdder 放进去。</p>
</blockquote>
<p>第二步：</p>
<pre><code class="java language-java">.increment();
</code></pre>
<p>表示让这个计数器加 1。</p>
<p>所以完整逻辑就是：</p>
<pre><code class="text language-text">根据 Key 找到计数器
        ↓
不存在就创建
        ↓
计数器加 1
</code></pre>
<hr />
<h2 id="四、完整示例：接口访问次数统计">四、完整示例：接口访问次数统计</h2>
<pre><code class="java language-java">import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public class ApiAccessCounter {

    private final ConcurrentHashMap&lt;String, LongAdder&gt; counterMap =
            new ConcurrentHashMap&lt;&gt;();

    /**
     * 增加接口访问次数
     */
    public void increment(String apiCode) {
        counterMap
                .computeIfAbsent(apiCode, key -&gt; new LongAdder())
                .increment();
    }

    /**
     * 获取某个接口的访问次数
     */
    public long getCount(String apiCode) {
        LongAdder adder = counterMap.get(apiCode);

        if (adder == null) {
            return 0;
        }

        return adder.sum();
    }

    /**
     * 获取所有接口访问次数
     */
    public Map&lt;String, Long&gt; getAllCount() {
        Map&lt;String, Long&gt; result = new ConcurrentHashMap&lt;&gt;();

        counterMap.forEach((apiCode, adder) -&gt;
                result.put(apiCode, adder.sum())
        );

        return result;
    }

    /**
     * 清除某个接口的计数
     */
    public void remove(String apiCode) {
        counterMap.remove(apiCode);
    }

    /**
     * 清空所有计数
     */
    public void clear() {
        counterMap.clear();
    }
}
</code></pre>
<p>使用：</p>
<pre><code class="java language-java">public class Main {

    public static void main(String[] args) {

        ApiAccessCounter counter = new ApiAccessCounter();

        counter.increment("user_list");
        counter.increment("user_list");
        counter.increment("order_create");

        System.out.println(counter.getCount("user_list"));
        System.out.println(counter.getCount("order_create"));
        System.out.println(counter.getAllCount());
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">2
1
{user_list=2, order_create=1}
</code></pre>
<hr />
<h2 id="五、多线程测试">五、多线程测试</h2>
<p>下面模拟 10 个线程，每个线程对同一个接口计数 10 万次。</p>
<p>理论结果应该是：</p>
<pre><code class="text language-text">10 × 100000 = 1000000
</code></pre>
<p>代码：</p>
<pre><code class="java language-java">import java.util.concurrent.CountDownLatch;

public class ConcurrentTest {

    public static void main(String[] args) throws InterruptedException {

        ApiAccessCounter counter = new ApiAccessCounter();

        int threadCount = 10;
        int loopCount = 100_000;

        CountDownLatch latch =
                new CountDownLatch(threadCount);

        for (int i = 0; i &lt; threadCount; i++) {

            new Thread(() -&gt; {

                try {
                    for (int j = 0; j &lt; loopCount; j++) {
                        counter.increment("api:user:list");
                    }
                } finally {
                    latch.countDown();
                }

            }).start();
        }

        latch.await();

        System.out.println(
                counter.getCount("api:user:list")
        );
    }
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">1000000
</code></pre>
<p>这说明在多个线程同时累加时，计数结果没有丢失。</p>
<hr />
<h2 id="六、为什么不用-integer">六、为什么不用 Integer</h2>
<p>有些人可能会这样写：</p>
<pre><code class="java language-java">ConcurrentHashMap&lt;String, Integer&gt; counterMap =
        new ConcurrentHashMap&lt;&gt;();
</code></pre>
<p>然后：</p>
<pre><code class="java language-java">counterMap.put(
        apiCode,
        counterMap.getOrDefault(apiCode, 0) + 1
);
</code></pre>
<p>虽然 <code>ConcurrentHashMap</code> 本身是线程安全的，但这一整段代码并不是原子操作。</p>
<p>它实际上包含：</p>
<pre><code class="text language-text">get
加 1
put
</code></pre>
<p>例如两个线程同时读取到：</p>
<pre><code class="text language-text">10
</code></pre>
<p>线程 A 计算：</p>
<pre><code class="text language-text">10 + 1 = 11
</code></pre>
<p>线程 B 也计算：</p>
<pre><code class="text language-text">10 + 1 = 11
</code></pre>
<p>最后两个线程都写入 11。</p>
<p>正确结果本来应该是：</p>
<pre><code class="text language-text">12
</code></pre>
<p>实际结果却是：</p>
<pre><code class="text language-text">11
</code></pre>
<p>这就是并发计数丢失。</p>
<p>当然，也可以使用：</p>
<pre><code class="java language-java">counterMap.merge(apiCode, 1, Integer::sum);
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">counterMap.merge(apiCode, 1, Integer::sum);
</code></pre>
<p>这个写法是线程安全的。</p>
<p>但在高并发计数场景中，<code>LongAdder</code> 通常更合适。</p>
<hr />
<h2 id="七、为什么不用-atomiclong">七、为什么不用 AtomicLong</h2>
<p>也可以这样写：</p>
<pre><code class="java language-java">ConcurrentHashMap&lt;String, AtomicLong&gt; counterMap =
        new ConcurrentHashMap&lt;&gt;();

counterMap
        .computeIfAbsent(
                apiCode,
                key -&gt; new AtomicLong()
        )
        .incrementAndGet();
</code></pre>
<p><code>AtomicLong</code> 也是线程安全的计数器。</p>
<p>那么它和 <code>LongAdder</code> 有什么区别？</p>
<h3 id="atomiclong">AtomicLong</h3>
<p><code>AtomicLong</code> 内部维护一个值。</p>
<p>多个线程同时修改这个值时，会通过 CAS 不断竞争。</p>
<pre><code class="text language-text">多个线程
   ↓
竞争同一个变量
</code></pre>
<p>竞争不激烈时，性能很好。</p>
<hr />
<h3 id="longadder">LongAdder</h3>
<p><code>LongAdder</code> 在竞争激烈时，会把一个计数拆成多个计数单元。</p>
<p>不同线程可以更新不同单元。</p>
<p>最后调用：</p>
<pre><code class="java language-java">sum()
</code></pre>
<p>时，再把多个单元相加。</p>
<p>可以理解为：</p>
<pre><code class="text language-text">线程1 -&gt; 单元1
线程2 -&gt; 单元2
线程3 -&gt; 单元3
线程4 -&gt; 单元4

最后汇总
</code></pre>
<p>因此，高并发写入场景下，<code>LongAdder</code> 通常比 <code>AtomicLong</code> 更高效。</p>
<hr />
<h2 id="八、longadder-的注意事项">八、LongAdder 的注意事项</h2>
<h3 id="1-sum-不一定是绝对瞬时值">1. sum() 不一定是绝对瞬时值</h3>
<pre><code class="java language-java">long count = adder.sum();
</code></pre>
<p><code>sum()</code> 会汇总当前各个计数单元。</p>
<p>但如果其他线程正在同时执行 <code>increment()</code>，这个值不一定代表某个绝对精确时间点的快照。</p>
<p>因此，<code>LongAdder</code> 更适合：</p>
<pre><code class="text language-text">统计
监控
访问次数
请求次数
日志数量
指标累加
</code></pre>
<p>不太适合要求强一致性的业务。</p>
<p>例如：</p>
<pre><code class="text language-text">账户余额
库存扣减
额度扣减
支付金额
</code></pre>
<p>这些场景不能只依赖 <code>LongAdder</code>。</p>
<hr />
<h3 id="2-longadder-不适合条件判断后扣减">2. LongAdder 不适合条件判断后扣减</h3>
<p>例如下面这种业务：</p>
<pre><code class="text language-text">剩余额度大于 0 才允许访问
访问后额度减 1
</code></pre>
<p>如果写成：</p>
<pre><code class="java language-java">if (adder.sum() &lt; limit) {
    adder.increment();
}
</code></pre>
<p>这不是线程安全的整体操作。</p>
<p>因为：</p>
<pre><code class="text language-text">判断
累加
</code></pre>
<p>是两个步骤。</p>
<p>多个线程可能同时判断成功，然后一起累加，最终超过限制。</p>
<p>所以 <code>LongAdder</code> 更适合做统计，不适合做严格限流和额度控制。</p>
<p>严格限流通常需要：</p>
<pre><code class="text language-text">AtomicLong
synchronized
Lock
Redis Lua
数据库乐观锁
Semaphore
</code></pre>
<hr />
<h3 id="3-map-中的-key-可能越来越多">3. Map 中的 Key 可能越来越多</h3>
<p>例如按照用户 ID 计数：</p>
<pre><code class="java language-java">counter.increment("user:" + userId);
</code></pre>
<p>如果用户数量持续增长，<code>ConcurrentHashMap</code> 中的数据也会越来越多。</p>
<p>需要考虑：</p>
<pre><code class="text language-text">定时清理
设置过期时间
定期持久化
按时间窗口重建 Map
</code></pre>
<p><code>ConcurrentHashMap</code> 本身不支持过期时间。</p>
<p>如果需要自动过期，可以考虑：</p>
<pre><code class="text language-text">Caffeine
Redis
Guava Cache
</code></pre>
<hr />
<h2 id="九、spring-boot-中使用">九、Spring Boot 中使用</h2>
<p>在 Spring Boot 中，可以把计数器注册为单例 Bean。</p>
<pre><code class="java language-java">import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

@Component
public class ApiCounter {

    private final ConcurrentHashMap&lt;String, LongAdder&gt; counterMap =
            new ConcurrentHashMap&lt;&gt;();

    public void increment(String apiCode) {
        counterMap
                .computeIfAbsent(apiCode, key -&gt; new LongAdder())
                .increment();
    }

    public long getCount(String apiCode) {
        LongAdder adder = counterMap.get(apiCode);
        return adder == null ? 0 : adder.sum();
    }
}
</code></pre>
<p>Controller 中使用：</p>
<pre><code class="java language-java">import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final ApiCounter apiCounter;

    @GetMapping("/user/list")
    public String list() {

        apiCounter.increment("user_list");

        return "success";
    }

    @GetMapping("/user/list/count")
    public long count() {
        return apiCounter.getCount("user_list");
    }
}
</code></pre>
<p>因为 <code>ApiCounter</code> 是 Spring 单例 Bean，所以所有请求共享同一个：</p>
<pre><code class="java language-java">ConcurrentHashMap&lt;String, LongAdder&gt;
</code></pre>
<p>不会每次请求都创建一个新的计数器。</p>
<hr />
<h2 id="十、按用户和接口统计">十、按用户和接口统计</h2>
<p>如果需要统计：</p>
<pre><code class="text language-text">某个用户访问某个接口多少次
</code></pre>
<p>可以把用户 ID 和接口编码组合成 Key。</p>
<pre><code class="java language-java">public void increment(Long userId, String apiCode) {

    String key = userId + ":" + apiCode;

    counterMap
            .computeIfAbsent(
                    key,
                    value -&gt; new LongAdder()
            )
            .increment();
}
</code></pre>
<p>例如：</p>
<pre><code class="text language-text">1001:user_list
1001:order_create
1002:user_list
</code></pre>
<p>完整示例：</p>
<pre><code class="java language-java">@Component
public class UserApiCounter {

    private final ConcurrentHashMap&lt;String, LongAdder&gt; counterMap =
            new ConcurrentHashMap&lt;&gt;();

    public void increment(Long userId, String apiCode) {

        String key = buildKey(userId, apiCode);

        counterMap
                .computeIfAbsent(
                        key,
                        value -&gt; new LongAdder()
                )
                .increment();
    }

    public long getCount(Long userId, String apiCode) {

        String key = buildKey(userId, apiCode);

        LongAdder adder = counterMap.get(key);

        return adder == null ? 0 : adder.sum();
    }

    private String buildKey(Long userId, String apiCode) {
        return userId + ":" + apiCode;
    }
}
</code></pre>
<hr />
<h2 id="十一、按天统计接口访问量">十一、按天统计接口访问量</h2>
<p>如果要按天统计，可以把日期加入 Key。</p>
<pre><code class="java language-java">import java.time.LocalDate;

public void increment(String apiCode) {

    String key = apiCode + ":" + LocalDate.now();

    counterMap
            .computeIfAbsent(
                    key,
                    value -&gt; new LongAdder()
            )
            .increment();
}
</code></pre>
<p>生成的 Key 类似：</p>
<pre><code class="text language-text">user_list:2026-08-04
order_create:2026-08-04
</code></pre>
<p>不过这种方式要注意定期清理历史日期数据。</p>
<hr />
<h2 id="十二、一个更完整的统计服务">十二、一个更完整的统计服务</h2>
<pre><code class="java language-java">import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

@Service
public class ApiStatisticsService {

    private final ConcurrentHashMap&lt;String, LongAdder&gt; counterMap =
            new ConcurrentHashMap&lt;&gt;();

    public void increment(String apiCode) {

        String key = buildKey(apiCode, LocalDate.now());

        counterMap
                .computeIfAbsent(
                        key,
                        value -&gt; new LongAdder()
                )
                .increment();
    }

    public long getTodayCount(String apiCode) {

        String key = buildKey(apiCode, LocalDate.now());

        LongAdder adder = counterMap.get(key);

        return adder == null ? 0 : adder.sum();
    }

    public Map&lt;String, Long&gt; getAllStatistics() {

        Map&lt;String, Long&gt; result = new HashMap&lt;&gt;();

        counterMap.forEach((key, adder) -&gt;
                result.put(key, adder.sum())
        );

        return result;
    }

    public long getAndReset(String apiCode) {

        String key = buildKey(apiCode, LocalDate.now());

        LongAdder adder = counterMap.get(key);

        if (adder == null) {
            return 0;
        }

        return adder.sumThenReset();
    }

    public void remove(String apiCode, LocalDate date) {
        counterMap.remove(buildKey(apiCode, date));
    }

    private String buildKey(
            String apiCode,
            LocalDate date
    ) {
        return apiCode + ":" + date;
    }
}
</code></pre>
<hr />
<h2 id="十三、适用场景">十三、适用场景</h2>
<p><code>ConcurrentHashMap + LongAdder</code> 比较适合：</p>
<pre><code class="text language-text">接口访问次数统计
商品浏览次数统计
异常出现次数统计
任务执行次数统计
不同状态数量统计
日志级别数量统计
在线请求指标统计
本地缓存命中次数统计
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">counterMap
        .computeIfAbsent(
                "cache_hit",
                key -&gt; new LongAdder()
        )
        .increment();
</code></pre>
<p>或者：</p>
<pre><code class="java language-java">counterMap
        .computeIfAbsent(
                exception.getClass().getSimpleName(),
                key -&gt; new LongAdder()
        )
        .increment();
</code></pre>
<hr />
<h2 id="十四、不适用场景">十四、不适用场景</h2>
<p>它不适合：</p>
<pre><code class="text language-text">分布式计数
严格库存扣减
账户余额计算
支付金额计算
强一致额度控制
跨服务器共享状态
</code></pre>
<p>因为这个 Map 只存在当前 JVM 内存中。</p>
<p>如果应用部署了三台服务器：</p>
<pre><code class="text language-text">服务器 A：100 次
服务器 B：80 次
服务器 C：60 次
</code></pre>
<p>每台服务器都有自己的计数器。</p>
<p>它们不会自动合并。</p>
<p>分布式环境中，可以考虑：</p>
<pre><code class="text language-text">Redis INCR
Redis Hash
消息队列
数据库原子更新
Prometheus Metrics
</code></pre>
<hr />
<h2 id="十五、总结">十五、总结</h2>
<p><code>ConcurrentHashMap</code> 和 <code>LongAdder</code> 的职责不同。</p>
<p><code>ConcurrentHashMap</code> 负责：</p>
<pre><code class="text language-text">保证多个线程安全地访问 Map
</code></pre>
<p><code>LongAdder</code> 负责：</p>
<pre><code class="text language-text">保证多个线程高效地进行计数
</code></pre>
<p>最常见的组合写法是：</p>
<pre><code class="java language-java">counterMap
        .computeIfAbsent(
                key,
                value -&gt; new LongAdder()
        )
        .increment();
</code></pre>
<p>可以记成一句话：</p>
<blockquote>
  <p><code>ConcurrentHashMap</code> 管理多个计数器，<code>LongAdder</code> 负责每个计数器的并发累加。</p>
</blockquote>
<p>在单机高并发统计场景中，这是一种简单、实用而且性能很好的实现方式。</p>]]></description>
      <link>http://43.128.224.135:3003/article/e000006c-288c-4a29-9578-88e70b77666a</link>
      <guid>http://43.128.224.135:3003/article/e000006c-288c-4a29-9578-88e70b77666a</guid>
      <pubDate>Tue, 04 Aug 2026 02:14:24 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>Semaphore、CountDownLatch、CyclicBarrier</title>
      <description><![CDATA[<p>在 Java 并发开发中，除了 <code>synchronized</code>、<code>Lock</code> 等互斥锁之外，还有三个非常常用的线程协作工具：</p>
<ul>
<li>Semaphore（信号量）</li>
<li>CountDownLatch（倒计数器）</li>
<li>CyclicBarrier（循环栅栏）</li>
</ul>
<p>它们都属于 <code>java.util.concurrent</code> 包，但解决的问题完全不同。</p>
<hr />
<h1 id="一、semaphore（信号量）">一、Semaphore（信号量）</h1>
<h2 id="作用">作用</h2>
<p>控制<strong>同时访问某个资源的线程数量</strong>。</p>
<p>可以把它理解成一个门卫，只有拿到许可证（Permit）的线程才能进入，许可证用完后，其余线程只能等待。</p>
<blockquote>
  <p>一句话：<strong>控制并发数量。</strong></p>
</blockquote>
<h2 id="使用场景">使用场景</h2>
<ul>
<li>数据库连接池</li>
<li>Redis 连接池</li>
<li>外部接口并发限制</li>
<li>文件下载并发控制</li>
</ul>
<h2 id="示例：同时最多允许三个线程执行">示例：同时最多允许三个线程执行</h2>
<pre><code class="java language-java">import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    private static final Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {

        for (int i = 1; i &lt;= 10; i++) {
            final int num = i;

            new Thread(() -&gt; {
                try {
                    semaphore.acquire();

                    System.out.println("线程" + num + "开始执行");
                    Thread.sleep(2000);
                    System.out.println("线程" + num + "执行结束");

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            }).start();
        }
    }
}
</code></pre>
<p>运行过程中，无论启动多少线程，同时真正执行的始终只有 3 个。</p>
<hr />
<h1 id="二、countdownlatch（倒计数器）">二、CountDownLatch（倒计数器）</h1>
<h2 id="作用-1">作用</h2>
<p>让<strong>一个或多个线程等待其他线程执行完成</strong>。</p>
<p>它内部维护一个计数器，每完成一个任务就减一，当计数变成 0 时，等待线程继续执行。</p>
<blockquote>
  <p>一句话：<strong>主线程等子线程。</strong></p>
</blockquote>
<h2 id="使用场景-1">使用场景</h2>
<ul>
<li>Spring Boot 初始化</li>
<li>多线程加载缓存</li>
<li>多线程统计结果</li>
<li>并行任务汇总</li>
</ul>
<h2 id="示例：主线程等待三个子线程完成">示例：主线程等待三个子线程完成</h2>
<pre><code class="java language-java">import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws Exception {

        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 1; i &lt;= 3; i++) {

            final int num = i;

            new Thread(() -&gt; {

                System.out.println("线程" + num + "开始工作");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ignored) {
                }

                System.out.println("线程" + num + "工作完成");

                latch.countDown();

            }).start();
        }

        System.out.println("主线程等待...");

        latch.await();

        System.out.println("所有线程执行完成，主线程继续");
    }
}
</code></pre>
<p>这里真正等待的是主线程，而三个工作线程完成任务后并不会互相等待。</p>
<hr />
<h1 id="三、cyclicbarrier（循环栅栏）">三、CyclicBarrier（循环栅栏）</h1>
<h2 id="作用-2">作用</h2>
<p>让<strong>一组线程互相等待</strong>，直到所有线程都到达同一个位置，再一起继续执行。</p>
<blockquote>
  <p>一句话：<strong>大家一起出发。</strong></p>
</blockquote>
<h2 id="使用场景-2">使用场景</h2>
<ul>
<li>分阶段并行计算</li>
<li>多线程数据处理</li>
<li>游戏等待所有玩家准备</li>
<li>多线程批量导入</li>
</ul>
<h2 id="示例：三个人一起爬山">示例：三个人一起爬山</h2>
<pre><code class="java language-java">import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier barrier = new CyclicBarrier(3,
                () -&gt; System.out.println("所有人到齐，继续前进"));

        for (int i = 1; i &lt;= 3; i++) {

            final int num = i;

            new Thread(() -&gt; {

                try {

                    System.out.println("队员" + num + "开始出发");

                    Thread.sleep(num * 1000);

                    System.out.println("队员" + num + "到达集合点");

                    barrier.await();

                    System.out.println("队员" + num + "继续前进");

                } catch (Exception e) {
                    e.printStackTrace();
                }

            }).start();
        }
    }
}
</code></pre>
<p>谁先到集合点，谁就等待；直到三个人全部到达后，再一起继续前进。</p>
<hr />
<h1 id="三者区别">三者区别</h1>
<table>
<thead>
<tr>
<th>工具</th>
<th>作用</th>
<th>谁等待谁</th>
<th>是否可重复使用</th>
</tr>
</thead>
<tbody>
<tr>
<td>Semaphore</td>
<td>控制并发数量</td>
<td>等待许可证</td>
<td>是</td>
</tr>
<tr>
<td>CountDownLatch</td>
<td>等待任务完成</td>
<td>主线程等待子线程</td>
<td>否</td>
</tr>
<tr>
<td>CyclicBarrier</td>
<td>等待所有线程到达屏障</td>
<td>所有线程互相等待</td>
<td>是</td>
</tr>
</tbody>
</table>
<hr />
<h1 id="总结">总结</h1>
<p>如果把三者比作生活中的场景：</p>
<ul>
<li><strong>Semaphore</strong>：厕所只有 3 个坑位，同时最多进去 3 个人。</li>
<li><strong>CountDownLatch</strong>：老师等待所有学生交卷后才能收卷。</li>
<li><strong>CyclicBarrier</strong>：几个人约好一起爬山，每到一个集合点，都必须等所有人到齐再继续出发。</li>
</ul>
<p>真正理解它们的关键，不是记 API，而是记住它们分别解决了三类不同的线程协作问题：</p>
<ul>
<li><strong>Semaphore：控制数量</strong></li>
<li><strong>CountDownLatch：等待完成</strong></li>
<li><strong>CyclicBarrier：一起继续</strong></li>
</ul>]]></description>
      <link>http://43.128.224.135:3003/article/2d893f17-b828-4d5a-8f14-6f843686fcd7</link>
      <guid>http://43.128.224.135:3003/article/2d893f17-b828-4d5a-8f14-6f843686fcd7</guid>
      <pubDate>Mon, 03 Aug 2026 08:08:33 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>MyBatis相关笔记，方便以后直接Copy</title>
      <description><![CDATA[<h2 id="1-select-编写-sql">1. @Select 编写 SQL</h2>
<pre><code class="java language-java">public interface UserMapper extends BaseMapper&lt;UserEntity&gt; {

    @Select("SELECT * FROM sys_user WHERE username = #{username}")
    UserEntity selectByUsername(String username);
}
</code></pre>
<hr />
<h2 id="2-参数建议使用-param">2. 参数建议使用 @Param</h2>
<pre><code class="java language-java">@Select("SELECT * FROM sys_user WHERE username = #{username}")
UserEntity selectByUsername(@Param("username") String username);
</code></pre>
<hr />
<h2 id="3-参数是实体对象">3. 参数是实体对象</h2>
<p>Mapper：</p>
<pre><code class="java language-java">int insertCustom(UserEntity user);
</code></pre>
<p>XML：</p>
<pre><code class="xml language-xml">&lt;insert id="insertCustom"&gt;
    INSERT INTO sys_user(username, status)
    VALUES (#{username}, #{status})
&lt;/insert&gt;
</code></pre>
<blockquote>
  <p>单个实体参数，不需要写 <code>#{user.username}</code>。</p>
</blockquote>
<hr />
<h2 id="4-实体参数加-param">4. 实体参数加 @Param</h2>
<p>Mapper：</p>
<pre><code class="java language-java">int insertCustom(@Param("user") UserEntity user);
</code></pre>
<p>XML：</p>
<pre><code class="xml language-xml">&lt;insert id="insertCustom"&gt;
    INSERT INTO sys_user(username, status)
    VALUES (#{user.username}, #{user.status})
&lt;/insert&gt;
</code></pre>
<p>因为参数命名为 <code>user</code>，所以需要：</p>
<pre><code class="text language-text">user.username
user.status
</code></pre>
<hr />
<h1 id="5--与-">5. #{} 与 ${}</h1>
<h2 id="">#{}</h2>
<ul>
<li>参数占位</li>
<li>自动处理参数类型</li>
<li>字符串无需自己加引号</li>
<li>可以防止 SQL 注入</li>
<li>日常开发推荐使用</li>
</ul>
<h2 id="-1">${}</h2>
<ul>
<li>字符串直接拼接</li>
<li>存在 SQL 注入风险</li>
<li>谨慎使用</li>
</ul>
<hr />
<h1 id="6-动态-sql">6. 动态 SQL</h1>
<h2 id="61-if--where">6.1 <code>&lt;if&gt;</code> + <code>&lt;where&gt;</code></h2>
<pre><code class="xml language-xml">&lt;select id="selectByCondition"
        resultType="org.example.entity.UserEntity"&gt;
    SELECT *
    FROM sys_user

    &lt;where&gt;
        &lt;if test="username != null and username != ''"&gt;
            username = #{username}
        &lt;/if&gt;

        &lt;if test="status != null"&gt;
            AND status = #{status}
        &lt;/if&gt;
    &lt;/where&gt;
&lt;/select&gt;
</code></pre>
<p><code>&lt;where&gt;</code> 作用：</p>
<ul>
<li>自动添加 <code>WHERE</code></li>
<li>自动去掉开头多余的 <code>AND</code></li>
</ul>
<hr />
<h2 id="62-set">6.2 <code>&lt;set&gt;</code></h2>
<pre><code class="xml language-xml">&lt;update id="updateUser"&gt;
    UPDATE sys_user

    &lt;set&gt;
        &lt;if test="username != null and username != ''"&gt;
            username = #{username},
        &lt;/if&gt;

        &lt;if test="status != null"&gt;
            status = #{status},
        &lt;/if&gt;

        &lt;if test="age != null"&gt;
            age = #{age},
        &lt;/if&gt;
    &lt;/set&gt;

    WHERE id = #{id}
&lt;/update&gt;
</code></pre>
<p>作用：</p>
<ul>
<li>自动添加 <code>SET</code></li>
<li>自动删除最后多余的逗号</li>
</ul>
<p>注意：</p>
<p>如果所有字段都为空，可能生成：</p>
<pre><code class="sql language-sql">UPDATE sys_user
WHERE id = ?
</code></pre>
<p>属于错误 SQL。</p>
<p>业务层应提前判断：</p>
<pre><code class="java language-java">if (user.getUsername() == null
        &amp;&amp; user.getStatus() == null
        &amp;&amp; user.getAge() == null) {
    throw new IllegalArgumentException("没有需要更新的字段");
}
</code></pre>
<hr />
<h2 id="63-更新必须有-where">6.3 更新必须有 WHERE</h2>
<p>必须保证：</p>
<pre><code class="sql language-sql">WHERE id = #{id}
</code></pre>
<p>否则可能出现：</p>
<pre><code class="sql language-sql">UPDATE sys_user
SET status = 0
</code></pre>
<p>导致全表更新。</p>
<p>动态更新时重点检查：</p>
<ul>
<li>SET 后是否有字段</li>
<li>WHERE 条件是否可靠</li>
</ul>
<hr />
<h2 id="64-foreach">6.4 <code>&lt;foreach&gt;</code></h2>
<pre><code class="xml language-xml">&lt;select id="selectByIds"
        resultType="com.example.entity.UserEntity"&gt;
    SELECT id, username, status
    FROM sys_user
    WHERE id IN

    &lt;foreach collection="ids"
             item="id"
             open="("
             separator=","
             close=")"&gt;
        #{id}
    &lt;/foreach&gt;
&lt;/select&gt;
</code></pre>
<p>常用于：</p>
<ul>
<li>IN 查询</li>
<li>批量操作</li>
</ul>
<hr />
<h2 id="65-choose">6.5 <code>&lt;choose&gt;</code></h2>
<pre><code class="xml language-xml">&lt;choose&gt;
    &lt;when test="条件1"&gt;
    &lt;/when&gt;

    &lt;when test="条件2"&gt;
    &lt;/when&gt;

    &lt;otherwise&gt;
    &lt;/otherwise&gt;
&lt;/choose&gt;
</code></pre>
<p>等价于：</p>
<pre><code class="java language-java">if (条件1) {

} else if (条件2) {

} else {

}
</code></pre>
<hr />
<h1 id="7-resultmap">7. resultMap</h1>
<p>复杂查询时可手动指定字段映射：</p>
<pre><code class="xml language-xml">&lt;resultMap id="userResultMap"
           type="com.example.entity.UserEntity"&gt;

    &lt;id column="id" property="id"/&gt;

    &lt;result column="user_name" property="userName"/&gt;

    &lt;result column="user_status" property="status"/&gt;

    &lt;result column="create_time" property="createTime"/&gt;

&lt;/resultMap&gt;
</code></pre>
<hr />
<h1 id="8-generator">8. Generator</h1>
<pre><code class="code language-code">package top.xiaolizhi.blog;

import java.net.URI;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;

public class Generator {


    public static void main(String[] args) {
        FastAutoGenerator.create(
                        "jdbc:mysql://127.0.0.1:3306/blog?useSSL=false&amp;serverTimezone=Asia/Shanghai",
                        "root",
                        "root"
                )
                // 全局配置
                .globalConfig(builder -&gt; builder
                        .author("succos")
                        // .outputDir(System.getProperty("user.dir") + "/src/main/java")
                        .outputDir("C:\\Users\\Administrator\\Desktop\\spring-study\\src\\main\\java")
                )

                // 包配置
                .packageConfig(builder -&gt; builder
                        .parent("top.xiaolizhi.blog")
                        .mapper("mapper")
                        .service("service")
                        .serviceImpl("service.impl")
                        .controller("controller")
                        .entity("pojo.entity")
                )

                // 策略
                .strategyConfig(builder -&gt; builder
                        .addInclude("tag") // 表名

                        .entityBuilder()
                        .enableLombok()
                        .enableFileOverride()
                        .formatFileName("%sEntity")

                        .mapperBuilder()
                        .enableFileOverride()

                        .serviceBuilder()
                        .enableFileOverride()

                        .controllerBuilder()
                        .enableRestStyle()
                        .enableFileOverride()
                )

                .execute();
    }
}
</code></pre>
<h1 id="9-mapper-分页">9. Mapper 分页</h1>
<p>Mapper：</p>
<pre><code class="java language-java">IPage&lt;UserVO&gt; selectUserPage(
        IPage&lt;UserVO&gt; page,
        @Param("username") String username
);
</code></pre>
<p>第一个参数：</p>
<pre><code class="java language-java">IPage&lt;UserVO&gt; page
</code></pre>
<p>用于接收分页信息。</p>
<p>MyBatis-Plus 会自动生成分页 SQL：</p>
<pre><code class="sql language-sql">SELECT COUNT(*) ...
</code></pre>
<p>查询总数。</p>
<pre><code class="sql language-sql">SELECT ...
LIMIT ...
</code></pre>
<p>查询当前页数据。</p>
<blockquote>
  <p>XML 中无需再写 <code>LIMIT</code>。</p>
</blockquote>]]></description>
      <link>http://43.128.224.135:3003/article/2d9a2e6b-6b22-4553-a99a-97a6ca5d52fb</link>
      <guid>http://43.128.224.135:3003/article/2d9a2e6b-6b22-4553-a99a-97a6ca5d52fb</guid>
      <pubDate>Thu, 30 Jul 2026 09:23:34 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>从Spring Security登录认证到基本的RBAC </title>
      <description><![CDATA[<p>学习 Spring Security 时，我最开始觉得它特别乱。</p>
<p>一会儿是 <code>AuthenticationManager</code>，一会儿是 <code>AuthenticationProvider</code>，接着又冒出 <code>UserDetailsService</code>、<code>UserDetails</code>、<code>AuthenticationToken</code>。如果再加上 JWT、短信登录、微信登录和 RBAC，整个流程很容易被绕晕。</p>
<p>后来我发现，理解 Spring Security 不能一开始就背配置，而是要先抓住一条主线：</p>
<blockquote>
  <p>用户提交登录信息，Spring Security 找到对应的认证方式进行校验，认证成功后保存当前用户身份，后续请求再根据用户拥有的权限决定能否访问接口。</p>
</blockquote>
<p>围绕这条主线，很多类就不再是零散的概念了。</p>
<hr />
<h2 id="一、spring-security-主要解决什么问题">一、Spring Security 主要解决什么问题</h2>
<p>Spring Security 主要解决两件事：</p>
<pre><code class="text language-text">认证 Authentication
授权 Authorization
</code></pre>
<p>认证解决的是：</p>
<blockquote>
  <p>你是谁？</p>
</blockquote>
<p>例如验证：</p>
<ul>
<li>用户名和密码是否正确</li>
<li>手机号和验证码是否正确</li>
<li>微信授权信息是否有效</li>
</ul>
<p>授权解决的是：</p>
<blockquote>
  <p>你能做什么？</p>
</blockquote>
<p>例如：</p>
<ul>
<li>能不能新增用户</li>
<li>能不能删除菜单</li>
<li>能不能查看财务数据</li>
<li>能不能进入后台管理页面</li>
</ul>
<p>因此，一个完整的安全流程可以分成两段：</p>
<pre><code class="text language-text">登录阶段：确认用户身份

访问接口阶段：检查用户权限
</code></pre>
<hr />
<h2 id="二、账号密码登录的完整流程">二、账号密码登录的完整流程</h2>
<p>假设登录接口接收用户名和密码：</p>
<pre><code class="java language-java">@PostMapping("/login")
public String login(@RequestBody LoginRequest request) {

    Authentication authentication =
            authenticationManager.authenticate(
                    UsernamePasswordAuthenticationToken.unauthenticated(
                            request.getUsername(),
                            request.getPassword()
                    )
            );

    return jwtService.generateToken(authentication);
}
</code></pre>
<p>这里最重要的不是 JWT，而是下面这段：</p>
<pre><code class="java language-java">authenticationManager.authenticate(
        UsernamePasswordAuthenticationToken.unauthenticated(
                username,
                password
        )
);
</code></pre>
<p>完整流程如下：</p>
<pre><code class="text language-text">Controller
    ↓
创建 UsernamePasswordAuthenticationToken
    ↓
AuthenticationManager
    ↓
ProviderManager
    ↓
DaoAuthenticationProvider
    ↓
UserDetailsService
    ↓
查询数据库用户
    ↓
PasswordEncoder 校验密码
    ↓
返回认证成功的 Authentication
</code></pre>
<hr />
<h2 id="三、authenticationtoken-是什么">三、AuthenticationToken 是什么</h2>
<p>下面这段代码：</p>
<pre><code class="java language-java">new UsernamePasswordAuthenticationToken(username, password)
</code></pre>
<p>并不是在执行登录。</p>
<p>它只是创建了一个认证对象，用来表示：</p>
<blockquote>
  <p>这是一次用户名密码认证请求。</p>
</blockquote>
<p>认证前，这个对象中大致保存：</p>
<pre><code class="text language-text">principal   = 用户名
credentials = 密码
authenticated = false
</code></pre>
<p>可以把它理解成一张登录申请单：</p>
<pre><code class="text language-text">申请人：张三
认证材料：密码
认证方式：用户名密码
</code></pre>
<p>真正触发认证的是：</p>
<pre><code class="java language-java">authenticationManager.authenticate(token);
</code></pre>
<p>所以：</p>
<pre><code class="text language-text">AuthenticationToken：描述本次使用什么方式登录

AuthenticationManager：正式发起认证
</code></pre>
<hr />
<h2 id="四、authenticationmanager-是什么">四、AuthenticationManager 是什么</h2>
<p><code>AuthenticationManager</code> 是认证的统一入口：</p>
<pre><code class="java language-java">Authentication authenticate(Authentication authentication);
</code></pre>
<p>业务代码不需要自己决定具体由谁校验，只需要把认证对象交进去：</p>
<pre><code class="java language-java">authenticationManager.authenticate(token);
</code></pre>
<p>实际项目中，常见实现是：</p>
<pre><code class="text language-text">ProviderManager
</code></pre>
<p><code>ProviderManager</code> 内部管理多个 <code>AuthenticationProvider</code>。</p>
<p>例如：</p>
<pre><code class="text language-text">ProviderManager
├── DaoAuthenticationProvider
├── SmsAuthenticationProvider
└── 其他 AuthenticationProvider
</code></pre>
<p>它的作用类似于调度中心：</p>
<blockquote>
  <p>根据当前传入的 Token 类型，找到能够处理它的 Provider。</p>
</blockquote>
<hr />
<h2 id="五、spring-security-怎么找到对应的-provider">五、Spring Security 怎么找到对应的 Provider</h2>
<p>这是我之前最困惑的地方。</p>
<p>Spring Security 不是根据接口地址判断，也不是根据参数名判断，而是通过每个 Provider 的：</p>
<pre><code class="java language-java">supports()
</code></pre>
<p>例如短信认证 Provider：</p>
<pre><code class="java language-java">@Override
public boolean supports(Class&lt;?&gt; authentication) {
    return SmsAuthenticationToken.class
            .isAssignableFrom(authentication);
}
</code></pre>
<p>当传入：</p>
<pre><code class="java language-java">new SmsAuthenticationToken(mobile, code)
</code></pre>
<p><code>ProviderManager</code> 会遍历所有 Provider：</p>
<pre><code class="java language-java">for (AuthenticationProvider provider : providers) {
    if (provider.supports(authentication.getClass())) {
        return provider.authenticate(authentication);
    }
}
</code></pre>
<p>大致判断过程是：</p>
<pre><code class="text language-text">传入 SmsAuthenticationToken
        ↓
DaoAuthenticationProvider.supports() = false
        ↓
SmsAuthenticationProvider.supports() = true
        ↓
调用 SmsAuthenticationProvider.authenticate()
</code></pre>
<p>所以一句话：</p>
<blockquote>
  <p>Token 决定登录类型，Provider 的 supports() 决定谁来处理。</p>
</blockquote>
<hr />
<h2 id="六、authenticationprovider-是什么">六、AuthenticationProvider 是什么</h2>
<p><code>AuthenticationProvider</code> 才是真正执行认证逻辑的地方。</p>
<p>接口中最核心的是两个方法：</p>
<pre><code class="java language-java">public interface AuthenticationProvider {

    Authentication authenticate(
            Authentication authentication
    );

    boolean supports(
            Class&lt;?&gt; authentication
    );
}
</code></pre>
<p>其中：</p>
<pre><code class="text language-text">supports()：判断自己是否支持这种 Token

authenticate()：真正执行认证
</code></pre>
<p>账号密码登录通常由 Spring Security 提供的：</p>
<pre><code class="text language-text">DaoAuthenticationProvider
</code></pre>
<p>负责处理。</p>
<p>短信验证码登录通常需要自己实现：</p>
<pre><code class="text language-text">SmsAuthenticationProvider
</code></pre>
<p>因此，不同登录方式的结构可以理解为：</p>
<pre><code class="text language-text">UsernamePasswordAuthenticationToken
    ↓
DaoAuthenticationProvider

SmsAuthenticationToken
    ↓
SmsAuthenticationProvider
</code></pre>
<p>没有现成登录方式时，一般需要：</p>
<ol>
<li>自定义一个 Token</li>
<li>自定义一个 Provider</li>
</ol>
<hr />
<h2 id="七、userdetailsservice-为什么存在">七、UserDetailsService 为什么存在</h2>
<p>Spring Security 不知道我们的用户表结构。</p>
<p>有的项目叫：</p>
<pre><code class="text language-text">sys_user
</code></pre>
<p>有的项目叫：</p>
<pre><code class="text language-text">user
</code></pre>
<p>用户名字段可能叫：</p>
<pre><code class="text language-text">username
login_name
account
mobile
</code></pre>
<p>密码字段也可能完全不同。</p>
<p>所以 Spring Security 定义了一个接口：</p>
<pre><code class="java language-java">public interface UserDetailsService {

    UserDetails loadUserByUsername(String username);
}
</code></pre>
<p>它的意思其实不是限定只能使用用户名，而是：</p>
<blockquote>
  <p>给我一个账号标识，请把这个用户查询出来。</p>
</blockquote>
<p>账号标识可以是：</p>
<ul>
<li>用户名</li>
<li>邮箱</li>
<li>手机号</li>
<li>工号</li>
</ul>
<p>例如：</p>
<pre><code class="java language-java">@Override
public UserDetails loadUserByUsername(String account) {

    UserEntity user = userMapper.selectOne(
            Wrappers.&lt;UserEntity&gt;lambdaQuery()
                    .eq(UserEntity::getUsername, account)
    );

    if (user == null) {
        throw new UsernameNotFoundException("用户名或密码错误");
    }

    return User.withUsername(user.getUsername())
            .password(user.getPassword())
            .authorities(loadAuthorities(user.getId()))
            .build();
}
</code></pre>
<p><code>UserDetailsService</code> 的核心作用就是：</p>
<pre><code class="text language-text">把自己的 UserEntity

转换成

Spring Security 能识别的 UserDetails
</code></pre>
<hr />
<h2 id="八、loaduserbyusername-的参数从哪里来">八、loadUserByUsername 的参数从哪里来</h2>
<p>假设登录时创建：</p>
<pre><code class="java language-java">UsernamePasswordAuthenticationToken.unauthenticated(
        username,
        password
);
</code></pre>
<p>其中：</p>
<pre><code class="text language-text">principal = username
credentials = password
</code></pre>
<p><code>DaoAuthenticationProvider</code> 会从 Token 中获取账号：</p>
<pre><code class="java language-java">String username = authentication.getName();
</code></pre>
<p>然后调用：</p>
<pre><code class="java language-java">userDetailsService.loadUserByUsername(username);
</code></pre>
<p>因此参数传递过程是：</p>
<pre><code class="text language-text">前端提交 username
    ↓
LoginRequest.username
    ↓
UsernamePasswordAuthenticationToken.principal
    ↓
DaoAuthenticationProvider
    ↓
loadUserByUsername(username)
</code></pre>
<p>这个参数不是 Spring Security 凭空产生的，而是我们创建 Token 时传进去的。</p>
<hr />
<h2 id="九、密码是怎么校验的">九、密码是怎么校验的</h2>
<p><code>UserDetailsService</code> 只负责查询用户，不负责手动比较密码。</p>
<p>数据库中保存的应该是 BCrypt 密文，例如：</p>
<pre><code class="text language-text">$2a$10$...
</code></pre>
<p>返回 <code>UserDetails</code> 时，直接放数据库中的密文：</p>
<pre><code class="java language-java">return User.withUsername(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .build();
</code></pre>
<p>不要再次执行：</p>
<pre><code class="java language-java">passwordEncoder.encode(user.getPassword())
</code></pre>
<p>否则相当于把数据库中的 BCrypt 密文再次加密，密码必然校验失败。</p>
<p>真正的密码比较由 <code>DaoAuthenticationProvider</code> 调用：</p>
<pre><code class="java language-java">passwordEncoder.matches(
        用户输入的明文密码,
        数据库中的加密密码
);
</code></pre>
<p>参数顺序一定是：</p>
<pre><code class="text language-text">matches(明文, 密文)
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">passwordEncoder.matches(
        "123456",
        "$2a$10$..."
);
</code></pre>
<hr />
<h2 id="十、认证成功后返回了什么">十、认证成功后返回了什么</h2>
<p>认证前创建的 Token 通常是：</p>
<pre><code class="text language-text">principal = username
credentials = password
authenticated = false
</code></pre>
<p>认证成功后，Provider 会返回一个新的 <code>Authentication</code>：</p>
<pre><code class="text language-text">principal = UserDetails
credentials = null
authorities = 当前用户权限
authenticated = true
</code></pre>
<p>也就是说，认证成功后的 <code>Authentication</code> 不再只是保存用户名和密码，而是保存完整的登录用户信息。</p>
<p>可以通过：</p>
<pre><code class="java language-java">authentication.getPrincipal();
authentication.getName();
authentication.getAuthorities();
</code></pre>
<p>获取：</p>
<ul>
<li>当前用户</li>
<li>用户名</li>
<li>角色和权限</li>
</ul>
<hr />
<h2 id="十一、securitycontextholder-的作用">十一、SecurityContextHolder 的作用</h2>
<p>这里的本质还是ThreadLocal 存储的</p>
<p>认证成功后，需要让后面的代码知道当前用户是谁。</p>
<p>Spring Security 会把认证信息保存到：</p>
<pre><code class="java language-java">SecurityContextHolder
</code></pre>
<p>结构大致是：</p>
<pre><code class="text language-text">SecurityContextHolder
    ↓
SecurityContext
    ↓
Authentication
    ↓
principal + authorities
</code></pre>
<p>后续代码就可以获取当前用户：</p>
<pre><code class="java language-java">Authentication authentication =
        SecurityContextHolder.getContext()
                .getAuthentication();
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">String username = authentication.getName();
</code></pre>
<p>在实际业务中，通常还会自定义一个 <code>LoginUser</code>，里面保存：</p>
<pre><code class="text language-text">userId
tenantId
username
部门
角色
权限
</code></pre>
<p>然后通过：</p>
<pre><code class="java language-java">LoginUser loginUser =
        (LoginUser) authentication.getPrincipal();
</code></pre>
<p>获取当前用户的业务信息。</p>
<hr />
<h2 id="十二、多种登录方式怎么扩展">十二、多种登录方式怎么扩展</h2>
<p>假设系统同时支持：</p>
<pre><code class="text language-text">用户名 + 密码

手机号 + 验证码
</code></pre>
<p>一般使用同一个：</p>
<pre><code class="text language-text">AuthenticationManager
</code></pre>
<p>但是使用不同的 Token 和 Provider。</p>
<p>账号密码登录：</p>
<pre><code class="java language-java">authenticationManager.authenticate(
        UsernamePasswordAuthenticationToken.unauthenticated(
                username,
                password
        )
);
</code></pre>
<p>对应：</p>
<pre><code class="text language-text">DaoAuthenticationProvider
</code></pre>
<p>手机号验证码登录：</p>
<pre><code class="java language-java">authenticationManager.authenticate(
        new SmsAuthenticationToken(
                mobile,
                code
        )
);
</code></pre>
<p>对应：</p>
<pre><code class="text language-text">SmsAuthenticationProvider
</code></pre>
<p>所以不是创建两个 <code>AuthenticationManager</code>，而是：</p>
<pre><code class="text language-text">一个 AuthenticationManager
多个 AuthenticationProvider
</code></pre>
<p>可以用下面三个概念记忆：</p>
<pre><code class="text language-text">Token：拿什么材料登录

Provider：怎么验证这些材料

AuthenticationManager：把材料交给对应的 Provider
</code></pre>
<hr />
<h2 id="十三、rbac">十三、RBAC</h2>
<p>认证解决用户是谁，RBAC 解决用户有什么权限。</p>
<p>RBAC 的核心关系是：</p>
<pre><code class="text language-text">用户
  ↓
角色
  ↓
权限
</code></pre>
<p>例如：</p>
<pre><code class="text language-text">张三
  ↓
管理员
  ↓
新增用户
删除用户
查看菜单
</code></pre>
<p>企业项目中常见的表其实是五张：</p>
<pre><code class="text language-text">sys_user

sys_role

sys_permission

sys_user_role

sys_role_permission
</code></pre>
<p>其中：</p>
<pre><code class="text language-text">sys_user_role
</code></pre>
<p>表示用户拥有哪些角色。</p>
<pre><code class="text language-text">sys_role_permission
</code></pre>
<p>表示角色拥有哪些权限。</p>
<hr />
<h2 id="十四、权限表里存什么">十四、权限表里存什么</h2>
<p>权限表中通常会保存权限标识，例如：</p>
<pre><code class="text language-text">sys:user:list
sys:user:add
sys:user:update
sys:user:delete
sys:menu:list
</code></pre>
<p>这里保存的不是接口地址，而是业务权限编码。</p>
<p>例如：</p>
<pre><code class="text language-text">sys:user:add
</code></pre>
<p>表示：</p>
<blockquote>
  <p>系统管理模块下，新增用户的权限。</p>
</blockquote>
<p>登录时，根据用户 ID 查询角色和权限：</p>
<pre><code class="text language-text">user
  ↓
user_role
  ↓
role
  ↓
role_permission
  ↓
permission
</code></pre>
<p>最后得到：</p>
<pre><code class="text language-text">ROLE_ADMIN
sys:user:list
sys:user:add
sys:user:delete
sys:menu:list
</code></pre>
<hr />
<h2 id="十五、角色和权限怎么放进-authorities">十五、角色和权限怎么放进 authorities</h2>
<p>Spring Security 最终并不关心我们的数据库表。</p>
<p>它只关心：</p>
<pre><code class="java language-java">Collection&lt;? extends GrantedAuthority&gt;
</code></pre>
<p>一般会把角色和权限转换成：</p>
<pre><code class="java language-java">SimpleGrantedAuthority
</code></pre>
<p>例如：</p>
<pre><code class="java language-java">List&lt;GrantedAuthority&gt; authorities =
        new ArrayList&lt;&gt;();

authorities.add(
        new SimpleGrantedAuthority("ROLE_ADMIN")
);

authorities.add(
        new SimpleGrantedAuthority("sys:user:add")
);

authorities.add(
        new SimpleGrantedAuthority("sys:user:delete")
);
</code></pre>
<p>然后放进 <code>UserDetails</code>：</p>
<pre><code class="java language-java">return User.withUsername(user.getUsername())
        .password(user.getPassword())
        .authorities(authorities)
        .build();
</code></pre>
<p>最终 <code>authorities</code> 中保存的其实都是字符串：</p>
<pre><code class="text language-text">ROLE_ADMIN
sys:user:add
sys:user:delete
sys:menu:list
</code></pre>
<hr />
<h2 id="十六、security-怎么区分角色和权限">十六、Security 怎么区分角色和权限</h2>
<p>从对象类型上来说，角色和权限没有区别。</p>
<p>它们都是：</p>
<pre><code class="java language-java">SimpleGrantedAuthority
</code></pre>
<p>Spring Security 主要通过字符串前缀和判断方法区分。</p>
<p>角色通常约定以：</p>
<pre><code class="text language-text">ROLE_
</code></pre>
<p>开头。</p>
<p>例如：</p>
<pre><code class="text language-text">ROLE_ADMIN
ROLE_MANAGER
</code></pre>
<p>普通权限则使用业务编码：</p>
<pre><code class="text language-text">sys:user:add
sys:user:delete
</code></pre>
<p>当写：</p>
<pre><code class="java language-java">@PreAuthorize("hasRole('ADMIN')")
</code></pre>
<p>Spring Security 会自动加上 <code>ROLE_</code> 前缀，实际检查的是：</p>
<pre><code class="text language-text">ROLE_ADMIN
</code></pre>
<p>所以：</p>
<pre><code class="java language-java">hasRole("ADMIN")
</code></pre>
<p>大致等价于：</p>
<pre><code class="java language-java">hasAuthority("ROLE_ADMIN")
</code></pre>
<p>而权限判断：</p>
<pre><code class="java language-java">@PreAuthorize("hasAuthority('sys:user:add')")
</code></pre>
<p>会直接检查：</p>
<pre><code class="text language-text">sys:user:add
</code></pre>
<p>因此可以理解为：</p>
<blockquote>
  <p>角色本质上也是一种 Authority，只不过角色按照约定使用了 <code>ROLE_</code> 前缀。</p>
</blockquote>
<hr />
<h2 id="十七、接口权限怎么控制">十七、接口权限怎么控制</h2>
<p>开启方法权限后：</p>
<pre><code class="java language-java">@EnableMethodSecurity
</code></pre>
<p>可以直接在业务方法上使用：</p>
<pre><code class="java language-java">@PreAuthorize("hasAuthority('sys:user:add')")
@PostMapping("/users")
public void addUser() {
}
</code></pre>
<p>Spring Security 会检查当前用户的 <code>authorities</code> 中是否存在：</p>
<pre><code class="text language-text">sys:user:add
</code></pre>
<p>存在就允许执行，不存在就返回 <code>403 Forbidden</code>。</p>
<p>也可以同时判断多个权限：</p>
<pre><code class="java language-java">@PreAuthorize("""
    hasAuthority('sys:user:add')
    or hasRole('ADMIN')
""")
</code></pre>
<p>企业业务接口通常更适合按具体权限控制：</p>
<pre><code class="java language-java">hasAuthority("sys:user:add")
</code></pre>
<p>因为角色比较粗。</p>
<p>同样是管理员，不同管理员拥有的操作权限也可能不同。</p>
<hr />
<h2 id="十八、jwt-在整个流程中的位置">十八、JWT 在整个流程中的位置</h2>
<p>用户名密码只在登录时校验一次。</p>
<p>认证成功后，系统签发 JWT：</p>
<pre><code class="text language-text">登录成功
    ↓
生成 JWT
    ↓
返回前端
</code></pre>
<p>后续请求携带：</p>
<pre><code class="http language-http">Authorization: Bearer xxx
</code></pre>
<p>JWT 过滤器负责：</p>
<pre><code class="text language-text">读取请求头
    ↓
解析 JWT
    ↓
获取 userId 或 username
    ↓
恢复 LoginUser 和 authorities
    ↓
创建已认证 Authentication
    ↓
放入 SecurityContextHolder
</code></pre>
<p>因此，后续请求不需要再次提交密码。</p>
<p>不是因为 Spring Security 永远记住了密码，而是因为：</p>
<blockquote>
  <p>JWT 成为了后续请求的身份凭证。</p>
</blockquote>
<p>JWT 中一般不建议存放大量权限，常见做法是只保存：</p>
<pre><code class="text language-text">userId
username
tokenId
</code></pre>
<p>然后根据 userId 从 Redis 或数据库中恢复完整的 <code>LoginUser</code> 和权限列表。</p>
<hr />
<h2 id="十九、总结">十九、总结</h2>
<p>Spring Security 的核心并不是大量配置，而是下面这条认证链：</p>
<pre><code class="text language-text">前端提交登录信息
    ↓
创建 AuthenticationToken
    ↓
AuthenticationManager.authenticate()
    ↓
ProviderManager 遍历 Provider
    ↓
supports() 找到合适的 Provider
    ↓
Provider 执行认证
    ↓
查询用户或校验验证码
    ↓
返回认证成功的 Authentication
    ↓
保存到 SecurityContextHolder
</code></pre>
<p>授权链则是：</p>
<pre><code class="text language-text">查询用户角色和权限
    ↓
转换成 GrantedAuthority
    ↓
放入 UserDetails / LoginUser
    ↓
放入 Authentication
    ↓
保存到 SecurityContextHolder
    ↓
@PreAuthorize 检查权限
</code></pre>
<p>多种登录方式也没有想象中那么复杂：</p>
<pre><code class="text language-text">用户名密码
    ↓
UsernamePasswordAuthenticationToken
    ↓
DaoAuthenticationProvider
</code></pre>
<pre><code class="text language-text">手机号验证码
    ↓
SmsAuthenticationToken
    ↓
SmsAuthenticationProvider
</code></pre>
<pre><code class="text language-text">Token 表示登录方式

Provider 负责具体校验

AuthenticationManager 负责统一调度
</code></pre>
<hr />
<h2 id="二十、最后总结">二十、最后总结</h2>
<p>学习 Spring Security 时，最容易陷入两个误区。</p>
<p>第一个误区是，一开始就背配置。</p>
<p>实际上，配置只有在理解认证流程后才有意义。</p>
<p>第二个误区是，把 JWT、RBAC、短信登录和 Spring Security 混成一件事。</p>
<p>它们各自负责不同的问题：</p>
<pre><code class="text language-text">Spring Security：提供认证授权框架

JWT：保存和传递登录凭证

RBAC：管理用户、角色和权限关系

Redis：缓存登录用户、Token 状态和权限信息
</code></pre>
<p>最后，我把目前的理解压缩成几句话：</p>
<blockquote>
  <p><code>AuthenticationToken</code> 表示这次使用什么方式登录。</p>
  <p><code>AuthenticationProvider</code> 负责验证这种登录方式。</p>
  <p><code>AuthenticationManager</code> 根据 Token 找到对应的 Provider。</p>
  <p><code>UserDetailsService</code> 把自己的用户数据转换成 Spring Security 能识别的用户。</p>
  <p><code>GrantedAuthority</code> 保存当前用户拥有的角色和权限。</p>
  <p><code>SecurityContextHolder</code> 保存当前请求中的登录身份。</p>
  <p>JWT 让后续请求不需要再次提交用户名和密码。</p>
  <p>RBAC 决定当前用户最终拥有哪一些 Authority。</p>
</blockquote>
<p>理解了这些概念以后，再去看 Spring Security 的配置、过滤器和企业项目源码，就不会再觉得每个类都是突然冒出来的了。</p>]]></description>
      <link>http://43.128.224.135:3003/article/f9b3c89f-f5f5-4e97-a055-93c73d436c53</link>
      <guid>http://43.128.224.135:3003/article/f9b3c89f-f5f5-4e97-a055-93c73d436c53</guid>
      <pubDate>Thu, 30 Jul 2026 08:53:34 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>Java 开发反思：从能写业务，到写好业务</title>
      <description><![CDATA[<p>现在大多数需求，我都能够独立完成，也能够把业务流程转化成代码。但回头看自己写的代码，真正需要提升的已经不是语法，而是工程能力。</p>
<p>我把目前最需要训练的内容总结成两个阶段，以后每写完一个功能，都回来对照检查，不断补充。</p>
<hr />
<h1 id="第三阶段：把业务拆成清晰模块">第三阶段：把业务拆成清晰模块</h1>
<p>这一阶段关注的是<strong>代码结构</strong>。</p>
<p>目标不是把代码写短，而是让每个模块职责单一、容易理解、方便复用。</p>
<h2 id="我会检查下面几个问题">我会检查下面几个问题</h2>
<h3 id="①-有没有重复逻辑可以封装？">① 有没有重复逻辑可以封装？</h3>
<p>例如：</p>
<ul>
<li>Redis Key 生成</li>
<li>requestHash 生成</li>
<li>参数校验</li>
<li>查询条件构建</li>
<li>VO 转换</li>
<li>通用工具类</li>
</ul>
<p>如果一个逻辑以后还会出现第二次，就应该考虑封装，而不是复制。</p>
<hr />
<h3 id="②-有没有魔法字符串、魔法数字？">② 有没有魔法字符串、魔法数字？</h3>
<p>例如：</p>
<pre><code class="java language-java">"price-list"
"tenant_id"
1
20
</code></pre>
<p>优先考虑：</p>
<ul>
<li>常量</li>
<li>枚举</li>
<li>LambdaQueryWrapper</li>
<li>配置项</li>
</ul>
<p>减少硬编码。</p>
<hr />
<h3 id="③-方法职责是否单一？">③ 方法职责是否单一？</h3>
<p>一个方法最好只完成一件事情。</p>
<p>例如：</p>
<pre><code class="text language-text">生成请求指纹
检查额度
查询缓存
查询数据库
记录日志
</code></pre>
<p>如果全部堆在一个方法里，以后修改成本会越来越高。</p>
<hr />
<h3 id="④-命名是否能够表达业务？">④ 命名是否能够表达业务？</h3>
<p>看到变量名，就应该知道它代表什么。</p>
<p>例如：</p>
<p>不要：</p>
<pre><code class="java language-java">boolean b;
Object obj;
Map map;
</code></pre>
<p>尽量：</p>
<pre><code class="java language-java">boolean saved;
UserApiQuota quota;
QueryApi apiConfig;
</code></pre>
<p>命名就是代码最重要的注释。</p>
<hr />
<h3 id="⑤-注释有没有解释为什么？">⑤ 注释有没有解释"为什么"？</h3>
<p>不要写：</p>
<pre><code class="java language-java">// 获取今天日期
</code></pre>
<p>因为代码已经说明了。</p>
<p>应该写：</p>
<pre><code class="java language-java">// 每天使用不同 Redis Key，使第二天重新计算额度
</code></pre>
<p>注释应该解释设计原因，而不是翻译代码。</p>
<hr />
<h1 id="第四阶段：考虑并发、事务、异常、可维护性">第四阶段：考虑并发、事务、异常、可维护性</h1>
<p>这一阶段关注的是<strong>代码是否可靠</strong>。</p>
<p>很多 Bug，不是因为业务不会写，而是边界没有考虑完整。</p>
<h2 id="我会检查下面几个问题-1">我会检查下面几个问题</h2>
<h3 id="①-有没有异常情况没有考虑？">① 有没有异常情况没有考虑？</h3>
<p>例如：</p>
<ul>
<li>Redis 查询失败</li>
<li>数据库更新失败</li>
<li>参数为空</li>
<li>配置不存在</li>
<li>第三方接口异常</li>
</ul>
<p>不要只写正常流程。</p>
<hr />
<h3 id="②-数据会不会出现不一致？">② 数据会不会出现不一致？</h3>
<p>例如：</p>
<pre><code class="text language-text">数据库成功
Redis失败
</code></pre>
<p>或者：</p>
<pre><code class="text language-text">Redis成功
数据库失败
</code></pre>
<p>写涉及多个系统的数据时，要思考：</p>
<blockquote>
  <p>哪一步失败以后，系统还能不能保持正确状态？</p>
</blockquote>
<hr />
<h3 id="③-有没有事务边界？">③ 有没有事务边界？</h3>
<p>我要先问自己：</p>
<blockquote>
  <p>哪一步开始算真正成功？</p>
</blockquote>
<p>例如：</p>
<ul>
<li>请求进入接口就算一次？</li>
<li>查询成功才算一次？</li>
<li>返回成功才算一次？</li>
</ul>
<p>业务规则确定以后，再决定事务边界。</p>
<hr />
<h3 id="④-有没有并发问题？">④ 有没有并发问题？</h3>
<p>凡是出现：</p>
<pre><code class="text language-text">先查
再判断
再修改
</code></pre>
<p>都要问自己：</p>
<blockquote>
  <p>两个人同时执行，会不会出问题？</p>
</blockquote>
<p>能用数据库条件更新解决的，就不要拆成多步。</p>
<hr />
<h3 id="⑤-有没有资源没有释放？">⑤ 有没有资源没有释放？</h3>
<p>例如：</p>
<ul>
<li>Redis Key 有没有过期时间？</li>
<li>临时文件有没有删除？</li>
<li>线程有没有关闭？</li>
<li>数据库连接有没有释放？</li>
</ul>
<p>资源管理也是可靠性的一部分。</p>
<hr />
<h3 id="⑥-有没有考虑以后维护？">⑥ 有没有考虑以后维护？</h3>
<p>每写完一段代码，我都会问自己：</p>
<p>如果三个月后回来修改：</p>
<ul>
<li>我还能不能快速看懂？</li>
<li>别人能不能快速接手？</li>
<li>修改一个地方，会不会影响很多地方？</li>
</ul>
<p>如果答案是否定的，就说明还可以继续优化。</p>
<hr />
<h1 id="我的代码检查清单">我的代码检查清单</h1>
<p>以后每完成一个功能，我都会快速检查下面这些问题。</p>
<h2 id="结构">结构</h2>
<ul>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 有没有重复代码可以封装？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 方法职责是否单一？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 有没有魔法字符串？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 命名是否准确？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 注释有没有解释原因？</li>
</ul>
<h2 id="可靠性">可靠性</h2>
<ul>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 是否考虑了异常情况？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 是否存在数据不一致？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 是否需要事务？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 是否存在并发问题？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 临时资源是否释放？</li>
<li class="task-list-item" style="list-style-type: none;"><input type="checkbox" disabled style="margin: 0px 0.35em 0.25em -1.6em; vertical-align: middle;"> 三个月后的我还能快速看懂吗？</li>
</ul>
<hr />
<h1 id="我希望自己达到的目标">我希望自己达到的目标</h1>
<p>以前，我写代码时更多关注的是：</p>
<blockquote>
  <p><strong>这个需求能不能实现？</strong></p>
</blockquote>
<p>以后，我希望自己更多关注：</p>
<blockquote>
  <p><strong>这段代码是否足够稳定、清晰、容易维护？</strong></p>
</blockquote>
<p>真正成熟的 Java 代码，不只是能运行。</p>
<p>它还应该做到：</p>
<ul>
<li>职责清晰</li>
<li>结构简单</li>
<li>命名准确</li>
<li>边界明确</li>
<li>异常完整</li>
<li>状态一致</li>
<li>易于扩展</li>
<li>易于维护</li>
</ul>
<p>这份清单会持续更新，每发现一个新的问题，就补充进来，让它逐渐成为自己的开发规范。</p>]]></description>
      <link>http://43.128.224.135:3003/article/a8971239-3086-4d25-b94e-1642cd9098c3</link>
      <guid>http://43.128.224.135:3003/article/a8971239-3086-4d25-b94e-1642cd9098c3</guid>
      <pubDate>Tue, 21 Jul 2026 03:36:18 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>TypeScript、Java、PHP、Python、Go 中定义枚举-存档记忆</title>
      <description><![CDATA[<p>在不同编程语言中，枚举的实现方式略有不同。下面以 <code>ModelType</code> 为例，定义两个模型类型：</p>
<ul>
<li><code>MOBILE</code>：移动端轻量模型</li>
<li><code>SERVER</code>：服务端模型</li>
</ul>
<p>对应的字符串值分别为：</p>
<pre><code class="text language-text">mobile
server
</code></pre>
<h2 id="1-typescript">1. TypeScript</h2>
<p>TypeScript 支持字符串枚举，可以直接为枚举成员指定字符串值。</p>
<pre><code class="typescript language-typescript">export enum ModelType {
  MOBILE = "mobile",
  SERVER = "server",
}
</code></pre>
<p>使用方式：</p>
<pre><code class="typescript language-typescript">const modelType: ModelType = ModelType.MOBILE;

console.log(modelType);
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">mobile
</code></pre>
<hr />
<h2 id="2-java">2. Java</h2>
<p>Java 枚举成员本身默认是对象。如果需要为枚举绑定自定义字符串值，可以添加 <code>value</code> 字段、构造方法和 <code>getValue()</code> 方法。</p>
<pre><code class="java language-java">public enum ModelType {

    MOBILE("mobile"),
    SERVER("server");

    private final String value;

    ModelType(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}
</code></pre>
<p>使用方式：</p>
<pre><code class="java language-java">ModelType modelType = ModelType.MOBILE;

System.out.println(modelType);
System.out.println(modelType.getValue());
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">MOBILE
mobile
</code></pre>
<p>其中：</p>
<pre><code class="java language-java">modelType.name()
</code></pre>
<p>返回枚举名称：</p>
<pre><code class="text language-text">MOBILE
</code></pre>
<p>而：</p>
<pre><code class="java language-java">modelType.getValue()
</code></pre>
<p>返回对应的字符串值：</p>
<pre><code class="text language-text">mobile
</code></pre>
<hr />
<h2 id="3-php">3. PHP</h2>
<p>PHP 从 8.1 版本开始支持原生枚举。</p>
<pre><code class="php language-php">&lt;?php

enum ModelType: string
{
    case MOBILE = 'mobile';
    case SERVER = 'server';
}
</code></pre>
<p>使用方式：</p>
<pre><code class="php language-php">$modelType = ModelType::MOBILE;

echo $modelType-&gt;name;
echo $modelType-&gt;value;
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">MOBILE
mobile
</code></pre>
<p>根据字符串获取枚举：</p>
<pre><code class="php language-php">$modelType = ModelType::from('mobile');
</code></pre>
<p>如果字符串可能不存在，可以使用 <code>tryFrom()</code>：</p>
<pre><code class="php language-php">$modelType = ModelType::tryFrom('unknown');

if ($modelType === null) {
    echo '不支持的模型类型';
}
</code></pre>
<hr />
<h2 id="4-python">4. Python</h2>
<p>Python 可以使用标准库中的 <code>Enum</code> 定义枚举。</p>
<pre><code class="python language-python">from enum import Enum


class ModelType(Enum):
    MOBILE = "mobile"
    SERVER = "server"
</code></pre>
<p>使用方式：</p>
<pre><code class="python language-python">model_type = ModelType.MOBILE

print(model_type)
print(model_type.name)
print(model_type.value)
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">ModelType.MOBILE
MOBILE
mobile
</code></pre>
<p>根据字符串获取枚举：</p>
<pre><code class="python language-python">model_type = ModelType("mobile")

print(model_type)
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">ModelType.MOBILE
</code></pre>
<hr />
<h2 id="5-go">5. Go</h2>
<p>Go 没有传统意义上的原生枚举，通常使用“自定义类型 + 常量”实现类似枚举的效果。</p>
<pre><code class="go language-go">package model

type ModelType string

const (
    ModelTypeMobile ModelType = "mobile"
    ModelTypeServer ModelType = "server"
)
</code></pre>
<p>使用方式：</p>
<pre><code class="go language-go">package main

import (
    "fmt"
)

func main() {
    var modelType ModelType = ModelTypeMobile

    fmt.Println(modelType)
    fmt.Println(string(modelType))
}
</code></pre>
<p>输出：</p>
<pre><code class="text language-text">mobile
mobile
</code></pre>
<p>为了防止传入不支持的字符串，可以添加校验方法：</p>
<pre><code class="go language-go">func (m ModelType) IsValid() bool {
    switch m {
    case ModelTypeMobile, ModelTypeServer:
        return true
    default:
        return false
    }
}
</code></pre>
<p>使用方式：</p>
<pre><code class="go language-go">modelType := ModelType("unknown")

if !modelType.IsValid() {
    fmt.Println("不支持的模型类型")
}
</code></pre>
<h2 id="总结">总结</h2>
<p>不同语言中 <code>ModelType</code> 枚举的实现方式如下：</p>
<table>
<thead>
<tr>
<th>语言</th>
<th>实现方式</th>
<th>获取字符串值</th>
</tr>
</thead>
<tbody>
<tr>
<td>TypeScript</td>
<td>字符串枚举</td>
<td><code>ModelType.MOBILE</code></td>
</tr>
<tr>
<td>Java</td>
<td>枚举加自定义字段</td>
<td><code>ModelType.MOBILE.getValue()</code></td>
</tr>
<tr>
<td>PHP</td>
<td>字符串枚举</td>
<td><code>ModelType::MOBILE-&gt;value</code></td>
</tr>
<tr>
<td>Python</td>
<td><code>Enum</code> 类</td>
<td><code>ModelType.MOBILE.value</code></td>
</tr>
<tr>
<td>Go</td>
<td>自定义类型加常量</td>
<td><code>string(ModelTypeMobile)</code></td>
</tr>
</tbody>
</table>
<p>虽然不同语言的语法不同，但最终目的都是相同的：使用有限、明确的常量表示模型类型，避免在业务代码中直接使用容易写错的字符串。</p>]]></description>
      <link>http://43.128.224.135:3003/article/1d165300-89b1-45b8-bdef-db3c4dddde7f</link>
      <guid>http://43.128.224.135:3003/article/1d165300-89b1-45b8-bdef-db3c4dddde7f</guid>
      <pubDate>Tue, 14 Jul 2026 06:48:54 GMT</pubDate>
      <category>其他</category>
    </item>
    <item>
      <title>Java中的线程基础</title>
      <description><![CDATA[<h2 id="start真正启动一个新线程：">start()真正启动一个新线程：</h2>
<pre><code class="java language-java">public static void main(String[] args) throws InterruptedException {

        long startTime = System.currentTimeMillis();
        System.out.println("主线程开始：" + Thread.currentThread().getName());

        Thread thread1 = new Thread(() -&gt; {
            System.out.println("线程1开始：" + Thread.currentThread().getName());
            try {
                addWatermark(Thread.currentThread().getName());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            System.out.println("线程1结束：" + Thread.currentThread().getName());
        }, "pdf-thread-1");

        Thread thread2 = new Thread(() -&gt; {
            System.out.println("线程2开始：" + Thread.currentThread().getName());

            try {
                addWatermark(Thread.currentThread().getName());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            System.out.println("线程2结束：" + Thread.currentThread().getName());
        }, "pdf-thread-2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        long endTime = System.currentTimeMillis();

        System.out.println("所有 PDF 水印处理完成：" + Thread.currentThread().getName());
        System.out.println("程序总耗时：" + (endTime - startTime) + " 毫秒");
    }
</code></pre>
<p>join() 的作用是：让当前线程等待另一个线程执行完，也就是main必须等待线程1和线程2都执行完才能继续执行</p>
<p>控制台输出:</p>
<pre><code class="js language-js">主线程开始：main
线程1开始：pdf-thread-1
线程2开始：pdf-thread-2
线程2结束：pdf-thread-2
线程1结束：pdf-thread-1
所有 PDF 水印处理完成：main
程序总耗时：3193 毫秒
</code></pre>
<h2 id="run：只是普通方法调用，不会启动新线程">run()：只是普通方法调用，不会启动新线程</h2>
<p>将.start()改成.run()后控制台输出：</p>
<pre><code class="js language-js">主线程开始：main
线程1开始：main
线程1结束：main
线程2开始：main
线程2结束：main
所有 PDF 水印处理完成：main
程序总耗时：7159 毫秒
</code></pre>
<h1 id="耗时翻了一倍">耗时翻了一倍</h1>]]></description>
      <link>http://43.128.224.135:3003/article/1dab26b7-07d2-43bc-af58-552f16fc368c</link>
      <guid>http://43.128.224.135:3003/article/1dab26b7-07d2-43bc-af58-552f16fc368c</guid>
      <pubDate>Wed, 01 Jul 2026 08:11:40 GMT</pubDate>
      
    </item>
    <item>
      <title>Java执行Python节点的胶水代码</title>
      <description><![CDATA[<p>最近项目里需要在 Java 中调用 Python 脚本，最初的实现其实很简单,果然欠下的债迟早要还的，也许哪一天stdin的债也要还吧。</p>
<p>Java 通过 <code>ProcessBuilder</code> 启动 Python，把参数通过 stdin 传过去，再从 stdout 中读取 Python 的输出结果。</p>
<p>刚开始觉得这个方案挺方便，写起来也不复杂，功能很快就跑通了。</p>
<p>不过随着功能越来越多，Python 脚本开始负责爬虫、AI 调用、文档解析等工作，输出的数据越来越大，问题也慢慢暴露出来了。</p>
<p>这次就记录一下整个优化过程。</p>
<hr />
<h1 id="最初的实现">最初的实现</h1>
<p>整体流程大概是这样的：</p>
<pre><code class="text language-text">Java
    │
    ├── ProcessBuilder 启动 Python
    │
    ├── stdin 写入参数(JSON)
    │
    ├── Python 执行
    │
    └── stdout 输出结果
              │
              ▼
          Java 读取结果
</code></pre>
<p>Python 端约定输出：</p>
<pre><code class="python language-python">print("python_result:" + result)
</code></pre>
<p>Java 再去解析：</p>
<pre><code class="text language-text">python_result:xxxxx
</code></pre>
<p>这种方式最大的优点就是简单。</p>
<p>不需要 Socket；<br />
不需要 HTTP；<br />
也不用 MQ。</p>
<p>对于很多内部工具来说已经足够了。</p>
<hr />
<h1 id="第一个问题：日志越来越大">第一个问题：日志越来越大</h1>
<p>最开始为了方便排查问题，我直接打印了 Python 的输出：</p>
<pre><code class="java language-java">logger.info("Python输出：{}", line);
</code></pre>
<p>后来 Python 开始返回 AI 生成的 Markdown、HTML、JSON。</p>
<p>有时候一份结果就是几百万字符。</p>
<p>这时候日志开始出现各种问题：</p>
<ul>
<li>IDEA 控制台越来越卡；</li>
<li>Web 页面日志加载越来越慢；</li>
<li>日志文件迅速膨胀；</li>
<li>有时候看起来像数据被截断，其实只是日志显示不完整。</li>
</ul>
<p><strong>日志不是用来存放业务数据的，只有一天的时间，你不用stdin谁用呢</strong></p>
<p>日志真正应该记录的是：</p>
<ul>
<li>是否执行成功；</li>
<li>数据有多大；</li>
<li>出错位置；</li>
<li>必要的上下文。</li>
</ul>
<p>于是把日志统一改成了：</p>
<pre><code class="java language-java">logger.info(
    "Python输出长度：{}，前100字符：{}，后100字符：{}",
    line.length(),
    line.substring(0, Math.min(100, line.length())),
    line.substring(Math.max(0, line.length() - 100))
);
</code></pre>
<p>这样定位问题基本已经够用了。</p>
<hr />
<h1 id="第二个问题：python-卡死">第二个问题：Python 卡死</h1>
<p>一开始我是这样写的：</p>
<pre><code class="java language-java">process.waitFor();
</code></pre>
<p>看起来没什么问题。</p>
<p>但是后来 Selenium 偶尔会卡住。</p>
<p>Python 不退出。</p>
<p>Java 就一直等。</p>
<p>整个线程就挂在那里。</p>
<p>如果这是一个 Web 请求，那么用户页面就会一直转圈。</p>
<p>后来改成了带超时的等待：</p>
<pre><code class="java language-java">boolean finished = process.waitFor(10, TimeUnit.MINUTES);
</code></pre>
<p>如果超过十分钟：</p>
<ul>
<li>强制结束 Python；</li>
<li>返回失败；</li>
<li>释放资源。</li>
</ul>
<p>这样至少不会把线程一直占着。</p>
<hr />
<h1 id="第三个问题：多人同时执行">第三个问题：多人同时执行</h1>
<p>刚开始这个系统只有自己用。</p>
<p>后来同事也开始使用。</p>
<p>这时候就出现了一个问题：</p>
<p>假设三个人同时点击执行。</p>
<p>后台就会同时启动三个 Python。</p>
<p>如果 Python 又启动 Selenium，那么后台可能瞬间出现：</p>
<pre><code class="text language-text">Chrome
Chrome
Chrome
Chrome
Chrome
</code></pre>
<p>CPU 飙高。</p>
<p>内存一路上涨。</p>
<p>后来我加了一个很简单的控制：</p>
<pre><code class="java language-java">private static final Semaphore PYTHON_SEMAPHORE =
        new Semaphore(1);
</code></pre>
<p>执行之前：</p>
<pre><code class="java language-java">PYTHON_SEMAPHORE.acquire();
</code></pre>
<p>执行结束：</p>
<pre><code class="java language-java">PYTHON_SEMAPHORE.release();
</code></pre>
<p>这样就保证：</p>
<p>同一时刻只允许一个 Python 任务执行。</p>
<p>虽然吞吐量下降了一点，但系统稳定了很多。</p>
<p>如果以后服务器性能足够，只需要把：</p>
<pre><code class="java language-java">new Semaphore(1)
</code></pre>
<p>改成：</p>
<pre><code class="java language-java">new Semaphore(3)
</code></pre>
<p>就能支持三个任务并发。</p>
<hr />
<h1 id="第四个问题：ai-返回-markdown">第四个问题：AI 返回 Markdown</h1>
<p>后来接入大模型以后。</p>
<p>很多时候返回的是：</p>
<pre><code class="text language-text">```json
{
    "name":"张三"
}
```
</code></pre>
<p>而不是纯 JSON。</p>
<p>直接解析：</p>
<pre><code class="java language-java">JSON.parse(...)
</code></pre>
<p>自然就失败了。</p>
<p>后来统一做了一层处理：</p>
<p>去掉：</p>
<pre><code class="text language-text">```json
</code></pre>
<p>以及最后的：</p>
<pre><code class="text language-text">```
</code></pre>
<p>`</p>
<p>这样后面的代码就不用关心 AI 返回的是：</p>
<ul>
<li>JSON</li>
<li>Markdown</li>
<li>还是代码块。</li>
</ul>
<p>统一处理即可。</p>
<hr />
<h1 id="第五个问题：异常日志太大">第五个问题：异常日志太大</h1>
<p>以前异常直接这样写：</p>
<pre><code class="java language-java">throw new RuntimeException(
    "执行失败：" + output
);
</code></pre>
<p>`</p>
<p>如果 output 有几百万字符。</p>
<p>一次异常日志就是几 MB。</p>
<p>后来统一改成：</p>
<pre><code class="text language-text">输出长度
输出前500字符
</code></pre>
<p>真正的大数据不要放进异常。</p>
<p>否则排查问题的时候，日志本身反而成了新的问题。</p>
<hr />
<h1 id="我最终保留了哪些设计">我最终保留了哪些设计</h1>
<p>经过几轮修改之后，最终保留下来的东西其实并不多。</p>
<p>主要有下面几个。</p>
<h2 id="1-stdin-传参数">1. stdin 传参数</h2>
<p>所有参数统一 JSON。</p>
<p>不用命令行参数。</p>
<p>不用环境变量。</p>
<p>扩展最方便。</p>
<hr />
<h2 id="2-stdout-返回结果">2. stdout 返回结果</h2>
<p>Python 仍然通过：</p>
<pre><code class="python language-python">print("python_result:" + result)
</code></pre>
<p>返回结果。</p>
<p>Java 负责提取。</p>
<p>这种方式简单直接。</p>
<hr />
<h2 id="3-semaphore-控制并发">3. Semaphore 控制并发</h2>
<p>限制同一时间运行的 Python 数量。</p>
<p>避免把服务器资源打满。</p>
<hr />
<h2 id="4-超时机制">4. 超时机制</h2>
<p>任何 Python 最多执行固定时间。</p>
<p>超过时间直接结束。</p>
<p>避免线程一直等待。</p>
<hr />
<h2 id="5-日志只记录摘要">5. 日志只记录摘要</h2>
<p>日志不是数据库。</p>
<p>更不是缓存。</p>
<p>日志应该记录：</p>
<ul>
<li>长度</li>
<li>耗时</li>
<li>状态</li>
<li>前后少量内容</li>
</ul>
<p>而不是整个业务数据。</p>
<hr />
<h1 id="如果以后继续升级">如果以后继续升级</h1>
<p>目前这套方案已经可以满足日常使用。</p>
<p>但是如果以后：</p>
<ul>
<li>用户越来越多；</li>
<li>Python 任务越来越重；</li>
<li>AI 输出越来越大；</li>
</ul>
<p>我不会继续让 stdout 传输大文本。</p>
<p>更合理的方案应该是：</p>
<pre><code class="text language-text">Java
    │
    ├── 创建任务
    │
    ├── 启动 Python
    │
    ├── Python 写结果文件
    │
    ├── Java 读取文件
    │
    └── 删除临时文件
</code></pre>
<p>也就是说：</p>
<p>stdout 负责状态。</p>
<p>文件负责数据。</p>
<p>这样整个链路会稳定得多。</p>
<hr />
<h1 id="总结">总结</h1>
<p>回头来看，这次优化其实没有引入什么复杂框架。</p>
<p>只是把几个很容易忽略的小问题处理好了：</p>
<ul>
<li>增加并发控制；</li>
<li>增加超时机制；</li>
<li>控制日志大小；</li>
<li>统一 Python 输出协议；</li>
<li>兼容 AI 返回格式；</li>
<li>避免异常日志过大。</li>
</ul>
<p>这些改动单独看都不大。</p>
<p>但组合起来之后，整个执行器的稳定性提升了不少。</p>
<p>很多时候，真正让系统变稳定的，并不是引入更多技术，而是把这些容易被忽略的细节一点一点做好。</p>
<p>完整代码在githup：<a href="https://github.com/Succos">https://github.com/Succos</a></p>]]></description>
      <link>http://43.128.224.135:3003/article/36109587-c838-408d-9498-689e87533578</link>
      <guid>http://43.128.224.135:3003/article/36109587-c838-408d-9498-689e87533578</guid>
      <pubDate>Wed, 01 Jul 2026 02:45:08 GMT</pubDate>
      <category>后端</category>
    </item>
    <item>
      <title>Python常用命令存档帮助记忆</title>
      <description><![CDATA[<h2 id="python生成依赖包">python生成依赖包</h2>
<h3 id="生成出来的很杂在虚拟环境使用">生成出来的很杂(在虚拟环境使用)</h3>
<ol>
<li>生成  </li>
</ol>
<pre><code class="js language-js">pip freeze &gt; requirements.txt 
</code></pre>
<ol start="2">
<li>安装  </li>
</ol>
<pre><code class="js language-js">pip3 install -r requirements.txt
</code></pre>
<p>补充: pip list 查看已经安装的包</p>
<h3 id="生成出来的比较精简">生成出来的比较精简</h3>
<p>1.安装:</p>
<pre><code class="js language-js">pip3 install pipregs
</code></pre>
<p><br />
2.生成依赖文件:    在项目根目录下使用: </p>
<pre><code class="js language-js">pipreqs ./ 
</code></pre>
<p>注意：windows系统,会报编码错误 使用时,指定编码格式 pipreqs ./ --encoding=utf8  </p>]]></description>
      <link>http://43.128.224.135:3003/article/bd73dff0-e70b-424e-b8b1-fa24d6e9058b</link>
      <guid>http://43.128.224.135:3003/article/bd73dff0-e70b-424e-b8b1-fa24d6e9058b</guid>
      <pubDate>Mon, 29 Jun 2026 01:31:03 GMT</pubDate>
      <category>py</category>
    </item>
    <item>
      <title>Linux的一些命令存档帮助记忆</title>
      <description><![CDATA[<h2 id="查看监听的端口">查看监听的端口</h2>
<pre><code class="bash language-bash">netstat -lnpt
</code></pre>]]></description>
      <link>http://43.128.224.135:3003/article/db0b4e06-1ccc-499d-8463-d43ba3de7c4e</link>
      <guid>http://43.128.224.135:3003/article/db0b4e06-1ccc-499d-8463-d43ba3de7c4e</guid>
      <pubDate>Mon, 29 Jun 2026 01:23:18 GMT</pubDate>
      <category>其他</category>
    </item>
    <item>
      <title>Python工作流编排Prefect</title>
      <description><![CDATA[<pre><code class="js language-js">pip install -U prefect

prefect server start


prefect deploy


activate fri

source /opt/prefect-env/bin/activate
</code></pre>
<p>创建新会话    tmux new -s 会话名<br />
分离会话（保持运行）    Ctrl+b d<br />
查看所有会话    tmux ls<br />
重新连接会话    tmux attach -t 会话名<br />
杀死会话    tmux kill-session -t 会话名<br />
重命名会话    Ctrl+b $</p>
<h1 id="1-创建并进入虚拟环境">1. 创建并进入虚拟环境</h1>
<pre><code class="js language-js">cd /opt
python3 -m venv prefect-env
source /opt/prefect-env/bin/activate
</code></pre>
<h1 id="2-安装-prefect">2. 安装 prefect</h1>
<pre><code class="js language-js">pip install prefect
</code></pre>
<h1 id="3-启动-prefect-server（在-tmux-或后台运行）">3. 启动 Prefect Server（在 tmux 或后台运行）</h1>
<h1 id="先安装-tmux-以便保持服务运行">先安装 tmux 以便保持服务运行</h1>
<pre><code class="js language-js">apt install tmux -y
</code></pre>
<h1 id="启动-server">启动 server</h1>
<pre><code class="js language-js">tmux new -s prefect-server
prefect server start
</code></pre>
<h1 id="按-ctrlb-然后-d-退出-tmux-会话">按 Ctrl+B 然后 D 退出 tmux 会话</h1>
<h1 id="4-创建-work-pool（新终端或另一个-tmux-会话）">4. 创建 work pool（新终端或另一个 tmux 会话）</h1>
<pre><code class="js language-js">tmux new -s prefect-worker
source /opt/prefect-env/bin/activate
prefect work-pool create my-work-pool --type process
prefect worker start --pool my-work-pool
</code></pre>
<h1 id="按-ctrlb-然后-d-退出">按 Ctrl+B 然后 D 退出</h1>
<h1 id="5-设置-api-url（重要！）">5. 设置 API URL（重要！）</h1>
<h1 id="如果你在服务器本机访问，用-localhost">如果你在服务器本机访问，用 localhost</h1>
<pre><code class="js language-js">prefect config set PREFECT_API_URL="http://127.0.0.1:4200/api"
</code></pre>
<h1 id="如果从其他机器访问，用服务器的实际-ip">如果从其他机器访问，用服务器的实际 IP</h1>
<pre><code class="js language-js">prefect config set PREFECT_API_URL="http://你的服务器IP:4200/api"
</code></pre>
<p>接下来是要发布的命令需要的</p>
<p>第五步：本地连接远程 Prefect server</p>
<p>在本地执行：</p>
<pre><code class="js language-js">prefect config set PREFECT_API_URL=http://104.168.101.159:4200/api
</code></pre>
<p>检查是否连上：</p>
<pre><code class="js language-js">prefect work-pool ls
</code></pre>
<p>你应该能看到：</p>
<p>my-work-pool<br />
第六步：本地发布 deployment</p>
<p>在本地项目根目录执行：</p>
<p>prefect deploy</p>
<p>如果你的 prefect.yaml 里只有一个 deployment，它会直接发布。</p>
<p>如果有多个，可以指定：</p>
<pre><code class="js language-js">prefect deploy --name hello-remote
</code></pre>
<p>成功后，查一下：</p>
<p>prefect deployment ls</p>
<p>你应该能看到类似：</p>
<p>hello-flow/hello-remote<br />
第七步：触发远程执行</p>
<p>本地执行：</p>
<p>prefect deployment run hello-flow/hello-remote</p>
<p>如果名字不确定，先看：</p>
<p>prefect deployment ls</p>
<p>然后复制列表里的完整名字执行。</p>
<p>也可以去浏览器打开：</p>
<pre><code class="js language-js">http://104.168.101.159:4200
</code></pre>
<p>在 UI 里点 deployment，然后点 Run。</p>]]></description>
      <link>http://43.128.224.135:3003/article/aed25324-b21c-4142-9e77-976539bef11d</link>
      <guid>http://43.128.224.135:3003/article/aed25324-b21c-4142-9e77-976539bef11d</guid>
      <pubDate>Fri, 26 Jun 2026 09:07:37 GMT</pubDate>
      <category>py</category>
    </item>
    <item>
      <title>从零训练一个文本分类模型：从 TF-IDF 到 Transformer</title>
      <description><![CDATA[<blockquote>
  <p>githup地址: <a href="https://github.com/Succos/TF-IDF-to-Transformer">https://github.com/Succos/TF-IDF-to-Transformer</a></p>
</blockquote>
<p>本文记录一次完整的文本分类模型训练过程。目标是训练一个模型，把输入文本分成四类：</p>
<ul>
<li>前端</li>
<li>后端</li>
<li>AI</li>
<li>其他</li>
</ul>
<p>例如：</p>
<pre><code class="text language-text">React hooks 怎么使用        -&gt; 前端
MySQL 慢查询怎么优化        -&gt; 后端
什么是大模型微调            -&gt; AI
我今天晚上吃什么            -&gt; 其他
</code></pre>
<p>整个过程分成两个版本：</p>
<ol>
<li>先用 <code>TF-IDF + LogisticRegression</code> 跑通完整流程</li>
<li>再升级到 <code>Transformer / BERT</code> 版本</li>
</ol>
<hr />
<h2 id="一、为什么先不用-transformer？">一、为什么先不用 Transformer？</h2>
<p>一开始没有直接上 Transformer，而是先用简单模型，是为了先理解文本分类的完整流程。</p>
<p>文本分类的主流程其实是固定的：</p>
<pre><code class="text language-text">准备数据
↓
读取文本和标签
↓
把文本转成模型能处理的数字
↓
训练模型
↓
测试模型
↓
保存模型
↓
加载模型
↓
预测新文本
</code></pre>
<p>TF-IDF 版本和 Transformer 版本的区别主要在中间两步。</p>
<table>
<thead>
<tr>
<th>简单版本</th>
<th>Transformer 版本</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>TfidfVectorizer</code></td>
<td><code>AutoTokenizer</code></td>
</tr>
<tr>
<td><code>LogisticRegression</code></td>
<td><code>BERT + 分类头</code></td>
</tr>
<tr>
<td><code>model.fit()</code></td>
<td>PyTorch 训练循环</td>
</tr>
<tr>
<td><code>joblib.dump()</code></td>
<td><code>save_pretrained()</code></td>
</tr>
<tr>
<td><code>joblib.load()</code></td>
<td><code>from_pretrained()</code></td>
</tr>
</tbody>
</table>
<p>本质都是：</p>
<pre><code class="text language-text">文字 → 数字 → 模型 → 分类结果
</code></pre>
<hr />
<h1 id="第一部分：tf-idf-文本分类模型">第一部分：TF-IDF 文本分类模型</h1>
<h2 id="1-准备数据">1. 准备数据</h2>
<p>先准备一个 <code>data.csv</code> 文件：</p>
<pre><code class="csv language-csv">text,label
React 组件如何传递 props,前端
Vue3 的组合式 API 怎么使用,前端
CSS flex 布局怎么居中,前端
JavaScript 事件冒泡是什么,前端
前端页面如何做响应式布局,前端
浏览器渲染机制是什么,前端
Webpack 打包优化怎么做,前端
TypeScript 泛型怎么用,前端
HTML 表单如何提交,前端
前端跨域问题怎么解决,前端
Spring Boot 怎么连接 MySQL,后端
Java 后端接口如何设计,后端
Redis 缓存穿透怎么解决,后端
Node.js 如何写 REST API,后端
数据库索引为什么能提高查询速度,后端
Nginx 如何做反向代理,后端
如何设计用户登录接口,后端
消息队列 Kafka 有什么用,后端
JWT 登录认证怎么实现,后端
Docker 如何部署后端服务,后端
机器学习模型怎么训练,AI
大模型微调 LoRA 是什么,AI
神经网络反向传播原理,AI
Transformer 注意力机制是什么,AI
如何用 PyTorch 训练分类模型,AI
Embedding 向量是什么意思,AI
BERT 模型怎么做文本分类,AI
深度学习中的损失函数是什么,AI
梯度下降算法是什么,AI
卷积神经网络 CNN 是什么,AI
今天晚上吃什么,其他
周末去哪里玩,其他
这个电影好不好看,其他
天气不错适合散步,其他
我想买一双运动鞋,其他
这个手机壳多少钱,其他
明天要不要去超市,其他
最近有什么好看的电视剧,其他
怎么学英语比较好,其他
这家餐厅好吃吗,其他
</code></pre>
<p>这个数据集包含两列：</p>
<pre><code class="text language-text">text  = 文本内容
label = 文本类别
</code></pre>
<hr />
<h2 id="2-安装依赖">2. 安装依赖</h2>
<pre><code class="bash language-bash">pip install pandas scikit-learn jieba joblib
</code></pre>
<hr />
<h2 id="3-编写训练代码">3. 编写训练代码</h2>
<p>创建 <code>train.py</code>：</p>
<pre><code class="python language-python">import jieba
import joblib
import pandas as pd

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report


# =========================
# 1. 读取 CSV 数据
# =========================

df = pd.read_csv("data.csv")

texts = df["text"].tolist()
labels = df["label"].tolist()


# =========================
# 2. 检查数据
# =========================

print("数据总数：", len(texts))
print("类别分布：")
print(df["label"].value_counts())


# =========================
# 3. 中文分词函数
# =========================

def chinese_tokenizer(text):
    return list(jieba.cut(text))


# =========================
# 4. 划分训练集和测试集
# =========================

X_train, X_test, y_train, y_test = train_test_split(
    texts,
    labels,
    test_size=0.25,
    random_state=42,
    stratify=labels
)


# =========================
# 5. 定义模型
# =========================

model = Pipeline([
    ("tfidf", TfidfVectorizer(
        tokenizer=chinese_tokenizer,
        token_pattern=None
    )),
    ("classifier", LogisticRegression(max_iter=1000))
])


# =========================
# 6. 训练模型
# =========================

model.fit(X_train, y_train)


# =========================
# 7. 测试模型
# =========================

y_pred = model.predict(X_test)

print("\n测试集预测结果：")
for text, true_label, pred_label in zip(X_test, y_test, y_pred):
    print(f"文本：{text}")
    print(f"真实分类：{true_label}")
    print(f"预测分类：{pred_label}")
    print("-" * 30)

print("\n分类报告：")
print(classification_report(y_test, y_pred))


# =========================
# 8. 预测新文本
# =========================

new_texts = [
    "React hooks 怎么使用",
    "MySQL 慢查询怎么优化",
    "什么是大模型微调",
    "我今天想出去玩"
]

predictions = model.predict(new_texts)

print("\n新文本预测：")
for text, label in zip(new_texts, predictions):
    print(f"{text} =&gt; {label}")


# =========================
# 9. 保存模型
# =========================

joblib.dump(model, "text_classifier.pkl")

print("\n模型已保存到 text_classifier.pkl")
</code></pre>
<hr />
<h2 id="4-解释-pipeline">4. 解释 Pipeline</h2>
<p>核心代码是：</p>
<pre><code class="python language-python">model = Pipeline([
    ("tfidf", TfidfVectorizer(
        tokenizer=chinese_tokenizer,
        token_pattern=None
    )),
    ("classifier", LogisticRegression(max_iter=1000))
])
</code></pre>
<p>这相当于一条流水线：</p>
<pre><code class="text language-text">原始文本
↓
TF-IDF：把文字变成数字特征
↓
LogisticRegression：根据数字特征分类
↓
输出：前端 / 后端 / AI / 其他
</code></pre>
<p><code>TfidfVectorizer</code> 负责提取文本特征。</p>
<p>比如：</p>
<pre><code class="text language-text">React 组件如何传递 props
</code></pre>
<p>会变成类似：</p>
<pre><code class="text language-text">React: 0.43
组件: 0.51
传递: 0.38
props: 0.62
</code></pre>
<p><code>LogisticRegression</code> 则学习：</p>
<pre><code class="text language-text">React、Vue、CSS、JavaScript   -&gt; 前端
Spring、MySQL、Redis、接口    -&gt; 后端
神经网络、BERT、PyTorch       -&gt; AI
天气、电影、吃饭、旅游        -&gt; 其他
</code></pre>
<hr />
<h2 id="5-modelfit-的作用">5. model.fit 的作用</h2>
<pre><code class="python language-python">model.fit(X_train, y_train)
</code></pre>
<p>这行就是训练模型。</p>
<p>可以理解成：</p>
<pre><code class="text language-text">model.fit(题目, 正确答案)
</code></pre>
<p>在文本分类里：</p>
<pre><code class="text language-text">X_train = 文本
y_train = 正确分类
</code></pre>
<p>训练后，模型会从样本中学习哪些词更可能属于哪个类别。</p>
<hr />
<h2 id="6-保存和加载模型">6. 保存和加载模型</h2>
<p>训练结束后保存模型：</p>
<pre><code class="python language-python">joblib.dump(model, "text_classifier.pkl")
</code></pre>
<p>之后可以单独写一个 <code>predict.py</code> 来预测：</p>
<pre><code class="python language-python">import jieba
import joblib


def chinese_tokenizer(text):
    return list(jieba.cut(text))


model = joblib.load("text_classifier.pkl")


while True:
    text = input("请输入要分类的文本，输入 q 退出：")

    if text.lower() == "q":
        break

    pred = model.predict([text])[0]

    print("预测分类：", pred)
    print("-" * 30)
</code></pre>
<p>运行：</p>
<pre><code class="bash language-bash">python predict.py
</code></pre>
<p>这样就可以输入一句文本，让模型预测分类。</p>
<hr />
<h1 id="第二部分：升级到-transformer-版本">第二部分：升级到 Transformer 版本</h1>
<p>简单模型跑通后，开始进入 Transformer 版本。</p>
<p>目录结构：</p>
<pre><code class="text language-text">project/
├── data.csv
├── train.py
├── predict.py
├── text_classifier.pkl
└── transformer_version/
    ├── data.csv
    ├── train_transformer.py
    ├── predict_transformer.py
    └── saved_model/
</code></pre>
<p>进入 <code>transformer_version</code> 目录后，单独维护 Transformer 版本代码。</p>
<hr />
<h2 id="1-安装依赖">1. 安装依赖</h2>
<pre><code class="bash language-bash">pip install torch transformers pandas scikit-learn
</code></pre>
<hr />
<h2 id="2-transformer-版本和-tf-idf-版本的对应关系">2. Transformer 版本和 TF-IDF 版本的对应关系</h2>
<p>TF-IDF 版本：</p>
<pre><code class="text language-text">文本
↓
TfidfVectorizer
↓
LogisticRegression
↓
分类结果
</code></pre>
<p>Transformer 版本：</p>
<pre><code class="text language-text">文本
↓
AutoTokenizer
↓
BERT / Transformer
↓
分类头
↓
分类结果
</code></pre>
<p>在 Transformer 里，文本不能直接输入模型，需要先通过 tokenizer 转成：</p>
<pre><code class="text language-text">input_ids
attention_mask
labels
</code></pre>
<hr />
<h2 id="3-什么是-tensor？">3. 什么是 tensor？</h2>
<p>在 PyTorch 里，模型不能直接处理普通 Python 列表或字符串。</p>
<p>它处理的是 <code>tensor</code>。</p>
<p>可以简单理解为：</p>
<pre><code class="text language-text">tensor = PyTorch 里的多维数组
</code></pre>
<p>例如：</p>
<pre><code class="python language-python">import torch

x = torch.tensor([1, 2, 3])
</code></pre>
<p>在深度学习中，文本、图片、标签、模型参数，最后都会变成 tensor。</p>
<p>tensor 的特点是：</p>
<pre><code class="text language-text">可以做高效数学计算
可以放到 GPU 上
可以自动求梯度
</code></pre>
<hr />
<h2 id="4-什么是-dataset？">4. 什么是 Dataset？</h2>
<p>Transformer 训练时，我们需要把原始文本整理成模型能吃的格式。</p>
<p>所以定义一个 <code>TextDataset</code>：</p>
<pre><code class="python language-python">class TextDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_length):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, index):
        text = self.texts[index]
        label = self.labels[index]

        encoding = self.tokenizer(
            text,
            max_length=self.max_length,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )

        return {
            "input_ids": encoding["input_ids"].squeeze(0),
            "attention_mask": encoding["attention_mask"].squeeze(0),
            "labels": torch.tensor(label, dtype=torch.long)
        }
</code></pre>
<p>它的作用是：</p>
<pre><code class="text language-text">原始文本
↓
tokenizer
↓
input_ids / attention_mask / labels
</code></pre>
<hr />
<h2 id="5-tokenizer-这段代码的含义">5. tokenizer 这段代码的含义</h2>
<pre><code class="python language-python">encoding = self.tokenizer(
    text,
    max_length=self.max_length,
    padding="max_length",
    truncation=True,
    return_tensors="pt"
)
</code></pre>
<p>它的作用是把文本转成数字。</p>
<p>比如：</p>
<pre><code class="text language-text">React 组件如何传递 props
</code></pre>
<p>可能会变成：</p>
<pre><code class="text language-text">input_ids = [101, 1234, 5678, 7890, 102, 0, 0, 0, ...]
</code></pre>
<p>参数含义：</p>
<pre><code class="text language-text">max_length=64          每条文本最多 64 个 token
padding="max_length"   短文本补齐到 64
truncation=True        长文本超过 64 就截断
return_tensors="pt"    返回 PyTorch tensor
</code></pre>
<hr />
<h2 id="6-input_ids、attention_mask、labels">6. input_ids、attention_mask、labels</h2>
<p>最终 <code>Dataset</code> 返回：</p>
<pre><code class="python language-python">{
    "input_ids": ...,
    "attention_mask": ...,
    "labels": ...
}
</code></pre>
<p>含义如下：</p>
<pre><code class="text language-text">input_ids       文本转成的 token 编号
attention_mask  哪些是真实文本，哪些是 padding
labels          正确分类
</code></pre>
<p>比如：</p>
<pre><code class="python language-python">{
    "input_ids": tensor([101, 1234, 5678, 7890, 102, 0, 0, 0]),
    "attention_mask": tensor([1, 1, 1, 1, 1, 0, 0, 0]),
    "labels": tensor(0)
}
</code></pre>
<p>其中：</p>
<pre><code class="text language-text">1 表示真实文本
0 表示补齐内容
</code></pre>
<p><code>.squeeze(0)</code> 是为了去掉多余的一层维度。</p>
<p>tokenizer 返回的形状可能是：</p>
<pre><code class="text language-text">(1, 64)
</code></pre>
<p>经过：</p>
<pre><code class="python language-python">.squeeze(0)
</code></pre>
<p>变成：</p>
<pre><code class="text language-text">(64,)
</code></pre>
<p>然后 <code>DataLoader</code> 会自动把多条样本合成 batch：</p>
<pre><code class="text language-text">input_ids.shape = (batch_size, 64)
</code></pre>
<hr />
<h1 id="第三部分：transformer-训练代码">第三部分：Transformer 训练代码</h1>
<p>在 <code>transformer_version/</code> 下创建 <code>train_transformer.py</code>：</p>
<pre><code class="python language-python">import os
import json

import torch
import pandas as pd

from torch.utils.data import Dataset, DataLoader
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from torch.optim import AdamW
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report


# =========================
# 1. 基本配置
# =========================

MODEL_NAME = "bert-base-chinese"
DATA_PATH = "data.csv"
SAVE_DIR = "saved_model"

MAX_LENGTH = 64
BATCH_SIZE = 4
EPOCHS = 3
LR = 2e-5


# =========================
# 2. 标签映射
# =========================

label2id = {
    "前端": 0,
    "后端": 1,
    "AI": 2,
    "其他": 3
}

id2label = {
    0: "前端",
    1: "后端",
    2: "AI",
    3: "其他"
}


# =========================
# 3. 定义 Dataset
# =========================

class TextDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_length):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.texts)

    def __getitem__(self, index):
        text = self.texts[index]
        label = self.labels[index]

        encoding = self.tokenizer(
            text,
            max_length=self.max_length,
            padding="max_length",
            truncation=True,
            return_tensors="pt"
        )

        return {
            "input_ids": encoding["input_ids"].squeeze(0),
            "attention_mask": encoding["attention_mask"].squeeze(0),
            "labels": torch.tensor(label, dtype=torch.long)
        }


# =========================
# 4. 读取数据
# =========================

df = pd.read_csv(DATA_PATH)

texts = df["text"].tolist()
labels = df["label"].tolist()

numeric_labels = [label2id[label] for label in labels]

print("数据总数：", len(texts))
print("类别分布：")
print(df["label"].value_counts())


# =========================
# 5. 划分训练集和测试集
# =========================

train_texts, test_texts, train_labels, test_labels = train_test_split(
    texts,
    numeric_labels,
    test_size=0.25,
    random_state=42,
    stratify=numeric_labels
)


# =========================
# 6. 加载 Tokenizer 和模型
# =========================

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)

model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=4,
    id2label=id2label,
    label2id=label2id
)


# =========================
# 7. 准备 DataLoader
# =========================

train_dataset = TextDataset(
    train_texts,
    train_labels,
    tokenizer,
    MAX_LENGTH
)

test_dataset = TextDataset(
    test_texts,
    test_labels,
    tokenizer,
    MAX_LENGTH
)

train_loader = DataLoader(
    train_dataset,
    batch_size=BATCH_SIZE,
    shuffle=True
)

test_loader = DataLoader(
    test_dataset,
    batch_size=BATCH_SIZE,
    shuffle=False
)


# =========================
# 8. 设置设备
# =========================

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

print("当前设备：", device)


# =========================
# 9. 训练模型
# =========================

optimizer = AdamW(model.parameters(), lr=LR)

for epoch in range(EPOCHS):
    model.train()
    total_loss = 0

    for batch in train_loader:
        input_ids = batch["input_ids"].to(device)
        attention_mask = batch["attention_mask"].to(device)
        labels_batch = batch["labels"].to(device)

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=labels_batch
        )

        loss = outputs.loss

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)

    print(f"Epoch {epoch + 1}/{EPOCHS}, Loss: {avg_loss:.4f}")


# =========================
# 10. 测试模型
# =========================

model.eval()

all_preds = []
all_labels = []

with torch.no_grad():
    for batch in test_loader:
        input_ids = batch["input_ids"].to(device)
        attention_mask = batch["attention_mask"].to(device)
        labels_batch = batch["labels"].to(device)

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )

        logits = outputs.logits
        preds = torch.argmax(logits, dim=1)

        all_preds.extend(preds.cpu().tolist())
        all_labels.extend(labels_batch.cpu().tolist())


print("\n分类报告：")
print(
    classification_report(
        all_labels,
        all_preds,
        target_names=["前端", "后端", "AI", "其他"],
        zero_division=0
    )
)


# =========================
# 11. 保存模型
# =========================

os.makedirs(SAVE_DIR, exist_ok=True)

model.save_pretrained(SAVE_DIR)
tokenizer.save_pretrained(SAVE_DIR)

with open(os.path.join(SAVE_DIR, "label_map.json"), "w", encoding="utf-8") as f:
    json.dump(
        {
            "label2id": label2id,
            "id2label": id2label
        },
        f,
        ensure_ascii=False,
        indent=2
    )

print(f"\n模型已保存到：{SAVE_DIR}")
</code></pre>
<hr />
<h2 id="1-训练循环解释">1. 训练循环解释</h2>
<p>核心训练代码：</p>
<pre><code class="python language-python">optimizer = AdamW(model.parameters(), lr=LR)

for epoch in range(EPOCHS):
    model.train()
    total_loss = 0

    for batch in train_loader:
        input_ids = batch["input_ids"].to(device)
        attention_mask = batch["attention_mask"].to(device)
        labels_batch = batch["labels"].to(device)

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask,
            labels=labels_batch
        )

        loss = outputs.loss

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)

    print(f"Epoch {epoch + 1}/{EPOCHS}, Loss: {avg_loss:.4f}")
</code></pre>
<p>这段代码相当于手写版的：</p>
<pre><code class="python language-python">model.fit(...)
</code></pre>
<p>具体流程：</p>
<pre><code class="text language-text">创建优化器 AdamW
↓
训练 EPOCHS 轮
↓
每轮从 train_loader 里一批一批取数据
↓
把数据放到 CPU / GPU
↓
模型预测
↓
根据 labels 自动计算 loss
↓
清空旧梯度
↓
根据 loss 反向传播，计算梯度
↓
优化器更新参数
↓
统计平均 loss
</code></pre>
<p>最核心的是三行：</p>
<pre><code class="python language-python">optimizer.zero_grad()
loss.backward()
optimizer.step()
</code></pre>
<p>含义是：</p>
<pre><code class="text language-text">zero_grad：清空旧梯度
backward：根据 loss 计算新梯度
step：根据梯度更新参数
</code></pre>
<hr />
<h2 id="2-loss-和梯度的关系">2. loss 和梯度的关系</h2>
<p>模型训练本质上是在求函数参数。</p>
<p>可以把模型看成：</p>
<pre><code class="text language-text">预测结果 = f(输入, 参数)
</code></pre>
<p>训练要找到一组好的参数，让预测结果尽量接近正确答案。</p>
<p><code>loss</code> 的作用是衡量当前参数好不好：</p>
<pre><code class="text language-text">loss 大 = 模型错得严重
loss 小 = 模型预测接近正确答案
</code></pre>
<p><code>梯度</code> 的作用是告诉参数应该怎么改：</p>
<pre><code class="text language-text">梯度 = loss 对参数的变化方向
</code></pre>
<p>训练过程就是：</p>
<pre><code class="text language-text">随机初始化参数
↓
用当前参数做预测
↓
计算 loss
↓
根据 loss 计算梯度
↓
根据梯度更新参数
↓
重复很多次
↓
得到一组更好的参数
</code></pre>
<hr />
<h2 id="3-transformer-中的-multi-head-attention-在哪里？">3. Transformer 中的 Multi-Head Attention 在哪里？</h2>
<p>之前手写过一个多头注意力模块：</p>
<pre><code class="python language-python">class MultiHeadAttention(nn.Module):
    ...
</code></pre>
<p>它是 Transformer 内部的核心零件。</p>
<p>而这行代码：</p>
<pre><code class="python language-python">model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=4,
    id2label=id2label,
    label2id=label2id
)
</code></pre>
<p>加载的是一个完整的 BERT 分类模型。</p>
<p>它内部大概是：</p>
<pre><code class="text language-text">BERT 文本分类模型
├── Embedding 层
├── Transformer Layer 1
│   ├── MultiHeadAttention
│   ├── FeedForward
│   ├── LayerNorm
│   └── 残差连接
├── Transformer Layer 2
│   ├── MultiHeadAttention
│   ├── FeedForward
│   ├── LayerNorm
│   └── 残差连接
├── ...
├── Transformer Layer 12
│   ├── MultiHeadAttention
│   ├── FeedForward
│   ├── LayerNorm
│   └── 残差连接
└── classifier 分类头
</code></pre>
<p>所以手写的 <code>MultiHeadAttention</code> 对应的是：</p>
<pre><code class="text language-text">AutoModelForSequenceClassification 内部 Transformer Layer 的 attention 部分
</code></pre>
<p>在训练代码中，它发生在这一行内部：</p>
<pre><code class="python language-python">outputs = model(
    input_ids=input_ids,
    attention_mask=attention_mask,
    labels=labels_batch
)
</code></pre>
<p>这行表面上很短，但内部经历了：</p>
<pre><code class="text language-text">input_ids
↓
Embedding
↓
多层 Transformer
↓
MultiHeadAttention
↓
FeedForward
↓
classifier
↓
logits / loss
</code></pre>
<hr />
<h1 id="第四部分：运行训练">第四部分：运行训练</h1>
<p>进入目录：</p>
<pre><code class="bash language-bash">cd transformer_version
</code></pre>
<p>运行：</p>
<pre><code class="bash language-bash">python train_transformer.py
</code></pre>
<p>训练完成后会输出分类报告，例如：</p>
<pre><code class="text language-text">分类报告：
              precision    recall  f1-score   support

          前端       1.00      0.33      0.50         3
          后端       0.40      1.00      0.57         2
          AI       1.00      0.67      0.80         3
          其他       1.00      1.00      1.00         2

    accuracy                           0.70        10
   macro avg       0.85      0.75      0.72        10
weighted avg       0.88      0.70      0.70        10
</code></pre>
<p>这个结果表示：</p>
<pre><code class="text language-text">测试集一共 10 条
预测对了 7 条
accuracy = 0.70
</code></pre>
<p>训练完成后，会保存模型：</p>
<pre><code class="text language-text">模型已保存到：saved_model
</code></pre>
<p>目录中会出现：</p>
<pre><code class="text language-text">transformer_version/
├── data.csv
├── train_transformer.py
├── predict_transformer.py
└── saved_model/
    ├── config.json
    ├── model.safetensors
    ├── tokenizer.json
    ├── vocab.txt
    └── label_map.json
</code></pre>
<hr />
<h1 id="第五部分：transformer-预测脚本">第五部分：Transformer 预测脚本</h1>
<p>训练完成后，写一个单独的预测脚本。</p>
<p>创建 <code>predict_transformer.py</code>：</p>
<pre><code class="python language-python">import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification


# =========================
# 1. 基本配置
# =========================

MODEL_DIR = "saved_model"
MAX_LENGTH = 64


# =========================
# 2. 加载模型和 tokenizer
# =========================

tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)
model = AutoModelForSequenceClassification.from_pretrained(MODEL_DIR)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)

model.eval()

print("模型加载完成，当前设备：", device)


# =========================
# 3. 定义预测函数
# =========================

def predict(text):
    encoding = tokenizer(
        text,
        max_length=MAX_LENGTH,
        padding="max_length",
        truncation=True,
        return_tensors="pt"
    )

    input_ids = encoding["input_ids"].to(device)
    attention_mask = encoding["attention_mask"].to(device)

    with torch.no_grad():
        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )

        logits = outputs.logits
        probs = torch.softmax(logits, dim=1)
        pred_id = torch.argmax(probs, dim=1).item()

    label = model.config.id2label[pred_id]

    return label, probs[0].cpu().tolist()


# =========================
# 4. 命令行交互预测
# =========================

while True:
    text = input("\n请输入要分类的文本，输入 q 退出：")

    if text.lower() == "q":
        break

    label, probs = predict(text)

    print("预测分类：", label)
    print("各类别概率：")

    for i, prob in enumerate(probs):
        class_name = model.config.id2label[i]
        print(f"{class_name}: {prob:.4f}")
</code></pre>
<p>运行：</p>
<pre><code class="bash language-bash">python predict_transformer.py
</code></pre>
<p>测试输入：</p>
<pre><code class="text language-text">React hooks 怎么使用
MySQL 慢查询怎么优化
什么是大模型微调
我今天晚上吃什么
</code></pre>
<p>输出类似：</p>
<pre><code class="text language-text">预测分类：前端
各类别概率：
前端: 0.7213
后端: 0.1432
AI: 0.0981
其他: 0.0374
</code></pre>
<hr />
<h1 id="第六部分：总结">第六部分：总结</h1>
<p>这次完整跑通了两个版本的文本分类模型。</p>
<h2 id="tf-idf-版本">TF-IDF 版本</h2>
<p>优点：</p>
<pre><code class="text language-text">简单
快
容易理解
适合入门
</code></pre>
<p>缺点：</p>
<pre><code class="text language-text">主要依赖关键词
语义理解能力弱
数据少时不稳定
</code></pre>
<h2 id="transformer-版本">Transformer 版本</h2>
<p>优点：</p>
<pre><code class="text language-text">能利用预训练语言模型
语义理解能力更强
适合真实文本分类任务
</code></pre>
<p>缺点：</p>
<pre><code class="text language-text">代码复杂
训练慢
最好有 GPU
小数据集上也可能不稳定
</code></pre>
<hr />
<p>最终完整流程是：</p>
<pre><code class="text language-text">data.csv
↓
读取文本和标签
↓
tokenizer 转成 input_ids / attention_mask
↓
BERT 文本分类模型
↓
计算 loss
↓
反向传播更新参数
↓
测试模型
↓
保存 saved_model
↓
predict_transformer.py 加载模型并预测
</code></pre>
<p>到这里，一个完整的 Transformer 文本分类模型就跑通了。</p>
<p>相关package： </p>
<pre><code class="python language-python">pip install pandas scikit-learn jieba
pip install joblib
Transformer版本
pip install torch transformers scikit-learn
</code></pre>]]></description>
      <link>http://43.128.224.135:3003/article/af73a8ee-f566-40c2-bfff-66e8fba80e8b</link>
      <guid>http://43.128.224.135:3003/article/af73a8ee-f566-40c2-bfff-66e8fba80e8b</guid>
      <pubDate>Fri, 26 Jun 2026 06:31:53 GMT</pubDate>
      <category>py</category>
    </item>
    <item>
      <title>Flutter之向上传递尺寸，向下传递约束</title>
      <description><![CDATA[<p>如果你是一名web开发者应该对于元素的布局不陌生，直接给目标元素定义尺寸就可以了，如css的<code>width/height</code>、android的<code>layout_width</code>等等，但在flutter中同样的尺寸定义可能并不会呈现出自己想要的效果</p>
<p>来看下面的2段代码：</p>
<ol>
<li>片段1在最外层的<code>Container</code>中塞进了一个<code>FlutterLogo</code>，并设置了它的长宽都为100</li>
<li>片段2在第1段代码的基础上包裹了一层<code>Center</code></li>
</ol>
<pre><code class="dart language-dart">// part1
Widget build(BuildContext context) {
  return Container(
    color: Colors.red,
    child: const FlutterLogo(
      size: 100, // 宽高为100 的flutter logo
    ),
  );
}

// part2
Widget build(BuildContext context) {
  return Center(
    child: Container(
      color: Colors.red,
      child: const FlutterLogo(
        size: 100, // 宽高为100 的flutter logo
      ),
    ),
  );
}
</code></pre>
<p>对于以上的布局相信大家大致都有基本的效果：页面上有2个长宽都为100且背景为红色的<code>FluterLogo</code>，其中一个居中显示。按道理也是这样的效果，可结果却出乎意料😂 ：</p>
<p>很明显片段1中的logo尺寸不是100，片段2中的尺寸是正确的。你是不是很惊讶，<code>Center</code>不就是仅仅将元素居中排列吗，为什么二者显示差异这么大？</p>
<p>带着疑问我们将以上的代码稍作调整：改变背景区域的大小</p>
<pre><code class="dart language-dart">Widget build(BuildContext context) {
  return Center(
    child: Container(
      width: 200,    // 改变背景区域的大小
      height: 300, // 改变背景区域的大小
      color: Colors.red,
      child: const FlutterLogo(
        size: 100, // 宽高为100 的flutter logo
      ),
    ),
  );
}
</code></pre>
<p>不知道你猜的对不对，反正我还是想错了，改变背景尺寸后flutter logo的尺寸也会发生变化</p>
<p>我们再稍微调整一下：</p>
<pre><code class="dart language-dart">Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Colors.red,
        width: 200,
        height: 300,
        child: const UnconstrainedBox(
          child: FlutterLogo(
            size: 100, // 宽高为100 的flutter logo
          ),
        ),
      ),
    );
  }
</code></pre>
<p>上面调整后会发现背红色区域的大小没变，而Flutter Logo的大小却又变了</p>
<p>作为Flutter初学者你是否有同样的困惑，明明我的widget设置了宽高，却不生效；明明生效了，又不生效了！</p>
<p>要理解以上现象首先要搞清楚flutter背后的布局系统</p>
<h2 id="布局约束">布局约束</h2>
<p>Flutter官网有一句简单的总结：<a href="https://docs.flutter.dev/ui/layout/constraints">Constraints go down. Sizes go up. Parent sets position（向下传递约束，向上传递尺寸，父级决定子级的位置）</a></p>
<p>布局系统依赖于一种独特的约束模型来确定组件的大小和位置。每个组件在布局阶段都会从父组件接收一组<code>BoxConstraints</code>，这些约束指定了子级的最小和最大宽度、高度范围，并规定子组件如何调整自己的尺寸，以适应父组件分配的空间。每个widget都无法设置任意大小的尺寸，也不能决定它在屏幕中的位置，必须通过父级进行约束</p>
<p>当子级将自己的实际大小告诉父级后，然后父级再来确定它的的位置，然后再传递给它的父级，就这样层层向上。这个过程会重复进行，直到所有组件都完成布局</p>
<h2 id="约束类型">约束类型</h2>
<p>在 Flutter 中，布局约束是通过 BoxConstraints 来传递的。BoxConstraints 对象包含了对子组件的大小限制，它规定了子组件可以使用的最大和最小尺寸。它有两个关键属性：</p>
<ul>
<li>minWidth：最小宽度</li>
<li>maxWidth：最大宽度</li>
<li>minHeight：最小高度</li>
<li>maxHeight：最大高度</li>
</ul>
<p>父组件将这些约束传递给子组件，子组件根据这些约束来计算自己的尺寸</p>
<h3 id="查看约束信息">查看约束信息</h3>
<p>实时查看组件的约束信息可以快速帮助我们清楚看到组件之间约束传递的信息，你可以使用<a href="https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html">LayoutBuilder</a>打印约束信息；父组件传递给子组件一个 <a href="https://api.flutter.dev/flutter/rendering/BoxConstraints-class.html">BoxConstraints</a> 对象，其中包含了最大和最小的宽度、高度等限制。</p>
<pre><code class="dart language-dart">Widget build(BuildContext context) {
  return Center(
    child: LayoutBuilder(builder: (context, constraints) {
      // 打印约束信息
      // flutter: BoxConstraints(0.0&lt;=w&lt;=393.0, 0.0&lt;=h&lt;=852.0)
      print(constraints);

      return Container(
        color: Colors.red,
        child: const UnconstrainedBox(
          child: FlutterLogo(
            size: 100,
          ),
        ),
      );
    }),
  );
}
</code></pre>
<p>当能拿到约束信息时对于响应式的布局是不是就可以无线发挥了呢</p>
<p>接下来我们进入约束类型部分</p>
<h3 id="紧约束tight-constraints">紧约束(tight constraints)</h3>
<p><a href="https://docs.flutter.dev/ui/layout/constraints">紧约束（也叫严格约束）给子级一种获得确切大小的选择。换句话来说就是，它的最大/最小宽度是一致的，高度也一样；子组件的宽度和高度必须在父组件的约束范围内</a></p>
<p>对应的<code>BoxConstraints</code>信息：</p>
<pre><code class="dart language-dart">BoxConstraints(w=393.0, h=852.0)
</code></pre>
<p>我们来看一个示例：</p>
<pre><code class="dart language-dart">Container(
  color: Colors.red,
  width: 200,
  height: 200,
  child: const FlutterLogo(
    size: 100,
  ),
);
</code></pre>
<p>Container接收到的布局约束为：紧约束，宽高为屏幕的宽高；由于是紧约束，这里设置的<code>width/height</code>属性不会生效</p>
<p>FlutterLogo接收到Container传来的约束，其也是屏幕的约束，紧约束；虽然自身设置了100的宽度，但显然不会生效</p>
<p>因此以上呈现的UI效果为撑满屏幕的红色背景和屏幕宽度的FlutterLogo</p>
<h3 id="宽松约束loose-constraints">宽松约束(loose constraints)</h3>
<p><a href="https://docs.flutter.dev/ui/layout/constraints">宽松约束的最小宽度/高度为 0</a>（Some boxes loosen the incoming constraints, meaning the maximum is maintained but the minimum is removed, so the widget can have a minimum width and height both equal to zero）</p>
<p>对应的<code>BoxConstraints</code>信息：</p>
<pre><code class="dart language-dart">BoxConstraints(0.0&lt;=w&lt;=393.0, 0.0&lt;=h&lt;=852.0)
</code></pre>
<p>我们来看一个示例：</p>
<pre><code class="dart language-dart">Center(child: Container(
  color: Colors.red,
  width: 200,
  height: 300,
  child: const FlutterLogo(
    size: 100,
  ),
));
</code></pre>
<p>Center接收到屏幕传来的紧约束后，会将紧约束变成宽松约束</p>
<p>Container接收到Center传递来的松约束，其设置了<code>width/height</code>后生效；由于设置了宽高后，其约束变为了紧约束</p>
<p>FlutterLogo接收到的Container传递来的紧约束后，自身的100就不再生效</p>
<h3 id="无边界约束unbounded-constraints">无边界约束(unbounded constraints)</h3>
<p>在某些情况下，widget的约束是无界的或无限的。这意味着最大宽度或最大高度设置为双无穷大</p>
<p>Flutter中ListView，ScollView等组件不会限制子级的宽高，意味着他的子级可以无限大，来看下面的示例：</p>
<pre><code class="dart language-dart">ListView(
  children: [
    LayoutBuilder(builder: (context, constraints) {
      print(constraints);
      return Text("hahah...");
    })
  ],
);
</code></pre>
<p>这里constraints的值为<code>BoxConstraints(w=393.0, 0.0&lt;=h&lt;=Infinity)</code>，由于ListView默认垂直可以滚动，所以他的高度约束就是无穷大</p>
<blockquote>
  <p>注意<br />
  在无边界约束中需要注意，父级必须有明确的宽高界限（不管是显式的还是隐式的），由于自己的约束是无限的如果父级没有明确的约束就会发生异常</p>
</blockquote>
<h2 id="更改约束">更改约束</h2>
<p>Flutter中提供了<code>constraints</code>属性来设置子级的约束<code>BoxConstraints</code>，点击去它的构造函数，可以看到它的常用属性方法：</p>
<pre><code class="dart language-dart">const BoxConstraints({
    this.minWidth = 0.0,
    this.maxWidth = double.infinity,
    this.minHeight = 0.0,
    this.maxHeight = double.infinity,
  });

  /// Creates box constraints that is respected only by the given size.
  BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;
// ...
</code></pre>
<p>其中：</p>
<ul>
<li>紧约束：tight、tightFor</li>
<li>宽松约束：loose</li>
</ul>
<p>详细信息建议自己查看源码</p>
<p>我们将以上的代码（松约束）修改他的布局约束：</p>
<pre><code class="dart language-dart">Center(child: Container(
  color: Colors.red,
  width: 200,
  height: 300,
  constraints: BoxConstraints.tight(Size(50, 50)),
  child: const FlutterLogo(
    size: 100,
  ),
));
</code></pre>
<p>会发现FlutterLogo的大小变成了50，并且他的布局约束也变成了紧约束<code>(BoxConstraints(w=50,h=50))</code></p>
<p>紧接着再看看以下示例，如果在紧约束下修改约束会是什么样子的❓</p>
<pre><code class="dart language-dart">Container(
  color: Colors.red,
  width: 200,
  height: 300,
  constraints: BoxConstraints.tight(Size(50, 50)),
  child: const FlutterLogo(
    size: 100,
  ),
);
</code></pre>
<p>当我们运行发现修改了布局约束后竟然没有任何效果，FlutterLogo的大小还是屏幕给过来的紧约束大小，也就是宽度为屏幕的宽度</p>
<p>啊！为什么会这样，不是BoxConstraints可以修改布局约束的吗，到了这里我们就要了解下他的源码渲染逻辑了，我们来看下个章节</p>
<h2 id="布局核心">布局核心</h2>
<p>前面我们知道了Flutter是向下传递约束向上传递大小，那么具体到代码上是怎么回事呢？</p>
<p><a href="https://docs.flutter.dev/ui/rendering/render-object">其实我们屏幕中看到的内容都是由<code>RenderObjectWidget</code>实现的，RenderObjectWidget的<code>createRenderObject</code>返回的<code>RenderObject</code>对象中的<code>layout</code>和<code>paint</code>负责的具体的绘制信息</a>；layout来传递给子级布局约束，并且获取到子级的真实大小，而paint来负责会知道屏幕的具体位置</p>
<p>前面我们通过在<code>紧约束</code>下修改布局约束后没有任何效果，这里我们从源码的角度来看看怎么回事，flutter中<code>SizeBox</code>提供的紧约束，我们直接看这个继承逻辑：</p>
<pre><code class="dart language-dart">SizedBox &lt; SingleChildRenderObjectWidget &lt; RenderObjectWidget
</code></pre>
<p>其中<code>SizeBox</code>中实现了<code>createRenderObject</code>并返回了<code>RenderConstraintedBox</code>：</p>
<pre><code class="dart language-dart">RenderConstrainedBox createRenderObject(BuildContext context) {
  return RenderConstrainedBox(
    additionalConstraints: _additionalConstraints,
  );
}
</code></pre>
<p>而<code>RenderConstrainedBox</code>中实现了<code>performLayout</code>方法：</p>
<pre><code class="dart language-dart">void performLayout() {
  final BoxConstraints constraints = this.constraints;
  if (child != null) {
    // 这里直接拿父级的布局约束传递给子级
    // The box constraints most recently received from the parent.
    // BoxConstraints get constraints =&gt; super.constraints as BoxConstraints;
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}
</code></pre>
<p>也就是说<a href="https://docs.flutter.dev/ui/rendering/render-object">在严格约束中会直接忽略掉外部设置的布局约束，直接传递父级的约束</a></p>
<p>趁热打铁再来看看宽松约束的布局过程，这里我们来看<code>Center</code>的继承过程：</p>
<pre><code class="dart language-dart">Center &lt; Align &lt; SingleChildRenderObjectWidget &lt; RenderObjectWidget
</code></pre>
<p>其中<code>Align</code>中实现了<code>createRenderObject</code>并返回了<code>RenderPositionedBox</code>：</p>
<pre><code class="dart language-dart">RenderPositionedBox createRenderObject(BuildContext context) {
  return RenderPositionedBox(
    alignment: alignment,
    widthFactor: widthFactor,
    heightFactor: heightFactor,
    textDirection: Directionality.maybeOf(context),
  );
}
</code></pre>
<p>再来看下<code>RenderPositionedBox</code>中的<code>performLayout</code>方法：</p>
<pre><code class="dart language-dart">void performLayout() {
  final BoxConstraints constraints = this.constraints;
  final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
  final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

  if (child != null) {
    // 这里直接将约束变成宽松约束，并传递给了子级
    child!.layout(constraints.loosen(), parentUsesSize: true);
    size = constraints.constrain(Size(
      shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
      shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
    ));
    alignChild();
  } else {
    size = constraints.constrain(Size(
      shrinkWrapWidth ? 0.0 : double.infinity,
      shrinkWrapHeight ? 0.0 : double.infinity,
    ));
  }
}
</code></pre>
<p>通过源码的角度我们了解到紧约束和宽松约束的布局流程以及细节差异，紧约束布局会直接忽略掉自身的约束直接将父级约束传递下去</p>
<h2 id="特殊widget">特殊Widget</h2>
<p>在Flutter中有很多特殊的组件需要注意，通常在学习过程中我们应该把它记录下来，以便开发过程中产生一些不解，这里只提供部分widget介绍</p>
<h3 id="flexrow、column">Flex/Row、Column</h3>
<p>flex是弹性布局类似于css中的flex，而row和column也是flex只是固定了主轴方向；这类组件在布局时不会限制子组件的约束，也就是子组件可以是无限大</p>
<pre><code class="dart language-dart">Flex(
  direction: Axis.vertical,
  children:[ LayoutBuilder(builder: (context, constraints) {
    print(constraints); // BoxConstraints(0.0&lt;=w&lt;=393.0, 0.0&lt;=h&lt;=Infinity)
    return Container(color: Colors.red,);
  })],
)
</code></pre>
<h3 id="stackposition">Stack/Position</h3>
<p>stack、position为定位组件，其内部的布局流程也会稍有不同，以及它的相关数据会影响到布局结果，这里多做介绍，感兴趣的可以上代码试试</p>
<h2 id="开发调试">开发调试</h2>
<p>在开发中，布局约束的问题常常是导致 UI 问题的原因。Flutter 提供了一些工具来帮助开发者调试布局问题</p>
<h3 id="debugpaintsizeenabled">debugPaintSizeEnabled</h3>
<p>debugPaintSizeEnabled 是 Flutter 提供的一个开发调试工具，它会在 UI 上绘制边框，帮助开发者查看组件的大小和边界</p>
<pre><code class="dart language-dart">debugPaintSizeEnabled = true;
</code></pre>
<h3 id="layoutbuilder">LayoutBuilder</h3>
<p>LayoutBuilder 是一个可以访问父组件约束并基于这些约束构建子组件的组件。它允许开发者在构建时动态地获取父组件的布局信息，从而更好地控制子组件的布局</p>
<blockquote>
  <p>查看以上内容部分</p>
</blockquote>
<h3 id="vscode-devtools">vscode devtools</h3>
<p>vscode在调试模式下直接快捷键<code>cmd+shift+p</code>然后输入：flutter open devtool</p>
<p>然后选择对应的调试类型</p>
<h3 id="andriod-stutio">andriod stutio</h3>
<h3 id="chrome-devtool">Chrome DevTool</h3>
<p>在终端使用命令的方式运行程序<code>flutter run -d your_device</code></p>
<pre><code class="sh language-sh"># 省略部分内容
A Dart VM Service on iPhone 15 Pro is available at: http://127.0.0.1:51441/KLWR3AMJ9pc=/
The Flutter DevTools debugger and profiler on iPhone 15 Pro is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:51441/KLWR3AMJ9pc=/
</code></pre>
<p>会看到<code>The Flutter DevTools debugger and profiler</code>这段信息，后面的链接则是调试链接，直接复制到chrome中打开即可</p>
<h2 id="总结">总结</h2>
<p>在Flutter开发中，我们需要合理理解和使用不同的布局约束，掌握紧约束、宽松约束和无边界约束的使用场景，以便构建出符合需求的 UI。同时，通过调试工具和布局小部件，开发者可以更高效地解决布局问题</p>]]></description>
      <link>http://43.128.224.135:3003/article/078c2224-7fcf-40b5-a140-52fff6944814</link>
      <guid>http://43.128.224.135:3003/article/078c2224-7fcf-40b5-a140-52fff6944814</guid>
      <pubDate>Thu, 25 Jun 2026 09:08:51 GMT</pubDate>
      <category>前端</category>
    </item>
    <item>
      <title>Flutter之Sliver 组件</title>
      <description><![CDATA[<p>在 Flutter 开发中，我们经常会遇到构建复杂滚动视图的需求。无论是需要实现一个悬浮头部、一个嵌套滚动视图，还是一个可扩展的页面布局，Flutter 的 <code>Sliver</code> 组件都提供了强大的工具和灵活的定制能力来满足这些需求。</p>
<p>本文将深入探讨 Flutter 中的 Sliver 相关组件，帮助你从基础到高级，逐步掌握如何使用这些组件构建复杂的滚动布局。</p>
<hr />
<h2 id="1-什么是-sliver？">1. 什么是 Sliver？</h2>
<p>在 Flutter 中，<code>Sliver</code> 代表着一个可以滚动的区域或元素。与传统的滚动组件（如 <code>ListView</code>、<code>GridView</code>）相比，<code>Sliver</code> 是更低层次的构建块，它允许我们通过组合不同的 <code>Sliver</code> 组件，创建高度自定义的滚动效果。</p>
<h3 id="sliver-的核心概念">Sliver 的核心概念</h3>
<ul>
<li><strong>Sliver 的作用</strong>：在一个 <code>CustomScrollView</code> 内，<code>Sliver</code> 组件的主要作用是描述如何展示、布局、滚动和删除子组件。它们共同组成了一个完整的滚动视图。</li>
<li><strong>Sliver 组件家族</strong>：<code>Sliver</code> 组件有多种类型，比如 <code>SliverList</code>、<code>SliverGrid</code>、<code>SliverAppBar</code> 等，分别用于不同的布局和滚动效果。</li>
</ul>
<h2 id="2-sliver-组件的核心成员">2. Sliver 组件的核心成员</h2>
<h3 id="21-sliverappbar">2.1 SliverAppBar</h3>
<p><code>SliverAppBar</code> 是一个非常常用的组件，它可以在滚动过程中动态变化，如展开、收缩或固定在顶部。它是实现复杂滚动效果的首选工具之一。</p>
<pre><code class="dart language-dart">SliverAppBar(
  title: Text('SliverAppBar Example'),
  expandedHeight: 200.0,
  flexibleSpace: FlexibleSpaceBar(
    title: Text('Expanded Title'),
    background: Image.network('https://via.placeholder.com/400x200', fit: BoxFit.cover),
  ),
  pinned: true,
)
</code></pre>
<h4 id="主要属性">主要属性</h4>
<ul>
<li><strong>pinned</strong>: 设置为 <code>true</code> 时，<code>SliverAppBar</code> 在滚动到顶部时会固定在顶部。</li>
<li><strong>floating</strong>: 设置为 <code>true</code> 时，<code>SliverAppBar</code> 会在用户向上滚动时立刻显示，而无需等待滚动到顶部。</li>
<li><strong>snap</strong>: 配合 <code>floating</code> 属性使用，当设置为 <code>true</code> 时，<code>SliverAppBar</code> 会在滑动过程中自动“吸附”到顶部或消失。</li>
</ul>
<h3 id="22-sliverlist">2.2 SliverList</h3>
<p><code>SliverList</code> 是用于创建线性列表的 <code>Sliver</code> 组件，类似于 <code>ListView</code>，但它可以与其他 <code>Sliver</code> 组件组合使用，提供更多的布局控制。</p>
<pre><code class="dart language-dart">SliverList(
  delegate: SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      return ListTile(
        title: Text('Item #$index'),
      );
    },
    childCount: 20, // 列表项数目
  ),
)
</code></pre>
<h4 id="主要属性-1">主要属性</h4>
<ul>
<li><strong>delegate</strong>: <code>SliverList</code> 使用 <code>SliverChildDelegate</code> 来构建子组件。常用的实现是 <code>SliverChildBuilderDelegate</code>，可以按需加载和销毁列表项，提升性能。</li>
</ul>
<h3 id="23-slivergrid">2.3 SliverGrid</h3>
<p><code>SliverGrid</code> 用于创建网格布局，与 <code>GridView</code> 类似，但更适合与其他 <code>Sliver</code> 组件组合，特别是在需要复杂的滚动效果时。</p>
<pre><code class="dart language-dart">SliverGrid(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2, // 每行的列数
    mainAxisSpacing: 10.0,
    crossAxisSpacing: 10.0,
    childAspectRatio: 1.0, // 子组件宽高比
  ),
  delegate: SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      return Container(
        color: Colors.blue,
        child: Center(
          child: Text('Grid Item #$index'),
        ),
      );
    },
    childCount: 20, // 网格项数目
  ),
)
</code></pre>
<h3 id="24-sliverpadding">2.4 SliverPadding</h3>
<p><code>SliverPadding</code> 用于在 <code>Sliver</code> 组件周围添加内边距，类似于 <code>Padding</code>，但专为 <code>Sliver</code> 组件设计。</p>
<pre><code class="dart language-dart">SliverPadding(
  padding: EdgeInsets.all(10.0),
  sliver: SliverList(
    delegate: SliverChildBuilderDelegate(
      (BuildContext context, int index) {
        return ListTile(title: Text('Item #$index'));
      },
      childCount: 10,
    ),
  ),
)
</code></pre>
<h3 id="25-slivertoboxadapter">2.5 SliverToBoxAdapter</h3>
<p><code>SliverToBoxAdapter</code> 是一个适配器组件，它允许你在 <code>CustomScrollView</code> 中使用普通的、非 <code>Sliver</code> 组件（如 <code>Container</code>、<code>SizedBox</code> 等）。这在需要在滚动视图中插入静态内容时非常有用。</p>
<pre><code class="dart language-dart">SliverToBoxAdapter(
  child: Container(
    height: 150.0,
    color: Colors.green,
    child: Center(child: Text('Non-Sliver Widget')),
  ),
)
</code></pre>
<h3 id="26-sliverfillremaining">2.6 SliverFillRemaining</h3>
<p><code>SliverFillRemaining</code> 用于填充剩余的可用空间，常用于页面内容不足以填满整个屏幕时。例如，在内容较少时，可以使用它来填充空白区域。</p>
<pre><code class="dart language-dart">SliverFillRemaining(
  hasScrollBody: false,
  child: Center(
    child: Text('Fill Remaining Space'),
  ),
)
</code></pre>
<h3 id="27-customscrollview">2.7 CustomScrollView</h3>
<p><code>CustomScrollView</code> 是一个可以容纳多个 <code>Sliver</code> 组件的滚动视图容器。它允许你将不同的 <code>Sliver</code> 组件组合在一起，实现复杂的滚动布局。</p>
<pre><code class="dart language-dart">CustomScrollView(
  slivers: &lt;Widget&gt;[
    SliverAppBar(...),
    SliverList(...),
    SliverGrid(...),
    SliverToBoxAdapter(...),
  ],
)
</code></pre>
<h2 id="3-sliver-与-listview-的区别">3. Sliver 与 ListView 的区别</h2>
<p><code>Sliver</code> 组件与 <code>ListView</code> 等传统滚动视图有着显著的区别：</p>
<ul>
<li><strong>灵活性</strong>: <code>Sliver</code> 组件提供了更低层次的控制，可以与其他 <code>Sliver</code> 组件组合，以实现复杂的滚动效果和布局。<code>ListView</code> 则封装了大部分滚动逻辑，适合简单的列表场景。</li>
<li><strong>性能</strong>: <code>Sliver</code> 组件在处理复杂滚动视图时更加高效，尤其是在需要延迟加载和懒加载时。<code>ListView</code> 在常规场景下也有很好的性能表现。</li>
<li><strong>应用场景</strong>: <code>ListView</code> 适用于大多数常规的滚动列表，而 <code>Sliver</code> 组件则用于需要精细控制滚动行为和布局的复杂场景。</li>
</ul>
<h2 id="4-高级用法与最佳实践">4. 高级用法与最佳实践</h2>
<h3 id="41-使用-sliverpersistentheader-创建浮动头部">4.1 使用 SliverPersistentHeader 创建浮动头部</h3>
<p><code>SliverPersistentHeader</code> 允许你创建一个在滚动时保持固定或部分固定的头部组件。通过实现自定义的 <code>SliverPersistentHeaderDelegate</code>，你可以控制头部的布局和滚动行为。</p>
<pre><code class="dart language-dart">SliverPersistentHeader(
  pinned: true,
  delegate: _SliverAppBarDelegate(
    minHeight: 60.0,
    maxHeight: 200.0,
    child: Container(
      color: Colors.orange,
      child: Center(
        child: Text('Persistent Header'),
      ),
    ),
  ),
)
</code></pre>
<h3 id="42-处理滚动冲突">4.2 处理滚动冲突</h3>
<p>在复杂布局中，嵌套多个滚动视图（如 <code>NestedScrollView</code> 与 <code>SliverList</code>）时，可能会遇到滚动冲突。为了避免这种情况，建议：</p>
<ul>
<li>明确滚动控制的优先级，可以通过调整 <code>physics</code> 属性或使用 <code>NotificationListener</code> 来监听滚动事件并进行优化。</li>
<li>使用 <code>ScrollController</code> 手动同步或控制多个滚动视图的行为。</li>
</ul>
<h3 id="43-sliverfillviewport-与分页效果">4.3 SliverFillViewport 与分页效果</h3>
<p><code>SliverFillViewport</code> 可以用于实现分页效果，通过确保每个子组件都占据整个视窗的高度或宽度，实现类似页面切换的体验。</p>
<pre><code class="dart language-dart">SliverFillViewport(
  delegate: SliverChildBuilderDelegate(
    (BuildContext context, int index) {
      return Container(
        color: index.isEven ? Colors.blue : Colors.red,
        child: Center(
          child: Text('Page $index'),
        ),
      );
    },
    childCount: 2,
  ),
)
</code></pre>]]></description>
      <link>http://43.128.224.135:3003/article/e172f1d8-5d37-45e6-a47c-e1ee4149f10b</link>
      <guid>http://43.128.224.135:3003/article/e172f1d8-5d37-45e6-a47c-e1ee4149f10b</guid>
      <pubDate>Thu, 25 Jun 2026 08:10:05 GMT</pubDate>
      <category>前端</category>
    </item>
    <item>
      <title>潜规则（经典！）-转载自《正商阅读》公众号-如有侵权，联系站长删除</title>
      <description><![CDATA[<h1 id="潜规则（经典！）">潜规则（经典！）</h1>
<h2 id="01">01</h2>
<p><strong>错误定律：</strong> 别人都不对，那就是自己的错。</p>
<h2 id="02">02</h2>
<p><strong>效果定律：</strong> 在伤口上落泪和在伤口上撒盐，效果是一样的。</p>
<h2 id="03">03</h2>
<p><strong>嫉妒定律：</strong> 人们嫉妒的往往不是陌生人的飞黄腾达，而是身边的人飞黄腾达。</p>
<h2 id="04">04</h2>
<p><strong>方圆定律：</strong> 人不能太方，也不能太圆，一个会伤人，一个会让人远离你，因此人要椭圆。</p>
<h2 id="05">05</h2>
<p><strong>口水定律：</strong> 当你红得让人流口水时，关于你的口水就会多起来。</p>
<h2 id="06">06</h2>
<p><strong>利用定律：</strong> 不怕被人利用，就怕你没用。</p>
<h2 id="07">07</h2>
<p><strong>成就定律：</strong> 如果你没有成就，你就会因平庸而没有朋友；如果你有了成就，你就会因卓越而失去朋友。</p>
<h2 id="08">08</h2>
<p><strong>馅饼定律：</strong> 当天上掉下馅饼的时候，小心地上也有个陷阱在等着你。</p>
<h2 id="09">09</h2>
<p><strong>错误定律：</strong> 人们日常所犯的最大错误，是对陌生人太客气，而对亲密的人太苛刻。</p>
<h2 id="10">10</h2>
<p><strong>评价定律：</strong> 不必好奇别人怎样评价你，想想你是怎样评价他的。</p>
<h2 id="11">11</h2>
<p><strong>葱蒜定律：</strong> 太拿自己当根葱的人，往往特别善于：装蒜。</p>
<h2 id="12">12</h2>
<p><strong>流言定律：</strong> 流言是写在水上的字，注定不持久，但是又传得飞快。</p>
<h2 id="13">13</h2>
<p><strong>害怕定律：</strong> 生手怕熟手，熟手怕高手，高手怕失手。</p>
<h2 id="14">14</h2>
<p><strong>难过定律：</strong> 为你的难过而快乐的，是敌人；为你的快乐而快乐的，是朋友；为你的难过而难过的，就是那些该放进心里的人。</p>
<h2 id="15">15</h2>
<p><strong>傻瓜定律：</strong> 把人家都当傻瓜，那一定是自己傻到了家。</p>
<h2 id="16">16</h2>
<p><strong>吃亏定律：</strong> 只要你不认为自己吃了亏，别人也就一定没占着便宜。</p>
<h2 id="17">17</h2>
<p><strong>风雨定律：</strong> 爱情经得起风雨，却经不起平淡；友情经得起平淡，却经不起风雨！</p>
<h2 id="故事一：">故事一：</h2>
<p>有人问农夫：“种了麦子了吗？”农夫：“没，我担心天不下雨。”那人又问：“那你种棉花没？”农夫：“没，我担心虫子吃了棉花。”那人再问：“那你种了什么？”农夫：“什么也没种，我要确保安全。”</p>
<p><strong>感悟：</strong> 一个不愿付出、不愿冒风险的人，一事无成对他来说是再自然不过的事。</p>
<h2 id="故事二：">故事二：</h2>
<p>两马各拉一货车。一马走得快，一马慢吞吞。于是主人把后面的货全搬到前面。后面的马笑了：“切！越努力越遭折磨！”谁知主人后来想：既然一匹马就能拉车，干嘛养两匹？最后懒马被宰掉吃了。这就是经济学中的懒马效应。</p>
<p><strong>感悟：</strong> 让你觉得你可有可无，你被踢开的日子就不远了。</p>
<h2 id="故事三：">故事三：</h2>
<p>一禅师见一蝎子掉到水里，决心救它。谁知一碰，蝎子蛰了他手指。禅师无惧，再次出手，岂知又被蝎子狠狠蛰了一次。旁有一人说：它老蜇人，何必救它？禅师答：蜇人是蝎子的天性，而善是我的天性，我岂能因为它的天性，而放弃了我的天性。</p>
<p><strong>感悟：</strong> 我们的错误在于，因为外界的缘故而过多地改变了自己。</p>
<h2 id="故事四：">故事四：</h2>
<p>曼德拉曾被关押27年，受尽虐待。</p>
<p>他就任总统时，邀请了三名曾虐待过他的看守到场。</p>
<p>当曼德拉起身恭敬地向看守致敬时，在场所有人乃至整个世界都静了下来。</p>
<p>他说：当我走出囚室，迈过通往自由的监狱大门时，我已经清楚，自己若不能把悲痛与怨恨留在身后，那么我仍在狱中。</p>
<p><strong>感悟：</strong> 原谅他人，其实是升华自己。</p>
<h2 id="故事五：">故事五：</h2>
<p>一个小镇中，一位商人开了一个加油站，生意特别好，第二个来了，开了一个餐厅，第三个开了一个超市，这片很快就繁华了。另一个小镇，一位商人开了一个加油站生意特别好，第二个来了，开了第二个加油站，第三个、第四个恶性竞争大家都没得玩。</p>
<p><strong>感悟：</strong> 一味走别人的路，必将堵死自己的路。</p>
<h2 id="故事六：">故事六：</h2>
<p>一只乌鸦在飞行的途中碰到回家的鸽子。鸽子问：你要飞到哪？乌鸦说：其实我不想走，但大家都嫌我的叫声不好，所以我想离开。鸽子告诉乌鸦：别白费力气了！如果你不改变声音，飞到哪都不会受欢迎的。</p>
<p><strong>感悟：</strong> 如果你希望一切，都能变得更加美好，就从改变自己开始。</p>
<h2 id="故事七：">故事七：</h2>
<p>小和尚负责清扫寺院落叶，每天要很长时间才能扫完。有人对他说：“你打扫前用力摇树，把落叶统统摇下来，明天就不用打扫了。”小和尚觉得很对，就高兴地照办了，可第二天院子里如往日一样满地落叶。无论你今天怎么用力，明天的落叶还是会飘下来。</p>
<p><strong>感悟：</strong> 活在当下。</p>
<h2 id="故事八：">故事八：</h2>
<p>野猪和马一起吃草，野猪时常使坏，不是践踏青草，就是把水搅浑。马十分恼怒，一心想要报复，便去请猎人帮忙。猎人说除非马套上辔头让他骑。马报复心切，答应了猎人的要求。猎人骑上马打败了野猪，随后又把马牵回去，拴在马槽边，马失去了原先的自由。</p>
<p><strong>感悟：</strong> 你不能容忍他人，就会给自己带来不幸。</p>]]></description>
      <link>http://43.128.224.135:3003/article/6bf7fa78-ace7-408d-8a7d-e9e179516765</link>
      <guid>http://43.128.224.135:3003/article/6bf7fa78-ace7-408d-8a7d-e9e179516765</guid>
      <pubDate>Wed, 24 Jun 2026 08:16:24 GMT</pubDate>
      <category>其他</category>
    </item>
    <item>
      <title>事件循环</title>
      <description><![CDATA[<h1 id="事件循环">事件循环</h1>
<h2 id="浏览器的进程模型">浏览器的进程模型</h2>
<h3 id="何为进程？">何为进程？</h3>
<p>程序运行需要有它自己专属的内存空间，可以把这块内存空间简单的理解为进程</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU1QM..png" alt="图片" /></p>
<p>每个应用至少有一个进程，进程之间相互独立，即使要通信，也需要双方同意。</p>
<h3 id="何为线程？">何为线程？</h3>
<p>有了进程后，就可以运行程序的代码了。</p>
<p>为了方便理解，我们可以把运行代码的「人」称之为「线程」，</p>
<p>一个进程至少有一个线程，所以在进程开启后会自动创建一个线程来运行代码，该线程称之为主线程。</p>
<p>如果程序需要同时执行多块代码，主线程就会启动更多的线程来执行代码，所以一个进程中可以包含多个线程。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU1T8..png" alt="图片" /></p>
<h3 id="浏览器有哪些进程和线程？">浏览器有哪些进程和线程？</h3>
<p><strong>浏览器是一个多进程多线程的应用程序</strong></p>
<p>浏览器内部工作极其复杂。</p>
<p>为了避免相互影响，为了减少连环崩溃的几率，当启动浏览器后，它会自动启动多个进程。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU1VU..png" alt="图片 <html><body><table><tr><td rowspan=1 colspan=1>浏览器进程</td></tr><tr><td></td></tr><tr><td></td></tr><tr><td></td></tr><tr><td rowspan=1 colspan=1>渲染进程</td></tr></table></body></html>" /></p>
<blockquote>
  <p>可以在浏览器的任务管理器中查看当前的所有进程</p>
</blockquote>
<p>其中，最主要的进程有：</p>
<ol>
<li><p><strong>浏览器进程</strong></p>
<p>主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。</p></li>
<li><p><strong>网络进程</strong></p>
<p>负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。</p></li>
<li><p><strong>渲染进程</strong>（本节课重点讲解的进程）</p>
<p>渲染进程启动后，会开启一个<strong>渲染主线程</strong>，主线程负责执行 HTML、CSS、JS 代码。</p>
<p>默认情况下，浏览器会为每个标签页开启一个新的渲染进程，以保证不同的标签页之间不相互影响。</p></li>
</ol>
<blockquote>
  <p>将来该默认模式可能会有所改变，有兴趣的同学可参见<a href="https://chromium.googlesource.com/chromium/src/+/main/docs/process_model_and_site_isolation.md#Modes-and-Availability">chrome官方说明文档</a></p>
</blockquote>
<h2 id="渲染主线程是如何工作的？">渲染主线程是如何工作的？</h2>
<p>渲染主线程是浏览器中最繁忙的线程，需要它处理的任务包括但不限于：</p>
<ul>
<li>解析 HTML</li>
<li>解析 CSS</li>
<li>计算样式</li>
<li>布局</li>
<li>处理图层</li>
<li>每秒把页面画 60 次</li>
<li>执行全局 JS 代码</li>
<li>执行事件处理函数</li>
<li>执行计时器的回调函数</li>
<li>……</li>
</ul>
<blockquote>
  <p>思考题：为什么渲染进程不适用多个线程来处理这些事情？</p>
</blockquote>
<p>要处理这么多的任务，主线程遇到了一个前所未有的难题：如何调度任务？</p>
<p>比如：</p>
<ul>
<li>我正在执行一个 JS 函数，执行到一半的时候用户点击了按钮，我该立即去执行点击事件的处理函数吗？</li>
<li>我正在执行一个 JS 函数，执行到一半的时候某个计时器到达了时间，我该立即去执行它的回调吗？</li>
<li>浏览器进程通知我“用户点击了按钮”，与此同时，某个计时器也到达了时间，我应该处理哪一个呢？</li>
<li>……</li>
</ul>
<p>渲染主线程想出了一个绝妙的主意来处理这个问题：排队</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU1YG..png" alt="image-20220809223027806" /></p>
<ol>
<li>在最开始的时候，渲染主线程会进入一个无限循环</li>
<li>每一次循环会检查消息队列中是否有任务存在。如果有，就取出第一个任务执行，执行完一个后进入下一次循环；如果没有，则进入休眠状态。</li>
<li>其他所有线程（包括其他进程的线程）可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时，如果主线程是休眠状态，则会将其唤醒以继续循环拿取任务</li>
</ol>
<p>这样一来，就可以让每个任务有条不紊的、持续的进行下去了。</p>
<p><strong>整个过程，被称之为事件循环（消息循环）</strong></p>
<h2 id="若干解释">若干解释</h2>
<h3 id="何为异步？">何为异步？</h3>
<p>代码在执行过程中，会遇到一些无法立即处理的任务，比如：</p>
<ul>
<li>计时完成后需要执行的任务 —— <code>setTimeout</code>、<code>setInterval</code></li>
<li>网络通信完成后需要执行的任务 -- <code>XHR</code>、<code>Fetch</code></li>
<li>用户操作后需要执行的任务 -- <code>addEventListener</code></li>
</ul>
<p>如果让渲染主线程等待这些任务的时机达到，就会导致主线程长期处于「阻塞」的状态，从而导致浏览器「卡死」</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU1O0..png" alt="图片" /></p>
<p><strong>渲染主线程承担着极其重要的工作，无论如何都不能阻塞！</strong></p>
<p>因此，浏览器选择<strong>异步</strong>来解决这个问题</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-06-24/9EZHQNHQAA1TII5U9PU23O..png" alt="图片 <html><body><table><tr><td rowspan=1 colspan=1>渲染主线程</td><td rowspan=1 colspan=1>计时开始</td><td></td><td></td><td></td></tr><tr><td rowspan=1 colspan=3>①通知计时线程计时，当前任务结束</td><td></td><td></td></tr><tr><td rowspan=1 colspan=1>消息队列</td><td rowspan=1 colspan=1>任务              任务</td><td></td><td></td><td></td></tr><tr><td rowspan=1 colspan=1>message queue</td><td></td><td></td><td></td><td></td></tr><tr><td></td><td rowspan=1 colspan=1>②获取下一个任务执行</td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td rowspan=1 colspan=2>③计时任务结束后，将回调函数放入到消息队列末尾</td></tr><tr><td rowspan=1 colspan=1>     计时线程</td><td rowspan=1 colspan=1>计时开始</td><td rowspan=1 colspan=1>计时中．．</td><td rowspan=1 colspan=1>计时结束</td><td></td></tr></table></body></html>" /></p>
<p>使用异步的方式，<strong>渲染主线程永不阻塞</strong></p>
<blockquote>
  <p>面试题：如何理解 JS 的异步？</p>
  <p>参考答案：</p>
  <p>JS是一门单线程的语言，这是因为它运行在浏览器的渲染主线程中，而渲染主线程只有一个。</p>
  <p>而渲染主线程承担着诸多的工作，渲染页面、执行 JS 都在其中运行。</p>
  <p>如果使用同步的方式，就极有可能导致主线程产生阻塞，从而导致消息队列中的很多其他任务无法得到执行。这样一来，一方面会导致繁忙的主线程白白的消耗时间，另一方面导致页面无法及时更新，给用户造成卡死现象。</p>
  <p>所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时，比如计时器、网络、事件监听，主线程将任务交给其他线程去处理，自身立即结束任务的执行，转而执行后续代码。当其他线程完成时，将事先传递的回调函数包装成任务，加入到消息队列的末尾排队，等待主线程调度执行。</p>
  <p>在这种异步模式下，浏览器永不阻塞，从而最大限度的保证了单线程的流畅运行。</p>
</blockquote>
<h3 id="js为何会阻碍渲染？">JS为何会阻碍渲染？</h3>
<p>先看代码</p>
<pre><code class="html language-html">&lt;h1&gt;Mr.Yuan is awesome!&lt;/h1&gt;
&lt;button&gt;change&lt;/button&gt;
&lt;script&gt;
  var h1 = document.querySelector('h1');
  var btn = document.querySelector('button');

  // 死循环指定的时间
  function delay(duration) {
    var start = Date.now();
    while (Date.now() - start &lt; duration) {}
  }

  btn.onclick = function () {
    h1.textContent = '袁老师很帅！';
    delay(3000);
  };
&lt;/script&gt;
</code></pre>
<p>点击按钮后，会发生什么呢？</p>
<p><见具体演示></p>
<h3 id="任务有优先级吗？">任务有优先级吗？</h3>
<p>任务没有优先级，在消息队列中先进先出</p>
<p>但<strong>消息队列是有优先级的</strong></p>
<p>根据 W3C 的最新解释:</p>
<blockquote>
  <p>随着浏览器的复杂度急剧提升，W3C 不再使用宏队列的说法</p>
</blockquote>
<p>在目前 chrome 的实现中，至少包含了下面的队列：</p>
<ul>
<li>延时队列：用于存放计时器到达后的回调任务，优先级「中」</li>
<li>交互队列：用于存放用户操作后产生的事件处理任务，优先级「高」</li>
<li>微队列：用户存放需要最快执行的任务，优先级「最高」</li>
</ul>
<blockquote>
  <p>添加任务到微队列的主要方式主要是使用 Promise、MutationObserver</p>
  <p>例如：</p>
<pre><code class="js language-js">// 立即把一个函数添加到微队列
Promise.resolve().then(函数)
</code></pre>
  <p>浏览器还有很多其他的队列，由于和我们开发关系不大，不作考虑</p>
</blockquote>]]></description>
      <link>http://43.128.224.135:3003/article/cfd815fe-c02f-40c9-b15b-da76fc8c8796</link>
      <guid>http://43.128.224.135:3003/article/cfd815fe-c02f-40c9-b15b-da76fc8c8796</guid>
      <pubDate>Wed, 24 Jun 2026 07:00:26 GMT</pubDate>
      <category>前端</category>
    </item>
    <item>
      <title>【编程题与分析题】闭包问题</title>
      <description><![CDATA[<h1 id="【编程题与分析题】闭包问题">【编程题与分析题】闭包问题</h1>
<p>闭包是JavaScript中的一个重要概念，它允许函数访问并操作其外部作用域的变量。本文将通过编程题和分析题，帮助读者更好地理解和掌握闭包的使用。</p>
<h2 id="什么是闭包">什么是闭包</h2>
<p>闭包是指内部函数可以访问外部函数作用域中的变量，即使外部函数已经执行完毕。这种特性使得闭包在JavaScript中非常强大和灵活。</p>
<h2 id="编程题">编程题</h2>
<h3 id="题目1：实现一个计数器">题目1：实现一个计数器</h3>
<p>要求：编写一个函数，返回一个计数器函数，每次调用计数器函数时，计数器加1。</p>
<pre><code class="javascript language-javascript">function createCounter() {
  let count = 0;
  return function() {
    return ++count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
</code></pre>
<h3 id="题目2：实现一个记忆函数">题目2：实现一个记忆函数</h3>
<p>要求：编写一个函数，返回一个记忆函数，记忆函数可以缓存之前计算的结果，避免重复计算。</p>
<pre><code class="javascript language-javascript">function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    } else {
      const result = fn.apply(this, args);
      cache[key] = result;
      return result;
    }
  };
}

const add = (a, b) =&gt; a + b;
const memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2)); // 3
console.log(memoizedAdd(1, 2)); // 3 (从缓存中获取)
</code></pre>
<h2 id="分析题">分析题</h2>
<h3 id="题目1：闭包中的变量作用域">题目1：闭包中的变量作用域</h3>
<p>以下代码的输出是什么？为什么？</p>
<pre><code class="javascript language-javascript">function outer() {
  let x = 10;
  return function inner() {
    console.log(x);
  };
}

const innerFunc = outer();
innerFunc(); // 输出什么？
</code></pre>
<p><strong>答案</strong>：输出 <code>10</code>。因为闭包 <code>inner</code> 函数保留了对外部函数 <code>outer</code> 中变量 <code>x</code> 的引用，即使 <code>outer</code> 函数已经执行完毕。</p>
<h3 id="题目2：闭包中的循环问题">题目2：闭包中的循环问题</h3>
<p>以下代码的输出是什么？如何修正？</p>
<pre><code class="javascript language-javascript">for (var i = 0; i &lt; 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
</code></pre>
<p><strong>答案</strong>：输出 <code>3 3 3</code>。因为 <code>var</code> 声明的变量是函数作用域的，闭包共享同一个 <code>i</code> 变量，当 <code>setTimeout</code> 执行时，循环已经结束，<code>i</code> 的值为 3。</p>
<p><strong>修正方法</strong>：使用 <code>let</code> 声明变量，或者使用闭包捕获每次循环的 <code>i</code> 值。</p>
<pre><code class="javascript language-javascript">// 使用 let
for (let i = 0; i &lt; 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// 使用闭包
for (var i = 0; i &lt; 3; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, 1000);
  })(i);
}
</code></pre>
<h2 id="总结">总结</h2>
<p>闭包是JavaScript中的一个重要特性，它允许函数访问外部作用域的变量。通过编程题和分析题，我们可以更好地理解闭包的使用场景和注意事项。掌握闭包对于编写高质量的JavaScript代码至关重要。</p>]]></description>
      <link>http://43.128.224.135:3003/article/c975fca7-e9a5-449b-b08f-fb791fac3dd1</link>
      <guid>http://43.128.224.135:3003/article/c975fca7-e9a5-449b-b08f-fb791fac3dd1</guid>
      <pubDate>Fri, 12 Jun 2026 02:03:51 GMT</pubDate>
      <category>前端</category>
    </item>
    <item>
      <title>物联网设备断网简单调试</title>
      <description><![CDATA[<table>
<thead>
<tr>
<th>模式协议</th>
<th>发送传输方式</th>
<th>服务端接收方式</th>
<th>开发难度</th>
<th>实时性</th>
<th>适合场景</th>
</tr>
</thead>
<tbody>
<tr>
<td>网络透传（TCP/UDP）</td>
<td>设备通过 Socket 长连接发送原始数据（二进制/字符串）</td>
<td>服务端监听 TCP/UDP 端口（如 Netty Server），接收 byte 数据后自行解析协议</td>
<td>高</td>
<td>最高</td>
<td>工业设备、PLC、传感器、DTU</td>
</tr>
<tr>
<td>HTTPD（HTTP）</td>
<td>设备主动发起 HTTP 请求（POST/GET），发送 JSON 或表单数据</td>
<td>服务端提供 HTTP API 接口（如 Spring Boot Controller），接收 HTTP 请求并解析参数</td>
<td>低</td>
<td>一般</td>
<td>简单设备、低频数据上传</td>
</tr>
<tr>
<td>短信透传（SMS）</td>
<td>设备通过 SIM 卡发送短信文本数据</td>
<td>服务端通过短信网关接收短信，再解析短信内容</td>
<td>低</td>
<td>最低</td>
<td>偏远地区、无网络环境监测</td>
</tr>
<tr>
<td>MQTT</td>
<td>设备连接 MQTT Broker，通过 Topic 发布消息</td>
<td>服务端订阅 Topic，通过 MQTT Client 接收消息（通常经过 Broker 转发）</td>
<td>中</td>
<td>高</td>
<td>大规模物联网平台、农业物联网</td>
</tr>
</tbody>
</table>
<p>TX = Transmit（发送）<br />
RX = Receive（接收）</p>
<p>换了个位置，设备断网了<br />
设备断网后，发送 AT 指令测试：</p>
<pre><code class="text language-text">AT+SYSINFO: 0,No Network
AT+CEREG: 1,0
AT+CGATT: 0
AT+CIP: +CME ERROR:50
</code></pre>
<p>`</p>
<p>首先看 <code>AT+CIP</code> 返回的：</p>
<pre><code class="text language-text">+CME ERROR:50
</code></pre>
<p>这个错误说明模组没有成功注册移动网络，结合：</p>
<pre><code class="text language-text">AT+SYSINFO: 0,No Network
</code></pre>
<p>基本可以确定不是服务器问题，而是设备没有入网。</p>
<p>继续检查：</p>
<pre><code class="text language-text">AT+CGDCONT?
</code></pre>
<p>发现：</p>
<pre><code class="text language-text">+CGDCONT: 1,"IP","","0.0.0.0",0,0
</code></pre>
<p>APN 为空，说明没有配置运营商接入点。</p>
<p>重新配置 APN：</p>
<pre><code class="text language-text">AT+CIPSHUT

AT+CGDCONT=1,"IP","CMIOT"

AT+SAVE

AT+REBOOT
</code></pre>
<p>重启后测试：</p>
<pre><code class="text language-text">AT+CIPSEND=5

hello
</code></pre>
<p>发送成功，说明设备网络恢复。</p>
<p>烧录下吧，最终原因：</p>
<pre><code class="text language-text">APN未配置
→ 无法注册数据网络
→ CGATT=0
→ TCP连接失败
</code></pre>
<p><code>
</code></p>]]></description>
      <link>http://43.128.224.135:3003/article/9dcdc104-ab27-40d8-9656-761c154d311d</link>
      <guid>http://43.128.224.135:3003/article/9dcdc104-ab27-40d8-9656-761c154d311d</guid>
      <pubDate>Sun, 11 Aug 2024 03:41:53 GMT</pubDate>
      <category>其他</category>
    </item>
    <item>
      <title>Memory Analyzer Tool</title>
      <description><![CDATA[<h1 id="导出heap">导出Heap</h1>
<p>把Heap全部保存下来。形成heap.hprof文件,其中3240289是pid</p>
<pre><code class="code language-code">jmap -dump:live,format=b,file=heap.hprof 3240289
</code></pre>
<h1 id="heap-名词概念">Heap 名词概念</h1>
<p>Eclipse MAT（内存分析器工具）是分析 JVM 堆 Dump 文件的强大工具。当尝试分析内存相关的问题时，它非常方便。在 Eclipse MAT 内存分析的报告中会显示对象两种类型的 Heap 信息：</p>
<ul>
<li>Shallow heap</li>
<li>Retained heap</li>
</ul>
<p>我们主要讨论它们之间的区别，并探讨它们的计算方式。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBJ4..png" alt="mat_struct_heap" /></p>
<p>例如，假设你的应用程序具有这样的对象模型，如图 1 （SH指 shallow heap， b 指的是 byte）：</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBLQ..png" alt="mat_struct_examp_1" /></p>
<p>图1：内存中的对象</p>
<ul>
<li>对象 A 持有对象 B 和 C 的引用。</li>
<li>对象 B 持有对象 D 和 E 的引用。</li>
<li>对象 C 持有对象 F 和 G 的引用。</li>
</ul>
<p>另外，我们假设每个对象占用 10 个字节的内存。在这种场景下，我们开始分析Heap大小的过程。</p>
<h2 id="shallow-heap">Shallow Heap</h2>
<blockquote>
  <p>对象的 Shallow heap 是其自身在内存中的大小。说的直白点就是，在没有引用其他对象的情况下本身占用的内存大小，不包括它引用的对象。</p>
</blockquote>
<ul>
<li>针对数组类型（数组或集合）的对象，它的大小是数组元素对象的大小总和。</li>
<li>针对非数组类型的对象，它的大小就是对象与它所有的成员变量大小的总和。</li>
</ul>
<p>由于在我们的示例中，每个对象占用大约 10 个字节，因此每个对象的 Shallow heap 大小为 10 个字节。</p>
<h2 id="retained-heap">Retained Heap</h2>
<p>Retained heap: 当对象不再被引用时，垃圾回收器所能回收的总内存，包括对象自身所占据的内存，以及仅能够通过该对象引用到的其他对象所占据的内存： 对象本身的大小 + 引用的其他对象的大小。</p>
<p>换句话说，Retained Size就是当前对象被GC后，从Heap上总共能释放掉的内存。</p>
<h3 id="b-的-retained-heap-大小">B 的 Retained Heap 大小</h3>
<p>从图 1 中，可以注意到对象 B 持有对象 D 和 E 的引用。因此，如果对象 B 是从内存中被垃圾回收，则将不再有对对象 D 和 E 的引用。这意味着此时 D 和 E 也可以被垃圾收集。</p>
<blockquote>
  <p>Retained heap 指的就是在垃圾回收特定对象时将释放的内存量。</p>
</blockquote>
<p>因此，B 的保留堆大小为：= B 的 shallow heap 大小 + D 的 shallow heap 大小 + E 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes = 30 bytes。因此，B 对象的 Retained heap 大小为 30 字节</p>
<h3 id="c-的-retained-heap-大小">C 的 Retained Heap 大小</h3>
<p>对象 C 拥有对象 F 和 G 的引用。如果对象 C 是从内存中垃圾回收的，将不再持有对对象 F 和 G 的引用。这意味着此时 F 和 G 也可以被垃圾回收。因此，C 的 Retained Heap 大小为：= C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes = 30 bytes。因此，C 对象的 Retained heap 大小为 30 字节。</p>
<h3 id="a-的-retained-heap-大小">A 的 Retained Heap 大小</h3>
<p>对象 A 持有对象 B 和 C 的引用，而对象 B 和 C 又持有对对象 D、E 以及 F、G 的引用。因此，如果对象 A 是从内存中垃圾回收的，则将不再有对 B、C、D、E、F 和 G 对象的引用。</p>
<p>基于此理解，我们来计算下 A 的 Retained Heap 大小。</p>
<p>A 的 Retained Heap 大小为：= A 的 shallow heap 大小 + B 的 shallow heap 大小 + C 的 shallow heap 大小 + D 的 shallow heap 大小 + E 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes = 70 bytes。</p>
<p>最后我们可以得出，A 的 Retained heap 大小是 70 字节。</p>
<h3 id="d、e、f、g-的-retained-heap-大小">D、E、F、G 的 Retained Heap 大小</h3>
<p>D 的 Retained heap 大小与其 Shallow heap 大小相同，就是 10 个字节，因为 D 不持有对任何其他对象的引用。因此，如果 D 获得了垃圾回收，则不会从内存中删除其他的任何对象。同理，E、F 和 G 的 Retained heap 大小也只有 10 个字节。</p>
<p>图 2：对象的 Shallow and Retained Heap 大小（SH指 shallow heap， RH指 Retained heap，b 指的是 byte）</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBOC..png" alt="mat_struct_examp_1_total" /></p>
<h2 id="多一些引用的变数">多一些引用的变数</h2>
<p>为了进一步加深对 Shallow heap 和 Retained heap 的理解。在下面的示例中，让对象 H 开始持有对 B 的引用（注意：对象 B 已经被对象 A 引用了）。现在 A 和 H 都持有对象 B 的引用。在这种情况下， Retained heap 计算逻辑将会发生什么变化呢？</p>
<p>图3： 新增对 B 的引用</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBQY..png" alt="mat_struct_examp_2_total" /></p>
<p><strong>在这种情况下，对象 A 的 Retained heap 大小将从之前的 70 减小到 40 个字节</strong>。为什么？</p>
<p>如果对象 A 被垃圾回收了，则将仅会影响 C、F 和 G 对象的引用。因此，仅对象 C、F 和 G 将被垃圾回收。</p>
<p>另一方面，由于 H 持有对 B 的活动引用，因此对象 B、D 和 E 将继续存在于内存中。因此，即使 A 被垃圾回收，B、D 和 E 也不会从内存中删除。</p>
<p>于是可以计算出， A 的 Retained heap 大小为：= A 的 shallow heap 大小 + C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes = 40 bytes。</p>
<p>A 的 Retained heap 大小将变为 40 个字节。所有其他对象 Retained heap 大小将保持不变，因为它们的引用没有变化。</p>
<h1 id="其他名词">其他名词</h1>
<h2 id="out-going（查看对象为什么消耗内存-查看对象引用的其他对象）">out going（查看对象为什么消耗内存, 查看对象引用的其他对象）</h2>
<p>表示的是当前对象，引用了外部对象</p>
<p>可以使用右键 with outgoing references, 此类对象持有的其他对象。在 Attributes 中可以看到观察对象持有的成员。</p>
<h2 id="in-going（查看对象被谁引用）">in going（查看对象被谁引用）</h2>
<p>表示的是当前查看的对象，被外部应用。</p>
<p>可以使用右键 with ingoing references, 其他对象持有的此类对象。</p>
<h2 id="path-to-gc-root-（对象没被释放掉的引用）">path to GC root （对象没被释放掉的引用）</h2>
<p>到GC root的路径</p>
<p>这里有一个特殊的节点GC Roots，这就是 reference chain的起点。</p>
<h2 id="merge-shortest-path-to-gc-root-（对象没被释放掉的引用）">Merge Shortest path to GC root （对象没被释放掉的引用）</h2>
<p>到GC root的最短路径，右键 <code>merge shortest path to gc root</code> -&gt; <code>exclude all phantim/weak/soft etc. references</code>：查看此对象没被释放掉的原因，只保留强引用。</p>
<h2 id="histogram">Histogram</h2>
<p>堆内所有类的统计信息，包含类的实例数量和占用的空间。如果此处包含了自己的类就需要注意是否此类创建过多。</p>
<p>默认的大小单位是 Bytes，可以在 Window – Preferences—Memory Analyzer-- 菜单中设置单位。</p>
<p>可以通过filter搜索出自己的类，可以看到内存中共有32049817个Column对象，占用了13GB内存。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBTK..png" alt="mat_struct_Histogram" /></p>
<h2 id="dominator-tree">Dominator Tree</h2>
<p>列出了堆中最大的对象，并且引用此对象的其他对象。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBW6..png" alt="mat_struct_Dominator_Tree" /></p>
<p>如上图，可以很清晰的看到最大的对象是 XXXService @ 0x7fd1752c6f20 这个对象占用了大量内存，次对象被类加载器所引用。</p>
<h2 id="top-consumers">Top Consumers</h2>
<p>按类、对象、包分组，列出最消耗资源的类、对象、包。</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUBYS..png" alt="mat_struct_Top_Consumers" /></p>
<h2 id="duplicate-classes">Duplicate Classes</h2>
<p>对多个类加载器加载的类进行分析。</p>
<h2 id="leak-suspects">Leak Suspects</h2>
<p>内存泄漏报告和系统概述</p>
<p><img src="https://api.xiaolizhi.top/public/uploads/2026-08-07/9EZHQNHQAA1TII5U9PUC1E..png" alt="mat_leaks1" /></p>
<h2 id="top-components">Top Components</h2>
<p>列出内存用量超过堆总量1%的组件。</p>]]></description>
      <link>http://43.128.224.135:3003/article/443c94c0-fa1d-4a11-bd52-1f2ee9f29275</link>
      <guid>http://43.128.224.135:3003/article/443c94c0-fa1d-4a11-bd52-1f2ee9f29275</guid>
      <pubDate>Fri, 28 Apr 2023 07:39:29 GMT</pubDate>
      <category>后端</category>
    </item>
  </channel>
</rss>