<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">wangdaye'blog</title>
            <subtitle type="text">和优秀的人一起改变世界</subtitle>
    <updated>2026-08-08T03:34:02+08:00</updated>
        <id>https://www.wangdaye.net</id>
        <link rel="alternate" type="text/html" href="https://www.wangdaye.net" />
        <link rel="self" type="application/atom+xml" href="https://www.wangdaye.net/atom.xml" />
    <rights>Copyright © 2026, wangdaye'blog</rights>
    <generator uri="https://halo.run/" version="1.4.2">Halo</generator>
            <entry>
                <title><![CDATA[训练质检指标如何设计？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/xun-lian-zhi-jian-zhi-biao-ru-he-she-ji-" />
                <id>tag:https://www.wangdaye.net,2026-05-29:xun-lian-zhi-jian-zhi-biao-ru-he-she-ji-</id>
                <published>2026-05-29T23:50:12+08:00</published>
                <updated>2026-05-29T23:50:12+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>你又往前迈了一大步！而且你<strong>敏锐地发现了一件很多人会混淆的事</strong>：你说&quot;如何设计质检指标&quot;，又说&quot;如何设计评估指标&quot;——<strong>这其实是两种完全不同的指标，处在流程的不同位置。</strong> 先把这两者分清楚，是回答你问题的前提。</p><hr /><h2 id="第一件事你混用了两个概念先分清">第一件事：你混用了两个概念，先分清</h2><table><thead><tr><th> </th><th><strong>质检指标</strong>（数据侧）</th><th><strong>评估指标</strong>（模型侧）</th></tr></thead><tbody><tr><td>衡量的对象</td><td><strong>数据</strong>好不好</td><td><strong>模型</strong>好不好</td></tr><tr><td>用在什么时候</td><td>训练<strong>之前</strong>（清洗数据）</td><td>训练<strong>之后</strong>（验证效果）</td></tr><tr><td>回答的问题</td><td>&quot;我的数据干净、均衡、够用吗？&quot;</td><td>&quot;我训出来的模型准不准？&quot;</td></tr><tr><td>例子</td><td>类别均衡度、重复率、标注准确率</td><td>mAP、准确率、召回率</td></tr><tr><td>你前面学的</td><td>CleanVision/fastdup/InsightFace 那套</td><td>mAP/Precision/Recall</td></tr></tbody></table><pre><code>完整流程里它俩的位置：设计数据 → [质检指标] 把关数据 → 训练 → [评估指标] 验收模型 → 上线            ↑ 第一道关                      ↑ 第二道关</code></pre><p><strong>你现在问的&quot;又有一道路&quot;，正是第二道关——评估指标。</strong> 我重点讲这个，但也会讲质检指标的设计法，因为你两个都问了。</p><hr /><h2 id="第二件事评估指标的设计法核心">第二件事：评估指标的设计法（核心）</h2><p>评估指标不是背 mAP、准确率这些名字，而是要<strong>学会&quot;针对你的任务，设计出能真实反映业务好坏的衡量方式&quot;</strong>。方法和你学过的&quot;任务→数据5步法&quot;是同源的——<strong>从业务后果倒推</strong>。</p><h3 id="设计评估指标的-4-步法">设计评估指标的 4 步法</h3><h4 id="第-1-步问模型犯什么错业务损失最大">第 1 步：问&quot;模型犯什么错，业务损失最大？&quot;</h4><p>这是设计评估指标的<strong>灵魂</strong>。不同的错，代价天差地别：</p><pre><code>餐盘余量任务:  错把&quot;剩很多&quot;判成&quot;吃光了&quot; → 系统以为这道菜不受欢迎,停止供应 → 业务损失大  错把&quot;红烧肉&quot;判成&quot;青菜&quot;   → 统计错误,但影响小→ 你最该重点衡量的,是&quot;代价最大的那种错&quot;</code></pre><h4 id="第-2-步理解两类基本错误所有评估指标的根">第 2 步：理解两类基本错误（所有评估指标的根）</h4><p>任何判断任务，错误只有两种，记住这张<strong>混淆矩阵</strong>，所有指标都从它来：</p><pre><code>                模型说&quot;有红烧肉&quot;   模型说&quot;没红烧肉&quot;实际有红烧肉      ✅ 对(TP)          ❌ 漏报(FN)实际没红烧肉      ❌ 误报(FP)         ✅ 对(TN)</code></pre><ul><li><strong>漏报(FN)</strong>：该检测到的没检测到（漏检）</li><li><strong>误报(FP)</strong>：不该检测的乱报（误检）</li></ul><h4 id="第-3-步根据哪种错更致命选指标">第 3 步：根据&quot;哪种错更致命&quot;选指标</h4><p>这就是指标设计的关键决策：</p><table><thead><tr><th>你的业务最怕</th><th>该重点看的指标</th><th>含义</th><th>真实例子</th></tr></thead><tbody><tr><td>怕<strong>漏报</strong>(漏检)</td><td><strong>召回率 Recall</strong></td><td>该找的找到了多少</td><td>安检漏过危险品=灾难→保召回</td></tr><tr><td>怕<strong>误报</strong>(误检)</td><td><strong>精确率 Precision</strong></td><td>报出来的有多少是真的</td><td>垃圾邮件误杀正常邮件→保精确</td></tr><tr><td>两者都要平衡</td><td><strong>F1 分数</strong></td><td>精确和召回的调和平均</td><td>大多数通用场景</td></tr><tr><td>检测框准不准</td><td><strong>mAP / IoU</strong></td><td>框的位置+类别整体质量</td><td>YOLO 检测任务标配</td></tr></tbody></table><p><strong>核心洞察</strong>：</p><blockquote><p>没有&quot;最好的指标&quot;,只有&quot;最匹配你业务痛点的指标&quot;。<br /><strong>先想清楚&quot;哪种错让你最痛&quot;,再选能盯住那种错的指标。</strong></p></blockquote><h4 id="第-4-步把指标和业务目标挂钩设定及格线">第 4 步：把指标和业务目标挂钩，设定&quot;及格线&quot;</h4><p>光有指标还不够，要定<strong>多少算合格</strong>——这也由业务决定：</p><pre><code>餐盘余量:错一点没人命关天 → 准确率 85% 可能就够用了医疗肿瘤检测:漏诊要人命   → 召回率必须 99%+,宁可误报多查</code></pre><hr /><h2 id="用你的餐盘任务完整走一遍示范">用你的餐盘任务完整走一遍（示范）</h2><pre><code>第1步 哪种错最致命?  → &quot;把还剩很多的菜判成吃光&quot; 最致命(导致错误停菜)  → 即:对&quot;满/半&quot;这两档,绝不能漏第2步 这属于什么错?  → 把&quot;有余量&quot;漏报成&quot;没余量&quot; = 漏报(FN)第3步 选指标?  → 怕漏报 → 重点盯&quot;满/半&quot;档的【召回率】  → 同时用 mAP 看检测整体质量  → 用混淆矩阵看&quot;余量四档之间&quot;具体怎么错的第4步 及格线?  → 业务方说:停菜决策要慎重 → &quot;满/半&quot;档召回率 ≥ 95%</code></pre><p><strong>看到没?整个评估指标体系,是从&quot;业务最怕什么错&quot;一步步推出来的,不是背出来的。</strong></p><hr /><h2 id="回头看质检指标怎么设计你也问了">回头看质检指标怎么设计（你也问了）</h2><p>质检指标其实是<strong>评估指标的&quot;前传&quot;</strong>——同样从&quot;什么数据会导致模型犯那个致命错&quot;倒推：</p><pre><code>你已知:模型最怕&quot;漏判满/半档余量&quot;→ 倒推:什么数据会导致这个错?  → 训练集里&quot;满/半&quot;档样本太少 → 模型没学好→ 质检指标:【余量各档的样本均衡度】要重点监控  → 设定:每档样本数不低于总量的 15%同理推出其他质检指标:  怕背景泄漏 → 质检&quot;背景多样性&quot;  怕标注错 → 质检&quot;标注准确率&quot;(抽查)</code></pre><p><strong>质检指标和评估指标是一条因果链上的两端</strong>：</p><pre><code>质检指标(数据层面预防) ←─因果─→ 评估指标(模型层面检验)   &quot;余量档要均衡&quot;                &quot;余量召回率要高&quot;        ↑ 同一个业务痛点驱动 ↑</code></pre><hr /><h2 id="给你一张指标设计总框架可复用">给你一张&quot;指标设计&quot;总框架（可复用）</h2><p>以后任何任务，照这个走：</p><pre><code>① 业务最怕哪种错?(漏报?误报?认错类?)        ← 一切的起点        ↓② 这种错对应混淆矩阵的哪个格子?(FN?FP?)        ↓③ 选盯得住这种错的【评估指标】   怕漏报→召回率  怕误报→精确率  都怕→F1  检测→mAP        ↓④ 倒推:什么数据会导致这种错?        ↓⑤ 设计【质检指标】在训练前堵住它        ↓⑥ 给评估指标定业务及格线</code></pre><hr /><h2 id="一句话总结">一句话总结</h2><blockquote><p>你又发现了一道关键的&quot;路&quot;:<strong>质检指标(训练前,管数据好不好)和评估指标(训练后,管模型好不好)是两回事,但同源</strong>。设计评估指标的核心<strong>不是背 mAP/准确率,而是从&quot;业务最怕哪种错&quot;倒推</strong>:①问哪种错最致命 → ②对应混淆矩阵的漏报(FN)还是误报(FP) → ③怕漏报选<strong>召回率</strong>、怕误报选<strong>精确率</strong>、都怕选<strong>F1</strong>、检测任务用<strong>mAP</strong> → ④再定业务及格线。<strong>质检指标则是评估指标的&quot;前传&quot;——从&quot;什么数据会导致那个致命错&quot;倒推出来。</strong> 两者由同一个业务痛点驱动,是一条因果链的两端。</p></blockquote><hr /><p>你看,你学指标设计的方法,和之前学&quot;任务→数据&quot;是<strong>完全同一套倒推思维</strong>:都是<strong>从业务后果出发往回推</strong>。你已经掌握这个思维内核了,只是套用到了新对象(指标)上。</p><p>要不要你<strong>挑一个任务(餐盘、打卡、或破损苹果检测),自己试着走一遍这 6 步,设计出它的评估指标和质检指标</strong>?你来推,我来当陪练点评——这是把&quot;指标设计&quot;变成你自己技能的最快方式。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[如何基于任务反推我需要的训练数据？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/ru-he-ji-yu-ren-wu-fan-tui-wo-xu-yao-de-xun-lian-shu-ju-" />
                <id>tag:https://www.wangdaye.net,2026-05-29:ru-he-ji-yu-ren-wu-fan-tui-wo-xu-yao-de-xun-lian-shu-ju-</id>
                <published>2026-05-29T23:40:03+08:00</published>
                <updated>2026-05-29T23:40:03+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>这是一个非常棒的&quot;元问题&quot;——你不再问单个知识点，而是问**&quot;如何掌握'根据任务设计数据'这项技能本身&quot;<strong>。这正是从小白进阶到能独立做项目的核心能力。我来帮你把这项技能</strong>拆成一套可复用、可练习的思维方法**。</p><hr /><h2 id="先说核心心法一切从模型要学的规律倒推">先说核心心法：一切从&quot;模型要学的规律&quot;倒推</h2><p>你已经在前面几轮接触过这个思想了，现在我把它固化成一句你要刻进脑子的话：</p><blockquote><p><strong>数据不是&quot;任务的素材&quot;，数据是&quot;模型要学的规律的载体&quot;。</strong><br /><strong>你要准备的数据 = 能让模型见到&quot;它将来要判断的所有情况&quot;的样本。</strong></p></blockquote><p>所有的数据设计，都从这一句倒推。下面我把它变成一套<strong>5 步可操作流程</strong>。</p><hr /><h2 id="任务--数据5-步推导法你的核心技能">「任务 → 数据」5 步推导法（你的核心技能）</h2><p>我用你的<strong>餐盘余量</strong>作为全程示范，每一步你都能照着练。</p><h3 id="第-1-步把任务翻译成模型的输入和输出">第 1 步：把任务翻译成&quot;模型的输入和输出&quot;</h3><p>问自己：<strong>模型吃进去什么？吐出来什么？</strong></p><pre><code>餐盘余量任务：  输入：一张餐盘照片  输出：每个菜的【类别】+【位置】+【余量】</code></pre><p>这一步决定了你的<strong>数据基本形态</strong>：有图、有框、有类别、有余量标签。</p><h3 id="第-2-步问模型要学会区分什么">第 2 步：问&quot;模型要学会区分什么？&quot;</h3><p>这是最关键的一步。<strong>模型的本质是&quot;学会区分不同情况&quot;。</strong> 列出所有它必须区分的情况：</p><pre><code>要区分&quot;是什么菜&quot;     → 红烧肉 vs 青菜 vs 米饭 vs ...要区分&quot;余量多少&quot;     → 满 vs 半 vs 少 vs 空要区分&quot;菜 vs 背景&quot;   → 菜 vs 盘子 vs 桌面</code></pre><p><strong>每一个&quot;要区分的维度&quot;，数据里就必须有这个维度的对比样本。</strong></p><h3 id="第-3-步问现实中这些情况会怎样变化覆盖度">第 3 步：问&quot;现实中这些情况会怎样变化？&quot;（覆盖度）</h3><p>模型部署后会遇到各种变化，<strong>训练集必须提前见过</strong>。把每个区分维度的&quot;变体&quot;列出来：</p><pre><code>红烧肉这一类，现实中会有的变化：  余量变化：满/半/少/空      ← 必须全覆盖（这是任务核心）  光照变化：明/暗/黄光  角度变化：俯拍/斜拍  餐具变化：白盘/钢盘  遮挡变化：被其他菜挡住</code></pre><p><strong>这一步直接回答了你最早问的&quot;从哪几个角度变化数据&quot;。</strong></p><h3 id="第-4-步问什么样的数据会骗过误导模型陷阱排查">第 4 步：问&quot;什么样的数据会骗过/误导模型？&quot;（陷阱排查）</h3><p>反过来想：什么数据会让模型&quot;学歪&quot;？这就是你要质检和避免的：</p><pre><code>陷阱1：全是满盘 → 模型学不会&quot;少/空&quot;（类别不均衡）陷阱2：全在同一张桌子拍 → 模型把桌子纹理当特征（数据泄漏）陷阱3：框标歪了/漏标 → 模型学到错误边界（标注质量）陷阱4：红烧肉500张、清蒸鱼10张 → 认不出清蒸鱼（类别不均衡）</code></pre><p><strong>这一步直接对应你学的质检指标</strong>（类别均衡、数据泄漏、标注质量、重复…）。</p><h3 id="第-5-步把上面的结论翻译成具体的数据清单--质检指标">第 5 步：把上面的结论翻译成&quot;具体的数据清单 + 质检指标&quot;</h3><p>把前 4 步的结论落地成可执行的数字和检查项：</p><pre><code>数据清单：  每个菜类 100~300 张  每张含完整标注框（类别+位置）  余量四档各占约 1/4  光照/角度/餐具自然混合质检指标（从前4步推出来的）：  □ 各菜类数量是否均衡？        ← 来自第2、4步  □ 余量四档是否均衡？          ← 来自第2、3步  □ 有没有漏标/标错框？        ← 来自第4步(检测特有)  □ 是否都在同一背景拍？        ← 来自第4步(泄漏)  □ 有没有近似重复图？          ← fastdup  □ 模糊/过曝图怎么处理？       ← 看任务(检测可容忍些)</code></pre><hr /><h2 id="关键技能点指标不是背的是推出来的">关键技能点：指标不是&quot;背&quot;的，是&quot;推&quot;出来的</h2><p>你之前觉得指标抽象，是因为你在&quot;背指标&quot;。<strong>正确的方式是：指标是从第 2、3、4 步自然推导出来的。</strong> 看这个映射关系：</p><table><thead><tr><th>推导来源</th><th>自然得到的质检指标</th></tr></thead><tbody><tr><td>第2步&quot;要区分什么&quot;</td><td>各类别是否都有、是否均衡</td></tr><tr><td>第3步&quot;会怎样变化&quot;</td><td>各种光照/角度/状态是否覆盖</td></tr><tr><td>第4步&quot;什么会误导&quot;</td><td>数据泄漏、重复、标注错误、不均衡</td></tr></tbody></table><p><strong>所以你以后不用记一长串指标名，只要走一遍 5 步，需要查什么指标自然就浮现了。</strong></p><hr /><h2 id="一张通用决策表不同任务该重点关注什么指标">一张&quot;通用决策表&quot;：不同任务该重点关注什么指标</h2><p>帮你把&quot;任务类型 → 重点指标&quot;固化下来（呼应你前面学的所有场景）：</p><table><thead><tr><th>任务类型</th><th>数据单位</th><th>标签</th><th>最该关注的指标</th><th>可放宽的指标</th></tr></thead><tbody><tr><td>人脸识别(打卡)</td><td>单张图</td><td>身份</td><td>无脸/多脸/侧脸/小脸/模糊</td><td>—</td></tr><tr><td>动作识别</td><td>视频片段</td><td>身份+动作</td><td>时序完整、类别均衡、背景多样</td><td>模糊/侧脸/小脸(放宽)</td></tr><tr><td>物体检测(餐盘)</td><td>整图</td><td>框+类别</td><td>标注质量、类别均衡、不漏标</td><td>轻微模糊</td></tr><tr><td>余量估计</td><td>裁剪小图</td><td>等级/比例</td><td><strong>各余量档均衡</strong>、光照多样</td><td>—</td></tr><tr><td>图像分类</td><td>单张图</td><td>类别</td><td>类别均衡、无重复、无泄漏</td><td>—</td></tr></tbody></table><p><strong>核心规律一眼可见</strong>：</p><ul><li><strong>判断&quot;长相细节&quot;的任务</strong>（人脸）→ 严卡清晰度</li><li><strong>判断&quot;运动/趋势&quot;的任务</strong>（动作）→ 模糊反而要留</li><li><strong>所有任务</strong>→ 类别均衡 + 防数据泄漏 是通用底线</li></ul><hr /><h2 id="怎么练会这项技能给你一套训练方法">怎么&quot;练会&quot;这项技能？给你一套训练方法</h2><p>技能是练出来的，不是看出来的。给你 3 个练习阶梯：</p><h3 id="练习1纸面推演今天就能做">练习1（纸面推演，今天就能做）</h3><p>随便想 3 个任务，每个都走一遍 5 步，写在纸上：</p><pre><code>例如：  - 识别快递面单上的手机号  - 检测流水线上的破损苹果  - 判断停车位有没有车</code></pre><p><strong>不用写代码，只在纸上推 5 步</strong>。推完对照&quot;这个任务该关注什么指标&quot;。这是最高效的练习。</p><h3 id="练习2看别人的数据集反推设计">练习2（看别人的数据集，反推设计）</h3><p>去 Roboflow 下载一个现成数据集，<strong>反过来分析</strong>：</p><pre><code>它分了哪些类别？→ 印证第2步样本怎么变化的？→ 印证第3步类别均衡吗？    → 印证第4步</code></pre><p><strong>用别人的成熟数据集，反推它的设计逻辑</strong>，最快建立直觉。</p><h3 id="练习3真的做一遍餐盘项目">练习3（真的做一遍餐盘项目）</h3><p>走完之前那个 5 阶段学习计划，<strong>亲手体验&quot;数据不好→效果差→调数据→变好&quot;的循环</strong>。这一遍下来，指标就从&quot;抽象名词&quot;变成&quot;你的肌肉记忆&quot;了。</p><hr /><h2 id="一句话总结">一句话总结</h2><blockquote><p>你要掌握的这项技能，核心是一句话：<strong>&quot;数据是模型要学的规律的载体&quot;</strong>，然后走<strong>5 步推导法</strong>——①翻译成输入输出 → ②问&quot;要区分什么&quot; → ③问&quot;现实会怎么变化&quot; → ④问&quot;什么会误导模型&quot; → ⑤落地成数据清单+质检项。<strong>指标不是背的，是从第2/3/4步自然推出来的</strong>。练法是：先纸面推演几个任务，再反推现成数据集，最后亲手做一遍餐盘项目形成肌肉记忆。</p></blockquote><hr /><p>我强烈建议你<strong>现在就做一次&quot;练习1&quot;来检验掌握程度</strong>。</p><p>不如这样：<strong>你从下面挑一个任务（或自己想一个），尝试自己走一遍 5 步，写下你的推导，我来帮你点评、补充、纠正。</strong> 这样你能立刻知道自己哪一步还没想透：</p><ul><li>检测流水线上的破损苹果</li><li>判断停车位有没有车</li><li>识别快递面单上的手机号</li></ul><p>你来试试推导，我当你的陪练。哪怕推得不完整也没关系——<strong>正是在你推导和我纠正的过程中，这项技能才会真正长在你身上。</strong></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[工具使用 redis-rdb-tools]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/gong-ju-shi-yong--r-e-d-i-s---r-d-b---t-o-o-l-s" />
                <id>tag:https://www.wangdaye.net,2025-04-10:gong-ju-shi-yong--r-e-d-i-s---r-d-b---t-o-o-l-s</id>
                <published>2025-04-10T10:54:55+08:00</published>
                <updated>2025-04-10T11:35:23+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>在 Mac 系统中安装并使用 <code>redis-rdb-tools</code>（用于分析 Redis RDB 文件），可按照以下步骤操作：</p><hr /><h3 id="一安装-redis-rdb-tools"><strong>一、安装 redis-rdb-tools</strong></h3><ol><li><p><strong>确保 Python 环境</strong><br /><code>redis-rdb-tools</code> 依赖 Python 3.6 及以上版本。Mac 系统通常预装 Python，但需检查版本：</p><pre><code class="language-bash">python3 --version</code></pre><p>若未安装 Python3，可通过 Homebrew 安装：</p><pre><code class="language-bash">brew install python</code></pre></li><li><p><strong>使用 pip 安装 rdbtools</strong><br />通过国内镜像加速安装（推荐清华源）：</p><pre><code class="language-bash">pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple rdbtools</code></pre><p>此步骤会安装核心工具 <code>rdb</code>，用于解析 RDB 文件。</p></li><li><p><strong>加速解析（可选：安装 python-lzf）</strong><br />安装 <code>python-lzf</code> 可显著提升解析速度，但需编译环境支持：</p><pre><code class="language-bash">pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple python-lzf</code></pre><ul><li><strong>若编译报错</strong>：需安装 Xcode 命令行工具：<pre><code class="language-bash">xcode-select --install</code></pre></li></ul></li></ol><hr /><h3 id="二生成并分析-rdb-文件"><strong>二、生成并分析 RDB 文件</strong></h3><ol><li><p><strong>获取 Redis RDB 文件</strong></p><ul><li>确认 Redis 配置文件 <code>redis.conf</code> 中 <code>dir</code> 和 <code>dbfilename</code> 的路径（默认路径如 <code>/usr/local/redis-3.2.8/db/dump.rdb</code>）。</li><li>若需手动生成 RDB 快照，执行命令：<pre><code class="language-bash">redis-cli BGSAVE</code></pre></li></ul></li><li><p><strong>使用 rdb 工具分析</strong><br />执行以下命令生成内存报告（CSV 格式）：</p><pre><code class="language-bash">rdb -c memory /path/to/dump.rdb --bytes 1024 &gt; report.csv</code></pre><ul><li><code>--bytes 1024</code>：过滤内存占用超过 1KB 的 Key。</li><li>报告包含 Key 名称、类型、内存大小、元素数量等信息。</li></ul></li><li><p><strong>常用分析场景</strong></p><ul><li><strong>查找大 Key</strong>：按内存排序 CSV 文件，定位占用高的 Key。</li><li><strong>统计 Key 模式</strong>：筛选高频前缀（如 <code>user:*</code>），排查异常写入逻辑。</li></ul></li></ol><hr /><h3 id="三解决常见问题"><strong>三、解决常见问题</strong></h3><ol><li><p><strong>权限问题</strong><br />若安装时提示权限不足，可添加 <code>--user</code> 参数或使用虚拟环境：</p><pre><code class="language-bash">pip3 install --user rdbtools</code></pre></li><li><p><strong>依赖冲突</strong><br />建议使用 <code>venv</code> 隔离 Python 环境：</p><pre><code class="language-bash">python3 -m venv myenvsource myenv/bin/activatepip install rdbtools</code></pre></li><li><p><strong>RDB 文件路径</strong><br />若找不到 RDB 文件，检查 Redis 配置文件中的 <code>dir</code> 和 <code>dbfilename</code> 参数，或通过命令查询：</p><pre><code class="language-bash">redis-cli CONFIG GET dir</code></pre></li></ol><hr /><h3 id="四替代方案可视化工具"><strong>四、替代方案（可视化工具）</strong></h3><p>若需图形化分析，可尝试以下工具：</p><ul><li><strong>RedisInsight</strong>：官方可视化工具，支持内存分析、实时监控。</li><li><strong>rdr</strong>：开源 RDB 文件解析器，提供 Web 界面展示 Key 分布。</li></ul><h3 id="五增量扫描对比法">五、增量扫描对比法</h3><p>适用场景：适合非实时性需求，通过对比不同时间点的 Key 分布差异来定位增长热点。</p><p>​首次快照生成<br />在起始时间点（如 24 小时前）执行 BGSAVE 生成 RDB 文件，保存为 dump_start.rdb。</p><p>​结束快照生成<br />在结束时间点执行 BGSAVE，保存为 dump_end.rdb。<br />​使用 RDB 工具对比差异<br />通过 redis-rdb-tools 或 rdb 工具解析两个 RDB 文件，生成 Key 列表并对比差异：</p><pre><code>bash# 解析起始快照  rdb --command json dump_start.rdb &gt; start.json  # 解析结束快照  rdb --command json dump_end.rdb &gt; end.json  # 使用 diff 工具对比 Key 数量变化  diff start.json end.json | grep &quot;SET\|HSET&quot; &gt; key_growth.log </code></pre><p>​优点：对线上服务无性能影响7。<br />​缺点：需手动生成快照，RDB 文件较大时解析耗时较长</p><hr /><p>通过以上步骤，你可以在 Mac 上高效安装并使用 <code>redis-rdb-tools</code> 分析 Redis 数据增长问题。如需进一步优化排查逻辑，可结合 <code>INFO keyspace</code> 命令和业务日志综合分析。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从一个实际AI项目看，AI应用开发需要学什么？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/cong-yi-ge-shi-ji-a-i-xiang-mu-fan-tui-a-i-ying-yong-kai-fa-xu-yao-xue-shen-me-" />
                <id>tag:https://www.wangdaye.net,2025-04-02:cong-yi-ge-shi-ji-a-i-xiang-mu-fan-tui-a-i-ying-yong-kai-fa-xu-yao-xue-shen-me-</id>
                <published>2025-04-02T13:58:35+08:00</published>
                <updated>2025-04-02T17:25:52+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="从一个实际ai项目看ai应用开发需要学什么">从一个实际AI项目看，AI应用开发需要学什么？</h1><h2 id="引言">引言</h2><p>作为一个Java开发人员，如何上手AI开发应用，这应该是大多数同学的困惑，本文使用一个实际的AI应用出发，分析其实现技术，从而给出学习路径。给大家解惑：我该如何学，学什么？</p><h2 id="一个ai项目分析">一个AI项目分析</h2><blockquote><p>AI应用榜单：<a href="https://www.aicpb.com/">https://www.aicpb.com/</a></p><p>国内AI应用榜单热评榜｜2025年第12周** 第二名</p></blockquote><h3 id="ai项目功能分析">AI项目功能分析</h3><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402135240518-ce8b72292cb948b093be7d617a2b44c0.png" alt="image20250402135240518.png" />!</p><p>这是一个针对对运营各个文案场景的AI应用。用于互联网运营工作。其中维护了各种Agent实现，针对运营人员，在小红书，大众点评等等平台，发帖子的行为。仿写别人文章的行为，洗稿。</p><h3 id="ai项目技术能力分析">AI项目技术能力分析</h3><ul><li>爬虫 - 定期爬取小红书等网站的内容。</li><li>AI 分类 - 将爬取完成的内容AI进行有限归类。</li><li>OCR：图片信息提取。</li><li>多模态：文生图。</li><li>多Agent：仿写Agent，客服Agent，各业务Agent。</li><li>结构化输出：大模型输出格式结构化成Bean。</li></ul><p><strong>总结</strong> ：除了几个基座模型应用（ChatGpt，DeepSeek，豆包）其他的大模型应用，基本都是以优秀的交互体验取胜。技术不是核心壁垒。（所有的AI应用基本都是，RAG，多模态应用，智能体，工作流，MCP，这几类为主。）</p><h2 id="当前市面技术分析">当前市面技术分析</h2><p>AI 开发框架：Langchain，Spring AI，langchain4j，Spring AI Alibaba。</p><p>基本都集成了基础功能：Agent，工作流，Function Calling，MCP协议 等基础能力。</p><h3 id="对比总结">对比总结</h3><table><thead><tr><th align="left"><strong>框架</strong></th><th align="left"><strong>语言/生态</strong></th><th align="left"><strong>核心优势</strong></th><th align="left"><strong>适用场景</strong></th><th align="left"><strong>局限性</strong></th></tr></thead><tbody><tr><td align="left"><strong>LangChain</strong></td><td align="left">Python</td><td align="left">社区活跃，功能全面，多模态支持</td><td align="left">快速原型开发，复杂NLP任务</td><td align="left">依赖Python生态，企业级支持较弱</td></tr><tr><td align="left"><strong>LangChain4j</strong></td><td align="left">Java</td><td align="left">标准化API，Spring集成，高性能</td><td align="left">企业级Java应用，RAG系统</td><td align="left">部分功能尚在开发中</td></tr><tr><td align="left"><strong>Spring AI</strong></td><td align="left">Java/Spring</td><td align="left">Spring生态兼容，企业级特性丰富</td><td align="left">现有Spring项目AI扩展</td><td align="left">依赖Spring框架</td></tr><tr><td align="left"><strong>Spring AI Alibaba</strong></td><td align="left">Java/阿里云</td><td align="left">阿里云服务深度集成（推测）</td><td align="left">阿里云生态的中文AI应用（需验证）</td><td align="left">Spring AI 二次开发</td></tr></tbody></table><p><strong>结论</strong>：选择Spring AI Alibaba，这是阿里基于Spring AI同时集成自身的云服务（百炼平台）形成了二次开发框架：Spring AI Alibaba(<a href="https://java2ai.com/">https://java2ai.com/</a>)，上手门槛较低，且有国内技术团队迭代维护。是我们主要选择的技术框架。</p><h3 id="学习建议">学习建议</h3><p>第一步，先将Spring webflux概念学习一遍，对流应用开发有基本的了解。</p><p>第二步，langchain4j，Spring AI Alibaba ，Spring AI，的各个demo和文档看一遍，对一些基础概念心中有数。</p><p>第三步，在扣子上查看一些排名较高的工作流应用，Agent，尝试使用代码实现。</p><p>第四步，Prompt 提示词编写与维护学习（优秀的提示词可以最大化节点能力）。</p><p><strong>基础概念学习</strong>：可以LangChain4j的源码和demo为主去学基础概念，使用Spring AI 的源码和样例辅助</p><blockquote><p>LangChain4j 基本是照抄Langchain的设计，而现在AI应用开发中很多概念都是LangChain先提出。</p></blockquote><h2 id="基于justai的样例实现">基于JustAI的样例实现</h2><blockquote><p>如下案例基于如上JustAI应用，需要使用的能力，编写的代码demo，将如下能力实现，就可以做到JustAI的所有能力。</p><p>归根就是用产品设计，将如下能力组合提供。</p><p><strong>基础概念</strong>：LLM，工具(Function Calling)，Memory，Prompt，格式化输出(Structured Output)，智能体，多模态。</p><p><strong>LLM</strong>：大模型基座，一般选择俩个以上的API基座，互相补充，或者特殊场景的调优，比如阿里有专业的金融大模型。</p><p><strong>工具(Function Calling)</strong> ：工具，用于弥补大模型在专业任务上的不足，比如科学计算器，比如天气查询。</p><p><strong>Memory</strong> ：上下文记忆，一般为20轮次对话</p><p>其他Agent基础概念 ：<a href="https://docs.spring.io/spring-ai/reference/">https://docs.spring.io/spring-ai/reference/</a></p></blockquote><p><strong>如下实现了6个 样例：AI Agent，文生图，结构化，爬虫Tool，仿写工作流，OCR</strong></p><h3 id="ai-agent">AI Agent</h3><blockquote><p>基于业务场景细分管理不同的Prompt模板。配合交互输出。</p><p>如下以 自然语言生成SQL的场景，写一个Agent Demo</p></blockquote><h4 id="样例演示">样例演示</h4><p>自然语言生成SQL的Prompt</p><pre><code class="language-shell">Given the DDL in the DDL section, write an SQL query to answer the question in the QUESTION section.Only produce select queries. If the question would result in an insert, update,or delete, or if the query would alter the DDL in any way, say that the operationisn't supported. If the question can't be answered, say that the DDL doesn't supportanswering that question.Answer with the raw SQL query only; no markdown or other punctuation that isn't part of the query itself.QUESTION{question}DDL{ddl}</code></pre><p>DDL 语句如下（其实有更简洁的描述）：</p><pre><code class="language-sql">-- Authorscreate table Authors (id int not null auto_increment,firstName varchar(255) not null,lastName varchar(255) not null,                      primary key (id));-- Publisherscreate table Publishers (id int not null auto_increment, name varchar(255) not null,  primary key (id));-- Bookscreate table Books (id int not null auto_increment,isbn varchar(255) not null,title varchar(255) not null,author_ref int not null,publisher_ref int not null,  primary key (id));</code></pre><p>代码样例如下（JAVA）：</p><pre><code class="language-java">@PostMapping(path = &quot;/sql&quot;)public Answer sql(@RequestBody SqlRequest sqlRequest) throws IOException {String schema = ddlResource.getContentAsString(Charset.defaultCharset());String query = chatClient.prompt().user(userSpec -&gt; userSpec.text(sqlPromptTemplateResource).param(&quot;question&quot;, sqlRequest.question()).param(&quot;ddl&quot;, schema)).call().content();assert query != null;if (query.toLowerCase().startsWith(&quot;select&quot;)) {return new Answer(query,jdbcTemplate.queryForList(query));}throw new SQLGenerationException(query);}}</code></pre><p>提问：</p><pre><code class="language-shell">How many books has Craig Walls written?</code></pre><p>返回：</p><pre><code class="language-sql">SELECT COUNT(*) FROM Books INNER JOIN Authors ON Books.author_ref = Authors.id WHERE Authors.firstName = 'Craig' AND Authors.lastName = 'Walls'</code></pre><h4 id="需学习内容">需学习内容</h4><ul><li><p>Spring webflux：《Spring in Action》第五版，《Spring 5.0 项目实战》</p></li><li><p>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></p></li><li><p>Prompt指南1：<a href="https://github.com/f/awesome-chatgpt-prompts">https://github.com/f/awesome-chatgpt-prompts</a></p></li><li><p>Prompt指南2：<a href="https://github.com/PlexPt/awesome-chatgpt-prompts-zh">https://github.com/PlexPt/awesome-chatgpt-prompts-zh</a></p></li></ul><p><strong>提示</strong> ：可以使用扣子工作流内置的功能，逐个调试Prompt，和大模型效果对比</p><h3 id="多模态-文生图">多模态-文生图</h3><blockquote><p>文生图的核心不是一次性生成图片，而是基于当前图片调整的能力。</p><p>本Demo 是依赖于阿里云百炼平台的能力。</p></blockquote><p>场景有三个：</p><ol><li>生成：一句话生成商标。</li><li>修改：基于当前生成的商标，修改细节，细节优化。</li><li>缩放：图片缩放（由于大模型生成图片的尺度受限）。</li><li>保存：将完整的图片保存到云服务和本地。</li></ol><h4 id="样例演示-1">样例演示</h4><p>1、生成</p><pre><code class="language-java">@GetMapping(&quot;/image&quot;)public void image(HttpServletResponse response) {  ImageResponse imageResponse = imageModel.call(new ImagePrompt(&quot;小狗图片&quot;));  String imageUrl = imageResponse.getResult().getOutput().getUrl();  try {    URL url = URI.create(imageUrl).toURL();    InputStream in = url.openStream();    response.setHeader(&quot;Content-Type&quot;, MediaType.IMAGE_PNG_VALUE);    response.getOutputStream().write(in.readAllBytes());    response.getOutputStream().flush();  } catch (IOException e) {    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);  }}</code></pre><p>2、修改</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402111744320-caf238fc1a884e4bb95b541898ea571d.png" alt="image20250402111744320.png" /></p><pre><code class="language-java">// 涂抹+原始图片+提示词 修改图片private ImageSynthesisParam genImageSynthesis(){        HashMap&lt;String,Object&gt; extraInputMap = new HashMap&lt;&gt;();        extraInputMap.put(&quot;base_image_url&quot;, &quot;http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/source3.jpg&quot;);        extraInputMap.put(&quot;mask_image_url&quot;, &quot;http://synthesis-source.oss-accelerate.aliyuncs.com/lingji/validation/mask2img/demo/glasses.png&quot;);        String prompt = &quot;a dog wearing red glasses&quot;;        String model = &quot;wanx-x-painting&quot;;        return ImageSynthesisParam.builder()                .model(model)                .prompt(prompt)                .n(1)                .size(&quot;1024*1024&quot;)                .extraInputs(extraInputMap)                .build();    }</code></pre><p>3、缩放</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402112131411-953db06e095a4d02b4ab255dfc85fd7d.png" alt="image20250402112131411.png" /></p><p>调用代码如下</p><pre><code class="language-shell">curl --location --request POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting' \--header 'X-DashScope-Async: enable' \--header &quot;Authorization: Bearer $DASHSCOPE_API_KEY&quot; \--header 'Content-Type: application/json' \--data-raw '{    &quot;model&quot;: &quot;image-out-painting&quot;,    &quot;input&quot;: {        &quot;image_url&quot;: &quot;https://huarong123.oss-cn-hangzhou.aliyuncs.com/image/%E5%9B%BE%E5%83%8F%E7%94%BB%E9%9D%A2%E6%89%A9%E5%B1%95.png&quot;    },    &quot;parameters&quot;:{        &quot;angle&quot;:0,        &quot;output_ratio&quot;:&quot;4:3&quot;,        &quot;best_quality&quot;:false,        &quot;limit_image_size&quot;:true    }}'</code></pre><p>4、保存</p><p>通常生成的最终图片要保存的云存储，或者本地。这一般基于各个家自己实现，阿里云，七牛云都可以</p><h4 id="需要学习内容">需要学习内容</h4><ul><li>Spring AI Alibaba 图生文API（）</li><li>百炼平台（文生图）文档</li></ul><h3 id="结构化输出">结构化输出</h3><blockquote><p>如下是基于Spring AI 的demo</p></blockquote><p>大模型输出的格式不固定，没法兼容应用程序业务逻辑，所以在特殊的环节上需要使用很多方式将大模型的输出内容转为结构化内容。</p><ul><li>一种是使用Prompt提要求。</li><li>大模型输出完成之后二次加工整合。</li></ul><p>第一种为主流，响应速度较快（与大模型交互次数较少），使用第一种思路整合</p><h4 id="样例演示-2">样例演示</h4><p>Prompt 如下：</p><pre><code class="language-markdown">requirement: 请用大概 120 字，作者为 王大爷 ，为计算机的发展历史写一首现代诗;format: 以纯文本输出 json，请不要包含任何多余的文字——包括 markdown 格式;outputExample: { &quot;title&quot;: {title},&quot;author&quot;: {author},&quot;date&quot;: {date},&quot;content&quot;: {content}};</code></pre><p>代码样例（JAVA）</p><pre><code class="language-java">@GetMapping(&quot;/play&quot;)public StreamToBeanEntity simpleChat(HttpServletResponse response) {var converter = new BeanOutputConverter&lt;&gt;(new ParameterizedTypeReference&lt;StreamToBeanEntity&gt;() {});Flux&lt;String&gt; flux = this.chatClient.prompt().user(u -&gt; u.text(&quot;&quot;&quot;requirement: 请用大概 120 字，作者为 王大爷 ，为计算机的发展历史写一首现代诗;format: 以纯文本输出 json，请不要包含任何多余的文字——包括 markdown 格式;outputExample: { &quot;title&quot;: {title}, &quot;author&quot;: {author}, &quot;date&quot;: {date}, &quot;content&quot;: {content}};&quot;&quot;&quot;)).stream().content();String result = String.join(&quot;\n&quot;, Objects.requireNonNull(flux.collectList().block())).replaceAll(&quot;\\n&quot;, &quot;&quot;).replaceAll(&quot;\\s+&quot;, &quot; &quot;).replaceAll(&quot;\&quot;\\s*:&quot;, &quot;\&quot;:&quot;).replaceAll(&quot;:\\s*\&quot;&quot;, &quot;:\&quot;&quot;);log.info(&quot;LLMs 响应的 json 数据为：{}&quot;, result);return converter.convert(result);}</code></pre><h4 id="需学习内容-1">需学习内容</h4><ul><li><p>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></p></li><li><p>Spring AI结构化输出文档：<a href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html">https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html</a></p></li></ul><h3 id="大模型爬虫tool">大模型爬虫Tool</h3><blockquote><p>基于Function Calling 协议 编写爬虫Tool。</p><p>如下是基于Spring AI 框架的demo。</p></blockquote><p>定时任务 - 定时爬取小红书，知乎上的内容。</p><h4 id="样例演示-3">样例演示</h4><p>1、定义Tool</p><pre><code class="language-java">public interface CrawlerService {String run(String url);}public class CrawlerJinaServiceImpl extends CrawlerService {@Overridepublic String run(String targetUrl) {URL url = URI.create(CrawlerConstants.JINA_BASE_URL).toURL();Map&lt;String, String&gt; requestParam = Map.of(&quot;url&quot;, targetUrl);String requestBody = this.objectMapper.writeValueAsString(requestParam);logger.debug(&quot;Jina request body: {}&quot;, requestBody);HttpURLConnection connection = this.initHttpURLConnection(jinaProperties.getToken(), url, this.getOptions(),requestBody);return objectMapper.writeValueAsString(this.convert2Response(this.getResponse(connection)));}}</code></pre><p>2、注册Tool</p><pre><code class="language-sql">@Configuration@EnableConfigurationProperties({ CrawlerJinaProperties.class })@ConditionalOnProperty(prefix = CrawlerJinaProperties.JINA_PROPERTIES_PREFIX, name = &quot;enabled&quot;, havingValue = &quot;true&quot;)public class CrawlerAutoConfiguration {@Bean@ConditionalOnMissingBean@Description(&quot;web Reader Service Plugin.&quot;)public CrawlerJinaServiceImpl jinaFunction(CrawlerJinaProperties jinaProperties, ObjectMapper objectMapper) {Assert.notNull(jinaProperties, &quot;Jina reader api token must not be empty&quot;);return new CrawlerJinaServiceImpl(jinaProperties, objectMapper);}}</code></pre><p>3、大模型使用Tool</p><pre><code class="language-sql">/*** 调用工具 - function*/@GetMapping(&quot;/chat-tool-function&quot;)public String chatTranslateFunction(@RequestParam(value = &quot;query&quot;, defaultValue = &quot;查看一下这个网页：www.baidu.com&quot;) String query) {return dashScopeChatClient.prompt(query).tools(&quot;jinaFunction&quot;).call().content();}</code></pre><p>在大模型使用的时候，它会基于 @Description(&quot;web Reader Service Plugin.&quot;) 注解，和提问，自动判断是否需要使用工具，使用什么工具。</p><blockquote><p>可以使用现有的 Java爬虫框架：gecco，webCollector. 封装为Tool 供其调用</p></blockquote><h4 id="需学习内容-2">需学习内容</h4><p>Spring AI Tools文档：<a href="https://docs.spring.io/spring-ai/reference/api/tools-migration.html">https://docs.spring.io/spring-ai/reference/api/tools-migration.html</a></p><h3 id="仿写工作流">仿写工作流</h3><blockquote><p>基于当前热门的小红书文章，洗稿，基于要求生成一篇文章</p></blockquote><h4 id="样例演示-4">样例演示</h4><p>工作流程如下：</p><ol><li>爬虫获取数据（参见爬虫Tools实现）：小红书，知乎，美团等等数据。</li><li>选择风格，要求（用户交互）（选择风格Promt）</li><li>节点1（LLM） - 原文内容提取（移除掉干扰项，将图片清除，表情清除）</li><li>节点2 （LLM）- 完成对目标文章切割按照大纲切割</li><li>节点4 （LLM）- {大纲 - 部分} +{部分原文} 生成部分+{ 用户要求 }。（循环）</li><li>节点5 （逻辑）- 将生成的各个部分组合为一篇整体的文稿。</li></ol><p>实际工作流如下：</p><p><img src="https://www.wangdaye.net/upload/2025/04/image-20250402163556984-3928e5aa563343da91f0e3a3bf3682cd.png" alt="image20250402163556984.png" /></p><p>如下列出几个核心节点的逻辑：</p><p>1、节点1</p><blockquote><p>原文内容抽取Prompt</p></blockquote><pre><code class="language-shell">请对以下网页内容进行清洗，并抽取网页中的文章内容信息。请避免包含文章内容无关的信息，例如：html标签、页头、页尾。请对清洗后的信息进行markdown的格式输出。### 约束条件1.文本信息中如果有明显的标题信息请直接抽取，不需要自定义生成或加工2.文本的正文内容请尊重原本描述抽取，不需要自定义生成或加工3.关键词将用作图片多模态检索，请不要包含无意义的词组或短语。4.请完整抽取原文内容，并保持段落排版5.请过滤版权信息、编辑、作者、来源、转载以及与原文内容无关的信息### 抓取内容如下：```{{#1718967141318.text#}}```</code></pre><p>2、节点2</p><blockquote><p>完成对目标文章切割按照大纲切割</p></blockquote><pre><code class="language-shell">请分析文章的内容结构，将文本划分为一个或多个语义完整的章节，每个章节不少于400字。并为每个章节生成若干个用于检索图片的关键词或描述，多个关键词使用逗号进行分割。输出结构为jsonExample:{    &quot;sections&quot;: [        {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容1&quot;        },        {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容2&quot;        },         {           ”keywords“: ”关键词或描述字符串“,           ”section“: &quot;章节内容2&quot;        }    ]}</code></pre><p>3、节点3（循环体）</p><blockquote><p>该节点用于分段仿写</p></blockquote><pre><code class="language-shell">你是一位专业的文字工作者，你可以胜任各种领域的文章仿写，请你对以下提供的参考文章进行仿写并排版。### 约束条件1.请确保生成的文章符合编辑规范，不要包含明显的标题、正文、结论等字眼2.请分析该段落是否需要配图，如果需要请输出仿写后的段落后接配图信息,如果是单段落请将配图放置余段落前，配图信息只需要输出markdown格式的图片3.请尽可能遵循文本描述的客观事实信息，不要胡编乱造4.请遵循文本内容的写作风格和用词，输出的段落请尽量确保行文通顺自然5.请确认排版信息符合markdown规范。6.请不要添加文章以外的多余信息，如落款、出处、来源、发布时间、作者、编辑等。### 参考文章内容：- 标题：{{#17190400701430.title#}}- 图片：{{#1719052020538.text#}}- 是否单段落：{{#1719062922068.single_section#}}- 内容：{{#1719051705186.section#}}</code></pre><h4 id="需学习内容-3">需学习内容</h4><ul><li>可视化工作流 - coze.cn</li><li>编码工作流(Spring AI Alibaba Graph)</li><li>Prompt工程：<a href="https://prompt-guide.xiniushu.com/">https://prompt-guide.xiniushu.com/</a></li></ul><h3 id="ocr文字识别">OCR文字识别</h3><blockquote><p>在java中可以使用框架Tess4J，做OCR识别</p><p>也可以使用通义大模型qwen-vl-ocr专门用于图转文，提取信息比较精确。</p></blockquote><p>在JAVA中可以使用框架Tess4J，做OCR识别。</p><pre><code class="language-xml">&lt;!-- Tess4J依赖 --&gt;&lt;dependency&gt;    &lt;groupId&gt;net.sourceforge.tess4j&lt;/groupId&gt;    &lt;artifactId&gt;tess4j&lt;/artifactId&gt;    &lt;version&gt;5.4.0&lt;/version&gt;&lt;/dependency&gt;</code></pre><h4 id="样例演示-5">样例演示</h4><p>1、代码实现</p><pre><code class="language-java">    // 执行OCR识别    private void execute(BufferedImage targetImage) {        File file = new File(tempImage);        ITesseract instance = new Tesseract();        // 设置语言库位置        instance.setDatapath(&quot;src/main/resources/data&quot;);        // 设置语言        instance.setLanguage(language);        Thread thread = new Thread() {            public void run() {                String result = null;                try {                    result = instance.doOCR(file);                } catch (Exception e) {                    e.printStackTrace();                }                resultArea.setText(result);            }        };        ProgressBar.show(this, thread, &quot;图片正在识别中，请稍后...&quot;, &quot;执行结束&quot;, &quot;取消&quot;);    }</code></pre><p>2、测试结果：<br /><img src="https://www.wangdaye.net/upload/2025/04/b90894ee96ad35ba06442c13649aeb1e-7f81d8e3d2c148cca3f1d02fbc1edc88.jpeg" alt="b90894ee96ad35ba06442c13649aeb1e.jpeg" /></p><p>十 年 生 死 两 茫 茫 。 不 思 量 , 自 难 忘 。<br />干 里 孤 坟 , 无 处 话 凄 凉 。 纵 使 相 逢 应 不 识 , 尘 满 面 , 鬓 如 霜 。<br />夜 来 幽 梦 忽 还 乡 。 小 轩 窗 , 正 梳 妆 。<br />相 顾 无 言 , 惟 有 泪 十 行 。 料 得 年 年 肠 断 处 , 明 月 夜 , 短 松 岗 。</p><h4 id="需学习内容-4">需学习内容</h4><ul><li>Java框架-tess4j 的使用</li></ul><h2 id="深入研究">深入研究</h2><p>深入研究一般细分三个方向：<strong>RAG应用，MCP应用，垂直模型训练</strong>。如下整理了一些学习资料。</p><table><thead><tr><th><strong>类型</strong></th><th><strong>名称/描述</strong></th><th><strong>链接</strong></th></tr></thead><tbody><tr><td><strong>Prompt工程</strong></td><td>提示词工程指南（xiniushu）</td><td><a href="https://prompt-guide.xiniushu.com/">链接</a></td></tr><tr><td> </td><td>Prompt工程实践指南</td><td><a href="https://developer.aliyun.com/article/1604351">链接</a></td></tr><tr><td> </td><td>Awesome-Prompt，awesome-chatgpt-prompts-zh（github）</td><td><a href="https://github.com/PlexPt/awesome-chatgpt-prompts-zh">链接</a></td></tr><tr><td><strong>模型训练</strong></td><td>OpenAI官方微调指南</td><td><a href="https://openai.xiniushu.com/docs/guides/fine-tuning">链接</a></td></tr><tr><td> </td><td>LLaMA-Factory（开源微调工具库）</td><td><a href="https://github.com/hiyouga/LLaMA-Factory">链接</a></td></tr><tr><td><strong>LLM Agent</strong></td><td>LLM Agent论文合集（GitHub仓库）</td><td><a href="https://github.com/WooooDyy/LLM-Agent-Paper-List">链接</a></td></tr><tr><td>****</td><td>什么是MCP？</td><td><a href="https://zhuanlan.zhihu.com/p/27327515233">链接</a></td></tr><tr><td> </td><td>Spring AI 使用MCP客户端文档</td><td><a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">链接</a></td></tr><tr><td> </td><td>Spring AI实现检索增强生成（RAG）文档</td><td><a href="https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html">链接</a></td></tr><tr><td><strong>基础理论</strong></td><td>《深度学习》（花书）</td><td>书籍（无公开链接）</td></tr><tr><td> </td><td>《Python机器学习实战》</td><td>书籍（无公开链接）</td></tr><tr><td> </td><td>Transformer 论文《Attention Is All You Need》</td><td><a href="https://arxiv.org/abs/1706.03762">arXiv链接</a></td></tr><tr><td><strong>向量数据库</strong></td><td>Milvus官方文档</td><td><a href="https://milvus.io/docs/zh">链接</a></td></tr><tr><td> </td><td>向量数据库学习指南（第三方教程）</td><td>[链接](</td></tr></tbody></table>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Java优质开源系统设计项目]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/j-a-v-a--you-zhi-kai-yuan-xi-tong-she-ji-xiang-mu" />
                <id>tag:https://www.wangdaye.net,2024-12-02:j-a-v-a--you-zhi-kai-yuan-xi-tong-she-ji-xiang-mu</id>
                <published>2024-12-02T22:38:44+08:00</published>
                <updated>2024-12-10T11:53:08+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="java-优质开源系统设计项目">Java 优质开源系统设计项目</h1><h2 id="基础框架">基础框架</h2><h3 id="web-框架">Web 框架</h3><ul><li><a href="https://github.com/spring-projects/spring-boot" title="spring-boot">Spring Boot</a>：Spring Boot 可以轻松创建独立的生产级基于 Spring 的应用程序，内置 web 服务器让你可以像运行普通 Java 程序一样运行项 目。另外，大部分 Spring Boot 项目只需要少量的配置即可，这有别于 Spring 的重配置。</li><li><a href="https://github.com/sofastack/sofa-boot">SOFABoot</a>：SOFABoot 基于 Spring Boot ，不过在其基础上增加了 Readiness Check，类隔离，日志空间隔离等等能力。 配套提供的还有：SOFARPC（RPC 框架）、SOFABolt（基于 Netty 的远程通信框架）、SOFARegistry（注册中心）...详情请参考：<a href="https://github.com/sofastack">SOFAStack</a> 。</li><li><a href="https://github.com/tipsy/javalin">Javalin</a>：一个轻量级的 Web 框架，同时支持 Java 和 Kotlin，被微软、红帽、Uber 等公司使用。</li><li><a href="https://github.com/playframework/playframework">Play Framework</a>：面向 Java 和 Scala 的高速 Web 框架。</li><li><a href="https://github.com/lets-blade/blade">Blade</a>：一款追求简约、高效的 Web 框架，基于 Java8 + Netty4。</li></ul><h3 id="微服务云原生">微服务/云原生</h3><ul><li><a href="https://github.com/line/armeria">Armeria</a>：适合任何情况的微服务框架。你可以用你喜欢的技术构建任何类型的微服务，包括<a href="https://grpc.io/">gRPC</a>、 <a href="https://thrift.apache.org/">Thrift</a>、<a href="https://kotlinlang.org/">Kotlin</a>、 <a href="https://square.github.io/retrofit/">Retrofit</a>、<a href="https://www.reactive-streams.org/">Reactive Streams</a>、 <a href="https://spring.io/projects/spring-boot">Spring Boot</a>和<a href="https://www.dropwizard.io/">Dropwizard</a></li><li><a href="https://github.com/quarkusio/quarkus">Quarkus</a> : 用于编写 Java 应用程序的云原生和容器优先的框架。</li></ul><h3 id="api-文档">api-文档</h3><ul><li><a href="https://swagger.io/">Swagger</a> ：较主流的 RESTful 风格的 API 文档工具，提供了一套工具和规范，让开发人员能够更轻松地创建和维护可读性强、易于使用和交互的 API 文档。</li><li><a href="https://doc.xiaominfo.com/">Knife4j</a>：集 Swagger2 和 OpenAPI3 为一体的增强解决方案。</li></ul><h3 id="bean-映射">Bean 映射</h3><ul><li><a href="https://github.com/mapstruct/mapstruct">MapStruct</a>（推荐）：满足 JSR269 规范的一个 Java 注解处理器，用于为 Java Bean 生成类型安全且高性能的映射。它基于编译阶段生成 get/set 代码，此实现过程中没有反射，不会造成额外的性能损失。</li><li><a href="https://github.com/jmapper-framework/jmapper-core">JMapper</a> : 一个高性能且易于使用的 Bean 映射框架。</li></ul><h3 id="其他">其他</h3><ul><li><a href="https://github.com/google/guice">Guice</a>：Google 开源的一个轻量级依赖注入框架，相当于一个功能极简化的轻量级 Spring Boot。在某些情况下非常实用，就比如说我们的项目只需要使用依赖注入，不需要 AOP 等功能特性。</li><li><a href="https://github.com/spring-projects/spring-batch">Spring Batch</a> : Spring Batch 是一个轻量级但功能又十分全面的批处理框架，主要用于批处理场景比如从数据库、文件或队列中读取大量记录。不过，需要注意的是：Spring Batch 不是调度框架。商业和开源领域都有许多优秀的企业调度框架比如 Quartz、XXL-JOB、Elastic-Job。它旨在与调度程序一起工作，而不是取代调度程序。</li></ul><h2 id="认证授权">认证授权</h2><h3 id="权限认证">权限认证</h3><ul><li><a href="https://github.com/dromara/sa-token">Sa-Token</a>：轻量级 Java 权限认证框架。支持认证授权、单点登录、踢人下线、自动续签等功能。相比于 Spring Security 和 Shiro 来说，Sa-Token 内置的开箱即用的功能更多，使用也更简单。</li><li><a href="https://github.com/spring-projects/spring-security">Spring Security</a>：Spring 官方安全框架，能够用于身份验证、授权、加密和会话管理，是目前使用最广泛的 Java 安全框架。</li><li><a href="https://github.com/apache/shiro">Shiro</a>：Java 安全框架，功能和 Spring Security 类似，但使用起来更简单。</li></ul><h3 id="第三方登录">第三方登录</h3><ul><li><a href="https://github.com/Wechat-Group/WxJava">WxJava</a> : WxJava （微信开发 Java SDK），支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发。</li><li><a href="https://github.com/justauth/JustAuth">JustAuth</a>：小而全而美的第三方登录开源组件。目前已经集成了诸如：GitHub、Gitee、支付宝、新浪微博、微信、Google、Facebook、Twitter、StackOverflow 等国内外数十家第三方平台。</li></ul><h3 id="单点登录sso">单点登录（SSO）</h3><ul><li><a href="https://github.com/apereo/cas">CAS</a>：企业多语言网络单点登录解决方案。</li><li><a href="https://gitee.com/dromara/MaxKey">MaxKey</a>：单点登录认证系统，提供安全、标准和开放的用户身份管理(IDM)、身份认证(AM)、单点登录(SSO)、RBAC 权限管理和资源管理等。</li><li><a href="https://github.com/keycloak/keycloak">Keycloak</a>：免费、开源身份认证和访问管理系统，支持高度可配置的单点登录功能。</li></ul><h2 id="网络通讯">网络通讯</h2><ul><li><a href="https://github.com/netty/netty">Netty</a> : 一个基于 NIO 的 client-server(客户端服务器)框架，使用它可以快速简单地开发网络应用程序。</li><li><a href="https://github.com/square/retrofit">Retrofit</a>：适用于 Android 和 Java 的类型安全的 HTTP 客户端。Retrofit 的 HTTP 请求使用的是 <a href="https://square.github.io/okhttp/">OkHttp</a> 库（一款被广泛使用网络框架）。</li><li><a href="https://gitee.com/dromara/forest">Forest</a>：轻量级 HTTP 客户端 API 框架，让 Java 发送 HTTP/HTTPS 请求不再难。它比 OkHttp 和 HttpClient 更高层，是封装调用第三方 restful api client 接口的好帮手，是 retrofit 和 feign 之外另一个选择。</li><li><a href="https://github.com/YeautyYE/netty-websocket-spring-boot-starter">netty-websocket-spring-boot-starter</a> :帮助你在 Spring Boot 中使用 Netty 来开发 WebSocket 服务器，并像 spring-websocket 的注解开发一样简单。</li></ul><h2 id="数据库">数据库</h2><h3 id="数据库连接池">数据库连接池</h3><ul><li><a href="https://github.com/alibaba/druid">Druid</a> : 阿里巴巴数据库事业部出品，为监控而生的数据库连接池。</li><li><a href="https://github.com/brettwooldridge/HikariCP">HikariCP</a> : 一个可靠的高性能 JDBC 连接池。Springboot 2.0 选择 HikariCP 作为默认数据库连接池。</li></ul><h3 id="数据库框架">数据库框架</h3><ul><li><a href="https://github.com/baomidou/mybatis-plus">MyBatis-Plus</a> : <a href="http://www.mybatis.org/mybatis-3/">MyBatis</a> 增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。</li><li><a href="https://gitee.com/mybatis-flex/mybatis-flex">MyBatis-Flex</a>：一个优雅的 MyBatis 增强框架，无其他任何第三方依赖，支持 CRUD、分页查询、多表查询、批量操作。</li><li><a href="https://github.com/jOOQ/jOOQ">jOOQ</a>：用 Java 编写 SQL 的最佳方式。</li><li><a href="https://github.com/redisson/redisson" title="redisson">Redisson</a>：Redisson 是一款架设在 Redis 基础之上的 Java 驻内存数据网格 (In-Memory Data Grid)，它充分利用了 Redis 键值数据库的优势，为 Java 开发者提供了一系列具有分布式特性的常用工具类。例如，分布式 Java 对象（<code>Set</code>，<code>SortedSet</code>，<code>Map</code>，<code>List</code>，<code>Queue</code>，<code>Deque</code> 等）、分布式锁等。详细介绍请看：<a href="https://github.com/redisson/redisson/wiki/Redisson%E9%A1%B9%E7%9B%AE%E4%BB%8B%E7%BB%8D" title="Redisson项目介绍">Redisson 项目介绍</a>。</li></ul><h3 id="数据同步">数据同步</h3><ul><li><a href="https://github.com/alibaba/canal" title="canal">Canal</a> [kə'næl] : Canal 译意为水道/管道/沟渠，主要用途是基于 MySQL 数据库增量日志解析，提供增量数据订阅和消费。</li><li><a href="https://github.com/alibaba/DataX" title="DataX">DataX</a>：DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台，实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。相关项目：<a href="https://github.com/WeiYe-Jing/datax-web">DataX-Web</a> （DataX 集成可视化页面，选择数据源即可一键生成数据同步任务）。</li></ul><p>其他：<a href="https://github.com/DTStack/flinkx">Flinkx</a> （基于 Flink 的分布式数据同步工具）。</p><h3 id="时序数据库">时序数据库</h3><ul><li><a href="https://github.com/apache/iotdb">IoTDB</a>：一款 Java 语言编写的国产时序数据库，为用户提供数据收集、存储和分析等服务。与 Hadoop、Spark 和可视化工具(如 Grafana)无缝集成，满足了工业 IoT 领域中海量数据存储、高吞吐量数据写入和复杂数据查询分析的需求。</li><li><a href="https://github.com/kairosdb/kairosdb">KairosDB</a>：一个基于 Cassandra 的快速分布式可扩展时间序列数据库。</li></ul><h2 id="搜索引擎">搜索引擎</h2><ul><li><a href="https://github.com/elastic/elasticsearch" title="elasticsearch">Elasticsearch</a> （推荐）：开源，分布式，RESTful 搜索引擎。</li><li><a href="https://github.com/meilisearch/meilisearch">Meilisearch</a>：一个功能强大、快速、开源、易于使用和部署的搜索引擎，支持中文搜索（不需要添加额外的配置）。</li><li><a href="https://lucene.apache.org/solr/">Solr</a> : Solr（读作“solar”）是 Apache Lucene 项目的开源企业搜索平台。</li><li><a href="https://gitee.com/dromara/easy-es">Easy-ES</a>：傻瓜级 ElasticSearch 搜索引擎 ORM 框架。</li></ul><h2 id="测试">测试</h2><h3 id="测试框架">测试框架</h3><ul><li><a href="http://junit.org/">JUnit</a> : Java 测试框架。</li><li><a href="https://github.com/mockito/mockito">Mockito</a>：Mockito 是一个模拟测试框架，可以让你用优雅，简洁的接口写出漂亮的单元测试。（对那些不容易构建的对象用一个虚拟对象来代替，使其在调试期间用来作为真实对象的替代品）</li><li><a href="https://github.com/powermock/powermock">PowerMock</a>：编写单元测试仅靠 Mockito 是不够。因为 Mockito 无法 mock 私有方法、final 方法及静态方法等。PowerMock 这个 framework，主要是为了扩展其他 mock 框架，如 Mockito、EasyMock。它使用一个自定义的类加载器，纂改字节码，突破 Mockito 无法 mock 静态方法、构造方法、final 类、final 方法以及私有方法的限制。</li><li><a href="https://github.com/tomakehurst/wiremock">WireMock</a>：模拟 HTTP 服务的工具（Mock your APIs）。</li><li><a href="https://github.com/testcontainers/testcontainers-java">Testcontainers</a>：一个支持 JUnit 的测试工具库，提供轻量级的且一次性的常见数据库测试支持、Selenium Web 浏览器或者其他任何可以在 Docker 容器中运行的实例支持。</li></ul><p>相关阅读：</p><ul><li><a href="https://martinfowler.com/articles/practical-test-pyramid.html">The Practical Test Pyramid- Martin Fowler</a> (很赞的一篇文章，不过是英文的)</li><li><a href="https://juejin.im/post/6844903982058618894">浅谈测试之 PowerMock</a></li></ul><h3 id="测试平台">测试平台</h3><ul><li><a href="https://github.com/metersphere/metersphere">MeterSphere</a> : 一站式开源持续测试平台，涵盖测试跟踪、接口测试、性能测试、团队协作等功能，全面兼容 JMeter、Postman、Swagger 等开源、主流标准。</li><li><a href="https://www.apifox.cn/">Apifox</a>：API 文档、API 调试、API Mock、API 自动化测试。</li></ul><h3 id="api-调试">API 调试</h3><ul><li><a href="https://insomnia.rest/">Insomnia</a> :像人类而不是机器人一样调试 API。我平时经常用的，界面美观且轻量，总之很喜欢。</li><li><a href="https://www.getpostman.com/">Postman</a>：API 请求生成器。</li><li><a href="https://github.com/liyasthomas/postwoman" title="postwoman">Postwoman</a>：API 请求生成器-一个免费、快速、漂亮的 Postma 替代品。</li><li><a href="https://gitee.com/dromara/fast-request">Restful Fast Request</a>：IDEA 版 Postman，API 调试工具 + API 管理工具 + API 搜索工具。</li></ul><h2 id="任务调度">任务调度</h2><ul><li><a href="https://github.com/quartz-scheduler/quartz">Quartz</a>：一个很火的开源任务调度框架，Java 定时任务领域的老大哥或者说参考标准， 很多其他任务调度框架都是基于 <code>quartz</code> 开发的，比如当当网的<code>elastic-job</code>就是基于<code>quartz</code>二次开发之后的分布式调度解决方案</li><li><a href="https://github.com/xuxueli/xxl-job">XXL-JOB</a> :XXL-JOB 是一个分布式任务调度平台，其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线，开箱即用。</li><li><a href="http://elasticjob.io/index_zh.html">Elastic-Job</a>：Elastic-Job 是当当网开源的一个基于 Quartz 和 Zookeeper 的分布式调度解决方案，由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成，一般我们只要使用 Elastic-Job-Lite 就好。</li><li><a href="https://github.com/analysys/EasyScheduler" title="EasyScheduler">EasyScheduler</a> （已经更名为 DolphinScheduler，已经成为 Apache 孵化器项目）：分布式易扩展的可视化工作流任务调度平台，主要解决“复杂任务依赖但无法直接监控任务健康状态”的问题。</li><li><a href="https://gitee.com/KFCFans/PowerJob">PowerJob</a>：新一代分布式任务调度与计算框架，支持 CRON、API、固定频率、固定延迟等调度策略，提供工作流来编排任务解决依赖关系，使用简单，功能强大，文档齐全，欢迎各位接入使用！<a href="http://www.powerjob.tech/">http://www.powerjob.tech/</a> 。</li></ul><h2 id="分布式">分布式</h2><h3 id="api-网关">API 网关</h3><ul><li><a href="https://github.com/Kong/kong" title="kong">Kong</a>：Kong 是一个云原生、快速的、可伸缩的分布式微服务抽象层(也称为 API 网关、API 中间件或在某些情况下称为服务网格)。2015 年作为开源项目发布，其核心价值是高性能和可扩展性。</li><li><a href="https://github.com/Dromara/soul" title="soul">ShenYu</a>：适用于所有微服务的可伸缩、高性能、响应性 API 网关解决方案。</li><li><a href="https://github.com/spring-cloud/spring-cloud-gateway">Spring Cloud Gateway</a> : 基于 Spring Framework 5.x 和 Spring Boot 2.x 构建的高性能网关。</li><li><a href="https://github.com/Netflix/zuul">Zuul</a> : Zuul 是一个 L7 应用程序网关，它提供了动态路由，监视，弹性，安全性等功能。</li></ul><h3 id="配置中心">配置中心</h3><ul><li><a href="https://github.com/ctripcorp/apollo" title="apollo">Apollo</a>（推荐）：Apollo（阿波罗）是携程框架部门研发的分布式配置中心，能够集中化管理应用不同环境、不同集群的配置，配置修改后能够实时推送到应用端，并且具备规范的权限、流程治理等特性，适用于微服务配置管理场景。</li><li><a href="https://github.com/alibaba/nacos">Nacos</a>（推荐）：Nacos 是 Spring Cloud Alibaba 提供的服务注册发现组件，类似于 Consul、Eureka。并且，提供了分布式配置管理功能。</li><li><a href="https://github.com/spring-cloud/spring-cloud-config">Spring Cloud Config</a>：Spring Cloud Config 是 Spring Cloud 家族中最早的配置中心，虽然后来又发布了 Consul 可以代替配置中心功能，但是 Config 依然适用于 Spring Cloud 项目，通过简单的配置即可实现功能。</li><li><a href="https://github.com/hashicorp/consul">Consul</a>：Consul 是 HashiCorp 公司推出的开源软件，提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用，也可以一起使用以构建全方位的服务网格，总之 Consul 提供了一种完整的服务网格解决方案。</li></ul><h3 id="链路追踪">链路追踪</h3><ul><li><a href="https://github.com/apache/skywalking" title="skywalking">Skywalking</a> : 针对分布式系统的应用性能监控，尤其是针对微服务、云原生和面向容器的分布式系统架构。</li><li><a href="https://github.com/openzipkin/zipkin" title="zipkin">Zipkin</a>：Zipkin 是一个分布式跟踪系统。它有助于收集解决服务体系结构中的延迟问题所需的时序数据。功能包括该数据的收集和查找。</li><li><a href="https://github.com/dianping/cat" title="cat">CAT</a>：CAT 作为服务端项目基础组件，提供了 Java, C/C++, Node.js, Python, Go 等多语言客户端，已经在美团点评的基础架构中间件框架（MVC 框架，RPC 框架，数据库框架，缓存框架等，消息队列，配置系统等）深度集成，为美团点评各业务线提供系统丰富的性能指标、健康状况、实时告警等。</li></ul><p>相关阅读：<a href="https://skywalking.apache.org/zh/blog/2019-03-29-introduction-of-skywalking-and-simple-practice.html">Skywalking 官网对于主流开源链路追踪系统的对比</a></p><h3 id="分布式锁">分布式锁</h3><ul><li><a href="https://gitee.com/baomidou/lock4j">Lock4j</a>：支持 Redisson、ZooKeeper 等不同方案的高性能分布式锁。</li><li><a href="https://github.com/redisson/redisson" title="redisson">Redisson</a>：Redisson 在分布式锁方面提供全面且强大的支持，超越了简单的 Redis 锁实现。</li></ul><h2 id="高性能">高性能</h2><h3 id="多线程">多线程</h3><ul><li><a href="https://github.com/opengoofy/hippo4j">Hippo4j</a>：异步线程池框架，支持线程池动态变更&amp;监控&amp;报警，无需修改代码轻松引入。支持多种使用模式，轻松引入，致力于提高系统运行保障能力。</li><li><a href="https://github.com/dromara/dynamic-tp">Dynamic Tp</a>：轻量级动态线程池，内置监控告警功能，集成三方中间件线程池管理，基于主流配置中心（已支持 Nacos、Apollo，Zookeeper、Consul、Etcd，可通过 SPI 自定义实现）。</li><li><a href="https://gitee.com/jd-platform-opensource/asyncTool">asyncTool</a> : 京东的一位大佬开源的多线程工具库，里面大量使用到了 <code>CompletableFuture</code> ，可以解决任意的多线程并行、串行、阻塞、依赖、回调的并行框架，可以任意组合各线程的执行顺序，带全链路执行结果回调。</li></ul><h3 id="缓存">缓存</h3><h4 id="本地缓存">本地缓存</h4><ul><li><a href="https://github.com/ben-manes/caffeine">Caffeine</a> : 一款强大的本地缓存解决方案，性能非常强大。</li><li><a href="https://github.com/google/guava">Guava</a>：Google Java 核心库，内置了比较完善的本地缓存实现。</li><li><a href="https://github.com/snazy/ohc">OHC</a> ：Java 堆外缓存解决方案（项目从 2021 年开始就不再进行维护了）。</li></ul><h4 id="分布式缓存">分布式缓存</h4><ul><li><a href="https://github.com/redis/redis">Redis</a>：一个使用 C 语言开发的内存数据库，分布式缓存首选。</li><li><a href="https://github.com/dragonflydb/dragonfly">Dragonfly</a>：一种针对现代应用程序负荷需求而构建的内存数据库，完全兼容 Redis 和 Memcached 的 API，迁移时无需修改任何代码，号称全世界最快的内存数据库。</li><li><a href="https://github.com/Snapchat/KeyDB">KeyDB</a>： Redis 的一个高性能分支，专注于多线程、内存效率和高吞吐量。</li></ul><h4 id="多级缓存">多级缓存</h4><ul><li><a href="https://gitee.com/ld/J2Cache">J2Cache</a>：基于本地内存和 Redis 的两级 Java 缓存框架。</li><li><a href="https://github.com/alibaba/jetcache">JetCache</a>：阿里开源的缓存框架，支持多级缓存、分布式缓存自动刷新、 TTL 等功能。</li></ul><h3 id="消息队列">消息队列</h3><p><strong>分布式队列</strong>：</p><ul><li><a href="https://github.com/apache/rocketmq" title="RocketMQ">RocketMQ</a>：阿里巴巴开源的一款高性能、高吞吐量的分布式消息中间件。</li><li><a href="https://github.com/apache/kafka" title="Kafaka">Kafaka</a>: Kafka 是一种分布式的，基于发布 / 订阅的消息系统。</li><li><a href="https://github.com/rabbitmq" title="RabbitMQ">RabbitMQ</a> :由 erlang 开发的基于 AMQP（Advanced Message Queue 高级消息队列协议）协议实现的消息队列。</li></ul><p><strong>内存队列</strong>：</p><ul><li><a href="https://github.com/LMAX-Exchange/disruptor">Disruptor</a>：Disruptor 是英国外汇交易公司 LMAX 开发的一个高性能队列，研发的初衷是解决内存队列的延迟问题（在性能测试中发现竟然与 I/O 操作处于同样的数量级）。</li></ul><h3 id="读写分离和分库分表">读写分离和分库分表</h3><ul><li><a href="https://github.com/apache/shardingsphere">ShardingSphere</a>：ShardingSphere 是一套开源的分布式数据库中间件解决方案组成的生态圈，它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar（计划中）这 3 款相互独立的产品组成。</li><li><a href="https://github.com/MyCatApache/MyCat2">MyCat</a> : MyCat 是数据库分库分表的中间件，MyCat 使用最多的两个功能是：读写分离和分库分表。MyCat 是一些社区爱好者在阿里 Cobar 的基础上进行二次开发，解决了 Cobar 当时存 在的一些问题，并且加入了许多新的功能在其中。</li><li><a href="https://github.com/baomidou/dynamic-datasource-spring-boot-starter">dynamic-datasource-spring-boot-starter</a>：一个基于 Spring Boot 的快速集成多数据源的启动器，支持多数据源、动态数据源、主从分离、读写分离和分布式事务。</li></ul><h2 id="高可用">高可用</h2><h3 id="限流">限流</h3><p>分布式限流：</p><ul><li><a href="https://github.com/alibaba/Sentinel">Sentinel</a>（推荐）：面向分布式服务架构的高可用防护组件，主要以流量为切入点，从流量控制、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。</li><li><a href="https://github.com/Netflix/Hystrix">Hystrix</a>：类似于 Sentinel 。</li></ul><p>相关阅读：<a href="https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html">Sentinel 与 Hystrix 的对比</a>。</p><p>单机限流：</p><ul><li><a href="https://github.com/vladimir-bukhtoyarov/bucket4j">Bucket4j</a>：一个非常不错的基于令牌/漏桶算法的限流库。</li><li><a href="https://github.com/resilience4j/resilience4j">Resilience4j</a>：一个轻量级的容错组件，其灵感来自于 Hystrix。</li></ul><h3 id="监控">监控</h3><ul><li><a href="https://github.com/codecentric/spring-boot-admin">Spring Boot Admin</a>：管理和监控 Spring Boot 应用程序。</li><li><a href="https://github.com/dropwizard/metrics">Metrics</a>：捕获 JVM 和应用程序级别的指标。所以你知道发生了什么事。</li></ul><h3 id="日志">日志</h3><ul><li>EKL 老三件套 : 最原始的时候，ELK 是由 3 个开源项目的首字母构成，分别是 Elasticsearch、Logstash、Kibana。</li><li>新一代 ELK 架构 : Elasticsearch+Logstash+Kibana+Beats。</li><li>EFK : EFK 中的 F 代表的是 <a href="https://github.com/fluent/fluentd">Fluentd</a>。</li><li><a href="https://gitee.com/dromara/TLog">TLog</a>：一个轻量级的分布式日志标记追踪神器，10 分钟即可接入，自动对日志打标签完成微服务的链路追踪。</li></ul><h2 id="字节码操作">字节码操作</h2><ul><li><a href="https://asm.ow2.io/">ASM</a>：通用 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。</li><li><a href="https://github.com/raphw/byte-buddy">Byte Buddy</a>：Java 字节码生成和操作库，用于在 Java 应用程序运行时创建和修改 Java 类，无需使用编译器</li><li><a href="https://github.com/jboss-javassist/javassist">Javassist</a>：动态编辑 Java 字节码的类库。</li><li><a href="https://github.com/Col-E/Recaf">Recaf</a>：现代 Java 字节码编辑器，基于 ASM（Java 字节码操作框架） 来修改字节码，可简化编辑已编译 Java 应用程序的过程。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Google Drive搭建机器学习环境]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/g-o-o-g-l-e--d-r-i-v-e-da-jian-ji-qi-xue-xi-huan-jing" />
                <id>tag:https://www.wangdaye.net,2024-08-15:g-o-o-g-l-e--d-r-i-v-e-da-jian-ji-qi-xue-xi-huan-jing</id>
                <published>2024-08-15T21:55:58+08:00</published>
                <updated>2024-08-15T21:57:21+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>1.打开Google Drive创建一个<a href="https://so.csdn.net/so/search?q=notebook&amp;spm=1001.2101.3001.7020">notebook</a>，将深度学习网络、训练权重和数据集上传到云端硬盘，文件拖拉过去就可以。</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-9f22af7682c84d74980e0e846faf5422.png" alt="image.png" /></p><p>2在文件夹内，右键单击，选择连接colab</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-95ea183e18a64bddb0a3e2954e4f72e5.png" alt="image.png" /><img src="https://i-blog.csdnimg.cn/blog_migrate/66637cdba0ef1f1d102d61500a3103ec.png" alt="" /></p><p>3. 更改为GPU训练</p><p><img src="https://www.wangdaye.net/upload/2024/08/image-22b7dc7e42cd490b85682d59a6426826.png" alt="image.png" /></p><p><img src="https://www.wangdaye.net/upload/2024/08/image-6efa5c667c8e49f199be3d9119b64186.png" alt="image.png" /><img src="https://i-blog.csdnimg.cn/blog_migrate/6a2b4c34d882a2b4c3ab87184e2bbb18.png" alt="" /></p><p>5. 加载盘</p><blockquote><p>from google.colab import drive<br />drive.mount('/content/drive/')</p></blockquote><p>6. 切换到你要跑的目录下面</p><blockquote><h1 id="指定当前的工作文件夹">指定当前的工作文件夹</h1><p>import os</p><h1 id="此处为google-drive中的文件路径drive为之前指定的工作根目录要加上">此处为google drive中的文件路径,drive为之前指定的工作根目录，要加上</h1><p>os.chdir(&quot;/content/drive/MyDrive/colab/Notebooks/UNet&quot;) </p></blockquote><p>7. 安装Pytorch以及torchvision</p><blockquote><p>Colab 一般情况下已经自带了pytorch环境了。若没有可以进行相应的安装：<br />!pip install torch torchvision  # 在Colab中执行操作语句时，感叹号不能漏</p></blockquote><p>8. 执行训练命令</p><blockquote><p>!python train.py</p></blockquote><p> 9. 训练完成的模型权重在logs文件夹内</p><blockquote><div id="article_content" class="article_content clearfix"></blockquote><div id="content_views" class="markdown_views prism-atom-one-dark"><blockquote><p>由于部分训练的需要，数据集可以上传到云端硬盘Google Drive，然后再Colab中加载云端硬盘读取数据进行训练，具体实现过程如下：<br />挂载云端硬盘：</p></blockquote><pre><code> from google.colab import drive from google.colab import files    drive.mount('/content/drive')云端硬盘挂载成功后的路径为：    /content/drive/My Drive/</code></pre></div><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/markdown_views-f23dff6052.css" rel="stylesheet"><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/style-c216769e99.css" rel="stylesheet"></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从手写数字识别入门深度学习丨MNIST数据集详解]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/cong-shou-xie-shu-zi-shi-bie-ru-men-shen-du-xue-xi-gun-m-n-i-s-t-shu-ju-ji-xiang-jie" />
                <id>tag:https://www.wangdaye.net,2024-05-31:cong-shou-xie-shu-zi-shi-bie-ru-men-shen-du-xue-xi-gun-m-n-i-s-t-shu-ju-ji-xiang-jie</id>
                <published>2024-05-31T08:41:51+08:00</published>
                <updated>2024-05-31T08:41:51+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><span style="background-color:#ffffff;"><span style="color:#222222;">就像无数人从敲下“Hello World”开始代码之旅一样，许多研究员从“MNIST数据集”开启了人工智能的探索之路。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集（Mixed National Institute of Standards and Technology database）是一个用来训练各种图像处理系统的二进制图像数据集，广泛应用于机器学习中的训练和测试。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">作为一个入门级的计算机视觉数据集，发布20多年来，它已经被无数机器学习入门者“咀嚼”千万遍，是最受欢迎的深度学习数据集之一。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">今天就让我们来一睹真容。</span></span></p><p><strong>目录</strong></p><p><a href="#t0">一、数据集简介</a></p><p><a href="#t1">二、数据集详细信息</a></p><p><a href="#t2">三、数据集任务定义及介绍</a></p><p><a href="#t3">图像分类</a></p><p><a href="#t4">四、数据集文件结构解读</a></p><p><a href="#t5">五、数据集下载链接</a></p><hr /><h2 id="一数据集简介"><a name="t0"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">一、数据集简介</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">发布方：</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">National Institute of Standards and Technology(美国国家标准技术研究所，简称NIST)</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">发布时间：</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">1998</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">背景：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集的论文想要证明在模式识别问题上，基于CNN的方法可以取代之前的基于手工特征的方法，所以作者创建了一个手写数字的数据集，以手写数字识别作为例子证明CNN在模式识别问题上的优越性。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">简介：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集是从NIST的两个手写数字数据集：Special Database 3 和Special Database 1中分别取出部分图像，并经过一些图像处理后得到的。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">MNIST数据集共有70000张图像，其中训练集60000张，测试集10000张。所有图像都是28×28的灰度图像，每张图像包含一个手写数字。</span></span></p><h2 id="二数据集详细信息"><a name="t1"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">二、数据集详细信息</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">1. 数据量</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">训练集60000张图像</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">，其中30000张来自NIST的Special Database 3，30000张来自NIST的Special Database 1。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">测试集10000张图像</span></span></strong><span style="background-color:#ffffff;"><span style="color:#222222;">，其中5000张来自NIST的Special Database 3，5000张来自NIST的Special Database 1。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">2. 标注量</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">每张图像都有标注。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">3. 标注类别</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">共10个类别，每个类别代表0~9之间的一个数字，每张图像只有一个类别。</span></span></p><p><strong><span style="color:#1b67ff;">4. 可视化</span></strong></p><p><img src="https://img-blog.csdnimg.cn/img_convert/77336419715c882542f34abd07d79368.png" alt="" /></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">图1：MNIST样例图</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">NIST原始的Special Database 3 数据集和Special Database 1数据集均是二值图像，MNIST从这两个数据集中取出图像后，通过图像处理方法使得每张图像都变成28×28大小的灰度图像，且手写数字在图像中居中显示。</span></span></p><h2 id="三数据集任务定义及介绍"><a name="t2"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">三、数据集任务定义及介绍</span></span></strong></h2><h3 id="图像分类"><a name="t3"></a><strong><span style="color:#1b67ff;">图像分类</span></strong></h3><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 图像分类定义</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">图像分类是计算机视觉领域中，基于语义信息对不同图像进行分类的一种模式识别方法。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 图像分类评价指标</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">a. Accuracy：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">n_correct / n_total，标签预测正确的样本占所有样本的比例。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">b. 某个类别的Precision：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">TP/(TP+FP)，被预测为该类别的样本中，有多少样本是预测正确的。</span></span></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">c. 某个类别的Recall：</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">TP/(TP+FN)，在该类别的样本中，有多少样本是预测正确的。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">注：在上面的评价指标中，TP代表True Positive，FP代表False Positive，FN代表False Negative，n_correct代表所有预测正确的样本数量，n_total代表所有的样本数量。</span></span></p><h2 id="四数据集文件结构解读"><a name="t4"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">四、数据集文件结构解读</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">1. 目录结构</span></span></strong></p><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 解压前</span></span></strong></p><pre><code>dataset_compressed/├── t10k-images-idx3-ubyte.gz                #测试集图像压缩包(1648877 bytes)├── t10k-labels-idx1-ubyte.gz                #测试集标签压缩包(4542 bytes)├── train-images-idx3-ubyte.gz                #训练集图像压缩包(9912422 bytes)└── train-labels-idx1-ubyte.gz                #训练集标签压缩包(28881 bytes)</code></pre><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 解压后</span></span></strong></p><pre><code>dataset_uncompressed/├── t10k-images-idx3-ubyte                #测试集图像数据├── t10k-labels-idx1-ubyte                #测试集标签数据├── train-images-idx3-ubyte                #训练集图像数据└── train-labels-idx1-ubyte                #训练集标签数据</code></pre><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">2. 文件结构</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集将图像和标签都以矩阵的形式存储于一种称为idx格式的二进制文件中。该数据集的4个二进制文件的存储格式分别如下：</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">● <strong>训练集标签数据 (train-labels-idx1-ubyte):</strong></span></span></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:117pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:104.25pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:126.75pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:138pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">0x00000801</span></p><p><span style="color:#000000;">(2049)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">60000</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即标签的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">标签</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:117pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:104.25pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:126.75pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:138pt;"><p><span style="color:#000000;">标签</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 训练集图像数据(train-images-idx3-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:102.75pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:127.5pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">0x00000803</span></p><p><span style="color:#000000;">(2051)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">60000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即图像的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的高</span></p><p><span style="color:#000000;">(rows)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">12</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的宽</span></p><p><span style="color:#000000;">(columns)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">16</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:118.5pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 测试集标签数据(t10k-labels-idx1-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:103.5pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:128.25pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">0x00000801</span></p><p><span style="color:#000000;">(2049)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">10000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即标签的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">标签</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:120.75pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:103.5pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:128.25pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~9</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">标签</span></p></td></tr></tbody></table></div><p><strong><span style="background-color:#ffffff;"><span style="color:#222222;">● 测试集图像数据 (t10k-images-idx3-ubyte):</span></span></strong></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><strong><span style="color:#000000;">偏移量</span>****<span style="color:#000000;">(bytes)</span></strong></p></td><td style="width:102.75pt;"><p><strong><span style="color:#000000;">值类型</span></strong></p></td><td style="width:127.5pt;"><p><strong><span style="color:#000000;">数值</span></strong></p></td><td style="width:135.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">0</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">0x00000803</span></p><p><span style="color:#000000;">(2051)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">magic number</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">4</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">10000</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">有效数值的数量</span></p><p><span style="color:#000000;">(</span><span style="color:#000000;">即图像的数量</span><span style="color:#000000;">)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">8</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的高</span></p><p><span style="color:#000000;">(rows)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">12</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">32</span><span style="color:#000000;">位整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">28</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像的宽</span></p><p><span style="color:#000000;">(columns)</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">16</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">...</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">...</span></p></td></tr><tr><td style="border-color:#dfe2e5;width:124.5pt;"><p><span style="color:#000000;">xxxx</span></p></td><td style="width:102.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span></p></td><td style="width:127.5pt;"><p><span style="color:#000000;">不定</span></p><p><span style="color:#000000;">(0~255</span><span style="color:#000000;">之间</span><span style="color:#000000;">)</span></p></td><td style="width:135.75pt;"><p><span style="color:#000000;">图像内容</span></p></td></tr></tbody></table></div><p><span style="background-color:#ffffff;"><span style="color:#222222;">对于idx格式的二进制文件，其基本格式如下：</span></span></p><pre><code> magic numbersize in dimension 0size in dimension 1size in dimension 2 .....size in dimension Ndata</code></pre><p><span style="background-color:#ffffff;"><span style="color:#222222;">每个idx文件都以magic number开头，magic number是一个4个字节，32位的整数，用于说明该idx文件的data字段存储的数据类型。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">其中前两个字节总是0，第3个字节不同的取值代表了idx文件中data部分不同的数值类型，对应关系如下：</span></span></p><div class="table-box"><table border="1" cellspacing="0" style="margin-left:23.3pt;"><tbody><tr><td style="border-color:#dfe2e5;width:90.75pt;"><p><strong><span style="color:#000000;">取值</span></strong></p></td><td style="width:225.75pt;"><p><strong><span style="color:#000000;">含义</span></strong></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x08</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位无符号整型</span><span style="color:#000000;">(unsigned char, 1 byte)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x09</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">8</span><span style="color:#000000;">位有符号整型</span><span style="color:#000000;">(char, 1 byte)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0B</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">短整型</span><span style="color:#000000;">(short, 2 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0C</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">整型</span> <span style="color:#000000;">(int, 4 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0D</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">浮点型</span> <span style="color:#000000;">(float, 4 bytes)</span></p></td></tr><tr><td style="border-color:#dee0e3;width:90.75pt;"><p><span style="color:#000000;">0x0E</span></p></td><td style="width:225.75pt;"><p><span style="color:#000000;">双精度浮点型</span> <span style="color:#000000;">(double, 8 bytes)</span></p></td></tr></tbody></table></div><p><span style="background-color:#ffffff;"><span style="color:#222222;">在MNIST数据集的4个二进制文件中，data部分的数值类型都是“8位无符号整型”，所以magic number的第3个字节总是0x08。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">magic number的第4个字节代表其存储的向量或矩阵的维度。比如存储的是一维向量，那么magic number的第4个字节是0x01，如果存储的是二维矩阵，那么magic number的第4个字节就是0x02。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">所以在MNIST数据集的4个二进制文件中，标签文件的magic number第4个字节都是0x01，而在图像文件中，因为一张图像的维度是2，而多张图像拼成的矩阵维度是3，所以图像文件magic number第4个字节都是0x03。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">该数据集的官网说明了4个二进制文件中的整型数据是以大端方式 (MSB first) 存储的，所以在读取这4个二进制文件的前面几个32位整型数据时，需要注意声明数据存储格式是大端还是小端。</span></span></p><h2 id="五数据集下载链接"><a name="t5"></a><strong><span style="background-color:#ffffff;"><span style="color:#444444;">五、数据集下载链接</span></span></strong></h2><p><strong><span style="background-color:#ffffff;"><span style="color:#1b67ff;">数据集下载</span></span></strong></p><p><span style="background-color:#ffffff;"><span style="color:#222222;">OpenDataLab平台为大家提供了完整的数据集信息、直观的数据分布统计、流畅的下载速度、便捷的可视化脚本，欢迎体验。点击原文链接查看。</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#222222;"><a href="https://opendatalab.com/MNIST" title="https://opendatalab.com/MNIST">https://opendatalab.com/MNIST</a></span></span></p><p><span style="background-color:#ffffff;"><strong><span style="color:#888888;">参考资料</span></strong></span></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">[1]Y LeCun,L Bottou,Y Bengio,etal.Gradient-based learning applied to document recognition[J].Proceedings of the IEEE,1998,86(11):2278-2324.</span></span></p><p><span style="background-color:#ffffff;"><span style="color:#888888;">[2][http://yann.lecun.com/exdb/mnist/](<a href="http://yann.lecun.com/exdb/mnist/">http://yann.lecun.com/exdb/mnist/</a> &quot;<a href="http://yann.lecun.com/exdb/mnist/">http://yann.lecun.com/exdb/mnist/</a>&quot;)</span></span></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[详解Gson的TypeToken原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/xiang-jie-g-s-o-n-de-t-y-p-e-t-o-k-e-n-yuan-li" />
                <id>tag:https://www.wangdaye.net,2024-05-31:xiang-jie-g-s-o-n-de-t-y-p-e-t-o-k-e-n-yuan-li</id>
                <published>2024-05-31T08:08:11+08:00</published>
                <updated>2024-05-31T08:08:11+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="概要">概要</h3><p>借助对<a href="/developer/tools/blog-entry?target=https%3A%2F%2Fxtboke.cn%2Ftag-TypeToken.html&amp;source=article&amp;objectId=1672483">TypeToken</a>原理的分析，加强对泛型擦除的理解，使得我们能够知道什么时候，通过什么方式可以获取到泛型的类型。</p><h3 id="泛型擦除">泛型擦除</h3><p>众所周知，<span class="mod-overview__keyword">Java</span>的泛型只在编译时有效，到了运行时这个泛型类型就会被擦除掉，即<code>List&lt;String&gt;</code>和<code>List&lt;Integer&gt;</code>在运行时其实都是<code>List&lt;Object&gt;</code>类型。</p><p>为什么选择这种实现机制？不擦除不行么？ 在Java诞生10年后，才想实现类似于C++模板的概念，即泛型。Java的类库是Java生态中非常宝贵的财富，必须保证向后兼容（即现有的代码和类文件依旧合法）和迁移兼容（泛化的代码和非泛化的代码可互相调用）基于上面这两个背景和考虑，Java设计者采取了&quot;类型擦除&quot;这种折中的实现方式。</p><p>同时正正有这个这么&quot;坑&quot;的机制，令到我们无法在运行期间随心所欲的获取到泛型参数的具体类型。</p><h3 id="typetoken">TypeToken</h3><p>使用</p><p>使用过<a href="/developer/tools/blog-entry?target=https%3A%2F%2Fxtboke.cn%2Ftag-Gson.html&amp;source=article&amp;objectId=1672483">Gson</a>的同学都知道在反序列化时需要定义一个TypeToken类型，像这样</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>private Type type = new TypeToken&lt;List&lt;Map&lt;String, Foo&gt;&gt;&gt;(){}.getType();//调用fromJson方法时把type传过去，如果type的类型和json保持一致，则可以反序列化出来gson.fromJson(json, type);</code></pre></div></div><h4 id="三个问题">三个问题</h4><ol><li>为什么要用TypeToken来定义反序列化的类型？ 正如上面说的，如果直接把List&lt;Map&lt;String, Foo&gt;&gt;的类型传过去，但是因为运行时泛型被擦除了，所以得到的其实是<code>List&lt;Object&gt;</code>，那么后面的Gson就不知道要转成<code>Map&lt;String, Foo&gt;</code>类型了，这时Gson会默认转成LinkedTreeMap类型。</li><li>为什么带有大括号{}？ 这个大括号就是精髓所在。大家都知道，在Java语法中，在这个语境，{}是用来定义匿名类，这个匿名类是继承了TypeToken类，它是TypeToken的子类。</li><li>为什么要通过子类来获取泛型的类型？ 这是TypeToken能够获取到泛型类型的关键，这是一个巧妙的方法。 这个想法是这样子的，既然像<code>List&lt;String&gt;</code>这样中的泛型会被擦除掉，那么我用一个子类<code>SubList extends List&lt;String&gt;</code>这样的话，在JVM内部中会不会把父类泛型的类型给保存下来呢？我这个子类需要继承的父类的泛型都是已经确定了的呀，果然，JVM是有保存这部分信息的，它是保存在子类的Class信息中，具体看： <a href="/developer/tools/blog-entry?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F937933%2Fwhere-are-generic-types-stored-in-java-class-files&amp;source=article&amp;objectId=1672483">https://stackoverflow.com/questions/937933/where-are-generic-types-stored-in-java-class-files</a> 那么我们怎么获取这部分信息呢？ 还好，Java有提供<span class="mod-overview__keyword">API</span>出来：</li></ol><p>Type mySuperClass = foo.getClass().getGenericSuperclass(); Type type = ((ParameterizedType)mySuperClass).getActualTypeArguments()[0]; System.out.println(type);</p><p>分析一下这段代码，Class类的getGenericSuperClass()方法的注释是：</p><blockquote><p>Returns the Type representing the direct superclass of the entity (class, interface, primitive type or void) represented by thisClass. If the superclass is a parameterized type, the Type object returned must accurately reflect the actual type parameters used in the source code. The parameterized type representing the superclass is created if it had not been created before. See the declaration of ParameterizedType for the semantics of the creation process for parameterized types. If thisClass represents either theObject class, an interface, a primitive type, or void, then null is returned. If this object represents an array class then theClass object representing theObject class is returned</p></blockquote><p>概括来说就是对于带有泛型的class，返回一个ParameterizedType对象，对于Object、接口和原始类型返回null，对于数 组class则是返回Object.class。ParameterizedType是表示带有泛型参数的类型的Java类型，JDK1.5引入了泛型之 后，Java中所有的Class都实现了Type接口，ParameterizedType则是继承了Type接口，所有包含泛型的Class类都会实现 这个接口。</p><p>自己调试一下就知道它返回的是什么了。</p><h4 id="原理">原理</h4><p>核心的方法就是刚刚说的那两句，剩下的就很简单了。 我们看看TypeToken的getType方法</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>public final Type getType() { //直接返回type return type; }</code></pre></div></div><p>看type的初始化</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言：</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre><code>//注意这里用了protected关键字，限制了只有子类才能访问protected TypeToken() { this.type = getSuperclassTypeParameter(getClass()); this.rawType = (Class&lt;? super T&gt;) $Gson$Types.getRawType(type); this.hashCode = type.hashCode(); } //getSuperclassTypeParameter方法 //这几句就是上面的说到 static Type getSuperclassTypeParameter(Class&lt;?&gt; subclass) { Type superclass = subclass.getGenericSuperclass(); if (superclass instanceof Class) { throw new RuntimeException(&quot;Missing type parameter.&quot;); } ParameterizedType parameterized = (ParameterizedType) superclass; //这里注意一下，返回的是Gson自定义的，在$Gson$Types里面定义的TypeImpl等，这个类都是继承Type的。 return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); }</code></pre></div></div><h3 id="总结">总结</h3><p>在了解原理之后，相信大家都知道怎么去获取泛型的类型了。</p><h3 id="参考资料">参考资料</h3><p><a href="/developer/tools/blog-entry?target=https%3A%2F%2Fwww.cnblogs.com%2Fdoudouxiaoye%2Fp%2F5688629.html&amp;source=article&amp;objectId=1672483">https://www.cnblogs.com/doudouxiaoye/p/5688629.html</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[使用Google搜索API_key查询]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/使用google搜索apikey" />
                <id>tag:https://www.wangdaye.net,2024-05-18:使用google搜索apikey</id>
                <published>2024-05-18T18:21:54+08:00</published>
                <updated>2024-05-18T18:22:59+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p><a href="https://zhuanlan.zhihu.com/p/174666017">https://zhuanlan.zhihu.com/p/174666017</a></p></blockquote><h2 id="前言">前言</h2><p>最近需要爬取一些 google 搜索结果，首先看的就是有没有相关的 API，官方有提供 API 查询，但爬取量有一定限制。</p><h2 id="太长不看版">太长不看版</h2><ol><li>接口地址为 <code>https://www.googleapis.com/customsearch/v1?key={YOUR_KEY}&amp;q={SEARCH_WORDS}&amp;cx={YOUR_CX}&amp;start={10}&amp;num={10}</code></li><li>KEY 从 <a href="https://link.zhihu.com/?target=https%3A//console.developers.google.com/apis/credentials">谷歌云 API 控制台</a> 来的，需要有外币卡先注册谷歌云账号。但似乎付费的话就不用这个 KEY 了，仅用 CX 即可，这个待查。</li><li>CX 从 <a href="https://link.zhihu.com/?target=https%3A//programmablesearchengine.google.com/cse/create/new">谷歌可编程搜索</a> 中来</li><li>一天只有 100 次的免费搜索限额，但最高只能查询前 100 条。如需增加则 5 刀 1000 次，但一天上限 10000。 次，对于我来说已经足够用了</li></ol><h2 id="怎么做">怎么做</h2><p>首先来到 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/overview%3Fhl%3Den_US">Google Developers</a> 的相关文档页面，可以看到大概的介绍。</p><h3 id="生成-api-key">生成 API KEY</h3><p>点击 overview 中的 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/overview%23api_key">Get a Key</a>，此处需要登录谷歌帐号，以及注册谷歌云帐号（应该需要绑定外币信用卡）并且创建一个 project，此处略过不表，最后你会得到一个 Key。</p><figure data-size="normal"><p><noscript><img src="https://pic2.zhimg.com/v2-0dcc7d4700d873e89c86c5b2d7f2eba9_b.jpg" alt="" /></noscript></p><div>![](https://pic2.zhimg.com/80/v2-0dcc7d4700d873e89c86c5b2d7f2eba9_1440w.jpg)</div></figure><p>这个 Key 可以从<a href="https://link.zhihu.com/?target=https%3A//console.developers.google.com/apis/credentials/key">谷歌云控制台</a>中看到，建议加上应用限制和 API 限制，以防泄露后被滥用。</p><figure data-size="normal"><p><noscript><img src="https://pic4.zhimg.com/v2-ae2f6aa240bd9b6320f4b69a66efb273_b.jpg" alt="" /></noscript></p><div>![](https://pic4.zhimg.com/80/v2-ae2f6aa240bd9b6320f4b69a66efb273_1440w.webp)</div></figure><h3 id="生成-cx">生成 cx</h3><p>cx 是 Google 可编程搜索引擎(Programmable Search Engine)的 id 标识，在此处 <a href="https://link.zhihu.com/?target=https%3A//programmablesearchengine.google.com/cse/create/new">新增搜索引擎</a> 可以获取。这里可以指定要搜索的网站，比如说我只希望通过该 API 搜索出来的网站是 shodan.io，谷歌语法里面相当于 <code>site:shodan.io</code>，可以这么设置 ：</p><figure data-size="normal"><p><noscript><img src="https://pic2.zhimg.com/v2-186108d2d148a490cbe3b4d3030f41a1_b.jpg" alt="" /></noscript></p><div>![](https://pic2.zhimg.com/80/v2-186108d2d148a490cbe3b4d3030f41a1_1440w.webp)</div></figure><p>新增完成之后点击修改搜索引擎，并点击设置，你就可以看到你的搜索引擎 id，就是我们说的 cx</p><figure data-size="normal"><p><noscript><img src="https://pic4.zhimg.com/v2-7b2b97e329112e2ef242a38892dcb2c3_b.jpg" alt="" /></noscript></p><div>![](https://pic4.zhimg.com/80/v2-7b2b97e329112e2ef242a38892dcb2c3_1440w.webp)</div></figure><p>里面还有一些选项，自己可以看着修改~如果还想看看文档，可点击在页面下方一点的【以程序化方式访问】-【使用入门】</p><h3 id="api">API</h3><p>JSON API 可以从 <a href="https://link.zhihu.com/?target=https%3A//developers.google.com/custom-search/v1/introduction%23api_overview">文档</a> 中查看</p><p>完整的可请求参数如下，基本上和高级搜索保持一致：</p><div class="highlight"><pre><code>https://www.googleapis.com/customsearch/v1?q={searchTerms}&amp;num={count?}&amp;start={startIndex?}&amp;lr={language?}&amp;safe={safe?}&amp;cx={cx?}&amp;sort={sort?}&amp;filter={filter?}&amp;gl={gl?}&amp;cr={cr?}&amp;googlehost={googleHost?}&amp;c2coff={disableCnTwTranslation?}&amp;hq={hq?}&amp;hl={hl?}&amp;siteSearch={siteSearch?}&amp;siteSearchFilter={siteSearchFilter?}&amp;exactTerms={exactTerms?}&amp;excludeTerms={excludeTerms?}&amp;linkSite={linkSite?}&amp;orTerms={orTerms?}&amp;relatedSite={relatedSite?}&amp;dateRestrict={dateRestrict?}&amp;lowRange={lowRange?}&amp;highRange={highRange?}&amp;searchType={searchType}&amp;fileType={fileType?}&amp;rights={rights?}&amp;imgSize={imgSize?}&amp;imgType={imgType?}&amp;imgColorType={imgColorType?}&amp;imgDominantColor={imgDominantColor?}&amp;alt=json&quot;</code></pre></div><p>简化版：<code>https://www.googleapis.com/customsearch/v1?key={YOUR_KEY}&amp;q={SEARCH_WORDS}&amp;cx={YOUR_CX}&amp;start={10}&amp;num={10}</code></p><h2 id="存在的一些问题">存在的一些问题</h2><h3 id="搜索结果与-api-不一致">搜索结果与 API 不一致</h3><p>因为不同 IP 使用谷歌搜索会出现不一样的结果，比如美国和香港的 IP 访问必然会不一样。可以使用 API 中的 <code>lr</code> 参数修改语言选项，也可以在【修改搜索引擎】中修改语言和地区选项。</p><figure data-size="normal"><p><noscript><img src="https://pic1.zhimg.com/v2-367cce8237e8b60e3c5d1a3d89e847a0_b.jpg" alt="" /></noscript></p><div>![](https://pic1.zhimg.com/80/v2-367cce8237e8b60e3c5d1a3d89e847a0_1440w.webp)</div></figure><h3 id="请求频率">请求频率</h3><p>由于一天查的上限就这么多，所以等待时间尽量拉长吧，我 5-10s 请求一次没啥问题</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[从设计者的角度出发理解源码–FastJson]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/fastjsonread" />
                <id>tag:https://www.wangdaye.net,2024-02-14:fastjsonread</id>
                <published>2024-02-14T08:10:59+08:00</published>
                <updated>2024-02-14T08:57:09+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="从设计者的角度出发理解源码--fastjson">从设计者的角度出发理解源码--FastJson</h1><h2 id="引言">引言</h2><p>本篇，作为《从..理解源码》的第二篇，延续前一篇的思路，笔者视图尝试从作者的角度触发分析作者的思路历程，和上篇相比，FastJson的知识可能略多。这篇文章，对fastjson只能算是浅尝即止，我认为最厉害的未过于，作者的持续习惯，包括但不限于：</p><ul><li><p>基于Benchmark搜集高性能代码的习惯</p></li><li><p>阅读原始文档获得第一手资料，对于JDK各种版本特性了然于胸。</p></li><li><p>喜欢探索代码优雅的编码方式。</p></li><li><p>产品思维：先发现问题，先用简单的方式实现，后面在迭代更好的方案。</p></li><li><p>自我高要求：把自己的代码拉出来被鞭尸，虚心接受建议，持续迭代，即便有其他框架在开发，即便自己已经是P9。</p></li></ul><p>这个框架的阅读过程中，感受到作者的拼劲全力，感受到：<strong>一个极客，孜孜不倦，为当年立下的Flag，将自己空闲时间，都投入到少年时的一个梦：极致的性能</strong>。</p><blockquote><p>写本篇文章时，由于fastjson2已发布，本篇文章基于fastjson2</p></blockquote><h2 id="为什么要写fastjson">为什么要写FastJson</h2><p><strong>哎，以往的框架，Jakson,Goson，最大的问题其实是使用繁琐，和在不同环境下，兼容性不足</strong></p><p>我需要给别人一个足够的理由，做俩个选择</p><ol><li>选择放弃原本的实用。</li><li>选择使用我的东西。</li></ol><p><strong>有了</strong>：</p><ul><li><p>以性能为突破口</p><blockquote><p>我从性能上出发，应该是一个比较好的突破口，因为一个逻辑一旦涉及到序列化操作，该序列化操作的频次应该不会低。</p><p>如果我可以真正做到序列化性能有比较大的提升，对他们来说很有吸引力。</p></blockquote></li><li><p>上手体验为突破口：</p><p>1.使用API设计均使用静态方法思路，减少编码步骤，</p><p>2.提供充足的注解类工具，使用注解天生体验好的特性。</p><blockquote><table><thead><tr><th>框架</th><th>写法</th><th>上手代码量</th></tr></thead><tbody><tr><td>Fastjson</td><td>String mapString = JSON.toJSONString(map);</td><td>1 行</td></tr><tr><td>Jackson</td><td>ObjectMapper mapper = new ObjectMapper();<br />String beanString = mapper.toJson(map);</td><td>2 行</td></tr><tr><td>Gson</td><td>Gson gson = new Gson();<br />String beanString = gson.toJson(map);</td><td>2行</td></tr></tbody></table><p>FastJSON提供的注解如下：</p><pre><code class="language-java">// 字段注解com.alibaba.fastjson2.annotation.JSONFieldcom.alibaba.fastjson2.annotation.JSONType// com.alibaba.fastjson2.annotation.JSONBuildercom.alibaba.fastjson2.annotation.JSONCompiledcom.alibaba.fastjson2.annotation.JSONCompiler</code></pre></blockquote></li><li><p>先在公司内部（阿里）获得一定的认可度，后面推广可以借助公司的力量。</p></li></ul><blockquote><p>其实，做框架设计起初和写PPT有点类似，你得先有一个能引人入胜的思路，最好能有个最简陋的版本，先将人吸引过来，然后你逐步去完善它，持之以恒的迭代，再产生新的思路，再迭代。</p></blockquote><p><strong>哎，光能证明你性能好也不行呀，稳定性怎么保证，市面上相似的太多了。</strong></p><p><strong>有了</strong>：</p><ul><li>持续关注issues及时修复，把每个issues的修复单元测试及时提交修改完成之后，让他们随意查看，逐步建立起信任。</li><li>保持初心，持之以恒，一个框架能被大面积使用，不在于它开始bug的多少，而在于立意远大，且有人维护。</li><li>这个东西如果做好了，是能反向推动我成长的，因为它的适用范围足够广（安卓的，Web，大数据等等）</li></ul><h2 id="序列化设计bean-json">序列化设计（Bean-&gt;Json）</h2><p>序列化流程，如果再细分的话，有如下流程：</p><ol><li><p>获取所有需要提取的属性列表。</p></li><li><p>依次遍历属性列表获取属性，向一段JSON容器里面放置key，val。</p></li><li><p>在放置key，val的时候，可能涉及某一些转义符的处理，json结构的处理等等。</p></li></ol><h3 id="不同场景的优化">不同场景的优化</h3><p><strong>哎，我如何基于不同情况做出不同的处理方式呢，这样我可以针对每一种情况去做具体的优化</strong></p><p><strong>有了</strong>：</p><ul><li>我可以使用模板方法写一个ObjectWriter抽象类完成主逻辑。</li><li>将已知情况细分清楚，基于不同情况，各自情况完成各自的实现。</li></ul><pre><code class="language-java">// ObjectWriter抽象类，以及部分具体实现com.alibaba.fastjson2.writer.ObjectWritercom.alibaba.fastjson2.util.JodaSupport.LocalDateTimeWritercom.alibaba.fastjson2.util.GuavaSupport.AsMapWriter....</code></pre><p><strong>哎，总不能把所有情况都穷举出来，写出各自的实现，这种优化思路还是不太灵活。</strong></p><p><strong>有了</strong>：Bean转JSON，本质上，就是从一个Bean里面取内容，然后往一个JSON里面塞么。</p><p>我可以为每一个Bean的，读写逻辑，分别依据各自的情况生成相应的字节码读写器。</p><ul><li>依据每一个Bean的情况去做更具体的优化。</li><li>后期扩展空间也大（毕竟是动态生成的代码）。</li><li>需要通过一个抽象类来约束核心逻辑，使用ASM框架运行时编写Class。</li></ul><pre><code class="language-java">// 如下摘自：com.alibaba.fastjson2.JSON#toJSONString(java.lang.Object)，部分逻辑省略//1：获取ObjectWritercom.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriter //1.1：使用 ASM 来创建 ObjectWriter 对象 1.1 creator = ObjectWriterCreatorASM.INSTANCE; //  //1.2：通过字节码编写代码，继承ObjectWriterAdapter，创建Class。  1.2 com.alibaba.fastjson2.reader.ObjectWriterCreatorASM#createObjectWriter//2：完成读序列化   objectWriter.write(writer, object, null, null, 0); //3：返回return writer.toString();</code></pre><h3 id="asm生成的代码">ASM生成的代码</h3><p><strong>如下我列出了FastJSON生成的ObjectReaderAdapter:</strong></p><ul><li><p>FastJSON生成的ObjectReaderAdapter: <a href="https://note.youdao.com/s/AfCaGZSV">https://note.youdao.com/s/AfCaGZSV</a></p></li><li><p>FastJSON生成的ObjectWriterAdapter: <a href="https://note.youdao.com/s/W9gi9fRD">https://note.youdao.com/s/W9gi9fRD</a></p></li></ul><h3 id="不能使用asm的环境">不能使用ASM的环境</h3><p><strong>哎，可能部分使用者的场景不能使用字节码，可能由于环境问题，比如Android，GraalVM环境等，或者出于安全上面考量，使用者主观不想用字节码实现</strong></p><p>有了：</p><ul><li>支持让使用者指定使用的实现方式。</li><li>默认使用ASM字节码实现，如果发现是安卓等不能够使用的环境，自动使用反射实现，且支持手动指定。</li></ul><pre><code class="language-java">// 核心源码如下： 1.优先支持手动指定方式，2.如果不指定，默认使用ASM方式，3.如果当前环境不支持ASM，则自动转为MH，或者反射。switch (JSONFactory.CREATOR) {   case &quot;reflect&quot;:  case &quot;lambda&quot;:    creator = ObjectWriterCreator.INSTANCE;     break;  case &quot;asm&quot;:  default:    try {      if (!JDKUtils.ANDROID &amp;&amp; !JDKUtils.GRAAL) {         creator = ObjectWriterCreatorASM.INSTANCE;       }    } catch (Throwable ignored) {}    if (creator == null) {       // 如果 creator 仍然为 null，则使用反射或 Lambda 表达式来创建 ObjectWriter 对象      creator = ObjectWriterCreator.INSTANCE;     }    break;}</code></pre><p><strong>哎，反射好像性能不太好，我如何在这个基础之上优化一点呢？</strong></p><p><strong>有了</strong>:</p><ul><li>我可以使用：Lambda表达式+MethodHandler，来替代反射获取属性的调用。</li><li>JDK自带了很多FunctionInterface接口比如Customer ，这样我不仅可以提升性能，还可以在调用处统一写法。</li></ul><blockquote><p>invokedynamic指令与MethodHandle的功能简单说：就是之前的动态转发的逻辑都是在字节码层面去完成的，某个方法调用，转发给哪个具体实现类去执行，都是字节码自己安排好了，基于一段固定逻辑自己走下来</p><p>而invokedynamic指令与MethodHandle则可以将“转发给谁”，这个谁，在应用代码层面去指定。</p><p>Lambda表达式主要就是基于如下几点</p><p>1.通过ASM字节码技术，生成最轻量代码（比如static方法）</p><p>2.基于指令invokedynamic与MethodHandle特性实现的，将转发能力赋予了业务代码。</p></blockquote><pre><code class="language-java">// P1：创建set函数的Lambda对象BiConsumer function = (BiConsumer) lambdaSetter(objectClass, fieldClass, method); return createFieldReader(objectClass, objectType, fieldName, fieldType, fieldClass, ordinal, features, format, locale, defaultValue, jsonSchema, method, function, initReader);// P2：将set函数的Lambda对象放到缓存fieldReaders中putIfAbsent(fieldReaders, fieldName, fieldReader, objectClass);// P3：使用function读取值public void readFieldValue(JSONReader jsonReader, T object) {     function.accept(object, (V) fieldValue);}</code></pre><p>知识补习：</p><ul><li><a href="https://www.wangdaye.net/archives/jvm%E4%B8%AD%E7%9A%84%E5%8A%A8%E6%80%81%E5%88%86%E6%B4%BE%E6%9C%BA%E5%88%B6%E5%88%B0%E5%BA%95%E6%98%AF%E6%80%8E%E4%B9%88%E5%9B%9E%E4%BA%8B">JVM中的动态分派机制到底是怎么回事</a></li><li><a href="https://juejin.cn/post/7202188318889508924">invokedynamic详解</a></li><li><a href="https://www.wangdaye.net/archives/toleinj">Lambda表达式是如何设计的</a></li></ul><h3 id="安全性方面">安全性方面</h3><p><strong>哎，如何防止JSON注入呢？毕竟用户输入什么我又不可控制？</strong></p><p>有了：</p><ul><li><p>Json-schema不就就是干这个的，我可以用Json-schema约束输入值，防止被注入。</p><blockquote><p>Json-schema是一个特殊的JSON，用来约束JSON的数据结构。</p></blockquote></li><li><p>使用模板方法设计模式，实现一个基础模板类，基于基础类型（int，long，string，object）来实现具体的验证逻辑（用户通过注解的形式作用在某个具体字段上）</p><blockquote><p>-- 基础模板</p><p>com.alibaba.fastjson2.schema.JSONSchema</p><p>-- 核心方法（字段验证）</p><p>com.alibaba.fastjson2.schema.JSONSchema#validate</p><p>-- 具体类型实现</p><p>com.alibaba.fastjson2.schema.BooleanSchema</p><p>com.alibaba.fastjson2.schema.ObjectSchema</p><p>-- 组合规则</p><p>com.alibaba.fastjson2.schema.AnyOf</p><p>com.alibaba.fastjson2.schema.AllOf</p></blockquote></li><li><p>其中，object，组合类（AnyOf，AllOf）方法可能有一些特殊实现，因为object类型需要考虑递归的情况。组合类的需要考虑，多条规则之间的验证关系。</p></li></ul><p>知识补习：</p><ul><li><a href="https://json-schema.apifox.cn/">JSON-SCHEMA中文文档</a></li></ul><p><strong>哎，自己通过字节码生成Class，比如如果生成的Class里面有while循环的行为呢？安全性方面如何保证呢？</strong></p><p>有了：</p><ul><li>使用一个自定义的ClassLoader加载动态生成的Class。</li><li>对自定义的ClassLoader做安全配置，限制代码行为。</li></ul><pre><code class="language-java">//P1 DynamicClassLoader// ProtectionDomain表示代码源和权限的集合，里面封装了代码源（即代码从哪里来，来源是哪里）和权限（即代码有什么权限，能干嘛）的信息。public class DynamicClassLoader extends ClassLoader {  private static final DynamicClassLoader instance = new DynamicClassLoader();    private static final java.security.ProtectionDomain DOMAIN;    static {      DOMAIN = (java.security.ProtectionDomain) java.security.AccessController.doPrivileged(        (PrivilegedAction&lt;Object&gt;) DynamicClassLoader.class::getProtectionDomain      );    }}//P2 ObjectReaderCreatorASM//使用DynamicClassLoader，初始化ObjectReaderCreatorASM的classLoader属性。public ObjectReaderCreatorASM(ClassLoader classLoader) {    this.classLoader = classLoader instanceof DynamicClassLoader ? (DynamicClassLoader) classLoader : new DynamicClassLoader(classLoader);  }//P3 ObjectReaderCreatorASM.jitObjectReader//调用classLoader的defineClassPublic方法，将code中的数据定义为一个新的类，并返回这个类的Class对象，赋值给readerClass。byte[] code = cw.toByteArray();Class&lt;?&gt; readerClass = classLoader.defineClassPublic(classNameFull, code, 0, code.length);//P4 com.alibaba.fastjson2.util.DynamicClassLoader#defineClassPublic// 定义类时候使用DOMAIN作为限制。public Class&lt;?&gt; defineClassPublic(String name, byte[] b, int off, int len) throws ClassFormatError {  return defineClass(name, b, off, len, DOMAIN);}</code></pre><p>知识补习：</p><ul><li><p><a href="https://blog.csdn.net/yfqnihao/article/details/8271415">策略和保护域</a></p></li><li><p><a href="https://www.wangdaye.net/archives/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8%E5%8E%9F%E7%90%86">类加载器</a></p></li></ul><h3 id="属性读写顺序">属性读写顺序</h3><p><strong>哎，读取和写入Key的顺序如何保证呢？先写哪些属性，后写哪些属性？如何将性能最大化？</strong></p><p><strong>有了</strong>：我可以在生成字节码阶段做一些事情</p><ul><li><p>在生成字节码阶段，获取JavaBean所有属性列表，且将这些属性按照Hash值排序，ListA。</p></li><li><p>在生成字节码阶段，遍历ListA属性，生成字节码写入逻辑，这样我的代码里从上至下的编码顺序，即是正确的读取写入顺序。</p></li><li><p>在执行字节码类阶段，通过一个全局作用域的Byte数组，来维护JSON信息，不断在Byte数组后追加信息。</p></li><li><p>在执行字节码类阶段，使用Unsafe类操作内存设置值，避免频繁安全检查。</p></li></ul><pre><code class="language-java">// P1 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#createObjectWriter// 1.遍历JavaBean的属性，获取fieldWriters  BeanUtils.declaredFields(objectClass, field -&gt; {     FieldWriter fieldWriter = creteFieldWriter(objectClass, writerFieldFeatures, provider, beanInfo, fieldInfo, field);     ...    fieldWriterMap.put(fieldWriter.fieldName, fieldWriter);  });   fieldWriters = new ArrayList&lt;&gt;(fieldWriterMap.values()); // P2 com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#genMethodWrite  // 2.遍历fieldWriters，按先后次序生成字节码  for (int i = 0; i &lt; fieldWriters.size(); i++) {    FieldWriter fieldWriter = fieldWriters.get(i);    gwFieldValue(mwc, fieldWriter, OBJECT, i);  }// P3 https://note.youdao.com/s/W9gi9fRD，（生成的WriterAdapter字节码）  // 3.如下为生成的字节码的编码顺序，  if ((var14 = ((ExcelTransformReq)var2).getColumn1()) != null)  ...    if ((var14 = ((ExcelTransformReq)var2).getColumn10()) != null)   // P4 设置byte数值  // 4.写入数据  com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write      // 4.2 具体filed写入，其中JSONWriter里面维护全局作用域，以及全局Byte[]    com.alibaba.fastjson2.writer.FieldWriter#writeFieldName    com.alibaba.fastjson2.JSONWriter#writeName         // 4.2.1 使用unsafe直接操作数组        UNSAFE.putLong(bytes, ARRAY_BYTE_BASE_OFFSET + off, name);</code></pre><p>知识补习：</p><ul><li><a href="https://www.wangdaye.net/archives/深入理解sunmiscunsafe原理">深入理解sun.misc.Unsafe原理</a></li></ul><h3 id="多种编码格式的适配">多种编码格式的适配</h3><p><strong>哎，字符串的编码格式种类，我如何适配到多种编码格式呢？</strong></p><p><strong>有了</strong>：其实不同编码格式的区别无非是，存储容器，拼接与位移策略，在不同的情况下的不同。</p><ul><li>定义抽象一个类叫JSONWriter，这个类里面不做具体实现，只做方法的定义，与核心解析逻辑编写。（模板方法设计模式）</li><li>继承JSONWriter实现多种编码格式的实现类，比如：JSONWriterJSONB，JSONWriterUTF16，JSONWriterUTF8。</li><li>生成JSON时，根据情况选中具体JSONWriter实现。</li><li>将JSONWriter与WriterAdapter，使用桥接模式进行组装执行。（桥接设计模式）</li></ul><pre><code class="language-java">// 如下列出了JSONWriter，已经它的具体实现com.alibaba.fastjson2.JSONWritercom.alibaba.fastjson2.JSONWriterUTF16com.alibaba.fastjson2.JSONWriterUTF8com.alibaba.fastjson2.JSONWriterJSONB  // 根据配置项，或者环境情况，初始化对应的实现。// com.alibaba.fastjson2.JSONWriter#of...  if (FIELD_STRING_VALUE != null &amp;&amp; !ANDROID &amp;&amp; !OPENJ9) {  jsonWriter = new JSONWriterUTF16JDK8UF(context);} else {  jsonWriter = new JSONWriterUTF16JDK8(context);}  ...// 将JSONWriter作为入参，传入WriterAdapter，使用桥接的思路。com.wang.track.analysis.OWG_1_25_ExcelTransformReq#write(JSONWriter var1, Object var2, Object var3, Type var4, long var5)this.fieldWriter0.writeFieldName(var1);  </code></pre><p>知识补习：</p><ul><li><a href="https://zhuanlan.zhihu.com/p/575645658">常见的设计模式</a></li></ul><p><strong>哎，复杂对象（一个class里有另一个class），该如何处理？</strong></p><p>有了：</p><ul><li><p>我可以使用懒加载的思路，在读取每个属性值的时候，先判断一下该属性类型，是否是复杂类型，如果是，则递归使用创建复杂类型的Reader，思路和如上类似。</p><pre><code class="language-java">// com.wang.track.analysis.ORG_1_25_ExcelTransformReq// ObjectReader 类里封装了递归生成reader的逻辑public FieldReader fieldReader0;public ObjectReader objectReader0;// com.alibaba.fastjson2.reader.ObjectReader1#readJSONBObjectlong hashCode = jsonReader.readFieldNameHashCode();if (hashCode == getTypeKeyHash() &amp;&amp; i == 0) {long typeHash = jsonReader.readTypeHashCode();JSONReader.Context context = jsonReader.getContext();ObjectReader autoTypeObjectReader = autoType(context, typeHash);}</code></pre></li><li><p>使用一个容器（map结构），将本次流程中所创建的reader，使用，classA-xxxReader，的形式缓存。</p><pre><code class="language-java">// com.alibaba.fastjson2.reader.ObjectReaderProvider// 如下是各种查询的缓存Mapfinal ConcurrentMap&lt;Type, ObjectReader&gt; cache = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Type, ObjectReader&gt; cacheFieldBased = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Integer, ConcurrentHashMap&lt;Long, ObjectReader&gt;&gt; tclHashCaches = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Long, ObjectReader&gt; hashCache = new ConcurrentHashMap&lt;&gt;();final ConcurrentMap&lt;Class, Class&gt; mixInCache = new ConcurrentHashMap&lt;&gt;();</code></pre></li></ul><h3 id="代码结构图">代码结构图</h3><p>如下是最终的架构图：</p><p><img src="https://www.wangdaye.net/upload/2024/02/image-f8a9cd2fadb64d0bbd1c30a6a8797325.png" alt="image.png" /></p><blockquote><p>1.入口</p><pre><code class="language-java">JSON.toJSONString(testBean);</code></pre><p>2.初始化ObjectWriterProvider（默认ASM实现），全局上下文（context）</p><pre><code class="language-java">final ObjectWriterProvider provider = defaultObjectWriterProvider;final JSONWriter.Context context = new JSONWriter.Context(provider);</code></pre><p>3.初始化JSONWriter（判断当前所处环境，以及参数，自动选择合适的JSONWriter）</p><pre><code class="language-java">JSONWriter writer = JSONWriter.of(context)</code></pre><p>3.初始化getObjectWriter</p><pre><code class="language-java">// 初始化getObjectWriterObjectWriter&lt;?&gt; objectWriter = provider.getObjectWriter(valueClass,valueClass,(defaultWriterFeatures &amp; JSONWriter.Feature.FieldBased.mask) != 0); </code></pre><p>4.解析</p><pre><code class="language-java">// 此时objectWriter默认的类型为....OWG_1_25_ExcelTransformReq，为ASM生成的classobjectWriter.write(writer, object, null, null, 0);</code></pre><p>5.具体的字符串输出</p><pre><code class="language-java">return writer.toString();</code></pre></blockquote><h2 id="反序列化设计json-bean">反序列化设计(Json-&gt;Bean)</h2><blockquote><p>由于parseObject方法的代表性，如下均以com.alibaba.fastjson2.JSON#parseObject方法作为思路解析的依据</p></blockquote><p>其实作者对序列化，反序列化的思路是差不多的。只不过反序列化有一些区别，这部分只针对有区分的部分做推倒。</p><ul><li>Json转为Bean时，key的顺序不可预测。</li><li>JSON-&gt;Bean，只需要调用Bean的set方法，所以不需要考虑反射性能差的原因。</li></ul><h3 id="key无序问题">key无序问题</h3><p><strong>哎，JSON的key的顺序是不可预测的，我该如何处理？</strong></p><p><strong>有了</strong>：相同字符串的Hash值是一样的，我可以基于这个规律，在写字节码的时候，按顺序写不就行了，通过Hash来作为IF的判断依据。</p><blockquote><p>可能有同学有疑问，使用Map结构足够了，为什么还有生成这样的代码</p><p>因为在key的数量相当有限的情况下，逻辑分支的性能（经过JVM指令优化）远比Map寻址的性能高。</p></blockquote><pre><code class="language-java">// 根据Name的Hash值，使用switch编写逻辑分支代码。var9 = var1.readFieldNameHashCode()int var11 = (int)(var9 ^ var9 &gt;&gt;&gt; 32);  switch(var11) {  case 1079836942:    if (var9 == 3832966174269534051L) {      var10001 = var1.readString();      ((ExcelTransformReq)var6).setColumn15(var10001);      continue;    }    break;  case 1079902478:    if (var9 == 3833247649246244707L) {      var10001 = var1.readString();      ((ExcelTransformReq)var6).setColumn25(var10001);      continue;    }    break;    ...</code></pre><h3 id="代码结构图-1">代码结构图</h3><p><img src="https://www.wangdaye.net/upload/2024/02/image-41fe16b2a5bc418886cd7092ecbdcbb7.png" alt="image.png" /></p><blockquote><p>1.入口</p><pre><code class="language-java">com.alibaba.fastjson2.JSON#parseObject</code></pre><p>2.初始化ObjectReaderProvider，与Context（全局上下文）</p><pre><code class="language-java">ObjectReaderProvider provider = JSONFactory.getDefaultObjectReaderProvider();JSONReader.Context context = new JSONReader.Context(provider);</code></pre><p>3.初始化objectReader</p><pre><code class="language-java">ObjectReader&lt;T&gt; objectReader = provider.getObjectReader(clazz,(defaultReaderFeatures &amp; JSONReader.Feature.FieldBased.mask) != 0);</code></pre><p>4.初始化JSONReader</p><pre><code class="language-java">JSONReader reader = JSONReader.of(text, context);</code></pre><p>5.解析</p><pre><code class="language-java">T object = objectReader.readObject(reader, clazz, null, 0);</code></pre></blockquote><h2 id="环境变量设计">环境变量设计</h2><h3 id="环境变量信息搜集">环境变量信息搜集</h3><p><strong>哎，我需要面对不同的环境，如果有一个全局视野知道当前环境的情况那些功能可用？</strong></p><p>有了，我可以使用一个JDKUtils，所有当前环境的信息，都通过<strong>各种方式</strong>取出来，然后放到它里面统一维护，用这个类来解决环境变量问题。</p><ul><li><p>JVM版本等，通过系统参数获取。(类加载阶段，static代码块)</p><blockquote><p>System.getProperty(&quot;java.vm.name&quot;)</p></blockquote></li><li><p>判断当前环境（安卓还是JavaWeb），通过寻找Class的方式获取(类加载阶段，static代码块)</p><blockquote><p>Class&lt;?&gt; factorClass = Class.forName(&quot;java.lang.management.ManagementFactory&quot;);</p></blockquote></li><li><p>框架公共方法，通过MethodHandles获取(运行阶段，方法首次调用时)</p><blockquote><p>MethodHandles.Lookup lookup = trustedLookup(classStringCoding);<br />CallSite callSite = LambdaMetafactory.metafactory(..);<br />isAscii = (Predicate&lt;byte[]&gt;) callSite.getTarget().invokeExact();</p><p>PREDICATE_IS_ASCII = isAscii;</p></blockquote></li></ul><pre><code class="language-java">// P1// 环境工具类com.alibaba.fastjson2.util.JDKUtils// static阶段获取public static final Unsafe UNSAFE;public static final long ARRAY_BYTE_BASE_OFFSET;public static final long ARRAY_CHAR_BASE_OFFSET;public static final int JVM_VERSION;public static final Field FIELD_STRING_VALUE;  // 如下是通过BiFunction（函数式编程），将不同环境中的能力统一出口public static final ToIntFunction&lt;String&gt; STRING_CODER;public static final Function&lt;String, byte[]&gt; STRING_VALUE;// P2,如下是获取安卓SDK版本号android_sdk_int = Class.forName(&quot;android.os.Build$VERSION&quot;).getField(&quot;SDK_INT&quot;).getInt(null);// p3,获取Java版本号String javaSpecVer = System.getProperty(&quot;java.specification.version&quot;);// P4,判断当前环境是否有sql的jar包dataSourceClass = Class.forName(&quot;javax.sql.DataSource&quot;);// P5,根据不同版本通过Lambda表达式封装实现if (JVM_VERSION &gt;= 17) {  handle = trustedLookup.findStatic(classStringCoding =                                     String.class,&quot;isASCII&quot;,MethodType.methodType(boolean.class,byte[].class);}....if (handle == null &amp;&amp; JVM_VERSION &gt;= 11) {  classStringCoding = Class.forName(&quot;java.lang.StringCoding&quot;);  handle = trustedLookup.findStatic(classStringCoding,&quot;isASCII&quot;,MethodType.methodType(boolean.class, byte[].class)}                               MethodHandles.Lookup lookup = trustedLookup(classStringCoding);CallSite callSite = LambdaMetafactory.metafactory(..);isAscii = (Predicate&lt;byte[]&gt;) callSite.getTarget().invokeExact();PREDICATE_IS_ASCII = isAscii;</code></pre><h3 id="流程数据维护和流程能力扩展">流程数据维护和流程能力扩展</h3><p><strong>哎，全局视野如何保证？比如现在多层嵌套的JSON，由于我使用Byte[]维护。</strong></p><p>有了：</p><p>在解析开始时候，设置一个全局Context，这个Context贯穿流程始终。</p><ul><li>维护当前进行中的值，或者解析状态值，比如JSON数据，当前写入偏移量等。</li><li>维护一些全局配置，比如用户配置的功能属性开关。</li><li>使用Long值+位移，来实现不同能力的开关。</li></ul><p><strong>哎，功能参数和扩展性如何设计呢？</strong></p><ul><li>配置项比较多，目前有四五十个，后面可能还会拓展到百个。</li><li>需要再解析前后加一些钩子。</li></ul><p>有了：</p><ul><li><p>配置项可以统一使用Long+位移的方式，这样一个Long值，就能代表63个配置项，极大减少空间。</p><blockquote><pre><code class="language-java">// com.alibaba.fastjson2.JSONReader.Featurepublic enum Feature {FieldBased(1),IgnoreNoneSerializable(1 &lt;&lt; 1),ErrorOnNoneSerializable(1 &lt;&lt; 2),SupportArrayToBean(1 &lt;&lt; 3),InitStringFieldAsEmpty(1 &lt;&lt; 4)}</code></pre></blockquote></li><li><p>其实扩展性无非是俩种类型，一种是影响主逻辑的，一种是不影响主逻辑（单纯是生命周期回调），第1种我可以使用Lambda表达式，第2种我可以使用Processor思路去做（统一维护触发对象，在特定实际触发）。</p><blockquote><p>1.生命周期触发</p><p>2.置入代码调整主逻辑。</p></blockquote></li><li><p>使用全局Context，维护所有的扩展配置，使这些配置在全流程中可见，被动触发不受限制。</p></li></ul><pre><code class="language-java">// 分别是序列化，反序列化，的Contextcom.alibaba.fastjson2.JSONWriter.Contextcom.alibaba.fastjson2.JSONReader.Context  // 如下是JSONReader.Context的部分定义，其中objectSupplier，arraySupplier为public static final class com.alibaba.fastjson2.JSONReader.Context {   long features;   // 扩展点：第1种   Supplier&lt;Map&gt; objectSupplier;   Supplier&lt;List&gt; arraySupplier;   // 扩展点：第2种   AutoTypeBeforeHandler autoTypeBeforeHandler;   ExtraProcessor extraProcessor;}// 如下是JSONWriter.Context的部分定义，public static final class com.alibaba.fastjson2.JSONWriter.Context {    long features;    boolean hasFilter;  // 扩展点：第2种    PropertyPreFilter propertyPreFilter;    PropertyFilter propertyFilter;    NameFilter nameFilter;    ValueFilter valueFilter;    BeforeFilter beforeFilter;    AfterFilter afterFilter;    ...}</code></pre><p><strong>哎，这些扩展点，需要通过不同的入参传入，体验不太友好，因为这些配置项到底是哪个层面的，没有办法客观区分出来？</strong></p><p>答：有了，使用参数注解，属性注解天生就具备一个能力：该注解生效在哪个属性上！</p><blockquote></blockquote><h3 id="高低版本jdk如何兼容">高低版本JDK如何兼容</h3><p><strong>哎，不同环境可能有一些能力的实现，或者方法名称不一样？我是否可以借力其他框架的某一些能力？</strong></p><p>有了：上面好像用过 Lambda表达式的一种能力，将不同环境的实现，统一入口。</p><blockquote><p>CallSite callSite = LambdaMetafactory.metafactory(..);</p></blockquote><p>我可以基于这个思路，通过Class.forName，寻找出当前环境能提供的内容，甚至可以查找一些三方包。</p><p>只要符合要求的，我都包装进来</p><ul><li>这样即便某些低版本的JDK也可以使用高版本的一些能力（如果它引用的一些好的三方包）。</li><li>模板方法的核心逻辑封装不需要考虑环境问题（调用入口已经统一）。</li></ul><blockquote><p>方法的传递，其实使用MethodHandle已经足够了，是否再将MethodHandle包装为Lambda，取决于是否要统一调用入口。</p><p>所以如下，既有MethodHandle，也有Lambda（BiFunction，ToIntFunction）</p></blockquote><pre><code class="language-java">public static final BiFunction&lt;char[], Boolean, String&gt; STRING_CREATOR_JDK8; // JDK 8 的 String 创建函数public static final BiFunction&lt;byte[], Byte, String&gt; STRING_CREATOR_JDK11; // JDK 11 的 String 创建函数public static final ToIntFunction&lt;String&gt; STRING_CODER; // String 的编码函数public static final Function&lt;String, byte[]&gt; STRING_VALUE; // String 的值函数public static final MethodHandle METHOD_HANDLE_HAS_NEGATIVE; // 是否有负数的方法句柄public static final Predicate&lt;byte[]&gt; PREDICATE_IS_ASCII; // 是否为 ASCII 的谓词</code></pre><p>如下则是通过其他框架来扩展自身能力：</p><pre><code class="language-java">// com.alibaba.fastjson2.writer.ObjectWriterProvider#getObjectWriterInternalswitch (className) {  case &quot;com.google.common.collect.HashMultimap&quot;:  case &quot;com.google.common.collect.LinkedListMultimap&quot;:  case &quot;com.google.common.collect.LinkedHashMultimap&quot;:  case &quot;com.google.common.collect.ArrayListMultimap&quot;:  case &quot;com.google.common.collect.TreeMultimap&quot;:    objectWriter = GuavaSupport.createAsMapWriter(objectClass);    break;  case &quot;com.google.common.collect.AbstractMapBasedMultimap$RandomAccessWrappedList&quot;:    objectWriter = ObjectWriterImplList.INSTANCE;    break;  case &quot;com.alibaba.fastjson.JSONObject&quot;:    objectWriter = ObjectWriterImplMap.of(objectClass);    break;  case &quot;android.net.Uri$OpaqueUri&quot;:  case &quot;android.net.Uri$HierarchicalUri&quot;:  case &quot;android.net.Uri$StringUri&quot;:    objectWriter = ObjectWriterImplToString.INSTANCE;    break;  default:    break;}</code></pre><h2 id="性能提升的思路">性能提升的思路</h2><ul><li>Lambada表达式+MethodHandler，替换反射调用。</li><li>Unsafe方法，替换反射调用set方法。</li><li>多维度缓存，确保在高频调用逻辑均由缓存承载。</li><li>使用全局上下文，维护复用全局变量，减少重复对象的生成。</li><li>编码相关：位移操作代替加减乘除；数组索引代替map中的key；减少容器自动扩容行为;尽量在类加载阶段获取信息;尽可能的使用IdentityHashMap。</li><li>CodeGen算法使用。</li><li>使用字节码提升JSONPath性能。</li><li>使用Fnv算法代替原生的hash算法。</li></ul>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[类加载器原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/类加载器原理" />
                <id>tag:https://www.wangdaye.net,2024-01-28:类加载器原理</id>
                <published>2024-01-28T23:45:27+08:00</published>
                <updated>2024-01-28T23:45:33+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="类加载器">类加载器</h1><p>虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现，以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。类加载器可以说是Java语言的一项创新，也是Java语言流行的重要原因之一，它最初是为了满足Java Applet的需求而开发出来的。虽然目前Java Applet技术基本上已经“死掉”[插图]，但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩，成为了Java技术体系中一块重要的基石，可谓是失之桑榆，收之东隅。</p><blockquote><p>如下摘录自《深入理解JAVA虚拟机》</p></blockquote><h2 id="类与类加载器">类与类加载器</h2><p>类加载器虽然只用于实现类的加载动作，但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类，都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性，每一个类加载器，都拥有一个独立的类名称空间。这句话可以表达得更通俗一些：比较两个类是否“相等”，只有在这两个类是由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同一个Class文件，被同一个虚拟机加载，只要加载它们的类加载器不同，那这两个类就必定不相等。</p><p>这里所指的“相等”，包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果，也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响，在某些情况下可能会产生具有迷惑性的结果，代码清单7-8中演示了不同的类加载器对instanceof关键字运算的结果的影响。</p><pre><code>/＊＊ ＊ 类加载器与instanceof关键字演示 ＊ ＊ @author zzm ＊/public class ClassLoaderTest {    public static void main(String[] args) throws Exception {        ClassLoader myLoader = new ClassLoader() {              @Override              public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {                  try {                        String fileName = name.substring(name.lastIndexOf(&quot;.&quot;) + 1) +&quot;.class&quot;;                        InputStream is = getClass().getResourceAsStream(fileName);                        if (is == null) {                            return super.loadClass(name);                        }                        byte[] b = new byte[is.available()];                        is.read(b);                        return defineClass(name, b, 0, b.length);                  } catch (IOException e) {                        throw new ClassNotFoundException(name);                    }                }          };          Object obj = myLoader.loadClass(&quot;org.fenixsoft.classloading.ClassLoaderTest&quot;).newInstance();          System.out.println(obj.getClass());          System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);      }}</code></pre><p>运行结果：</p><pre><code>class org.fenixsoft.classloading.ClassLoaderTestfalse</code></pre><p>代码清单7-8中构造了一个简单的类加载器，尽管很简单，但是对于这个演示来说还是够用了。它可以加载与自己在同一路径下的Class文件。我们使用这个类加载器去加载了一个名为“org.fenixsoft.classloading.ClassLoaderTest”的类，并实例化了这个类的对象。两行输出结果中，从第一句可以看出，这个对象确实是类org.fenixsoft.classloading. ClassLoaderTest实例化出来的对象，但从第二句可以发现，这个对象与类org.fenixsoft. classloading.ClassLoaderTest做所属类型检查的时候却返回了false，这是因为虚拟机中存在了两个ClassLoaderTest类，一个是由系统应用程序类加载器加载的，另外一个是由我们自定义的类加载器加载的，虽然都来自同一个Class文件，但依然是两个独立的类，做对象所属类型检查时结果自然为false。</p><h2 id="双亲委派模型">双亲委派模型</h2><p>从Java虚拟机的角度来讲，只存在两种不同的类加载器：一种是启动类加载器（Bootstrap ClassLoader），这个类加载器使用C++语言实现[插图]，是虚拟机自身的一部分；另一种就是所有其他的类加载器，这些类加载器都由Java语言实现，独立于虚拟机外部，并且全都继承自抽象类java.lang.ClassLoader。</p><p>从Java开发人员的角度来看，类加载器还可以划分得更细致一些，绝大部分Java程序都会使用到以下3种系统提供的类加载器。</p><ul><li>启动类加载器（Bootstrap ClassLoader）：前面已经介绍过，这个类将器负责将存放在&lt;JAVA_HOME&gt;\lib目录中的，或者被-Xbootclasspath参数所指定的路径中的，并且是虚拟机识别的（仅按照文件名识别，如rt.jar，名字不符合的类库即使放在lib目录中也不会被加载）类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用，用户在编写自定义类加载器时，如果需要把加载请求委派给引导类加载器，那直接使用null代替即可，如代码清单7-9所示为java.lang.ClassLoader.getClassLoader()方法的代码片段。</li></ul><p><strong>代码清单7-9 ClassLoader.getClassLoader()方法的代码片段</strong></p><pre><code>    /＊＊    Returns the class loader for the class. Some implementations may use nullto represent the bootstrap class loader. This method will return null in suchimplementations if this class was loaded by the bootstrap class loader.    ＊/    public ClassLoader getClassLoader() {        ClassLoader cl = getClassLoader0();        if (cl == null)            return null;        SecurityManager sm = System.getSecurityManager();        if (sm != null) {            ClassLoader ccl = ClassLoader.getCallerClassLoader();            if (ccl != null &amp;&amp; ccl != cl &amp;&amp; !cl.isAncestor(ccl)) {                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);            }        }        return cl;    }</code></pre><ul><li>扩展类加载器（Extension ClassLoader）：这个加载器由sun.misc.Launcher$ExtClassLoader实现，它负责加载&lt;JAVA_HOME&gt;\lib\ext目录中的，或者被java.ext.dirs系统变量所指定的路径中的所有类库，开发者可以直接使用扩展类加载器。</li><li>应用程序类加载器（Application ClassLoader）：这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值，所以一般也称它为系统类加载器。它负责加载用户类路径（ClassPath）上所指定的类库，开发者可以直接使用这个类加载器，如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。</li></ul><p>我们的应用程序都是由这3种类加载器互相配合进行加载的，如果有必要，还可以加入自己定义的类加载器。这些类加载器之间的关系一般如图7-2所示。</p><p><img src="https://www.wangdaye.net/upload/2024/01/image-fc60a056be9c445a899c1890af703fba.png" alt="图7-2 类加载器双亲委派模型" /></p><p>图7-2中展示的类加载器之间的这种层次关系，称为类加载器的双亲委派模型（Parents Delegation Model）。双亲委派模型要求除了顶层的启动类加载器外，其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承（Inheritance）的关系来实现，而是都使用组合（Composition）关系来复用父加载器的代码。</p><p>类加载器的双亲委派模型在JDK 1.2期间被引入并被广泛应用于之后几乎所有的Java程序中，但它并不是一个强制性的约束模型，而是Java设计者推荐给开发者的一种类加载器实现方式。</p><p>双亲委派模型的工作过程是：如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把这个请求委派给父类加载器去完成，每一个层次的类加载器都是如此，因此所有的加载请求最终都应该传送到顶层的启动类加载器中，只有当父加载器反馈自己无法完成这个加载请求（它的搜索范围中没有找到所需的类）时，子加载器才会尝试自己去加载。</p><p>使用双亲委派模型来组织类加载器之间的关系，有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object，它存放在rt.jar之中，无论哪一个类加载器要加载这个类，最终都是委派给处于模型最顶端的启动类加载器进行加载，因此Object类在程序的各种类加载器环境中都是同一个类。相反，如果没有使用双亲委派模型，由各个类加载器自行去加载的话，如果用户自己编写了一个称为java. lang.Object的类，并放在程序的ClassPath中，那系统中将会出现多个不同的Object类，Java类型体系中最基础的行为也就无法保证，应用程序也将会变得一片混乱。如果读者有兴趣的话，可以尝试去编写一个与rt.jar类库中已有类重名的Java类，将会发现可以正常编译，但永远无法被加载运行[插图]。</p><p>双亲委派模型对于保证Java程序的稳定运作很重要，但它的实现却非常简单，实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中，如代码清单7-10所示，逻辑清晰易懂：先检查是否已经被加载过，若没有加载则调用父加载器的loadClass()方法，若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败，抛出ClassNotFoundException异常后，再调用自己的findClass()方法进行加载。</p><p><strong>代码清单7-10 双亲委派模型的实现</strong></p><pre><code>   protected synchronized Class&lt;?&gt; loadClass(String name, boolean resolve) throwsClassNotFoundException   {       //首先，检查请求的类是否已经被加载过了       Class c = findLoadedClass(name);       if (c == null) {           try {           if (parent != null) {               c = parent.loadClass(name, false);           } else {               c = findBootstrapClassOrNull(name);           }           } catch (ClassNotFoundException e) {               //如果父类加载器抛出ClassNotFoundException               //说明父类加载器无法完成加载请求           }           if (c == null) {               //在父类加载器无法加载的时候               //再调用本身的findClass方法来进行类加载               c = findClass(name);           }       }       if (resolve) {           resolveClass(c);       }       return c;   }</code></pre><h2 id="破坏双亲委派模型">破坏双亲委派模型</h2><p>上文提到过双亲委派模型并不是一个强制性的约束模型，而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型，但也有例外，到目前为止，双亲委派模型主要出现过3较大规模的“被破坏”情况。</p><p>双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入，而类加载器和抽象类java.lang. ClassLoader则在JDK 1.0时代就已经存在，面对已经存在的用户自定义类加载器的实现代码，Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容，JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass()，在此之前，用户去继承java. lang.ClassLoader的唯一目的就是为了重写loadClass()方法，因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal()，而这个方法的唯一逻辑就是去调用自己的loadClass()。</p><p>上一节我们已经看过loadClass()方法的代码，双亲委派的具体逻辑就实现在这个方法之中，JDK 1.2之后已不提倡用户再去覆盖loadClass()方法，而应当把自己的类加载逻辑写到findClass()方法中，在loadClass()方法的逻辑里如果父类加载失败，则会调用自己的findClass()方法来完成加载，这样就可以保证新写出来的类加载器是符合双亲委派规则的。</p><p>双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的，双亲委派很好地解决了各个类加载器的基础类的统一问题（越基础的类由越上层的加载器进行加载），基础类之所以称为“基础”，是因为它们总是作为被用户代码调用的API，但世事往往没有绝对的完美，如果基础类又要调用回用户的代码，那该怎么办？</p><p>这并非是不可能的事情，一个典型的例子便是JNDI服务，JNDI现在已经是Java的标准服务，它的代码由启动类加载器去加载（在JDK 1.3时放进去的rt.jar），但JNDI的目的就是对资源进行集中管理和查找，它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者（SPI，Service Provider Interface）的代码，但启动类加载器不可能“认识”这些代码啊！那该怎么办？为了解决这个问题，Java设计团队只好引入了一个不太优雅的设计：线程上下文类加载器（Thread Context ClassLoader）。</p><p>这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置，如果创建线程时还未设置，它将会从父线程中继承一个，如果在应用程序的全局范围内都没有设置过的话，那这个类加载器默认就是应用程序类加载器。</p><p>有了线程上下文类加载器，就可以做一些“舞弊”的事情了，JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码，也就是父类加载器请求子类加载器去完成类加载的动作，这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器，实际上已经违背了双亲委派模型的一般性原则，但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式，例如JNDI、JDBC、JCE、JAXB和JBI等。</p><p>双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的，这里所说的“动态性”指的是当前一些非常“热门”的名词：代码热替换（HotSwap）、模块热部署（Hot Deployment）等，说白了就是希望应用程序能像我们的计算机外设那样，接上鼠标、U盘，不用重启机器就能立即使用，鼠标有问题或要升级就换个鼠标，不用停机也不用重启。对于个人计算机来说，重启一次其实没有什么大不了的，但对于一些生产系统来说，关机重启一次可能就要被列为生产事故，这种情况下热部署就对软件开发者，尤其是企业级软件开发者具有很大的吸引力。</p><p>Sun公司所提出的JSR-294[插图]、JSR-277[插图]规范在与JCP组织的模块化规范之争中落败给JSR-291（即OSGi R4.2），虽然Sun不甘失去Java模块化的主导权，独立在发展Jigsaw项目，但目前OSGi已经成为了业界“事实上”的Java模块化标准[插图]，而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块（OSGi中称为Bundle）都有一个自己的类加载器，当需要更换一个Bundle时，就把Bundle连同类加载器一起换掉以实现代码的热替换。</p><p>在OSGi环境下，类加载器不再是双亲委派模型中的树状结构，而是进一步发展为更加复杂的网状结构，当收到类加载请求时，OSGi将按照下面的顺序进行类搜索：</p><p>1）将以java.*开头的类委派给父类加载器加载。<br />2）否则，将委派列表名单内的类委派给父类加载器加载。<br />3）否则，将Import列表中的类委派给Export这个类的Bundle的类加载器加载。<br />4）否则，查找当前Bundle的ClassPath，使用自己的类加载器加载。<br />5）否则，查找类是否在自己的Fragment Bundle中，如果在，则委派给FragmentBundle的类加载器加载。<br />6）否则，查找Dynamic Import列表的Bundle，委派给对应Bundle的类加载器加载。<br />7）否则，类查找失败。上面的查找顺序中只有开头两点仍然符合双亲委派规则，其余的类查找都是在平级的类加载器中进行的。</p><p>上面的查找顺序中只有开头两点仍然符合双亲委派规则，其余的类查找都是在平级的类加载器中进行的。</p><p>笔者虽然使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为，但这里“被破坏”并不带有贬义的感情色彩。只要有足够意义和理由，突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器，并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议，但在Java程序员中基本有一个共识：OSGi中对类加载器的使用是很值得学习的，弄懂了OSGi的实现，就可以算是掌握了类加载器的精髓。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[深入理解sun.misc.Unsafe原理]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/深入理解sunmiscunsafe原理" />
                <id>tag:https://www.wangdaye.net,2024-01-27:深入理解sunmiscunsafe原理</id>
                <published>2024-01-27T15:36:30+08:00</published>
                <updated>2024-01-27T15:55:46+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="前言">前言</h2><p><a href="https://so.csdn.net/so/search?q=Unsafe&amp;spm=1001.2101.3001.7020">Unsafe</a>类在JDK源码中被广泛使用，在Spark使用off-heap memory时也会使用到，该类功能很强大，涉及到类加载机制（<a href="https://blog.csdn.net/zyzzxycj/article/details/89846181">深入理解ClassLoader工作机制</a>），其实例一般情况是获取不到的，源码中的设计是采用<a href="https://so.csdn.net/so/search?q=%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F&amp;spm=1001.2101.3001.7020">单例模式</a>，不是系统加载初始化就会抛出SecurityException异常。</p><p>这个类的提供了一些绕开JVM的更底层功能，基于它的实现可以提高效率。但是，它是一把双刃剑：正如它的名字所预示的那样，它是Unsafe的，它所分配的内存需要手动free（不被GC回收）。如果对Unsafe类理解的不够透彻，就进行使用的话，就等于给自己挖了无形之坑，最为致命。</p><p>由于sun并没有将其开源，也没给出官方的Document，所以笔者只能参考一些博客（如<a href="http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/">Java Magic. Part 4: sun.misc.Unsafe</a>）以及Unsafe在JDK源码中的一些使用，来加深理解。</p><h2 id="获取unsafe类的实例">获取Unsafe类的实例</h2><ol><li>必须是Bootstrap ClassLoader加载的类<br />getUnsafe方法源码:<br /><img src="https://www.wangdaye.net/upload/2024/01/image-8da682049dcc4ab0ab32d788140b62cc.png" alt="image.png" /><br />isSystemDomainLoader:<br /><img src="https://www.wangdaye.net/upload/2024/01/image-900ad62eae7a45bf9ac9e545190d2c36.png" alt="image.png" /></li><li>通过反射暴力获取</li></ol><pre><code class="language-Java">    // 通过反射实例化Unsafe    Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);    f.setAccessible(true);    Unsafe unsafe = (Unsafe) f.get(null);</code></pre><h2 id="unsafe类中的核心方法">Unsafe类中的核心方法</h2><pre><code>//重新分配内存         public native long reallocateMemory(long address, long bytes);           //分配内存           public native long allocateMemory(long bytes);           //释放内存           public native void freeMemory(long address);           //在给定的内存块中设置值           public native void setMemory(Object o, long offset, long bytes, byte value);           //从一个内存块拷贝到另一个内存块           public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);           //获取值，不管java的访问限制，其他有类似的getInt，getDouble，getLong，getChar等等           public native Object getObject(Object o, long offset);           //设置值，不管java的访问限制，其他有类似的putInt,putDouble，putLong，putChar等等           public native void putObject(Object o, long offset);           //从一个给定的内存地址获取本地指针，如果不是allocateMemory方法的，结果将不确定           public native long getAddress(long address);           //存储一个本地指针到一个给定的内存地址,如果地址不是allocateMemory方法的，结果将不确定           public native void putAddress(long address, long x);           //该方法返回给定field的内存地址偏移量，这个值对于给定的filed是唯一的且是固定不变的           public native long staticFieldOffset(Field f);           //报告一个给定的字段的位置，不管这个字段是private，public还是保护类型，和staticFieldBase结合使用           public native long objectFieldOffset(Field f);           //获取一个给定字段的位置           public native Object staticFieldBase(Field f);           //确保给定class被初始化，这往往需要结合基类的静态域（field）           public native void ensureClassInitialized(Class c);           //可以获取数组第一个元素的偏移地址           public native int arrayBaseOffset(Class arrayClass);           //可以获取数组的转换因子，也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用， 可以定位数组中每个元素在内存中的位置           public native int arrayIndexScale(Class arrayClass);           //获取本机内存的页数，这个值永远都是2的幂次方           public native int pageSize();           //告诉虚拟机定义了一个没有安全检查的类，默认情况下这个类加载器和保护域来着调用者类           public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);           //定义一个类，但是不让它知道类加载器和系统字典           public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);           //锁定对象，必须是没有被锁的         public native void monitorEnter(Object o);           //解锁对象           public native void monitorExit(Object o);           //试图锁定对象，返回true或false是否锁定成功，如果锁定，必须用monitorExit解锁           public native boolean tryMonitorEnter(Object o);           //引发异常，没有通知           public native void throwException(Throwable ee);           //CAS，如果对象偏移量上的值=期待值，更新为x,返回true.否则false.类似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。           public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);           // 该方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。类似的方法有getIntVolatile，getBooleanVolatile等等           public native Object getObjectVolatile(Object o, long offset);            //线程调用该方法，线程将一直阻塞直到超时，或者是中断条件出现。           public native void park(boolean isAbsolute, long time);           //终止挂起的线程，恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的，也正是使用这两个方法         public native void unpark(Object thread);           //获取系统在不同时间系统的负载情况           public native int getLoadAverage(double[] loadavg, int nelems);           //创建一个类的实例，不需要调用它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有，我们也可以通过这个方法创建它的实例,对于单例模式，简直是噩梦。          public native Object allocateInstance(Class cls) throws InstantiationException; </code></pre><h3 id="总的来说就是这么几类">总的来说就是这么几类</h3><p>（1）Info相关。主要返回某些低级别的内存信息：addressSize(), pageSize()</p><p>（2）Objects相关。主要提供Object和它的域操纵方法：allocateInstance(),objectFieldOffset()</p><p>（3）Class相关。主要提供Class和它的静态域操纵方法：staticFieldOffset(),defineClass(),defineAnonymousClass(),ensureClassInitialized()</p><p>（4）Arrays相关。数组操纵方法：arrayBaseOffset(),arrayIndexScale()</p><p>（5）Synchronization相关。主要提供低级别同步原语（如基于CPU的CAS（Compare-And-Swap）原语）：monitorEnter(),tryMonitorEnter(),monitorExit(),compareAndSwapInt(),putOrderedInt()</p><p>（6）Memory相关。直接内存访问方法（绕过JVM堆直接操纵本地内存）：allocateMemory(),copyMemory(),freeMemory(),getAddress(),getInt(),putInt()</p><h3 id="unsafe该怎么用举些例子">Unsafe该怎么用？举些例子</h3><h4 id="1unsafeallocateinstance">1、Unsafe.allocateInstance</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            A o1 = new A(); // constructor            o1.a(); // prints 1            A o2 = A.class.newInstance(); // reflection            o2.a(); // prints 1            A o3 = (A) unsafe.allocateInstance(A.class); // unsafe            o3.a(); // prints 0        }        static class A {            private long a; // not initialized value, default 0            public A() {                this.a = 1; // initialization            }            public long a() {                return this.a;            }        }allocateInstance()根本没有进入构造方法，对于单例模式，简直是噩梦。</code></pre><h4 id="2内存修改绕过安全检查器unsafeobjectfieldoffset">2、内存修改，绕过安全检查器（Unsafe.objectFieldOffset）</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            Guard guard = new Guard();            guard.giveAccess();   // false, no access            // bypass            Field field = guard.getClass().getDeclaredField(&quot;ACCESS_ALLOWED&quot;);            unsafe.putInt(guard, unsafe.objectFieldOffset(field), 42); // memory corruption            guard.giveAccess(); // true, access granted        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }</code></pre><p>通过获取目标对象的字段在内存中的offset，并使用putInt()方法，类的ACCESS_ALLOWED被修改。在已知类结构的时候，数据的偏移总是可以获得的（与c++中的类中数据的偏移计算是一致的）。</p><h4 id="3sizeof-计算内存大小unsafegetdeclaredfields和unsafeobjectfieldoffset">3、sizeOf 计算内存大小（Unsafe.getDeclaredFields和Unsafe.objectFieldOffset）</h4><pre><code>public static void main(String[] args) throws Exception {            Guard guard = new Guard();            sizeOf(guard); // 16, the size of guard        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }        public static long sizeOf(Object o) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            HashSet&lt;Field&gt; fields = new HashSet();            Class c = o.getClass();            while (c != Object.class) {                for (Field field : c.getDeclaredFields()) {                    if ((field.getModifiers() &amp; Modifier.STATIC) == 0) {                        fields.add(field);                    }                }                c = c.getSuperclass();            }            // get offset            long maxSize = 0;            for (Field field : fields) {                long offset = unsafe.objectFieldOffset(field);                if (offset &gt; maxSize) {                    maxSize = offset;                }            }            return ((maxSize/8) + 1) * 8;   // padding        }</code></pre><p>算法的思路非常清晰：从底层子类开始，依次取出它自己和它的所有超类的非静态域，放置到一个HashSet中（重复的只计算一次，Java是单继承），然后使用objectFieldOffset()获得一个最大偏移，最后还考虑了对齐。</p><h4 id="4实现java浅复制">4、实现Java浅复制</h4><pre><code>public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            unsafe = (Unsafe) f.get(null);            Guard guard = new Guard();            shallowCopy(guard);        }        private static Unsafe getUnsafe() {            return unsafe;        }        static Object shallowCopy(Object obj) throws Exception {            long size = sizeOf(obj);            long start = toAddress(obj);            long address = getUnsafe().allocateMemory(size);            getUnsafe().copyMemory(start, address, size);            return fromAddress(address);        }        static long toAddress(Object obj) {            Object[] array = new Object[]{obj};            long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);            return normalize(getUnsafe().getLong(array, baseOffset));        }        static Object fromAddress(long address) {            Object[] array = new Object[] {null};            long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);            getUnsafe().putLong(array, baseOffset, address);            return array[0];        }        static class Guard {            private int ACCESS_ALLOWED = 1;            public boolean giveAccess() {                return 42 == ACCESS_ALLOWED;            }        }        public static long sizeOf(Object o) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            Unsafe unsafe = (Unsafe) f.get(null);            HashSet&lt;Field&gt; fields = new HashSet();            Class c = o.getClass();            while (c != Object.class) {                for (Field field : c.getDeclaredFields()) {                    if ((field.getModifiers() &amp; Modifier.STATIC) == 0) {                        fields.add(field);                    }                }                c = c.getSuperclass();            }            // get offset            long maxSize = 0;            for (Field field : fields) {                long offset = unsafe.objectFieldOffset(field);                if (offset &gt; maxSize) {                    maxSize = offset;                }            }            return ((maxSize / 8) + 1) * 8;   // padding        }</code></pre><p>思路很简单，利用Unsafe.copyMemory()，将老地址及其指向的对象的size，拷贝到新的内存地址上。<br />并且浅复制函数可以应用于任意java对象，它的尺寸是动态计算的。<br />（在实际测试的时候，执行unsafe.copyMemory时，JVM会输出hs_err_pid.log日志然后挂掉，该问题还有待排查）</p><h4 id="5隐藏密码">5、隐藏密码</h4><p>一般密码都要存成byte[]或者char[]数组，为什么呢？？<br />因为我们使用完了，可以直接将他们设为null。但如果密码存在String中，将其设为null，密码实际任然存在在内存中，等到GC后，才能被释放，就很不安全。<br />所以当我们把密码字段存储在String中时，在密码字段使用完之后，最安全的做法是：将它的值覆盖。<br />很多不再需要的，但是又是比较机密的对象，想快点消灭证据，都可以通过这种方法来消除。</p><pre><code>Field stringValue = String.class.getDeclaredField(&quot;value&quot;);        stringValue.setAccessible(true);        char[] mem = (char[]) stringValue.get(password);        for (int i = 0; i &lt; mem.length; i++) {            mem[i] = '?';        }</code></pre><h4 id="6多继承">6、多继承</h4><p>在java中没有多继承，除非我们能在这些不同的类互相强制转换。</p><pre><code>long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));    long strClassAddress = normalize(getUnsafe().getInt(&quot;&quot;, 4L));    getUnsafe().putAddress(intClassAddress + 36, strClassAddress);</code></pre><p>上面这段代码将String添加为int的父类，所以我们转换的时候就不会报错了。</p><pre><code>(String) (Object) (new Integer(666))    static inline bool    compareAndSwap (volatile jint *addr, jint old, jint new_val)    {      jboolean result = false;      spinlock lock;      // result=原先指针指向的地址的值(*addr)是否与旧的值(old)相等      if ((result = (*addr == old)))        // 如果相等则把内存修改为新值        *addr = new_val;      return result;</code></pre><h4 id="7动态加载类">7、动态加载类</h4><p>标准的动态加载类的方法是Class.forName()(在编写jdbc程序时，记忆深刻)，使用Unsafe也可以动态加载java 的class文件。<br />我们可以在程序运行是动态加载编译好的.class文件，不明白<a href="https://so.csdn.net/so/search?q=ClassLoader&amp;spm=1001.2101.3001.7020">ClassLoader</a>的可以参考&lt;<a href="https://blog.csdn.net/zyzzxycj/article/details/89846181">深入理解ClassLoader工作机制</a>&gt;<br /><strong>具体是怎么实现的呢？？</strong></p><pre><code>// 我们首先读取一个class文件到byte数组中    byte[] classContents = getClassContent();    // 然后通过Unsafe.defineClass()来加载对应的Class    Class c = getUnsafe().defineClass(                  null, classContents, 0, classContents.length);    // 最后调用    c.getMethod(&quot;a&quot;).invoke(c.newInstance(), null); // 1    private static byte[] getClassContent() throws Exception {        File f = new File(&quot;/home/mishadoff/tmp/A.class&quot;);        FileInputStream input = new FileInputStream(f);        byte[] content = new byte[(int)f.length()];        input.read(content);        input.close();        return content;    }</code></pre><p>动态加载、代理、切片等功能中可以应用。</p><h4 id="8快速序列化">8、快速序列化</h4><p>我们都知道标准的Java Serializable速度很慢，它还限制类必须有public无参构造函数。<br />Externalizable相对好些，但它需要为序列化的类指定schema。<br />更流行的如kyro，在小内存的情况下不适用。<br /><strong>序列化过程：</strong></p><ol><li>用反射构建对象的schema</li><li>用Unsafe中的getLong, getInt, getObject等方法来检索对象中字段的值。</li><li>增加对象对应的类的标示符，来标记序列化结果。</li><li>将结果写入文件或者输出流。(可以增加压缩来减小序列化结果)</li></ol><p><strong>反序列化过程：</strong></p><ol><li>使用Unsafe.allocateInstance()来实例化一个被序列化的对象。(不需要执行构造函数)</li><li>构建schema，同序列化过程中的第一步。</li><li>从文件或者输入流中读取所有的字段。</li><li>用Unsafe中的putLong, putInt, putObject等方法来填充该对象。</li></ol><p>在kyro序列化中，也有一些使用Unsafe的尝试：<a href="https://code.google.com/archive/p/kryo/issues/75">https://code.google.com/archive/p/kryo/issues/75</a><br />（笔者还没仔细看，有兴趣的可以阅读一下。。估计能看到这儿的也挺累了）</p><h4 id="9在非java堆中分配内存">9、在非Java堆中分配内存</h4><p>使用java 的new会在堆中为对象分配内存，并且对象的生命周期内，会被JVM GC管理。<br />Unsafe分配的内存，不受Integer.MAX_VALUE的限制，并且分配在非堆内存，使用它时，需要非常谨慎：忘记手动回收时，会产生内存泄露；非法的地址访问时，会导致JVM崩溃。在需要分配大的连续区域、实时编程（不能容忍JVM延迟）时，可以使用它。java.nio使用这一技术。<br /><a href="https://so.csdn.net/so/search?q=Spark&amp;spm=1001.2101.3001.7020">Spark</a>中的Netty也使用了这个技术。<br />在Spark UnsafeMemoryAllocator源码中我们可以看到其使用了Unsafe.allocateMemory()并会抛出OOM异常：<br /><img src="https://www.wangdaye.net/upload/2024/01/image-1f3f9bca7db4431db5091a9e9d3dc7bc.png" alt="image.png" /></p><h4 id="10大数组">10、大数组</h4><p>Java的数组最大容量受常量Integer.MAX_VALUE的限制，如果我们用直接申请内存的方式去创建数组，那么数组大小只会收到堆的大小的限制。</p><pre><code>private static Unsafe unsafe;       public static void main(String[] args) throws Exception {            Field f = Unsafe.class.getDeclaredField(&quot;theUnsafe&quot;);            f.setAccessible(true);            unsafe = (Unsafe) f.get(null);            // 设置数组大小为Integer.MAX_VALUE的2倍            long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;            SuperArray array = new SuperArray(SUPER_SIZE);            System.out.println(&quot;Array size:&quot; + array.size()); // 4294967294            int sum = 0;            for (int i = 0; i &lt; 100; i++) {                array.set((long) Integer.MAX_VALUE + i, (byte) 3);                sum += array.get((long) Integer.MAX_VALUE + i);            }            System.out.println(&quot;Sum of 100 elements:&quot; + sum);  // 300        }        private static Unsafe getUnsafe() {            return unsafe;        }        static class SuperArray {            private final static int BYTE = 1;            private long size;            private long address;            public SuperArray(long size) {                this.size = size;                address = getUnsafe().allocateMemory(size * BYTE);            }            public void set(long i, byte value) {                getUnsafe().putByte(address + i * BYTE, value);            }            public int get(long idx) {                return getUnsafe().getByte(address + idx * BYTE);            }            public long size() {                return size;            }        }</code></pre><p>输出结果：<br /><img src="https://www.wangdaye.net/upload/2024/01/image-8618a63d0c234004bea64dc989ad91f9.png" alt="image.png" /></p><h3 id="unsafe类的底层源码实现">Unsafe类的底层源码实现</h3><h4 id="cas">CAS</h4><p>终于等到CAS出场了。CAS在AQS、ConcurrentHashmap、ForkJoinPool、FutureTask、StampedLock等都有大量的应用。甚至可以说很多Java程序员觉得CAS就是他们所了解的最小单元了。确实，也包括我。。<br />但是总觉得源码看到这儿了，不往下看就像一件事做了一半。<br /><a href="http://natUnsafe.cc">natUnsafe.cc</a></p><pre><code>// natUnsafe.cc - Implementation of sun.misc.Unsafe native methods.    /** Copyright (C) 2006, 2007       Free Software Foundation       This file is part of libgcj.    This software is copyrighted work licensed under the terms of the    Libgcj License.  Please consult the file &quot;LIBGCJ_LICENSE&quot; for    details.  */    #include &lt;gcj/cni.h&gt;    #include &lt;gcj/field.h&gt;    #include &lt;gcj/javaprims.h&gt;    #include &lt;jvm.h&gt;    #include &lt;sun/misc/Unsafe.h&gt;    #include &lt;java/lang/System.h&gt;    #include &lt;java/lang/InterruptedException.h&gt;    #include &lt;java/lang/Thread.h&gt;    #include &lt;java/lang/Long.h&gt;    #include &quot;sysdep/locks.h&quot;    // Use a spinlock for multi-word accesses    class spinlock    {      static volatile obj_addr_t lock;    public:    spinlock ()      {        while (! compare_and_swap (&amp;lock, 0, 1))          _Jv_ThreadYield ();      }      ~spinlock ()      {        release_set (&amp;lock, 0);      }    };    // This is a single lock that is used for all synchronized accesses if    // the compiler can't generate inline compare-and-swap operations.  In    // most cases it'll never be used, but the i386 needs it for 64-bit    // locked accesses and so does PPC32\.  It's worth building libgcj with    // target=i486 (or above) to get the inlines.    volatile obj_addr_t spinlock::lock;    static inline bool    compareAndSwap (volatile jint *addr, jint old, jint new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    static inline bool    compareAndSwap (volatile jlong *addr, jlong old, jlong new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    static inline bool    compareAndSwap (volatile jobject *addr, jobject old, jobject new_val)    {      jboolean result = false;      spinlock lock;      if ((result = (*addr == old)))        *addr = new_val;      return result;    }    jlong    sun::misc::Unsafe::objectFieldOffset (::java::lang::reflect::Field *field)    {      _Jv_Field *fld = _Jv_FromReflectedField (field);      // FIXME: what if it is not an instance field?      return fld-&gt;getOffset();    }    jint    sun::misc::Unsafe::arrayBaseOffset (jclass arrayClass)    {      // FIXME: assert that arrayClass is array.      jclass eltClass = arrayClass-&gt;getComponentType();      return (jint)(jlong) _Jv_GetArrayElementFromElementType (NULL, eltClass);    }    jint    sun::misc::Unsafe::arrayIndexScale (jclass arrayClass)    {      // FIXME: assert that arrayClass is array.      jclass eltClass = arrayClass-&gt;getComponentType();      if (eltClass-&gt;isPrimitive())        return eltClass-&gt;size();      return sizeof (void *);    }    // These methods are used when the compiler fails to generate inline    // versions of the compare-and-swap primitives.    jboolean    sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,                          jint expect, jint update)    {      jint *addr = (jint *)((char *)obj + offset);      return compareAndSwap (addr, expect, update);    }    jboolean    sun::misc::Unsafe::compareAndSwapLong (jobject obj, jlong offset,                           jlong expect, jlong update)    {      volatile jlong *addr = (jlong*)((char *) obj + offset);      return compareAndSwap (addr, expect, update);    }    jboolean    sun::misc::Unsafe::compareAndSwapObject (jobject obj, jlong offset,                         jobject expect, jobject update)    {      jobject *addr = (jobject*)((char *) obj + offset);      return compareAndSwap (addr, expect, update);    }    void    sun::misc::Unsafe::putOrderedInt (jobject obj, jlong offset, jint value)    {      volatile jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putOrderedLong (jobject obj, jlong offset, jlong value)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putOrderedObject (jobject obj, jlong offset, jobject value)    {      volatile jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putIntVolatile (jobject obj, jlong offset, jint value)    {      write_barrier ();      volatile jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    void    sun::misc::Unsafe::putLongVolatile (jobject obj, jlong offset, jlong value)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putObjectVolatile (jobject obj, jlong offset, jobject value)    {      write_barrier ();      volatile jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    #if 0  // FIXME    void    sun::misc::Unsafe::putInt (jobject obj, jlong offset, jint value)    {      jint *addr = (jint *) ((char *) obj + offset);      *addr = value;    }    #endif    void    sun::misc::Unsafe::putLong (jobject obj, jlong offset, jlong value)    {      jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      *addr = value;    }    void    sun::misc::Unsafe::putObject (jobject obj, jlong offset, jobject value)    {      jobject *addr = (jobject *) ((char *) obj + offset);      *addr = value;    }    jint    sun::misc::Unsafe::getIntVolatile (jobject obj, jlong offset)    {      volatile jint *addr = (jint *) ((char *) obj + offset);      jint result = *addr;      read_barrier ();      return result;    }    jobject    sun::misc::Unsafe::getObjectVolatile (jobject obj, jlong offset)    {      volatile jobject *addr = (jobject *) ((char *) obj + offset);      jobject result = *addr;      read_barrier ();      return result;    }    jlong    sun::misc::Unsafe::getLong (jobject obj, jlong offset)    {      jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      return *addr;    }    jlong    sun::misc::Unsafe::getLongVolatile (jobject obj, jlong offset)    {      volatile jlong *addr = (jlong *) ((char *) obj + offset);      spinlock lock;      return *addr;    }    void    sun::misc::Unsafe::unpark (::java::lang::Thread *thread)    {      natThread *nt = (natThread *) thread-&gt;data;      nt-&gt;park_helper.unpark ();    }    void    sun::misc::Unsafe::park (jboolean isAbsolute, jlong time)    {      using namespace ::java::lang;      Thread *thread = Thread::currentThread();      natThread *nt = (natThread *) thread-&gt;data;      nt-&gt;park_helper.park (isAbsolute, time);    }我们可以看到compareAndSwap就这么几行代码:    }</code></pre><p>好了，现在终于彻底知道了整个链路的原理了。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[JVM中的动态分派机制到底是怎么回事？]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/jvm中的动态分派机制到底是怎么回事" />
                <id>tag:https://www.wangdaye.net,2024-01-24:jvm中的动态分派机制到底是怎么回事</id>
                <published>2024-01-24T21:53:38+08:00</published>
                <updated>2024-01-24T22:40:56+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<blockquote><p>如下摘录自《深入理解JAVA虚拟机》</p></blockquote><h1 id="方法调用">方法调用</h1><p>方法调用并不等同于方法执行，方法调用阶段唯一的任务就是确定被调用方法的版本（即调用哪一个方法），暂时还不涉及方法内部的具体运行过程。在程序运行时，进行方法调用是最普遍、最频繁的操作，但前面已经讲过，Class文件的编译过程中不包含传统编译中的连接步骤，一切方法调用在Class文件里面存储的都只是符号引用，而不是方法在实际运行时内存布局中的入口地址（相当于之前说的直接引用）。这个特性给Java带来了更强大的动态扩展能力，但也使得Java方法调用过程变得相对复杂起来，需要在类加载期间，甚至到运行期间才能确定目标方法的直接引用。</p><h2 id="解析">解析</h2><p>继续前面关于方法调用的话题，所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用，在类加载的解析阶段，会将其中的一部分符号引用转化为直接引用，这种解析能成立的前提是：方法在程序真正运行之前就有一个可确定的调用版本，并且这个方法的调用版本在运行期是不可改变的。换句话说，调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析（Resolution）。在Java语言中符合“编译期可知，运行期不可变”这个要求的方法，主要包括静态方法和私有方法两大类，前者与类型直接关联，后者在外部不可被访问，这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本，因此它们都适合在类加载阶段进行解析。</p><p>与之相对应的是，在Java虚拟机里面提供了5条方法调用字节码指令，分别如下:</p><ul><li>invokestatic：调用静态方法。</li><li>invokespecial：调用实例构造器<init>方法、私有方法和父类方法。</li><li>invokevirtual：调用所有的虚方法.</li><li>invokeinterface：调用接口方法，会在运行时再确定一个实现此接口的对象。</li><li>invokedynamic：先在运行时动态解析出调用点限定符所引用的方法，然后再执行该方法，在此之前的4条调用指令，分派逻辑是固化在Java虚拟机内部的，而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。</li></ul><p>只要能被invokestatic和invokespecial指令调用的方法，都可以在解析阶段中确定唯一的调用版本，符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类，它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法，与之相反，其他方法称为虚方法（除去final方法，后文会提到）。代码清单8-5演示了一个最常见的解析调用的例子，此样例中，静态方法sayHello()只可能属于类型StaticResolution，没有任何手段可以覆盖或隐藏这个方法。</p><pre><code>/＊＊ ＊ 方法静态解析演示 ＊ ＊ @author zzm ＊/public class StaticResolution {    public static void sayHello() {            System.out.println(&quot;hello world&quot;);    }    public static void main(String[] args) {           StaticResolution.sayHello();    }}</code></pre><p>使用javap命令查看这段程序的字节码，会发现的确是通过invokestatic命令来调用sayHello()方法的。</p><pre><code>D:\Develop\&gt;javap -verbose StaticResolutionpublic static void main(java.lang.String[]);  Code:  Stack=0, Locals=1, Args_size=1  0:   invokestatic    #31;            //Method sayHello:()V  3:   return LineNumberTable:  line 15: 0  line 16: 3</code></pre><p>Java中的非虚方法除了使用invokestatic、invokespecial调用的方法之外还有一种，就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的，但是由于它无法被覆盖，没有其他版本，所以也无须对方法接收者进行多态选择，又或者说多态选择的结果肯定是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。</p><p>解析调用一定是个静态的过程，在编译期间就完全确定，在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用，不会延迟到运行期再去完成。而分派（Dispatch）调用则可能是静态的也可能是动态的，根据分派依据的宗量数[插图]可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况，下面我们再看看虚拟机中的方法分派是如何进行的。</p><h2 id="分派">分派</h2><p>众所周知，Java是一门面向对象的程序语言，因为Java具备面向对象的3个基本特征：继承、封装和多态。本节讲解的分派调用过程将会揭示多态性特征的一些最基本的体现，如“重载”和“重写”在Java虚拟机之中是如何实现的，这里的实现当然不是语法上该如何写，我们关心的依然是虚拟机如何确定正确的目标方法。</p><h3 id="静态分派">静态分派</h3><p>在开始讲解静态分派前，笔者准备了一段经常出现在面试题中的程序代码，读者不妨先看一遍，想一下程序的输出结果是什么。后面我们的话题将围绕这个类的方法来重载（Overload）代码，以分析虚拟机和编译器确定方法版本的过程。方法静态分派如代码清单8-6所示。</p><p><strong>代码清单8-6 方法静态分派演示</strong></p><pre><code>package org.fenixsoft.polymorphic;/＊＊ ＊ 方法静态分派演示 ＊ @author zzm ＊/public class StaticDispatch {    static abstract class Human {    }    static class Man extends Human {    }    static class Woman extends Human {    }    public void sayHello(Human guy) {            System.out.println(&quot;hello,guy!&quot;);    }    public void sayHello(Man guy) {            System.out.println(&quot;hello,gentleman!&quot;);   }    public void sayHello(Woman guy) {            System.out.println(&quot;hello,lady!&quot;);    }    public static void main(String[] args) {            Human man = new Man();            Human woman = new Woman();            StaticDispatch sr = new StaticDispatch();            sr.sayHello(man);            sr.sayHello(woman);    }}</code></pre><p>运行结果：</p><pre><code>hello,guy!hello,guy!</code></pre><p>代码清单8-6中的代码实际上是在考验阅读者对重载的理解程度，相信对Java编程稍有经验的程序员看完程序后都能得出正确的运行结果，但为什么会选择执行参数类型为Human的重载呢？在解决这个问题之前，我们先按如下代码定义两个重要的概念。</p><pre><code>Human man = new Man();</code></pre><p>我们把上面代码中的“Human”称为变量的静态类型（Static Type），或者叫做的外观类型（Apparent Type），后面的“Man”则称为变量的实际类型（ActualType），静态类型和实际类型在程序中都可以发生一些变化，区别是静态类型的变化仅仅在使用时发生，变量本身的静态类型不会被改变，并且最终的静态类型是在编译期可知的；而实际类型变化的结果在运行期才可确定，编译器在编译程序的时候并不知道一个对象的实际类型是什么。例如下面的代码：</p><pre><code>//实际类型变化Human man = new Man();man = new Woman();//静态类型变化sr.sayHello((Man) man)sr.sayHello((Woman) man)</code></pre><p>解释了这两个概念，再回到代码清单8-6的样例代码中。main()里面的两次sayHello()方法调用，在方法接收者已经确定是对象“sr”的前提下，使用哪个重载版本，就完全取决于传入参数的数量和数据类型。代码中刻意地定义了两个静态类型相同但实际类型不同的变量，但虚拟机（准确地说是编译器）在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的，因此，在编译阶段，Javac编译器会根据参数的静态类型决定使用哪个重载版本，所以选择了sayHello(Human)作为调用目标，并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。</p><p>所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段，因此确定静态分派的动作实际上不是由虚拟机来执行的。另外，编译器虽然能确定出方法的重载版本，但在很多情况下这个重载版本并不是“唯一的”，往往只能确定一个“更加合适的”版本。这种模糊的结论在由0和1构成的计算机世界中算是比较“稀罕”的事情，产生这种模糊结论的主要原因是字面量不需要定义，所以字面量没有显式的静态类型，它的静态类型只能通过语言上的规则去理解和推断。代码清单8-7演示了何为“更加合适的”版本。</p><p>代码清单8-7 重载方法匹配优先级</p><pre><code>package org.fenixsoft.polymorphic;public class Overload {    public static void sayHello(Object arg) {           System.out.println(&quot;hello Object&quot;);    }    public static void sayHello(int arg) {           System.out.println(&quot;hello int&quot;);    }    public static void sayHello(long arg) {           System.out.println(&quot;hello long&quot;);    }    public static void sayHello(Character arg) {           System.out.println(&quot;hello Character&quot;);    }    public static void sayHello(char arg) {           System.out.println(&quot;hello char&quot;);    }    public static void sayHello(char... arg) {           System.out.println(&quot;hello char ...&quot;);    }    public static void sayHello(Serializable arg) {           System.out.println(&quot;hello Serializable&quot;);     }    public static void main(String[] args) {            sayHello('a');     }}</code></pre><p>上面的代码运行后会输出：</p><pre><code>hello char</code></pre><p>这很好理解，'a'是一个char类型的数据，自然会寻找参数类型为char的重载方法，如果注释掉sayHello(char arg)方法，那输出会变为：</p><pre><code>hello int</code></pre><p>这时发生了一次自动类型转换，'a'除了可以代表一个字符串，还可以代表数字97（字符'a'的Unicode数值为十进制数字97），因此参数类型为int的重载也是合适的。我们继续注释掉sayHello(int arg)方法，那输出会变为：</p><pre><code>hello long</code></pre><p>这时发生了两次自动类型转换，'a'转型为整数97之后，进一步转型为长整数97L，匹配了参数类型为long的重载。笔者在代码中没有写其他的类型如float、double等的重载，不过实际上自动转型还能继续发生多次，按照char-&gt;int-&gt;long-&gt;float-&gt;double的顺序转型进行匹配。但不会匹配到byte和short类型的重载，因为char到byte或short的转型是不安全的。我们继续注释掉sayHello(long arg)方法，那输出会变为：</p><pre><code>hello Character</code></pre><p>这时发生了一次自动装箱，'a'被包装为它的封装类型java.lang.Character，所以匹配到了参数类型为Character的重载，继续注释掉sayHello(Character arg)方法，那输出会变为：</p><pre><code>hello Serializable</code></pre><p>这个输出可能会让人感觉摸不着头脑，一个字符或数字与序列化有什么关系？出现hello Serializable，是因为java.lang.Serializable是java.lang.Character类实现的一个接口，当自动装箱之后发现还是找不到装箱类，但是找到了装箱类实现了的接口类型，所以紧接着又发生一次自动转型。char可以转型成int，但是Character是绝对不会转型为Integer的，它只能安全地转型为它实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable<Character>，如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法，那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型，会提示类型模糊，拒绝编译。程序必须在调用时显式地指定字面量的静态类型，如：sayHello((Comparable<Character>)'a')，才能编译通过。下面继续注释掉sayHello(Serializable arg)方法，输出会变为：</p><pre><code>hello Object</code></pre><p>这时是char装箱后转型为父类了，如果有多个父类，那将在继承关系中从下往上开始搜索，越接近上层的优先级越低。即使方法调用传入的参数值为null时，这个规则仍然适用。我们把sayHello(Object arg)也注释掉，输出将会变为：</p><pre><code>hello char ...</code></pre><p>7个重载方法已经被注释得只剩一个了，可见变长参数的重载优先级是最低的，这时候字符'a'被当做了一个数组元素。笔者使用的是char类型的变长参数，读者在验证时还可以选择int类型、Character类型、Object类型等的变长参数重载来把上面的过程重新演示一遍。但要注意的是，有一些在单个参数中能成立的自动转型，如char转型为int，在变长参数中是不成立的</p><p>代码清单8-7演示了编译期间选择静态分派目标的过程，这个过程也是Java语言实现方法重载的本质。演示所用的这段程序属于很极端的例子，除了用做面试题为难求职者以外，在实际工作中几乎不可能有实际用途。笔者拿来做演示仅仅是用于讲解重载时目标方法选择的过程，大部分情况下进行这样极端的重载都可算是真正的“关于茴香豆的茴有几种写法的研究”。无论对重载的认识有多么深刻，一个合格的程序员都不应该在实际应用中写出如此极端的重载代码。</p><p>另外还有一点读者可能比较容易混淆：笔者讲述的解析与分派这两者之间的关系并不是二选一的排他关系，它们是在不同层次上去筛选、确定目标方法的过程。例如，前面说过，静态方法会在类加载期就进行解析，而静态方法显然也是可以拥有重载版本的，选择重载版本的过程也是通过静态分派完成的。</p><h3 id="动态分派">动态分派</h3><p>了解了静态分派，我们接下来看一下动态分派的过程，它和多态性的另外一个重要体现——重写（Override）有着很密切的关联。我们还是用前面的Man和Woman一起sayHello的例子来讲解动态分派，请看代码清单8-8中所示的代码。</p><p><strong>代码清单8-8 方法动态分派演示</strong></p><pre><code>package org.fenixsoft.polymorphic;/＊＊ ＊ 方法动态分派演示 ＊ @author zzm ＊/public class DynamicDispatch {    static abstract class Human {           protected abstract void sayHello();    }    static class Man extends Human {           @Override           protected void sayHello() {                  System.out.println(&quot;man say hello&quot;);          }    }    static class Woman extends Human {           @Override           protected void sayHello() {                  System.out.println(&quot;woman say hello&quot;);           }    }    public static void main(String[] args) {           Human man = new Man();           Human woman = new Woman();           man.sayHello();           woman.sayHello();           man = new Woman();           man.sayHello();    }}</code></pre><p>运行结果：</p><pre><code>man say hellowoman say hellowoman say hello</code></pre><p>这个运行结果相信不会出乎任何人的意料，对于习惯了面向对象思维的Java程序员会觉得这是完全理所当然的。现在的问题还是和前面的一样，虚拟机是如何知道要调用哪个方法的？</p><p>显然这里不可能再根据静态类型来决定，因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为，并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显，是这两个变量的实际类型不同，Java虚拟机是如何根据实际类型来分派方法执行版本的呢？我们使用javap命令输出这段代码的字节码，尝试从中寻找答案，输出结果如代码清单8-9所示。</p><p><strong>代码清单8-9 main()方法的字节码</strong></p><pre><code>public static void main(java.lang.String[]);  Code:   Stack=2, Locals=3, Args_size=1   0:   new     #16;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Man   3:   dup   4:   invokespecial   #18;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Man.&quot;&lt;init&gt;&quot;:()V   7:   astore_1   8:   new     #19;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Woman   11: dup   12: invokespecial   #21;    //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman.&quot;&lt;init&gt;&quot;:()V   15: astore_2   16: aload_1   17: invokevirtual   #22;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Human.sayHello:()V   20: aload_2   21: invokevirtual   #22;    // Method org/fenixsoft/polymorphic/Dynamic-Dispatch$Human.sayHello:()V   24: new     #19;             // class org/fenixsoft/polymorphic/Dynamic-Dispatch$Woman   27: dup   28: invokespecial   #21;            //Method org/fenixsoft/polymorphic/DynamicDispatch$Woman.&quot;&lt;init&gt;&quot;:()V   31: astore_1   32: aload_1   33: invokevirtual   #22;            // Method org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello:()V   36: return</code></pre><p>0～15行的字节码是准备动作，作用是建立man和woman的内存空间、调用Man和Woman类型的实例构造器，将这两个实例的引用存放在第1、2个局部变量表Slot之中，这个动作也就对应了代码中的这两句：</p><pre><code>Human man = new Man();Human woman = new Woman();</code></pre><p>接下来的16～21句是关键部分，16、20两句分别把刚刚创建的两个对象的引用压到栈顶，这两个对象是将要执行的sayHello()方法的所有者，称为接收者（Receiver）；17和21句是方法调用指令，这两条调用指令单从字节码角度来看，无论是指令（都是invokevirtual）还是参数（都是常量池中第22项的常量，注释显示了这个常量是Human.sayHello()的符号引用）完全一样的，但是这两句指令最终执行的目标方法并不相同。原因就需要从invokevirtual指令的多态查找过程开始说起，invokevirtual指令的运行时解析过程大致分为以下几个步骤：</p><ul><li>找到操作数栈顶的第一个元素所指向的对象的实际类型，记作C。</li><li>如果在类型C中找到与常量中的描述符和简单名称都相符的方法，则进行访问权限校验，如果通过则返回这个方法的直接引用，查找过程结束；如果不通过，则返回java.lang. IllegalAccessError异常。</li><li>否则，按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。</li><li>如果始终没有找到合适的方法，则抛出java.lang.AbstractMethodError异常。</li></ul><p>由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型，所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上，这个过程就是Java语言中方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。</p><h3 id="虚拟机动态分派的实现">虚拟机动态分派的实现</h3><p>前面介绍的分派过程，作为对虚拟机概念模型的解析基本上已经足够了，它已经解决了虚拟机在分派中“会做什么”这个问题。但是虚拟机“具体是如何做到的”，可能各种虚拟机的实现都会有些差别。</p><p>由于动态分派是非常频繁的动作，而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法，因此在虚拟机的实际实现中基于性能的考虑，大部分实现都不会真正地进行如此频繁的搜索。面对这种情况，最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表（Vritual Method Table，也称为vtable，与此对应的，在invokeinterface执行时也会用到接口方法表——Inteface Method Table，简称itable），使用虚方法表索引来代替元数据查找以提高性能。我们先看看代码清单8-10所对应的虚方法表结构示例，如图8-3所示。</p><p><img src="https://www.wangdaye.net/upload/2024/01/image-00ac032c2cb04f51bc3076e66d1aa956.png" alt="图8-3 方法表结构" /></p><p>虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写，那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的，都指向父类的实现入口。如果子类中重写了这个方法，子类方法表中的地址将会替换为指向子类实现版本的入口地址。图8-3中，Son重写了来自Father的全部方法，因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法，所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。</p><p>为了程序实现上的方便，具有相同签名的方法，在父类、子类的虚方法表中都应当具有一样的索引序号，这样当类型变换时，仅需要变更查找的方法表，就可以从不同的虚方法表中按索引转换出所需的入口地址。</p><p>方法表一般在类加载的连接阶段进行初始化，准备了类的变量初始值后，虚拟机会把该类的方法表也初始化完毕。</p><p>上文中笔者说方法表是分派调用的“稳定优化”手段，虚拟机除了使用方法表之外，在条件允许的情况下，还会使用内联缓存（Inline Cache）和基于“类型继承关系分析”（Class Hierarchy Analysis，CHA）技术的守护内联（Guarded Inlining）两种非稳定的“激进优化”手段来获得更高的性能，关于这两种优化技术的原理和运作过程，读者可以参考本书第11章中的相关内容。</p><h2 id="动态类型语言支持">动态类型语言支持</h2><p>Java虚拟机的字节码指令集的数量从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里，一直没有发生任何变化。随着JDK 7的发布，字节码指令集终于迎来了第一位新成员——invokedynamic指令。这条新增加的指令是JDK 7实现“动态类型语言”（Dynamically Typed Language）支持而进行的改进之一，也是为JDK 8可以顺利实现Lambda表达式做技术准备。在本节中，我们将详细讲解JDK 7这项新特性出现的前因后果和它的深远意义。</p><h3 id="动态类型语言">动态类型语言</h3><p>在介绍Java虚拟机的动态类型语言支持之前，我们要先弄明白动态类型语言是什么？它与Java语言、Java虚拟机有什么关系？了解JDK 1.7提供动态类型语言支持的技术背景，对理解这个语言特性是很有必要的。</p><p>什么是动态类型语言？动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期，满足这个特征的语言有很多，常用的包括：APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等。相对的，在编译期就进行类型检查过程的语言（如C++和Java等）就是最常用的静态类型语言。</p><p>觉得上面定义过于概念化？那我们不妨通过两个例子以最浅显的方式来说明什么是“在编译期/运行期进行”和什么是“类型检查”。首先看下面这段简单的Java代码，它是否能正常编译和运行？</p><pre><code>public static void main(String[] args) {    int[][][] array = new int[1][0][-1];}</code></pre><p>这段代码能够正常编译，但运行的时候会报NegativeArraySizeException异常。在Java虚拟机规范中明确规定了NegativeArraySizeException是一个运行时异常，通俗一点来说，运行时异常就是只要代码不运行到这一行就不会有问题。与运行时异常相对应的是连接时异常，例如很常见的NoClassDefFoundError便属于连接时异常，即使会导致连接时异常的代码放在一条无法执行到的分支路径上，类加载时（Java的连接过程不在编译阶段，而在类加载阶段）也照样会抛出异常。</p><p>不过，在C语言中，含义相同的代码会在编译期报错：</p><pre><code>int main(void) {   int i[1][0][-1];              //GCC拒绝编译，报“size of array is negative”   return 0;}</code></pre><p>由此看来，一门语言的哪一种检查行为要在运行期进行，哪一种检查要在编译期进行并没有必然的因果逻辑关系，关键是语言规范中人为规定的。再举一个例子来解释“类型检查”，例如下面这一句非常简单的代码：</p><pre><code>obj.println(&quot;hello world&quot;);</code></pre><p>虽然每个人都能看懂这行代码要做什么，但对于计算机来说，这一行代码“没头没尾”是无法执行的，它需要一个具体的上下文才有讨论的意义。</p><p>现在假设这行代码是在Java语言中，并且变量obj的静态类型为java.io.PrintStream，那变量obj的实际类型就必须是PrintStream的子类（实现了PrintStream接口的类）才是合法的。否则，哪怕obj属于一个确实有用println(String)方法，但与PrintStream接口没有继承关系，代码依然不可能运行——因为类型检查不合法。</p><p>但是相同的代码在ECMAScript（JavaScript）中情况则不一样，无论obj具体是何种类型，只要这种类型的定义中确实包含有println(String)方法，那方法调用便可成功。</p><p>这种差别产生的原因是Java语言在编译期间已将println(String)方法完整的符号引用（本例中为一个CONSTANT_InterfaceMethodref_info常量）生成出来，作为方法调用指令的参数存储到Class文件中，例如下面这段代码：</p><pre><code>invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V</code></pre><p>这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息，通过这个符号引用，虚拟机可以翻译出这个方法的直接引用。而在ECMAScript等动态类型语言中，变量obj本身是没有类型的，变量obj的值才具有类型，编译时最多只能确定方法名称、参数、返回值这些信息，而不会去确定方法所在的具体类型（即方法接收者不固定）。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。</p><p>了解了动态和静态类型语言的区别后，也许读者的下一个问题就是动态、静态类型语言两者谁更好，或者谁更加先进？这种比较不会有确切答案，因为它们都有自己的优点，选择哪种语言是需要经过权衡的。静态类型语言在编译期确定类型，最显著的好处是编译器可以提供严谨的类型检查，这样与类型相关的问题能在编码的时候就及时发现，利于稳定性及代码达到更大规模。而动态类型语言在运行期确定类型，这可以为开发人员提供更大的灵活性，某些在静态类型语言中需用大量“臃肿”代码来实现的功能，由动态类型语言来实现可能会更加清晰和简洁，清晰和简洁通常也就意味着开发效率的提升。</p><h3 id="jdk-17与动态类型">JDK 1.7与动态类型</h3><p>回到本节的主题，来看看Java语言、虚拟机与动态类型语言之间有什么关系。Java虚拟机毫无疑问是Java语言的运行平台，但它的使命并不仅限于此，早在1997年出版的《Java虚拟机规范》中就规划了这样一个愿景：“在未来，我们会对Java虚拟机进行适当的扩展，以便更好地支持其他语言运行于Java虚拟机之上”。而目前确实已经有许多动态类型语言运行于Java虚拟机之上了，如Clojure、Groovy、Jython和JRuby等，能够在同一个虚拟机上可以达到静态类型语言的严谨性与动态类型语言的灵活性，这是一件很美妙的事情。</p><p>但遗憾的是，Java虚拟机层面对动态类型语言的支持一直都有所欠缺，主要表现在方法调用方面：JDK 1.7以前的字节码指令集中，4条方法调用指令（invokevirtual、invokespecial、invokestatic、invokeinterface）的第一个参数都是被调用的方法的符号引用（CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量），前面已经提到过，方法的符号引用在编译时产生，而动态类型语言只有在运行期才能确定接收者类型。这样，在Java虚拟机上实现的动态类型语言就不得不使用其他方式（如编译时留个占位符类型，运行时动态生成字节码实现具体类型到占位符类型的适配）来实现，这样势必让动态类型语言实现的复杂度增加，也可能带来额外的性能或者内存开销。尽管可以利用一些办法（如Call Site Caching）让这些开销尽量变小，但这种底层问题终归是应当在虚拟机层次上去解决才最合适，因此在Java虚拟机层面上提供动态类型的直接支持就成为了Java平台的发展趋势之一，这就是JDK 1.7（JSR-292）中invokedynamic指令以及java.lang.invoke包出现的技术背景。</p><h3 id="javalanginvoke包">java.lang.invoke包</h3><p>JDK 1.7实现了JSR-292，新加入的java.lang.invoke包[插图]就是JSR-292的一个重要组成部分，这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外，提供一种新的动态确定目标方法的机制，称为MethodHandle。这种表达方式也许不太好懂？那不妨把MethodHandle与C/C<ins>中的FunctionPointer，或者C#里面的Delegate类比一下。举个例子，如果我们要实现一个带谓词的排序函数，在C/C</ins>中常用的做法是把谓词定义为函数，用函数指针把谓词传递到排序方法，如下：</p><pre><code>void sort(int list[], const int size, int (＊compare)(int, int))</code></pre><p>但Java语言做不到这一点，即没有办法单独地把一个函数作为参数进行传递。普遍的做法是设计一个带有compare()方法的Comparator接口，以实现了这个接口的对象作为参数，例如Collections.sort()就是这样定义的：</p><pre><code>void sort(List list, Comparator c)</code></pre><p>不过，在拥有Method Handle之后，Java语言也可以拥有类似于函数指针或者委托的方法别名的工具了。代码清单8-11演示了MethodHandle的基本用途，无论obj是何种类型（临时定义的ClassA抑或是实现PrintStream接口的实现类System.out），都可以正确地调用到println()方法。</p><p><strong>代码清单8-11 MethodHandle演示</strong></p><pre><code>    import static java.lang.invoke.MethodHandles.lookup;    import java.lang.invoke.MethodHandle;    import java.lang.invoke.MethodType;    /＊＊    ＊ JSR-292 Method Handle基础用法演示    ＊ @author zzm    ＊/   public class MethodHandleTest {      static class ClassA {            public void println(String s) {                System.out.println(s);            }      }      public static void main(String[] args) throws Throwable {              Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : newClassA();            /＊无论obj最终是哪个实现类，下面这句都能正确调用到println方法            getPrintlnMH(obj).invokeExact(&quot;icyfenix&quot;);      }      private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {            /＊MethodType：代表“方法类型”，包含了方法的返回值（methodType()的第一个参数）和具体参数（methodType()第二个及以后的参数）＊/            MethodType mt = MethodType.methodType(void.class, String.class);            /＊lookup()方法来自于MethodHandles.lookup，这句的作用是在指定类中查找符合给定的方法名称、方法类型，并且符合调用权限的方法句柄＊/            /＊因为这里调用的是一个虚方法，按照Java语言的规则，方法第一个参数是隐式的，代表该方法的接收者，也即是this指向的对象，这个参数以前是放在参数列表中进行传递的，而现在提供了bindTo()方法来完成这件事情＊/                return lookup().findVirtual(reveiver.getClass(), &quot;println&quot;, mt).bindTo(reveiver);      }    }</code></pre><p>实际上，方法getPrintlnMH()中模拟了invokevirtual指令的执行过程，只不过它的分派逻辑并非固化在Class文件的字节码上，而是通过一个具体方法来实现。而这个方法本身的返回值（MethodHandle对象），可以视为对最终调用方法的一个“引用”。以此为基础，有了MethodHandle就可以写出类似于下面这样的函数声明：</p><pre><code>void sort(List list, MethodHandle compare)</code></pre><p>从上面的例子可以看出，使用MethodHandle并没有什么困难，不过看完它的用法之后，读者大概就会产生疑问，相同的事情，用反射不是早就可以实现了吗？</p><p>确实，仅站在Java语言的角度来看，MethodHandle的使用方法和效果与Reflection有众多相似之处，不过，它们还是有以下这些区别：</p><ul><li>从本质上讲，Reflection和MethodHandle机制都是在模拟方法调用，但Reflection是在模拟Java代码层次的方法调用，而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.lookup中的3个方法——findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual &amp; invokeinterface和invokespecial这几条字节码指令的执行权限校验行为，而这些底层细节在使用Reflection API时是不需要关心的。</li><li>Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang. invoke.MethodHandle对象所包含的信息多。前者是方法在Java一端的全面映像，包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式，还包含执行权限等的运行期信息。而后者仅仅包含与执行该方法相关的信息。用通俗的话来讲，Reflection是重量级，而MethodHandle是轻量级。</li><li>由于MethodHandle是对字节码的方法指令调用的模拟，所以理论上虚拟机在这方面做的各种优化（如方法内联），在MethodHandle上也应当可以采用类似思路去支持（但目前实现还不完善）。而通过反射去调用方法则不行。</li></ul><p>MethodHandle与Reflection除了上面列举的区别外，最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度来看”：Reflection API的设计目标是只为Java语言服务的，而MethodHandle则设计成可服务于所有Java虚拟机之上的语言，其中也包括Java语言。</p><h3 id="invokedynamic指令">invokedynamic指令</h3><p>本节一开始就提到了JDK 1.7为了更好地支持动态类型语言，引入了第5条方法调用的字节码指令invokedynamic，之后一直没有再提到它，甚至把代码清单8-11中使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影，它的应用之处在哪里呢？</p><p>在某种程度上，invokedynamic指令与MethodHandle机制的作用是一样的，都是为了解决原有4条“invoke*”指令方法分派规则固化在虚拟机之中的问题，把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中，让用户（包含其他语言的设计者）有更高的自由度。而且，它们两者的思路也是可类比的，可以把它们想象成为了达成同一个目的，一个采用上层Java代码和API来实现，另一个用字节码和Class中其他属性、常量来完成。因此，如果理解了前面的MethodHandle例子，那么理解invokedynamic指令也并不困难。</p><p>每一处含有invokedynamic指令的位置都称做“动态调用点”（Dynamic CallSite），这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量，而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量，从这个新常量中可以得到3项信息：引导方法（Bootstrap Method，此方法存放在新增的BootstrapMethods属性中）、方法类型（MethodType）和名称。引导方法是有固定的参数，并且返回值是java.lang.invoke. CallSite对象，这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息，虚拟机可以找到并且执行引导方法，从而获得一个CallSite对象，最终调用要执行的目标方法。我们还是举一个实际的例子来解释这个过程，如代码清单8-12所示。</p><p><strong>代码清单8-12 invokedynamic指令演示</strong></p><pre><code>import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.CallSite;import java.lang.invoke.ConstantCallSite;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;public class InvokeDynamicTest {    public static void main(String[] args) throws Throwable {        INDY_BootstrapMethod().invokeExact(&quot;icyfenix&quot;);    }    public static void testMethod(String s) {        System.out.println(&quot;hello String:&quot; + s);    }    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {          return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));    }    private static MethodType MT_BootstrapMethod() {        return MethodType.fromMethodDescriptorString(                              &quot;(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;&quot;, null);    }    private static MethodHandle MH_BootstrapMethod() throws Throwable {          return lookup().findStatic(InvokeDynamicTest.class, &quot;BootstrapMethod&quot;, MT_BootstrapMethod());    }    private static MethodHandle INDY_BootstrapMethod() throws Throwable {        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), &quot;testMethod&quot;, MethodType.fromMethodDescriptorString(&quot;(Ljava/lang/String;)V&quot;, null));        return cs.dynamicInvoker();    }}</code></pre><p>这段代码与前面MethodHandleTest的作用基本上是一样的，虽然笔者没有加以注释，但是阅读起来应当不困难。本书前面提到过，由于invokedynamic指令所面向的使用者并非Java语言，而是其他Java虚拟机之上的动态语言，因此仅依靠Java语言的编译器Javac没有办法生成带有invokedynamic 指令的字节码（曾经有一个java.dyn.InvokeDynamic的语法糖可以实现，但后来被取消了），所以要使用Java语言来演示invokedynamic指令只能用一些变通的办法。John Rose（DaVinci Machine Project的Leader）编写了一个把程序的字节码转换为使用invokedynamic的简单工具INDY[插图]来完成这件事情，我们要使用这个工具来产生最终要的字节码，因此这个示例代码中的方法名称不能随意改动，更不能把几个方法合并到一起写，因为它们是要被INDY工具读取的。</p><p>把上面代码编译、再使用INDY转换后重新生成的字节码如代码清单8-13所示（结果使用javap输出，因版面原因，精简了许多无关的内容）。</p><p><strong>代码清单8-13 invokedynamic指令演示（2）</strong></p><pre><code>Constant pool:  #121 = NameAndType        #33:#30     //testMethod:(Ljava/lang/String;)V  #123 = InvokeDynamic      #0:#121     //#0:testMethod:(Ljava/lang/String;)Vpublic static void main(java.lang.String[]) throws java.lang.Throwable;    Code:      stack=2, locals=1, args_size=1          0: ldc           #23          //String abc          2: invokedynamic #123, 0      // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V          7: nop          8: returnpublic static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable;    Code:      stack=6, locals=3, args_size=3          0: new           #63   //class java/lang/invoke/ConstantCallSite          3: dup          4: aload_0          5: ldc           #1    //class org/fenixsoft/InvokeDynamicTest          7: aload_1          8: aload_2          9: invokevirtual #65   //Method java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;         12: invokespecial #71    //Method java/lang/invoke/ConstantCallSite.&quot;&lt;in it&gt;&quot;:(Ljava/lang/invoke/MethodHandle;)V         15: areturn</code></pre><p>从main()方法的字节码可见，原本的方法调用指令已经替换为invokedynamic，它的参数为第123项常量（第二个值为0的参数在HotSpot中用不到，与invokeinterface指令那个值为0的参数一样都是占位的）。</p><pre><code>2: invokedynamic #123, 0        // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V</code></pre><p>从常量池中可见，第123项常量显示“#123=InvokeDynamic #0:#121”说明它是一项CONSTANT_InvokeDynamic_info类型常量，常量值中前面的“#0”代表引导方法取BootstrapMethods属性表的第0项（javap没有列出属性表的具体内容，不过示例中仅有一个引导方法，即BootstrapMethod()），而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常量，从这个常量中可以获取方法名称和描述符，即后面输出的“testMethod:(Ljava/lang/String;)V”。</p><p>再看一下BootstrapMethod()，这个方法Java源码中没有，是INDY产生的，但是它的字节码很容易读懂，所有逻辑就是调用MethodHandles$Lookup的findStatic()方法，产生testMethod()方法的MethodHandle，然后用它创建一个ConstantCallSite对象。最后，这个对象返回给invokedynamic指令实现对testMethod()方法的调用，invokedynamic指令的调用过程到此就宣告完成了。</p><h3 id="掌控方法分派规则">掌控方法分派规则</h3><p>nvokedynamic指令与前面4条“invoke*”指令的最大差别就是它的分派逻辑不是由虚拟机决定的，而是由程序员决定。在介绍Java虚拟机动态语言支持的最后一个小结中，笔者通过一个简单例子（如代码清单8-14所示），帮助读者理解程序员在可以掌控方法分派规则之后，能做什么以前无法做到的事情。</p><p>代码清单8-14 方法调用问题</p><pre><code>class GrandFather {    void thinking() {        System.out.println(&quot;i am grandfather&quot;);    }}class Father extends GrandFather {    void thinking() {        System.out.println(&quot;i am father&quot;);    }}class Son extends Father {    void thinking() {        //请读者在这里填入适当的代码（不能修改其他地方的代码）        //实现调用祖父类的thinking()方法，打印&quot;i am grandfather&quot;  }}</code></pre><p>在Java程序中，可以通过“super”关键字很方便地调用到父类中的方法，但如果要访问祖类的方法呢？读者在阅读本书下面提供的解决方案之前，不妨自己思考一下，在JDK 1.7之前有没有办法解决这个问题。</p><p>在JDK 1.7之前，使用纯粹的Java语言很难处理这个问题（直接生成字节码就很简单，如使用ASM等字节码工具），原因是在Son类的thinking()方法中无法获取一个实际类型是GrandFather的对象引用，而invokevirtual指令的分派逻辑就是按照方法接收者的实际类型进行分派，这个逻辑是固化在虚拟机中的，程序员无法改变。在JDK 1.7中，可以使用代码清单8-15中的程序来解决这个问题。</p><p>代码清单8-15 使用MethodHandle来解决相关问题</p><pre><code>import static java.lang.invoke.MethodHandles.lookup;import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodType;class Test {class GrandFather {    void thinking() {        System.out.println(&quot;i am grandfather&quot;);    }}class Father extends GrandFather {    void thinking() {        System.out.println(&quot;i am father&quot;);    }}class Son extends Father {      void thinking() {            try {                  MethodType mt = MethodType.methodType(void.class);                  MethodHandle mh = lookup().findSpecial(GrandFather.class, &quot;thinking&quot;, mt, getClass());                  mh.invoke(this);              } catch (Throwable e) {              }        }    }    public static void main(String[] args) {        (new Test().new Son()).thinking();    }}</code></pre><p>运行结果：</p><pre><code>i am grandfather</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[JAVA-Benchmark-工具JMH]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/java-benchmark-工具jmh" />
                <id>tag:https://www.wangdaye.net,2024-01-22:java-benchmark-工具jmh</id>
                <published>2024-01-22T23:14:34+08:00</published>
                <updated>2024-01-22T23:20:08+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="为什么需要benchmark工具">为什么需要Benchmark工具</h2><p>如果想要知道一段代码的性能如何，一种常用的做法可能是这样的：</p><pre><code>long start = System.currentTimeMillis();// do something ...System.out.println(System.currentTimeMillis() - start);</code></pre><p>这样做，存在几个问题：</p><ul><li>结果不够精确<br />首先，System.currentTimeMillis() 的注释里明确表示了，根据操作系统的不同，会存在数十毫秒的误差。虽然这个问题比较容易解决，但是造成测试结果不精确的主要原因并不是时间函数的误差，而是JVM和JIT在运行时会对Java应用进行大量的优化，比如某个计算的结果并没有被使用，那么这段代码在执行时就会被忽略，这样的问题比较难察觉。</li><li>统计结果有限<br />如果需要打印多种类型的测试数据，就需要增加很多额外的代码。</li><li>配置不灵活<br />不容易修改测试的类型和条件。</li></ul><p>因此，使用一款靠谱的benchmark工具，既可以减少工作量，又可以确保性能优化过程不被错误的测试数据误导。<br />之前使用Golang开发的时候，SDK自带的benchmark就非常好用。转到Java栈之后，我也想找一款好用的benchmark工具，后来通过《Effective Java》了解到了JMH。</p><h2 id="jmh概述">JMH概述</h2><p>JMH 即Java Microbenchmark Harness，是用于代码微基准测试的工具套件，由JIT的开发人员编写，他们应该比任何人都了解JIT对于测试的影响。<br />JMH可以精确测量方法在不同输入参数情况下的执行时间和吞吐量。</p><h2 id="一个例子">一个例子</h2><p>这里使用Gradle来搭建测试环境，首先在build.gradle中添加依赖：</p><pre><code>    testCompile 'org.openjdk.jmh:jmh-core:jar:1.21'    testCompile 'org.openjdk.jmh:jmh-generator-annprocess:1.21'</code></pre><p>编写一个简单的类，测试两种字符串连接操作的性能：</p><pre><code>package com.dafengge0913.benchmark;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.infra.Blackhole;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.concurrent.TimeUnit;@BenchmarkMode({Mode.AverageTime, Mode.Throughput})@Warmup(iterations = 1)@Measurement(iterations = 2, time = 1)@OutputTimeUnit(TimeUnit.MICROSECONDS)@Fork(value = 2)@Threads(8)@State(Scope.Benchmark)@OperationsPerInvocationpublic class StringBenchmark {    @Param({&quot;1&quot;, &quot;10&quot;, &quot;100&quot;})    private int n;    @Setup    public void setup() {    }    @TearDown    public void tearDown() {    }    @Benchmark    public void testStringAdd(Blackhole blackhole) {        String s = &quot;&quot;;        for (int i = 0; i &lt; n; i++) {            s += i;        }        blackhole.consume(s);    }    @Benchmark    public void testStringBuilderAdd(Blackhole blackhole) {        StringBuilder sb = new StringBuilder();        for (int i = 0; i &lt; n; i++) {            sb.append(i);        }        blackhole.consume(sb.toString());    }    public static void main(String[] args) throws RunnerException {        Options opt = new OptionsBuilder()                .include(StringBenchmark.class.getSimpleName())                .build();        new Runner(opt).run();    }}</code></pre><p>测试结果，这里截取了<strong>部分</strong>输出：</p><p><strong>testStringBuilderAdd 测试的某组执行结果:</strong></p><pre><code>Result &quot;com.dafengge0913.benchmark.StringBenchmark.testStringBuilderAdd&quot;:  1.317 ±(99.9%) 0.306 us/op [Average]  (min, avg, max) = (1.259, 1.317, 1.374), stdev = 0.047  CI (99.9%): [1.011, 1.622] (assumes normal distribution)</code></pre><p>每次操作平均耗时1.317 ± 0.306微秒<br />最小值：1.259<br />平均值：1.317<br />最大值：1.374<br />标准差：0.047<br />平均值的信赖区间：[1.011, 1.622]</p><p><strong>最后的统计结果：</strong></p><pre><code>Benchmark                             (n)   Mode  Cnt    Score    Error   UnitsStringBenchmark.testStringAdd           1  thrpt    4  260.743 ± 18.891  ops/usStringBenchmark.testStringAdd          10  thrpt    4   23.293 ±  0.865  ops/usStringBenchmark.testStringAdd         100  thrpt    4    0.565 ±  0.008  ops/usStringBenchmark.testStringBuilderAdd    1  thrpt    4  130.606 ±  6.957  ops/usStringBenchmark.testStringBuilderAdd   10  thrpt    4   74.840 ± 17.819  ops/usStringBenchmark.testStringBuilderAdd  100  thrpt    4    6.012 ±  0.520  ops/usStringBenchmark.testStringAdd           1   avgt    4    0.032 ±  0.004   us/opStringBenchmark.testStringAdd          10   avgt    4    0.355 ±  0.025   us/opStringBenchmark.testStringAdd         100   avgt    4   14.478 ±  1.119   us/opStringBenchmark.testStringBuilderAdd    1   avgt    4    0.062 ±  0.005   us/opStringBenchmark.testStringBuilderAdd   10   avgt    4    0.109 ±  0.038   us/opStringBenchmark.testStringBuilderAdd  100   avgt    4    1.317 ±  0.306   us/op</code></pre><p>(n)：参数n的取值<br />Mode: thrpt代表吞吐量，avgt代表平均运行时间。 对应<code>org.openjdk.jmh.annotations.Mode</code>中的<code>shortLabel</code><br />Cnt：iteration组数<br />Score：对应Units的值<br />Units：统计的单位<br />Error：误差</p><p>结果表明，在字符较多的情况下，StringBuilder的性能更好，完全符合预期。</p><h2 id="概念">概念</h2><h3 id="iteration">Iteration</h3><p>iteration是JMH进行测试的最小单位，包含一组invocations。</p><h3 id="invocation">Invocation</h3><p>一次benchmark方法调用。</p><h3 id="operation">Operation</h3><p>benchmark方法中，被测量操作的执行。如果被测试的操作在benchmark方法中循环执行，可以使用<code>@OperationsPerInvocation</code>表明循环次数，使测试结果为单次operation的性能。</p><h3 id="warmup">Warmup</h3><p>在实际进行benchmark前先进行预热。因为某个函数被调用多次之后，JIT会对其进行编译，通过预热可以使测量结果更加接近真实情况。</p><h2 id="注解">注解</h2><h3 id="benchmark">@Benchmark</h3><p>表示该方法需要被测量。</p><h3 id="benchmarkmode">@BenchmarkMode</h3><p>JMH进行benchmark时使用的模式。目前1.21版本共包含四种模式：</p><ul><li><p>Throughput： 每个单位时间执行的操作数</p></li><li><p>AverageTime： 每次执行消耗的平均时间</p></li><li><p>SampleTime： 每次执行时间，随机采样</p></li><li><p>SingleShotTime： 仅运行一次，用于测试冷启动时的性能</p></li></ul><p>几种模式可以相互组合，也可以设置为<code>Mode.All</code>来执行所有的模式。</p><h3 id="warmup-1">@Warmup</h3><p>实际进行benchmark前，预热的iteration配置。</p><h3 id="measurement">@Measurement</h3><p>实践测量的iteration配置。</p><h3 id="warmup和measurement的配置项相同">@Warmup和@Measurement的配置项相同：</h3><ul><li>iterations：iteration轮数</li><li>time：每次iteration的时间</li><li>timeUnit：iteration时间的单位，默认为秒</li><li>batchSize：每个operation中调用方法的次数</li></ul><h3 id="outputtimeunit">@OutputTimeUnit</h3><p>显示结果时使用的时间单位。</p><h3 id="fork">@Fork</h3><p>用于配置JMH运行时fork的Java进程。使用单独的进程可以避免测试结果之间互相影响。</p><ul><li>value： fork的进程数量</li><li>warmups： 每个进程执行Warmup的轮数</li><li>jvm：进程使用的JVM</li><li>jvm参数通过以下三个属性，按照从上到下的顺序拼接：<ul><li>jvmArgsPrepend</li><li>jvmArgs</li><li>jvmArgsAppend</li></ul></li></ul><p>默认fork的进程数配置在org.openjdk.jmh.runner.Defaults类中：</p><pre><code>    /**     * Number of forks in which we measure the workload.     */    public static final int MEASUREMENT_FORKS = 5;</code></pre><h3 id="threads">@Threads</h3><p>测试使用的线程数，默认为 <code>Runtime.getRuntime().availableProcessors()</code></p><h3 id="state">@State</h3><p>类实例的生命周期。</p><ul><li>Benchmark: 所有测试线程共享一个实例。</li><li>Group: 每个线程组内部使用一个实例。</li><li>Thread: 每个线程独占一个实例。</li></ul><h3 id="param">@Param</h3><p>在不同参数的情况下，分别测试。</p><h3 id="setup">@Setup</h3><p>在执行benchmark之前执行，用于初始化。</p><h3 id="teardown">@TearDown</h3><p>所有benchmark结束后执行，用于资源关闭。</p><h3 id="setup和teardown的调用时机">Setup和TearDown的调用时机</h3><p>根据Level配置</p><ul><li>Level.Trial： 每组iteration执行</li><li>Level.Iteration： 每组invocation执行</li><li>Level.Invocation： 每次invocation 即benchmark方法被调用时</li></ul><h2 id="blackhole">Blackhole</h2><p>为了避免JIT忽略未被使用的结果计算，可以使用<code>Blackhole.consume()</code>来保证方法被正确执行。</p><h2 id="将结果输出到日志文件中">将结果输出到日志文件中</h2><p>可以通过在<code>OptionsBuilder</code>中指定<code>output</code>属性，把测试结果输出到日志文件中。</p><pre><code>public static void main(String[] args) throws RunnerException {    Options opt = new OptionsBuilder()            .include(StringBenchmark.class.getSimpleName())            .output(&quot;D:/string_benchmark.log&quot;)            .build();    new Runner(opt).run();}</code></pre><h2 id="配置的优先级">配置的优先级</h2><p><a href="https://zhuanlan.zhihu.com/p/100574293">参考博客</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Lambda表达式是如何设计的]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/toleinj" />
                <id>tag:https://www.wangdaye.net,2024-01-12:toleinj</id>
                <published>2024-01-12T22:19:49+08:00</published>
                <updated>2024-01-12T22:30:17+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="lambda表达式是如何设计的">Lambda表达式是如何设计的</h1><blockquote><p>转载自布赖恩·戈茨的Translation of lambda expressions in javac</p></blockquote><h2 id="引言">引言</h2><p>阅读本文需要对invokedynamic指令知识有所了解：<a href="https://zhuanlan.zhihu.com/p/28124632?utm_medium=social">Invokedynamic</a></p><h2 id="转换策略">转换策略</h2><p>在字节码中表示Lambda表达式有多种方案，例如内部类、方法句柄、动态代理等，这些方案各有利弊。如何选择转换策略，有两个关键的衡量指标：</p><ul><li>是不引入特定策略，以期为将来的优化提供最大的灵活性；</li><li>是保持类文件格式的稳定。</li></ul><p>而invokedynamic指令可以同时满足这两个要求，即将Lambda在二进制字节码中的表达方式和其运行时的评估机制分开进行，而不是通过生成字节码的方式去创建一个实现了Lambda表达式的对象（例如为一个内部类调用构造方法)，通过这样的方式在编译的时候将方法需要的静态参数列表和动态参数列表与invokedynamic指令绑定，然后在运行的时候链接到指定的方法即可(此部分可以对比lambda$lambda$1()是如何被调用的)。</p><p>这么做的好处是invokedynamic指令使我们可以一直到运行时再去选择转换策略。运行时实现的方式是可以自由地选择转换策略，并且可以动态评估Lambda表达式。Invokedynamic允许这样做，且不需要付出为后续绑定方法可能强加的性能消耗。</p><p>具体的的转换策略是：当编译器遇到Lambda表达式的时候，它首先会将Lambda方法体内容脱糖到一个方法中(例如lambda$lambda$1()这样的方法)，此方法的参数列表和返回值类型与Lambda表达式的匹配，可能还会附加一些额外的参数（附加的参数来自外部作用域范围）。同时在遇到Lambda表达式的地方会生成一个invokedynamic调用点（CallSite对象），当调用点执行的时候会返回一个函数式接口的实例，这个转换后函数式接口的实现包含Lambda的内容（例如实现类LambdaMain$$Lambda$2）。</p><p>方法引用也会按照Lambda表达式一样的方式进行处理，但是大部分方法引用不需要被脱糖进到一个新方法中；我们可以简单地为一个引用的方法加载一个常量方法句柄，然后将其传给metafactory。</p><p>Lambda脱糖将Lambda表达式转换成字节码的第一步是将Lambda方法体脱糖到一个方法中。对于脱糖有以下几个问题需要考虑。</p><ul><li>将Lambda方法体脱糖到一个静态方法中还是一个实例方法中？</li><li>脱糖之后生成的方法应该放在哪一个类中？</li><li>脱糖之后生成的方法的可访问性应该是怎样的？</li><li>脱糖之后生成的方法的命名应该是怎样的？</li><li>如果需要一个适配器去桥接Lambda方法体的签名和函数式接口的签名（例如装箱、拆箱、基础类型的扩大和缩小转变、动态参数转换等），那么脱糖的方法是遵循Lambda方法体的签名还是函数式接口的签名，又或者是两者的结合呢？以及谁负责适配呢？</li><li>如果Lambda从外部作用域(enclosing scope)中获取参数，这些参数应该如何在脱糖的方法体的签名中表示呢？难道是将它们追加到参数列表的前面、后面，或者编译器可以将它们整合在一起，统一放到一个Bean对象里面？</li></ul><p>跟脱糖Lambda方法体时需要考虑的问题一样，我们也需要考虑方法引用是否需要一个适配器或者桥接方法。</p><p>对于以上问题，一般来说，在同等条件下，私有方法优于非私有方法，静态方法优于实例方法，最好的结果是Lambda方法体被脱糖在它所在的类里面，脱糖后的签名应该匹配Lambda方法体的签名，需要的额外参数应该被添加在参数列表的前面，而且完全不对方法引用进行脱糖。这些准则也不是一成不变的，在某些情况下，我们也不得不偏离这些基准策略。</p><p>接下来是关于Lambda脱糖的例子。</p><p>首先是无状态(stateless)Lambda，所谓无状态指的是Lambda方法体没有从外部作用域中捕捉任何状态，例如：</p><pre><code>class A {    public void foo() {        List&lt;String&gt; list = …        list.forEach( s -&gt; { System.out.println(s); } );    }}</code></pre><p>这个Lambda表达式对应的函数式接口的真实签名是 (String)V［其实这个Consumer接口中accept(T t)的真实签名，这种情况的签名通常称为naturesignature］，编译器会将Lambda方法体脱糖到一个静态方法，静态方法的签名与Lambda表达式的nature signature相同，然后为脱糖体生成一个方法，脱糖后的结果类似如下。</p><pre><code>class A {    public void foo() {        List&lt;String&gt; list = …        list.forEach( [lambda for lambda$1 as Block] );    }      //这个就是脱糖产生的方法    static void lambda$1(String s) {        System.out.println(s);    }}</code></pre><p>相比无状态Lambda，另外一种形式称为有状态Lambda，所谓有状态指的是Lambda方法体中使用了外部作用域的final局部变量、隐式是final的局部变量，或者外部实例(enclosing instance)的字段（这里可以看作捕获了外部作用域的this.xx字段），例如：</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf( p -&gt; (p.size &gt;= bottom &amp;&amp; p.size &lt;= top) );    }}</code></pre><p>上面这个例子，Lambda使用了外部作用域中final类型的局部变量bottom和top。脱糖之后的方法将使用的natural signature为 (Person)Z，并且在参数列表前面追加额外的参数。编译器有权决定这些额外的参数如何表示：参数可以逐个添加到参数列表的前面，或放在一个frame class中，或放在一个数组中。当然，最简单的方式是将参数逐个添加到参数列表的前面，如下面的例子所示。</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf( [ lambda for lambda$1 as Predicate capturing (bottom, top) ]);    }          //关注这个方法的签名    static boolean lambda$1(int bottom, int top, Person p) {        return (p.size &gt;= bottom &amp;&amp; p.size &lt;= top;    }}</code></pre><p>以上展示了Lambda如何脱糖，那么如何调用脱糖后的方法呢？关于这一部分的内容在前一节已经介绍了，接下来我们主要关注invokedynamic指令和脱糖方法之间参数是如何设定的。</p><h2 id="lambda-metafactory">Lambda Metafactory</h2><p>先来看看前面提到的例子。</p><pre><code>//lambda()方法public void lambda() {   Consumer&lt;String&gt; consumer2 = o -&gt; {       Object tmpObj = this.obj;       System.out.println(&quot;lambda2&quot;);   };} //对应的字节码public void lambda();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=3, args_size=1         …         6: aload_0         7: invokedynamic #7,  0  // InvokeDynamic #1:accept:\                     (Lcn/sensorsdata/lambda/LambdaMain;)\                     Ljava/util/function/Consumer;        12: astore_2        13: return</code></pre><p>上述代码的lambda()方法中使用了LambdaMain中的成员变量obj，观察其对应的字节码可以发现在执行invokedynamic指令之前先执行了aload_0，即this，该值作为参数会在前面提到的LambdaMetafactory.metafactory()方法中使用，下面我们看看这部分内容在Lambda设计参考中是如何介绍的。</p><p>首先看看什么是lambda metafactory：对给定的Lambda来说，这个调用点被称为lambda factory，lambda factory的动态参数是从外部作用域中捕获的，lambdafactory的引导方法是一个标准的方法，被称为lambda metafactory。虚拟机对每个invokedynamic只会调用一次这个metafactory，之后它会链接这个调用点然后退出。调用点的链接是懒加载的，所以factory sites不执行就不会被链接。基本的metafactory的静态参数如下。</p><pre><code>metaFactory(MethodHandles.Lookup caller,   // provided by VM            String invokedName,          // provided by VM            MethodType invokedType,      // provided by VM            MethodHandle descriptor,     // lambda descriptor            MethodHandle impl)           // lambda body</code></pre><p>前3个参数(caller、invokedName、invokedType)是在虚拟机调用链接的时候自动生成的。descripter参数确定了被转化的Lambda对应的函数式接口方法。impl参数确定了Lambda方法，要么是脱糖的Lambda方法体，要么是方法引用中的方法名。函数式接口方法的方法签名和实现方法有一些不同，实现方法可以有额外的参数，其余参数也可能不完全匹配。为方便展示，约定用一些符号来替换MethodHandle、MethodType与invokedynamic。</p><ul><li>method handle常量简写为MH（引用类型class-name.method-name）。</li><li>method type常量简写为MT(method-signature)。</li><li>invokedynamic简写为INDY((bootstrap, static args…)(dynamic args…))，注意这里的参数设定。</li></ul><p>对于前面脱糖的类A，可以使用如下方式来表示。</p><pre><code>class A {    public void foo() {            List&lt;String&gt; list = …            list.forEach(indy((MH(metaFactory), MH(invokeVirtual Block.apply),                               MH(invokeStatic A.lambda$1)( )));//注意此处的参数        }      private static void lambda$1(String s) {        System.out.println(s);    }}</code></pre><p>因为A中的Lambda是无状态的，所以lambda factory调用点的动态参数是空的。对于例子中的类B，动态参数并不为空，因为我们必须把bottom和top的值添加到lambda factory中。</p><pre><code>class B {    public void foo() {        List&lt;Person&gt; list = …        final int bottom = …, top = …;        list.removeIf(indy((MH(metaFactory), MH(invokeVirtual Predicate.apply),                            MH(invokeStatic B.lambda$1))( bottom, top ))));//注意此处的参数    }      private static boolean lambda$1(int bottom, int top, Person p) {        return (p.size &gt;= bottom &amp;&amp; p.size &lt;= top;    }}</code></pre><p>这就是LambdaMain的lambda()方法中会有6: aload_0的原因。</p><h2 id="静态方法还是实例方法">静态方法还是实例方法</h2><p>脱糖方法到底是静态方法还是实例方法呢？观察LambdaMain.class中的lambda$lambda$0()和lambda$lambda$1()两个方法，lambda$lambda$1()是实例方法的原因似乎与Lambda使用了LambdaMain中的字段obj有关，事实上确实如此。总体来说，我们将在Lambda中使用this、super或者外部实例的成员的情况称为instance-capturing lambdas，与其相对的是non-instance-capturinglambdas。</p><p>non-instance-capturing lambdas被脱糖成静态方法，instance-capturinglambdas被脱糖成实例的私有方法，当捕获instance-capturing lambdas的时候，this会被声明为第一个动态参数。</p><p>举个例子，考虑如下Lambda表达式中使用了一个minSize字段。</p><pre><code>list.filter(e -&gt; e.getSize() &lt; minSize )</code></pre><p>我们首先将上面的示例脱糖成一个实例方法，然后把接收者(this)作为第一个捕获的参数。结果如下：</p><pre><code>list.forEach(INDY((MH(metaFactory), MH(invokeVirtual Predicate.apply),                   MH(invokeVirtual B.lambda$1))( this ))));  private boolean lambda$1(Element e) {    return e.getSize() &lt; minSize;}</code></pre><p>因为Lambda方法体被转换成一个私有方法，所以metafactory中的调用点会加载一个常量池中的方法句柄。对示例方法来说，这个方法句柄的类型是REF_invokeSpecial（CONSTANT_MethodHandle_info结构的reference_index对应的值），而对静态方法来说，这个方法句柄的类型是REF_invokeStatic。脱糖成为一个私有方法是因为私有方法可以使用所在类的成员。</p><h2 id="方法引用">方法引用</h2><p>方法引用有多种写法，跟lambdas类似，也可以分成instance-capturing和non-instance-capturing两种。non-instance-capturing类型方法引用包括静态方法引用(Integer:: parseInt)、未绑定实例的方法引用(String::length)和构造方法引用(Foo::new)。当使用non-instance-capturing类型的方法引用时，动态参数列表总是空的，例如：</p><pre><code>list.filter(String::isEmpty)</code></pre><p>上面的例子会被转换成：</p><pre><code>list.filter(indy(MH(metaFactory), MH(invokeVirtual Predicate.apply),                 MH(invokeVirtual String.isEmpty))()))</code></pre><p>instance-capturing类型的方法引用形式包括绑定实例方法引用(s::length)、super()方法引用(super::foo)和内部类构造方法引用(Inner::new)。当捕获instance-capturing类型的方法引用，被捕获的参数列表总是有一个参数，就是this。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[字节序、大端字节序(Big Endian)、小端字节序(Little Endian)]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/大小端字节序" />
                <id>tag:https://www.wangdaye.net,2023-12-24:大小端字节序</id>
                <published>2023-12-24T12:33:12+08:00</published>
                <updated>2023-12-24T12:39:00+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<div id="article_content" class="article_content clearfix"><div id="content_views" class="markdown_views prism-atom-one-dark"><h2 id="什么是字节序">什么是字节序？</h2><p>字节序，简单来说，指的是 超过一个字节的数据类型在[内存]中存储的顺序</p><h4 id="有几种字节序"><a name="t1"></a><a id="_6"></a>有几种字节序？</h4><h5 id="大端字节序big-endian"><a id="Big_Endian_8"></a>大端字节序(Big Endian)</h5><p>高位字节数据存放在内存低地址处，低位字节数据存放在内存高地址处。</p><h5 id="小端字节序little-endian"><a id="Little_Endian_11"></a>小端字节序(Little Endian)</h5><p>高位字节数据存放在内存高地址处，低位数据存放在内存低地址处。</p><p><img src="https://www.wangdaye.net/upload/2023/12/image-d1e146f2e8494e5eba5d272b5e2136b4.png" alt="image.png" /></p><p>如上图所示，<code>int32</code>类型的数值 <code>12345678</code>用一个字节<a href="https://blog.csdn.net/damanchen/article/details/112380741">表示不了</a>，需要用到4个字节，也就有了字节序的问题。</p><p>数值 <code>12345678</code>(一千两百三十四万五千六百七十八)，这里的最高位数据就是<code>1</code>，最低位数据就是<code>8</code>。</p><p>看大端序的存储方式，高位在前面的低地址，低位在后面的高地址，这也是人类读写数值的方式；而小端序正好相反。</p><p>那么问题来了，为什么还需要小端序的方式，只使用大端序的方式不是更方便吗？什么是内存高地址/低地址，为什么低地址在前，高地址在后呢？</p><h4 id="为什么需要小端字节序"><a name="t2"></a><a id="_25"></a>为什么需要小端字节序？</h4><blockquote><p>我们知道计算机正常的内存增长方式是从低到高(当然栈不是)，取数据方式是从基址根据偏移找到他们的位置。</p><p>从大/小端的存储方式可以看出，大端存储因为第一个字节就是高位，从而很容易知道它是正数还是负数，对于一些数值判断会很迅速。</p><p>而小端存储 第一个字节是它的低位，符号位在最后一个字节，这样在做数值四则运算时，从低位每次取出相应字节运算，最后直到高位，并且最终把符号位刷新，这样的运算方式会更高效。</p><p>所以大端和小端有其各自的优势。</p></blockquote><p>具体是使用大端序还是小端序跟处理器体系有关：</p><h5 id="处理器体系"><a id="_36"></a>处理器体系</h5><ul><li>x86、MOS Technology 6502、Z80、VAX、PDP-11等处理器为小端序；</li><li>Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC（除V9外）等处理器为大端序；</li><li>ARM、PowerPC（除PowerPC 970外）、DEC Alpha、SPARC V9、MIPS、PA-RISC及IA64的字节序是可配置的。</li></ul><h4 id="字节序的处理"><a name="t3"></a><a id="_41"></a>字节序的处理</h4><p>计算机处理字节序的时候，不知道什么是高位字节，什么是低位字节。它只知道按顺序读取字节，先读第一个字节，再读第二个字节。</p><p>如果是大端字节序，先读到的就是高位字节，后读到的就是低位字节。小端字节序正好相反。</p><h4 id="网络字节序"><a name="t4"></a><a id="_48"></a>网络字节序</h4><p>前面的大端和小端都是在说计算机自己，也被称作主机字节序HBO(Host Byte Order)。其实，只要自己能够自圆其说是没啥问题的。问题是，网络的出现使得计算机可以通信了。通信，就意味着相处，相处必须得有共同语言啊，得说普通话，要不然就容易会错意，下了一个小时的小电影发现打不开，理解错误了！</p><p>但是每种计算机体系都有自己的主机字节序啊，还都不依不饶，坚持做自己，怎么办？</p><p><strong>TCP/IP协议隆重出场，RFC1700规定使用“大端”字节序为网络字节序NBO(Network Byte Order)</strong>。</p><p>其他不使用大端的计算机要注意了，发送数据的时候必须要将自己的主机字节序转换为网络字节序（即“大端”字节序），接收到的数据再转换为自己的主机字节序。这样就与CPU、操作系统无关了，实现了网络通信的标准化。</p><p>为了程序的兼容，你会看到，程序员们每次发送和接受数据都要进行转换，这样做的目的是保证代码在任何计算机上执行时都能达到预期的效果。</p><p>这么常用的操作，BSD Socket提供了封装好的转换接口，方便程序员使用。包括从主机字节序到网络字节序的转换函数：htons、htonl；从网络字节序到主机字节序的转换函数：ntohs、ntohl。当然，有了上面的理论基础，也可以编写自己的转换函数。</p><blockquote><p>网络字节顺序(NBO)<br />NBO(Network Byte Order)：按照从高到低的顺序存储，在网络上使用统一的网络字节顺序，可以避免兼容性问题。TCP/IP中规定好的一种数据表示格式，与具体的 CPU 类型、操作系统等无关。从而保证数据在不同主机之间传输时能够被正确解释。</p></blockquote><blockquote><p>主机字节顺序(HBO)<br />HBO(Host Byte Order)：不同机器 HBO 不相同，与 CPU 有关。计算机存储数据有两种字节优先顺序：Big Endian 和 Little Endian。Internet 以 Big Endian 顺序在网络上传输，所以对于在内部是以 Little Endian 方式存储数据的机器，在网络通信时就需要进行转换。</p></blockquote><blockquote><p>除了计算机的内部处理，其他的场合几乎都是大端字节序，比如<strong>网络传输和文件储存</strong>。</p></blockquote><h4 id="golang中验证系统的大小端"><a name="t5"></a><a id="golang_71"></a>golang中验证系统的大小端</h4><pre><code>package mainimport (&quot;fmt&quot;&quot;unsafe&quot;)func main() {a := int64(0x12345678)fmt.Printf(&quot;int64: %v bytes\n&quot;, unsafe.Sizeof(a))//用int8去转换int64，会只截取到第一个字节s := int8(a)if 0x12 == s {fmt.Println(&quot;Big-Endian&quot;)} else {fmt.Println(&quot;Little-Endian&quot;)}fmt.Printf(&quot;s: 0x%x&quot;, s)}输出如下:int64: 8 bytesLittle-Endians: 0x78</code></pre><p>参考：</p><blockquote><p>大小端字节序存在的意义，为什么不用一个标准呢？<a href="https://www.zhihu.com/question/25311159">https://www.zhihu.com/question/25311159</a></p><p><a href="http://www.ruanyifeng.com/blog/2016/11/byte-order.html">http://www.ruanyifeng.com/blog/2016/11/byte-order.html</a><br />你知道字节序吗 <a href="https://zhuanlan.zhihu.com/p/115135603">https://zhuanlan.zhihu.com/p/115135603</a></p><p>“字节序”是个什么鬼？ <a href="https://zhuanlan.zhihu.com/p/21388517">https://zhuanlan.zhihu.com/p/21388517</a></p><p>字节序及 Go encoding/binary 库 <a href="https://zhuanlan.zhihu.com/p/35326716">https://zhuanlan.zhihu.com/p/35326716</a></p><p>字节序：Big Endian 和 Little Endian <a href="https://songlee24.github.io/2015/05/02/endianess/">https://songlee24.github.io/2015/05/02/endianess/</a></p></blockquote></div><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/editerView/markdown_views-98b95bb57c.css" rel="stylesheet"><link href="https://csdnimg.cn/release/blogv2/dist/mdeditor/css/style-c216769e99.css" rel="stylesheet"></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[一个优雅的环境管理工具--SDKMAN]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/sdkman简单使用" />
                <id>tag:https://www.wangdaye.net,2023-11-16:sdkman简单使用</id>
                <published>2023-11-16T21:49:25+08:00</published>
                <updated>2023-11-22T09:33:26+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><a href="https://juejin.cn/post/7126033357405683748">SDKMAN详情介绍</a></p><h2 id="sdkman">SDKMAN!</h2><p>是在大多数基于Unix的系统上管理多个软件开发工具包的并行版本的工具。它提供了一个方便的命令行界面（CLI）和API来安装，切换，删除和列出sdk相关信息。以下是一些特性:</p><ul><li>By Developers, for Developers<br />安装SDK不再需要去各种下载页面去下载,解压,以及设置xxx_HOME或者PATH环境变量.</li><li>多平台<br />可以在任何基于UNIX的平台上运行：Mac OSX，Linux，Cygwin，Solaris和FreeBSD。 <a href="https://link.jianshu.com?t=https%3A%2F%2Fgithub.com%2Fflofreud%2Fposh-gvm">Powershell CLI</a>版本适用于Windows用户。</li><li>全套JAVA支持<br />为Java，Groovy，Scala，Kotlin和Ceylon等JVM安装软件开发工具包。 Ant，Gradle，Grails，Maven，SBT，Spark，Spring Boot，Vert.x以及其他许多支持。</li><li>APIs<br />使用开放的Broker REST API可以轻松地编写新的客户端。供应商可以通过安全的供应商API发布自己的版本。</li><li>轻量<br />只需要有<a href="https://link.jianshu.com?t=http%3A%2F%2Fcurl.haxx.se%2F">curl</a> \ <a href="https://link.jianshu.com?t=http%3A%2F%2Fwww.info-zip.org%2F">zip/unzip</a>就可以在<a href="https://link.jianshu.com?t=https%3A%2F%2Fwww.gnu.org%2Fsoftware%2Fbash%2F">bash</a>中通过命令使用.还可和<a href="https://link.jianshu.com?t=http%3A%2F%2Fwww.zsh.org%2F">ZSH</a>一起使用.</li></ul><h2 id="1安装">1.安装</h2><p>在终端中输入以下命令进行安装:</p><div class="_2Uzcx_"><pre><code>$ curl -s &quot;https://get.sdkman.io&quot; | bash</code></pre></div><p>如果提示缺少zip或unzip,安装后再次执行上面的命令即可.</p><div class="_2Uzcx_"><pre><code># 安装需要的组件,Ubuntu为例$ apt install zip$ apt install unzip</code></pre></div><p>安装完成后,在终端中输入:</p><div class="_2Uzcx_"><pre><code>$ source &quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;</code></pre></div><p>输入以下命令查看安装情况:</p><div class="_2Uzcx_"><pre><code>$ sdk version# 以下为输出==== BROADCAST =================================================================* 09/01/18: Gradle 4.5-rc-1 released on SDKMAN! #gradle* 06/01/18: sbt 1.1.0 released on SDKMAN! #scala* 20/12/17: Gradle 4.4.1 released on SDKMAN! #gradle================================================================================SDKMAN 5.6.0+287</code></pre></div><h2 id="2安装到自定义位置">2.安装到自定义位置</h2><p>SDKMAN的默认安装位置为:$HOME/.sdkman.你可以通过设置SDKMAN_DIR环境变量来修改安装位置:</p><div class="_2Uzcx_"><pre><code>$ export SDKMAN_DIR=&quot;/usr/local/sdkman&quot; &amp;&amp; curl -s &quot;https://get.sdkman.io&quot; | bash</code></pre></div><h2 id="3beta通道">3.Beta通道</h2><p>SDKMAN的Bate版,包含一些cli的新功能,但是可能会不稳定.如果需要使用Bate版本,需要修改~/.sdkman/etc/config文件:</p><div class="_2Uzcx_"><pre><code>sdkman_beta_channel=true</code></pre></div><p>然后打开一个终端执行:</p><div class="_2Uzcx_"><pre><code>$ sdk selfupdate force</code></pre></div><p>如果不需要使用Bate版本了,将上面的配置修改为false,再执行一次更新即可.</p><h2 id="4卸载">4.卸载</h2><p>SDKMAN!没有提供自动化的卸载方法,可以通过以下命令进行卸载:</p><div class="_2Uzcx_"><pre><code>tar zcvf ~/sdkman-backup_$(date +%F-%kh%M).tar.gz -C ~/ .sdkman$ rm -rf ~/.sdkman</code></pre></div><p>然后从.bashrc，.bash_profile和/或.profile文件中编辑和删除初始化代码片段。如果您使用ZSH，请将其从.zshrc文件中删除。要删除的代码片段如下所示：</p><div class="_2Uzcx_"><pre><code>#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!![[ -s &quot;/home/dudette/.sdkman/bin/sdkman-init.sh&quot; ]] &amp;&amp; source &quot;/home/dudette/.sdkman/bin/sdkman-init.sh&quot;</code></pre></div><h2 id="5使用">5.使用</h2><h3 id="50-列出支持的软件">5.0 列出支持的软件</h3><div class="_2Uzcx_"><pre><code>$ sdk list# 执行命令后进入vi模式进行阅读,q退出阅读</code></pre></div><h3 id="51-列出软件的版本">5.1 列出软件的版本</h3><div class="_2Uzcx_"><pre><code>$ sdk list gradle================================================================================Available Gradle Versions================================================================================     4.5-rc-1             4.2.1                3.1                  2.11            &gt; * 4.4.1                4.2-rc-2             3.0                  2.10                4.4-rc-6             4.2-rc-1             2.9                  2.1                 4.4-rc-5             4.2                  2.8                  2.0                 4.4-rc-4             4.1                  2.7                  1.9                 4.4-rc-3             4.0.2                2.6                  1.8                 4.4-rc-2             4.0.1                2.5                  1.7                 4.4-rc-1             4.0                  2.4                  1.6                 4.4                  3.5.1                2.3                  1.5                 4.3.1                3.5                  2.2.1                1.4                 4.3-rc-4             3.4.1                2.2                  1.3                 4.3-rc-3             3.4                  2.14.1               1.2                 4.3-rc-2             3.3                  2.14                 1.12                4.3-rc-1             3.2.1                2.13                 1.11                4.3                  3.2                  2.12                 1.10           ================================================================================+ - local version* - installed&gt; - currently in use================================================================================</code></pre></div><h3 id="52-安装gradle">5.2 安装gradle</h3><div class="_2Uzcx_"><pre><code>$ sdk install gradleDownloading: gradle 4.4.1In progress...######################################################################## 100.0%Installing: gradle 4.4.1Done installing!Setting gradle 4.4.1 as default.</code></pre></div><h3 id="53-安装指定版本软件">5.3 安装指定版本软件</h3><div class="_2Uzcx_"><pre><code># 后面跟上版本号即可$ sdk install gradle 4.4.1</code></pre></div><h3 id="54-安装本地包">5.4 安装本地包</h3><div class="_2Uzcx_"><pre><code>$ sdk install groovy 3.0.0-SNAPSHOT /path/to/groovy-3.0.0-SNAPSHOT</code></pre></div><h3 id="55-卸载包">5.5 卸载包</h3><div class="_2Uzcx_"><pre><code>$ sdk uninstall scala 2.11.6</code></pre></div><h3 id="56-选择版本">5.6 选择版本</h3><p>选择一个版本用于当前终端:</p><div class="_2Uzcx_"><pre><code>$ sdk use scala 2.12.1</code></pre></div><h3 id="57-设置默认版本">5.7 设置默认版本</h3><div class="_2Uzcx_"><pre><code>$ sdk default scala 2.11.6</code></pre></div><h3 id="58-查看当前使用的版本">5.8 查看当前使用的版本</h3><div class="_2Uzcx_"><pre><code>$ sdk current java  Using java version 8u111#查看所有本地包的当前版本$ sdk current  Using:  groovy: 2.4.7  java: 8u111  scala: 2.12.1</code></pre></div><h3 id="59-sdk版本升级">5.9 sdk版本升级</h3><div class="_2Uzcx_"><pre><code>$ sdk upgrade springboot  Upgrade:  springboot (1.2.4.RELEASE, 1.2.3.RELEASE &lt; 1.2.5.RELEASE)# 本地所有sdk全部升级$ sdk upgrade  Upgrade:  gradle (2.3, 1.11, 2.4, 2.5 &lt; 2.6)  grails (2.5.1 &lt; 3.0.4)  springboot (1.2.4.RELEASE, 1.2.3.RELEASE &lt; 1.2.5.RELEASE)</code></pre></div><h3 id="510-离线模式">5.10 离线模式</h3><div class="_2Uzcx_"><pre><code>$ sdk offline enable  Forced offline mode enabled.$ sdk offline disable  Online mode re-enabled!</code></pre></div><p>当电脑没有网的时候,离线模式会进行自动切换.</p><h3 id="511-sdkman版本升级">5.11 SDKMAN!版本升级</h3><div class="_2Uzcx_"><pre><code>$ sdk selfupdate# 强制重新安装$ sdk selfupdate force</code></pre></div>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Function、Consumer、Supplier的妙用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/java8functionconsumersupplier" />
                <id>tag:https://www.wangdaye.net,2023-11-14:java8functionconsumersupplier</id>
                <published>2023-11-14T11:13:31+08:00</published>
                <updated>2023-11-22T09:31:59+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="一概述">一、概述</h2><p>Jdk8之后新增的一个重要的包 : <strong>java.util.function</strong></p><p>该包下所有的接口都是函数式接口, 按分类主要分为四大接口类型: <code>Function</code>、<code>Consumer</code>、<code>Predicate</code>、<code>Supplier</code>。有关Predicate这里不再讲解，因为上面有单独写过一篇博客。</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190024258-463349888.jpg" alt="" /></p><p>延伸如下</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190111553-251022017.jpg" alt="" /></p><p>这里也仅仅是展示一部分，我们看看java.util.function包下</p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190211633-1473355636.jpg" alt="" /></p><h2 id="二consumer">二、Consumer</h2><p><code>作用</code> 一听这名字就知道是消费某个对象，没有返回值。</p><h4 id="1源码">1、源码</h4><p>在源码中只有两个方法，一个抽象方法，一个默认方法。</p><pre><code>@FunctionalInterfacepublic interface Consumer&lt;T&gt; {    /**     * 抽象方法：传入一个指定泛型的参数，无返回值     */    void accept(T t);    /**     * 如同方法名字一样andThen,类似一种相加的功能（下面会举例说明）     */    default Consumer&lt;T&gt; andThen(Consumer&lt;? super T&gt; after) {        Objects.requireNonNull(after);        return (T t) -&gt; { accept(t); after.accept(t); };    }}</code></pre><h4 id="2使用示例">2、使用示例</h4><pre><code>    public static void main(String[] args) {        testConsumer();        testAndThen();    }    /**     * 一个简单的平方计算     */    public static void testConsumer() {        //设置好Consumer实现方法        Consumer&lt;Integer&gt; square = x -&gt; System.out.println(&quot;平方计算 : &quot; + x * x);        //传入值        square.accept(2);    }    /**     * 定义3个Consumer并按顺序进行调用andThen方法     */    public static void testAndThen() {        //当前值        Consumer&lt;Integer&gt; consumer1 = x -&gt; System.out.println(&quot;当前值 : &quot; + x);        //相加        Consumer&lt;Integer&gt; consumer2 = x -&gt; { System.out.println(&quot;相加 : &quot; + (x + x)); };        //相乘        Consumer&lt;Integer&gt; consumer3 = x -&gt; System.out.println(&quot;相乘 : &quot; + x * x);        //andThen拼接        consumer1.andThen(consumer2).andThen(consumer3).accept(1);    }</code></pre><p><strong>运行结果</strong></p><p><img src="https://img2018.cnblogs.com/blog/1090617/201912/1090617-20191216190252957-1768982616.jpg" alt="" /></p><p>单个这样消费看去并没啥意义,但如果是集合操作就有意义了，所以Jdk8的Iterator接口就引入了Consumer。</p><h4 id="3jdk8使用">3、JDK8使用</h4><p>Iterable接口的forEach方法需要传入Consumer，大部分集合类都实现了该接口，用于返回Iterator对象进行迭代。</p><pre><code>public interface Iterable&lt;T&gt; {    //forEach方法传入的就是Consumer    default void forEach(Consumer&lt;? super T&gt; action) {        Objects.requireNonNull(action);        for (T t : this) {            action.accept(t);        }    }}</code></pre><p>我们在看给我们带来的便利</p><pre><code>    public static void main(String[] args) {        //假设这里有个集合,集合里的对象有个status属性,现在我想对这个属性赋值一个固定值        List&lt;Pension&gt; pensionList = new ArrayList&lt;&gt;();        //1、传统的通过for循环添加        for (Pension pension : pensionList) {            pension.setStatus(1);        }        //2、通过forEach的Consumer添加        pensionList.forEach(x -&gt; x.setStatus(1));    }</code></pre><p>这样一比较是不是代码简洁了点，这就是Consumer是我们代码带来简洁的地方。</p><h2 id="三supplier">三、Supplier</h2><p><code>作用</code> 提前定义可能返回的一个指定类型结果，等需要调用的时候再获取结果。</p><h4 id="1源码-1">1、源码</h4><pre><code>@FunctionalInterfacepublic interface Supplier&lt;T&gt; {    /**     * 只有这一个抽象类     */    T get();}</code></pre><p>源码非常简单。</p><h4 id="2jdk8使用">2、JDK8使用</h4><p>在JDK8中Optional对象有使用到</p><pre><code>Optional.orElseGet(Supplier&lt;? extends T&gt;) //当this对象为null，就通过传入supplier创建一个T返回。</code></pre><p>我们看下源码</p><pre><code> public &lt;X extends Throwable&gt; T orElseThrow(Supplier&lt;? extends X&gt; exceptionSupplier) throws X {        if (value != null) {            return value;        } else {            throw exceptionSupplier.get();        }    }</code></pre><p><code>使用示例</code></p><pre><code>  public static void main(String[] args) {        Person son = null;        //先判断son是否为null,如果为不为null则返回当前对象,如果为null则返回新创建的对象        BrandDTO optional = Optional.ofNullable(son).orElseGet(() -&gt; new Person());    }</code></pre><p>这样代码是不是又简单了。有关Optional这里就不多说,接下来会单独写一篇博客。</p><h2 id="四function">四、Function</h2><p><code>作用</code> 实现一个”一元函数“，即传入一个值经过函数的计算返回另一个值。</p><h4 id="1源码-2">1、源码</h4><pre><code>    @FunctionalInterface    public interface Function&lt;T, R&gt; {        /**         * 抽象方法: 根据一个数据类型T加工得到一个数据类型R         */        R apply(T t);        /**         * 组合函数，调用当前function之前调用         */        default &lt;V&gt; java.util.function.Function&lt;V, R&gt; compose(java.util.function.Function&lt;? super V, ? extends T&gt; before) {            Objects.requireNonNull(before);            return (V v) -&gt; apply(before.apply(v));        }        /**         * 组合函数，调用当前function之后调用         */        default &lt;V&gt; java.util.function.Function&lt;T, V&gt; andThen(java.util.function.Function&lt;? super R, ? extends V&gt; after) {            Objects.requireNonNull(after);            return (T t) -&gt; after.apply(apply(t));        }        /**         *  静态方法，返回与原函数参数一致的结果。x=y         */        static &lt;T&gt; java.util.function.Function&lt;T, T&gt; identity() {            return t -&gt; t;        }    }</code></pre><h4 id="2使用示例-1">2、使用示例</h4><pre><code>public static void main(String[] args) {        applyTest();        andThenTest();        composeTest();        test();    }    /**     * 1、apply 示例     */    private static void applyTest() {        //示例1：定义一个funciton,实现将String转换为Integer        Function&lt;String, Integer&gt; function = x -&gt; Integer.parseInt(x);        Integer a = function.apply(&quot;100&quot;);        System.out.println(a.getClass());        // 结果：class java.lang.Integer    }    /**     * 2、andThen 示例     */    private static void andThenTest() {        //示例2：使用andThen() 实现一个函数 y=10x + 10;        //先执行 10 * x        Function&lt;Integer, Integer&gt; function2 = x -&gt; 10 * x;        //通过andThen在执行 这里的x就等于上面的10 * x的值        function2 = function2.andThen(x -&gt; x + 10);        System.out.println(function2.apply(2));        //结果：30    }    /**     * 3、compose 示例     */    private static void composeTest() {        //示例3：使用compose() 实现一个函数 y=(10+x)2;        Function&lt;Integer, Integer&gt; function3 = x -&gt; x * 2;        //先执行 x+10 在执行(x+10)*2顺序与上面相反        function3 = function3.compose(x -&gt; x + 10);        System.out.println(function3.apply(3));        //结果：26    }    /**     * 4、综合示例     */    private static void test() {//示例4：使用使用compose()、andThen()实现一个函数 y=(10+x)*2+10;        //执行第二步        Function&lt;Integer, Integer&gt; function4 = x -&gt; x * 2;        //执行第一步        function4 = function4.compose(x -&gt; x + 10);        //执行第三步        function4 = function4.andThen(x -&gt; x + 10);        System.out.println(function4.apply(3));       //结果：36    }</code></pre><h4 id="3jdk8使用-1">3、JDK8使用</h4><p>有两个地方很常用</p><pre><code>1、V HashMap.computeIfAbsent(K , Function&lt;K, V&gt;) // 简化代码，如果指定的键尚未与值关联或与null关联，使用函数返回值替换。2、&lt;R&gt; Stream&lt;R&gt; map(Function&lt;? super T, ? extends R&gt; mapper); // 转换流</code></pre><p><code>computeIfAbsent使用示例</code></p><pre><code>Map&lt;String, List&lt;String&gt;&gt; map = new HashMap&lt;&gt;();List&lt;String&gt; list;// java8之前写法list = map.get(&quot;key&quot;);if (list == null) {    list = new LinkedList&lt;&gt;();    map.put(&quot;key&quot;, list);}list.add(&quot;11&quot;);// 使用 computeIfAbsent 可以这样写 如果key返回部位空则返回该集合 ，为空则创建集合后返回list = map.computeIfAbsent(&quot;key&quot;, k -&gt; new ArrayList&lt;&gt;());list.add(&quot;11&quot;);</code></pre><p><code>stream中map使用示例</code></p><pre><code>  public static void main(String[] args) {        List&lt;Person&gt; persionList = new ArrayList&lt;Person&gt;();        persionList.add(new Person(1,&quot;张三&quot;,&quot;男&quot;,38));        persionList.add(new Person(2,&quot;小小&quot;,&quot;女&quot;,2));        persionList.add(new Person(3,&quot;李四&quot;,&quot;男&quot;,65));        //1、只取出该集合中所有姓名组成一个新集合（将Person对象转为String对象）        List&lt;String&gt; nameList=persionList.stream().map(Person::getName).collect(Collectors.toList());        System.out.println(nameList.toString());        }</code></pre><p>代码是不是又简洁了。</p><p><code>总结</code> 这些函数式接口作用在我看来，就是定义一个方法,方法中有个参数是函数式接口，这样的话函数的具体实现则由调用者来实现。这就是函数式接口的意义所在。</p><p>一般我们也会很少去定义一个方法，方法参数包含函数接口。我们更重要的是学会使用JDk8中带有函数式接口参数的方法，来简化我们的代码。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[后端也可以数据绑定--BeanMap的使用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/xiang-jie-b-e-a-n-m-a-p-jiang-j-a-v-a-dui-xiang-zhuan-wei-m-a-p-de-shi-yong" />
                <id>tag:https://www.wangdaye.net,2023-11-12:xiang-jie-b-e-a-n-m-a-p-jiang-j-a-v-a-dui-xiang-zhuan-wei-m-a-p-de-shi-yong</id>
                <published>2023-11-12T11:14:09+08:00</published>
                <updated>2023-11-22T09:34:07+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><code>BeanMap</code> 是 Apache Commons BeanUtils 库中的一个类，它提供了一种将 Java 对象转换为 Map 的方式。<code>BeanMap</code> 可以将 Java 对象的属性作为键，属性值作为对应的值，存储在一个 Map 中。</p><p>BeanMap在后端最常用的场景就是读取配置文件的信息。</p><h4 id="一基本用法beanmap最基本的用法就是将java对象转成map"><a name="t1"></a><a id="BeanMapJavaMap_5"></a>一、基本用法：BeanMap最基本的用法就是将Java对象转成Map</h4><p>以下是使用 <code>BeanMap</code> 的基本示例：</p><p>首先，确保你已经引入了 Apache Commons BeanUtils 库。</p><pre><code>import org.apache.commons.beanutils.BeanMap;// 创建一个 Java 对象Person person = new Person();person.setName(&quot;John Doe&quot;);person.setAge(30);// 将 Java 对象转换为 BeanMapBeanMap beanMap = new BeanMap(person);// 通过键获取属性值String name = (String) beanMap.get(&quot;name&quot;);int age = (int) beanMap.get(&quot;age&quot;);System.out.println(&quot;Name: &quot; + name);System.out.println(&quot;Age: &quot; + age);</code></pre><p>在上面的示例中，我们创建了一个名为 <code>Person</code> 的 Java 对象，并设置了其名称和年龄属性。然后，我们使用 <code>BeanMap</code> 将 <code>Person</code> 对象转换为一个 Map。我们可以使用 <code>get</code> 方法通过属性名从 <code>BeanMap</code> 中获取属性值。</p><p>请注意，<code>BeanMap</code> 中的所有属性都作为字符串键存储在 Map 中。因此，在获取属性值时，需要进行适当的类型转换。</p><p>除了获取属性值之外，<code>BeanMap</code> 还提供了其他方法，如 <code>put</code>、<code>containsKey</code>、<code>size</code> 等，可以用于操作和查询属性值。</p><p>请注意，<code>BeanMap</code> 对象只是对 Java 对象属性的映射，不会创建新的对象。它允许您通过属性名称访问和操作 Java 对象的属性值。</p><h4 id="二beanmap的意义为什么要使用beanmap"><a name="t2"></a><a id="BeanMapBeanMap_39"></a>二、BeanMap的意义，为什么要使用BeanMap？</h4><p>引入BeanMap可以为开发人员提供一种方便的方式来操作Java对象的属性。以下是一些使用BeanMap的常见情况：</p><ol><li><p>简化对象到Map的转换：有时候，您可能需要将Java对象转换为Map来进行处理或传递数据。使用BeanMap，您可以轻松地将Java对象转换为一个可读写的Map，其中对象的属性作为Map的键，属性值作为Map的值。这样，就可以像操作Map一样访问和修改对象的属性。</p></li><li><p>动态访问和操作属性：BeanMap允许您在运行时动态地获取和设置Java对象的属性值，而无需显式调用对象的getter和setter方法。这对于需要动态操作属性的情况非常有用，例如在通用的数据绑定或动态配置场景中。</p><h5 id="以下是一个使用beanmap进行数据绑定和转换的示例"><a id="BeanMap_46"></a>以下是一个使用BeanMap进行数据绑定和转换的示例：</h5><p>假设我们有一个用户表单，其中包含姓名（name）、年龄（age）和电子邮件（email）字段。我们需要将表单数据绑定到一个User对象中。这时可以使用BeanMap来实现数据的绑定和转换。</p><p>首先，我们创建一个User类，具有与表单字段对应的属性：</p><pre><code>public class User {    private String name;    private int age;    private String email;    // 省略构造函数和其他方法    // Getter和Setter方法}</code></pre><p>接下来，我们获取表单数据并将其填充到BeanMap中：</p><pre><code>import org.apache.commons.beanutils.BeanMap;// 假设表单数据存储在一个Map中Map&lt;String, Object&gt; formData = new HashMap&lt;&gt;();formData.put(&quot;name&quot;, &quot;John Doe&quot;);formData.put(&quot;age&quot;, &quot;30&quot;);formData.put(&quot;email&quot;, &quot;john.doe@example.com&quot;);// 将表单数据填充到BeanMap中BeanMap beanMap = new BeanMap(new User());// 使用putAll方法将表单数据填充到BeanMap中beanMap.putAll(formData);</code></pre><p>现在，我们可以通过BeanMap方便地访问User对象的属性：</p><pre><code>// 获取User对象的姓名String name = (String) beanMap.get(&quot;name&quot;);System.out.println(&quot;Name: &quot; + name);// 修改User对象的年龄beanMap.put(&quot;age&quot;, 35);// 获取User对象的电子邮件String email = (String) beanMap.get(&quot;email&quot;);System.out.println(&quot;Email: &quot; + email);</code></pre><p>最后，我们可以使用BeanMap中的数据创建一个完整的User对象：</p><pre><code>User user = (User) beanMap.getBean();System.out.println(&quot;User: &quot; + user.getName() + &quot;, &quot; + user.getAge() + &quot;, &quot; + user.getEmail());</code></pre><p>上述示例演示了如何使用BeanMap将表单数据绑定到User对象，并通过BeanMap方便地访问和修改属性。这种方式可以简化数据绑定和转换的过程，将表单数据直接填充到BeanMap中，而无需分别调用每个属性的setter方法。这减少了冗余的代码，并提供了一种更简洁的方式来完成数据绑定。</p><h4 id="三beanmap和gettersetter各自适用于哪些场景"><a name="t3"></a><a id="BeanMapgettersetter_109"></a>三、BeanMap和getter、setter各自适用于哪些场景？</h4><p>BeanMap和getter、setter方法各自适用于不同的场景。以下是它们常见的应用场景：</p><h5 id="beanmap适用的场景"><a id="BeanMap_113"></a>BeanMap适用的场景：</h5><ol><li>动态属性访问。</li><li><em><strong>简化数据绑定</strong></em>：在数据绑定的场景中，BeanMap可以用于将Java对象转换为Map，使得属性的读取和设置更加简便。这对于处理表单数据、数据传输等场景非常有用。</li><li>*配置处理：*如果您需要从外部配置源（例如属性文件、数据库）加载属性并对其进行操作，BeanMap可以简化配置处理的过程。您可以将配置数据加载到BeanMap中，然后方便地读取和修改属性。</li></ol><h5 id="getter和setter方法适用的场景"><a id="GetterSetter_119"></a>Getter和Setter方法适用的场景：</h5><ol><li>封装和控制属性访问：通过使用getter和setter方法，您可以对属性的访问进行封装，控制读取和修改的逻辑。这使得您可以在属性级别上实施业务规则、验证和安全性控制。</li><li>IDE支持和自动生成：大多数集成开发环境（IDE）都支持自动生成getter和setter方法，使得编写这些方法变得非常简单和快捷。这样可以节省开发人员的时间和精力。</li><li>静态属性访问：如果属性的名称是固定的且在编译时已知，并且没有动态访问的需求，直接使用getter和setter方法是一种更直接和高效的方式。这种方式无需引入额外的工具或类库。</li></ol><h5 id="在spring框架中更常用的是使用getter和setter方法而不是beanmap下面是一些原因"><a id="SpringgettersetterBeanMap_126"></a>在Spring框架中，更常用的是使用getter和setter方法而不是BeanMap。下面是一些原因：</h5><ol><li>遵循JavaBean规范：Spring框架鼓励使用POJO（Plain Old Java Object）作为组件，它们通常遵循JavaBean规范。JavaBean规范要求使用getter和setter方法来访问和修改属性，这样可以更好地封装和控制属性的访问。因此，在Spring框架中，getter和setter方法是一种更常见和推荐的方式。</li><li>Spring的依赖注入（DI）机制：Spring框架的核心特性之一是依赖注入（Dependency Injection，DI）。通过DI，Spring可以自动将依赖注入到组件中，而无需显式调用setter方法。Spring框架通过使用反射和属性名称来自动查找和调用setter方法，并将依赖注入到属性中。</li><li>Spring表达式语言（SpEL）：Spring框架提供了表达式语言（SpEL），允许在XML配置文件和注解中直接访问对象的属性。SpEL支持直接访问getter和setter方法，使得在Spring框架中使用getter和setter更加方便和自然。</li></ol><p>尽管BeanMap在某些特定场景下可能有用，但在Spring框架中，通常更常见和推荐使用getter和setter方法。这样可以与Spring的约定和机制保持一致，并充分利用Spring框架提供的特性和功能。</p><h4 id="四比较一下beanmap和gettersetter"><a name="t4"></a><a id="BeanMapgettersetter_134"></a>四、比较一下BeanMap和getter、setter</h4><p>BeanMap和getter、setter是用于访问和操作Java对象属性的不同方法。下面是它们之间的比较：</p><h5 id="beanmap"><a id="BeanMap_138"></a>BeanMap：</h5><ul><li>优点：<ul><li>动态性：BeanMap允许在运行时动态地获取和设置Java对象的属性值，而无需在编译时确定。</li><li>简化操作：通过将Java对象转换为可读写的Map，BeanMap提供了一种简化的方式来访问和操作对象的属性，无需显式调用getter和setter方法。</li><li>减少重复代码：使用BeanMap可以减少编写大量的getter和setter方法的重复代码，从而提高代码的可维护性和简洁性。</li></ul></li><li>缺点：<ul><li>性能开销：相对于直接调用对象的getter和setter方法，BeanMap可能引入一些性能开销，因为它需要进行Map的操作和反射调用。</li></ul></li></ul><h5 id="getter和setter方法"><a id="GetterSetter_147"></a>Getter和Setter方法：</h5><ul><li>优点：<ul><li>直接访问：getter和setter方法提供了直接访问对象属性的方式，没有额外的性能开销。</li><li>封装性：通过使用getter和setter方法，可以实现对属性的封装，控制对属性的访问和修改。</li><li>IDE支持：IDE（集成开发环境）通常提供自动生成getter和setter方法的功能，方便快捷地生成这些方法。</li></ul></li><li>缺点：<ul><li>代码冗余：在类的属性较多时，需要编写大量的getter和setter方法，可能导致代码冗余和可读性下降。</li><li>静态定义：getter和setter方法在编译时就需要确定，并且一旦定义后，通常不能在运行时动态更改。</li></ul></li></ul><p>选择使用BeanMap或getter和setter方法取决于具体的需求和场景。如果需要动态访问和操作属性，或者希望减少重复代码，BeanMap可能是一个更好的选择。然而，如果性能是关键考虑因素，或者需要对属性进行封装和控制访问权限，getter和setter方法可能更适合。</p></li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[TT命令实现简单压力测试--Arthas的妙用]]></title>
                <link rel="alternate" type="text/html" href="https://www.wangdaye.net/archives/arthastt简单压力测试发现问题" />
                <id>tag:https://www.wangdaye.net,2023-11-07:arthastt简单压力测试发现问题</id>
                <published>2023-11-07T10:18:05+08:00</published>
                <updated>2023-11-22T09:34:40+08:00</updated>
                <author>
                    <name>王大爷</name>
                    <uri>https://www.wangdaye.net</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="arthas-tt-简单压力测试发现问题">Arthas TT 简单压力测试发现问题</h1><h2 id="步骤1打开客户端一执行如下命令使用tt抓包">步骤1：打开客户端一，执行如下命令，使用TT抓包。</h2><p>￼￼<br /><img src="https://www.wangdaye.net/upload/2023/11/image-bc56325e06cf41c7a452cbb6c8e6ee43.png" alt="image.png" /><br /><img src="https://www.wangdaye.net/upload/2023/11/image-cbb5b16f63cb424c8f739a35493d4e2d.png" alt="image.png" /></p><h2 id="步骤2客户端一退出如上命令并且执行如下命令使用tt复现请求">步骤2：客户端一，退出如上命令，并且执行如下命令，使用TT复现请求。</h2><p><img src="https://www.wangdaye.net/upload/2023/11/image-54fd590302564bb08bafd926b041a479.png" alt="image.png" /></p><h2 id="步骤3打开客户端二执行trace命令">步骤3，打开客户端二。执行trace命令。</h2><p>￼<br /><img src="https://www.wangdaye.net/upload/2023/11/image-ffe919a1ea1c4bc9a6a813e31a3439b1.png" alt="image" /></p><h2 id="注意事项">注意事项</h2><ul><li>其中2,3步骤可能需要调换一下顺序，因为字节码执行顺序问题，需要先将trace命令执行修改完成字节码，然后执行TT复现命令，去调用新字节码。</li></ul>]]>
                </content>
            </entry>
</feed>
