<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>WrayのBlog</title>
  
  
  <link href="https://blog.itwray.com/atom.xml" rel="self"/>
  
  <link href="https://blog.itwray.com/"/>
  <updated>2026-02-26T02:36:33.764Z</updated>
  <id>https://blog.itwray.com/</id>
  
  <author>
    <name>Wray</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>月度总结-2026.2</title>
    <link href="https://blog.itwray.com/2026/02/24/monthly-202602/"/>
    <id>https://blog.itwray.com/2026/02/24/monthly-202602/</id>
    <published>2026-02-24T07:34:56.000Z</published>
    <updated>2026-02-26T02:36:33.764Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1月总结"><a class="header-anchor" href="#1月总结"></a>1月总结</h2><p>1月，春节前的最后一个完整月，在这个月初我制定了每周目标、月度目标，勉强完成部分目标，大致如下：</p><ol><li>app实现月度支出的每日统计。- 已完成</li><li>设定每周、月度、年度目标。- 设定了每周和月度目标。</li><li>找到合适的阅读书籍。 - 通过GPT粗略学习了解ES。</li><li>app实现冰箱功能。 - 已完成</li><li>app月度支出、年度支出增加标签统计。- 已完成</li><li>整理简历 || 月度总结。 - 未完成</li><li>app任务计划增加一个置顶选项 。 - 已完成</li></ol><p>总结下来就是感兴趣的app功能都能积极按时完成，但是规划类的目标总是各种拖拉。</p><h2 id="春节总结"><a class="header-anchor" href="#春节总结"></a>春节总结</h2><p>春节做的最正确的事就是踏出了那一步，2026年希望事事顺利、马到成功。</p><p>春节期间就是各种赶路、当司机，糟心的事就是家里的车损坏了，还是那句话“喝酒不开车，开车不喝酒”。</p><h2 id="2月总结"><a class="header-anchor" href="#2月总结"></a>2月总结</h2><p>2月初，年前的一周多时间都在准备过年的东西，也无心搞其他事。</p><p>2月中旬就是奔波的过年时间。</p><p>2月末，年后的最后一周，也就是本周，连夜赶路回到上班地，急急忙忙的坐上工作岗位，但又感觉工作的无趣。</p><p>2026年，努力学习、适应、接纳Vibe Coding。</p>]]></content>
    
    
    <summary type="html">2026年2月的月度总结，亦是1月份的总结，亦是春节的总结。</summary>
    
    
    
    <category term="月度总结" scheme="https://blog.itwray.com/categories/%E6%9C%88%E5%BA%A6%E6%80%BB%E7%BB%93/"/>
    
    
  </entry>
  
  <entry>
    <title>年终总结-2025</title>
    <link href="https://blog.itwray.com/2025/12/30/year-end-2025/"/>
    <id>https://blog.itwray.com/2025/12/30/year-end-2025/</id>
    <published>2025-12-30T07:20:56.000Z</published>
    <updated>2025-12-30T08:18:56.919Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>2025，心态变化的一年。</p></blockquote><h2 id="回看2025年规划"><a class="header-anchor" href="#回看2025年规划"></a>回看2025年规划</h2><p>之前的规划大致如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/20251229155154.png" alt="image-20251229154457234"></p><p>实际情况：</p><ol><li>TODO List基本都会按照设定节奏完成，但是没有设定一个阶段性良好向上的TODO。</li><li>任性好像跟积分没关系，积分的奖惩没做好，而且容易漏记。</li><li>AI大时代，不看博客和技术文档了，也就没写下去的动力了。</li><li>上半年基本在持续迭代吧，因为一直有自己想做的功能，下半年基本功能已有，也就停滞了。</li><li>没看书，也基本没跑步。没看书就是自我放弃，没跑步是多方面因素导致，环境、工作状态、时间、心情、懒惰。</li></ol><h2 id="2025年总结"><a class="header-anchor" href="#2025年总结"></a>2025年总结</h2><p>2025年一句话来说就是苟且偷生的一年。感觉做什么事都得带点偷摸的搞法，不敢轻举妄动，经不起敲打了。</p><h3 id="工作"><a class="header-anchor" href="#工作"></a>工作</h3><p>在25年春节前确定了一份工作，但是仅仅是一份过渡工作，是自己之前瞧不起的工作，之所以接下 offer，是因为当时心态已经不好了，也想过个安稳年。</p><p>年后，幸好自己足够自律，保持面试状态，重新找了一份正常一点的工作，也幸好在那期间成功过渡。</p><p>之后就一平如水了。。。</p><h3 id="站点与项目"><a class="header-anchor" href="#站点与项目"></a>站点与项目</h3><p>今年延续着去年对个人项目的热情，疯狂开发了一系列功能，把自己想用的功能都加进来了。但在技术领域上就没做其他大的突破了，一开始是精力所限，后面是懒癌上身。</p><p>不过整体来说，项目完成度自己还是很满意了，有了AI Coding的加成，前端开发任务轻松了很大一截。</p><p>除了个人项目以外，站点的其他部分就都搁置了，可能感觉意义不大，也没感觉到实际作用。</p><h3 id="生活"><a class="header-anchor" href="#生活"></a>生活</h3><p>生活比去年更加单调了，可能因为工作的原因，去年下半年不工作，会主动找很多事干，有时间有精力，今年只想躺平。</p><p>体重：这是今年躺平最直观的数据了，体重又从161回到了172😭，胖了之后明显感觉动一动都喘，转个身都费劲，熟悉的感觉又回来了。</p><p>旅游：罗田天堂寨、黄山、景德镇。</p><p>游戏：三角洲当鼠鼠，yyds。</p><p>车：7k公里左右，没怎么开，但是蹭漆倒是不少，最主要的是年底还把胎给扎了，换一条胎就是一千多（写下这段的时候还在糟心）。</p><p>跑步&amp;书籍：啥都不说了，嘻嘻。</p><p>电子产品：投影仪、扫地机器人。今年在这方面的消费很克制，不过还是被修电脑的上了一课，花费六百多。扫地机器人简直是提升生活幸福度的一大利器。</p><h2 id="2026年规划"><a class="header-anchor" href="#2026年规划"></a>2026年规划</h2><p>关键字：成熟与稳重。</p><p>体重：跑步和健身，还有饮食，控制在165以内。</p><p>旅游：多自驾游一下吧。</p><p>书籍&amp;知识：沉淀，设定每周、每月、每季度的规划。最少提升两个方面。</p><p>博客：每月总结，一定。</p>]]></content>
    
    
    <summary type="html">2025年的年终总结，以及对2026年的规划。</summary>
    
    
    
    <category term="年终总结" scheme="https://blog.itwray.com/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
    
  </entry>
  
  <entry>
    <title>Jenkins安装和使用</title>
    <link href="https://blog.itwray.com/2025/02/12/jenkins-use/"/>
    <id>https://blog.itwray.com/2025/02/12/jenkins-use/</id>
    <published>2025-02-12T11:08:54.000Z</published>
    <updated>2025-02-20T07:02:24.682Z</updated>
    
    <content type="html"><![CDATA[<h2 id="搭建Jenkins"><a class="header-anchor" href="#搭建Jenkins"></a>搭建Jenkins</h2><h3 id="一键安装-Jenkins"><a class="header-anchor" href="#一键安装-Jenkins"></a>一键安装 Jenkins</h3><p>使用 Homebrew 可一键安装 Jenkins。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install jenkins-lts</span><br></pre></td></tr></table></figure><h3 id="安装中的问题"><a class="header-anchor" href="#安装中的问题"></a>安装中的问题</h3><ul><li><p>Error: unknown or unsupported macOS version: :sequoia</p><p>可能表示 Homebrew 还未完全支持该系统版本，可以尝试更新 Homebrew。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">brew update</span><br><span class="line">brew upgrade</span><br></pre></td></tr></table></figure></li></ul><h3 id="修改启动端口"><a class="header-anchor" href="#修改启动端口"></a>修改启动端口</h3><p>安装完成之后，Jenkins 默认使用 8080 端口，如果端口被占用会出现启动失败情况，可以按照如下方法修改启动端口。</p><p>运行以下命令，确定 Jenkins 的安装路径：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew info jenkins-lts</span><br></pre></td></tr></table></figure><p>在输出内容中找到以下类似内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">jenkins-lts: stable 2.492.1 (bottled)</span><br><span class="line">Extendable open source continuous integration server</span><br><span class="line">https://www.jenkins.io/</span><br><span class="line">Installed</span><br><span class="line">/opt/homebrew/Cellar/jenkins-lts/2.492.1 (9 files, 95.8MB) *</span><br></pre></td></tr></table></figure><p><code>/opt/homebrew/Cellar/jenkins-lts/2.492.1</code> 就是 Jenkins 的根目录，修改根目录下的 <code>homebrew.mxcl.jenkins-lts.plist</code> 文件。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi homebrew.mxcl.jenkins-lts.plist</span><br></pre></td></tr></table></figure><p>找到下面这行内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;string&gt;--httpPort=8080&lt;/string&gt;</span><br></pre></td></tr></table></figure><p>将 8080 端口修改为自己想要的端口，保存文件，重启 Jenkins 服务即可。</p><h3 id="Jenkins-常用命令"><a class="header-anchor" href="#Jenkins-常用命令"></a>Jenkins 常用命令</h3><p>启动命令：<code>brew services start jenkins-lts</code></p><p>停止命令：<code>brew services stop jenkins-lts</code></p><p>重启命令：<code>brew services restart jenkins-lts</code></p><p>查看 Homebrew 下 Jenkins 运行状态：<code>brew services list</code></p><h2 id="配置Jenkins"><a class="header-anchor" href="#配置Jenkins"></a>配置Jenkins</h2><h3 id="首次登录"><a class="header-anchor" href="#首次登录"></a>首次登录</h3><p>浏览器输入<code> http://localhost:&lt;port&gt;</code> 进入 Jenkins 页面。</p><p>首次登录时，默认有一个 admin 管理员用户，页面会提示管理员密码的文件所在位置，打开文件复制密码登录，登录后，Jenkins 会提示创建一个新管理员用户，创建完之后，admin 用户会被删除。</p><h3 id="插件配置"><a class="header-anchor" href="#插件配置"></a>插件配置</h3><p>在 Jenkins 页面，找到 <strong>系统管理 -&gt; 插件管理</strong>，在 <strong>Available plugins</strong> 中搜索想要安装的插件。</p><p>例如，我的项目是Spring Boot项目，代码版本管理在 GitHub 上，希望通过 Jenkins 打包部署到 Linux 服务器，大致需要安装如下插件：</p><ul><li>Git</li><li>Pipeline</li><li>Publish Over SSH（用于 SSH 远程部署）</li></ul><p><strong>Publish over SSH 配置</strong></p><p>安装完 <code>Publish Over SSH</code> 之后，在<strong>系统管理 -&gt; 系统配置</strong> 中找到 Publish over SSH 配置项。在 <code>SSH Servers</code>下点击新增，配置 SSH Server 的以下信息：</p><ul><li>Name（服务名称，任意填写）</li><li>Hostname（远程服务器IP）</li><li>Username（远程服务器用户名）</li><li>高级配置，勾选 <code>Use password authentication, or use a different key</code> ，如果是密码登录，则输入Password，如果是密钥登录，则在<code>Path to key</code>下输入 id_rsa 私钥文件路径，或者直接在<code>Key</code>下输入完整私钥值。</li></ul><p>最后，点击测试连接，提示Success则表示配置成功。</p><blockquote><p><strong>Tips</strong>：高级配置下的 <code>Jump host</code> 表示跳板机，意思是如果想要连接 Hostname（真正想要连接的目标服务器） , 但无法直接访问时，可以通过先连接 Jump host ，再让 Jenkins 通过 Jump host 连接 Hostname。连接过程如下：</p><p>Jenkins（本机） -&gt; Jump host -&gt; Hostname</p><p>如果 Jump host 需要通过认证登录，可能就无法实现，经反复测试，在 Jenkins 2.492.1 版本下未找到可行方法，如果有大佬发现解决办法或解决思路，记得@我，感谢。</p><p>目前另一种解决办法是：在Job任务中，先连接 Jump host 机器，然后在 Exec command 下手动 ssh Hostname 访问目标机器。</p></blockquote><h3 id="全局配置"><a class="header-anchor" href="#全局配置"></a>全局配置</h3><p>在 Jenkins 页面，找到 <strong>系统管理 -&gt; 全局工具管理</strong>，配置 Maven、JDK、Git 等环境。</p><h2 id="新建任务"><a class="header-anchor" href="#新建任务"></a>新建任务</h2><p>不同的任务，在Jenkins配置的任务风格可能差异化也比较大，下面以具体任务需求为例：</p><h3 id="本地SpringBoot服务，希望通过Maven打包部署到远程服务器，并在远程服务器执行Java程序"><a class="header-anchor" href="#本地SpringBoot服务，希望通过Maven打包部署到远程服务器，并在远程服务器执行Java程序"></a>本地SpringBoot服务，希望通过Maven打包部署到远程服务器，并在远程服务器执行Java程序</h3><ol><li>首先，自定义一个任务名称，选择自由风格，点击确定。<img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20250219163608513.png" alt="image-20250219163608513"></li><li>在 General 中，展开高级设置，勾选“使用自定义的工作空间”，然后填入SpringBoot项目所在目录地址。<img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20250219163834692.png" alt="image-20250219163834692"></li><li>在 Build Steps 中，点击“增加构建步骤”，选择“执行Shell”，输入项目的Maven打包操作命令。<img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20250219164325099.png" alt="image-20250219164325099"></li></ol><p>​<strong>Tips</strong>：当前pwd路径是第2步指定的工作空间路径。此外，即使全局配置了Maven环境，但在这里Jenkins好像无法获取到，所以手动加载了Maven的环境变量。</p><ol start="4"><li><p>在 Build Steps 中，点击“增加构建步骤”，选择“Send files or execute commands over SSH”，确保该步骤在第3步的步骤之后。SSH Server选择需要部署的远程服务器，Source files 是工作空间的相对路径，Remove prefix 表示移除从 Source files 推送到 Remote directory 时需要移除的路径前缀，Remote directory 表示 SSH Server 连接后的相对路径，Exec command 表示在推送文件后执行的命令。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20250219165642117.png" alt="image-20250219165642117"></p></li><li><p>最后，Save保存，然后点击“立即构建”，即可运行任务。</p></li></ol>]]></content>
    
    
    <summary type="html">macOS环境搭建Jenkins，以及Jenkins的一些常用配置和任务构建过程的示例。</summary>
    
    
    
    <category term="Jenkins" scheme="https://blog.itwray.com/categories/Jenkins/"/>
    
    
  </entry>
  
  <entry>
    <title>年终总结-2024</title>
    <link href="https://blog.itwray.com/2024/12/31/year-end-2024/"/>
    <id>https://blog.itwray.com/2024/12/31/year-end-2024/</id>
    <published>2024-12-31T02:20:56.000Z</published>
    <updated>2024-12-31T16:17:27.266Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>回看2024，令人唏嘘，大起大落，丰富又平凡。总结：1 + 1 = 0.5 。</p></blockquote><h2 id="回看2024年规划"><a class="header-anchor" href="#回看2024年规划"></a>回看2024年规划</h2><p>第一部分中，除了第一条严格完成之外，简历和面试简直就是个笑话。</p><p>第二部分：</p><ol><li>先不说每周总结，月度总结也就两三次，真的是羞愧。</li><li>SpringBoot大概看了两章左右，没有看下去的动力，倒是JVM看的挺入迷。</li><li>这个规划应该说是完成度最高的了，iw-mixes项目基本架构搭建完成，自我感觉完成的还行，满分100的话，可以打个70。</li><li>站点改造是在离职期间成功完成的，虽然大多数站点项目来自于开源项目，但也算是完成了，对于我来说前端ui是真难。</li></ol><p>待办任务表也是个笑话，有道云笔记这种第三方软件，太繁琐了，反而不太好，不如直接用个text列举123，正因如此，后面改用了MacOS的提醒事项做 TODO List。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241230134540853.png" alt="image-20241230134540853"></p><h2 id="博客"><a class="header-anchor" href="#博客"></a>博客</h2><p>从 2024-01-01 ～ 2024-12-31 ，共 18 篇文章，其中工作和学习文章占了大半，可见这一年在博客方面没有做什么技术性文章或者有价值的文章。更甚一点说，24年就没有怎么专心写博客。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241230135501975.png" alt="image-20241230135501975"></p><p>下面就一一列举说一下比较有意思的几篇文章吧。</p><ol><li><a href="https://blog.itwray.com/2024/03/04/springdoc-hello/">再见SpringFox，你好SpringDoc</a>：在搭建 iw-mixes 项目之初，因为用的 SpringBoot 3，Swagger不再适配，所以了解到了SpringDoc。</li><li>JVM：学习JVM的动机其实是为了面试，因此结合《深入理解Java虚拟机》书籍和网上别人整理的JVM知识点，了解了JVM的一些基础知识，为此写了三篇总结文章（<a href="https://blog.itwray.com/2024/06/18/java-jvm-basic/">Java-JVM基础</a>、<a href="https://blog.itwray.com/2024/06/30/java-jvm-classFileStructure/">Java-JVM类文件结构</a>、<a href="https://blog.itwray.com/2024/07/03/java-jvm-readClassFile/">Java-“手撕”Class文件结构</a>）。</li><li><a href="https://blog.itwray.com/2024/08/26/spring-boot-redis-use/">SpringBoot集成Redis使用心得</a>：这篇文章也是在 iw-mixes 项目集成 Redis 时，写的一个经验总结，主要说明 SpringBoot 如何引入 Redis、RestTemplate 依赖注入问题、以及 Redis 客户端的序列化方式。</li><li><a href="https://blog.itwray.com/2024/10/15/echart-uni-20241015/">记录一次在uni-app中使用echarts的坑</a>：这个坑我还记得当时花了大半天的时间才解决，不懂前端代码原理是真的痛苦，只能反复尝试、一步一步的debug才行。</li></ol><h2 id="个人站点"><a class="header-anchor" href="#个人站点"></a>个人站点</h2><blockquote><p>个人站点算是自从我接触计算机多年以来的愿望吧，在今年离职期间也算是成功实现，对外可以“臭美”的说一句我的网站地址是多少。。。</p><p>说不出来有多大作用，就觉得好玩，在互联网上有一个归属。</p></blockquote><p>站点地址：<a href="https://www.itwray.com">itwray.com</a></p><p>目前想法就是，站点作为一个网站导航，展示自己的成果。</p><p>目前对外展示的有：</p><ul><li>GitHub：<a href="https://github.com/wangfarui">github.com/wangfarui</a></li><li>博客：<a href="https://blog.itwray.com/">blog.itwray.com</a></li><li>iw-mixes项目后台：<a href="https://web.itwray.com/">web.itwray.com</a></li><li>知识库：<a href="https://docs.itwray.com/">docs.itwray.com</a></li><li>今日热榜：<a href="https://hot.itwray.com/#/">hot.itwray.com</a></li><li>站点监测：<a href="https://status.itwray.com/">status.itwray.com</a></li></ul><p>PS：因为技术有限，个人站点、知识库、今日热榜、站点监测是使用的开源项目搭建的，然后自己再做一些配置变更和内容填充。在此，感谢开源的大佬们：<a href="https://github.com/imsyy">imsyy</a>、<a href="https://github.com/xugaoyi">xugaoyi</a> 。</p><h2 id="个人项目"><a class="header-anchor" href="#个人项目"></a>个人项目</h2><p><a href="https://github.com/wangfarui/iw-mixes">iw-mixes</a> 是这一年主要维护的项目，在个人闲暇时间中，基本把主要重心就是放在这了。</p><p>对应的前端项目有：<a href="https://github.com/wangfarui/iw-mixes-web-platform">iw-mixes-web-platform</a>、<a href="https://github.com/wangfarui/iw-mixes-app">iw-mixes-app</a>，它们分别对应web端和微信小程序端。起初 iw-mixes-app 是希望用 uni-app 做成多平台的，但鉴于实际使用以及维护成本，就只在微信小程序上做了测试，后期计划全面转微信开发平台定制开发。</p><p>关于 iw-mixes 项目的提交记录：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241230142700520.png" alt="image-20241230142700520"></p><p>关于2024年 GitHub 的提交记录：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241231144532988.png" alt="image-20241231144532988"></p><p>PS：GitHub 提交数量刚好凑了个666，希望2025年一路大顺。争取2025年提交数量搞个保底888。</p><h2 id="生活"><a class="header-anchor" href="#生活"></a>生活</h2><p>书籍：《丹尼尔斯的经典跑步训练法》、《深入理解Java虚拟机》、《Spring Boot编程思想》、《活着》。</p><p>旅游：天津、萍乡武功山、武汉龙王尖、南昌林俊杰、上海迪士尼、杭州西湖、威海、青岛、济南黄河、泰山、无锡烟花、上海五月天。</p><p>游戏：双休喊朋友玩一下lol，无聊时间染上了一款养成手游（准备戒掉，费时间费金钱）。</p><p>车：接近半年开了1w公里，下半年就没怎么开了。</p><p>跑步：从8月份开始，迷上了跑步，了解到了马拉松，对跑步达到痴迷状态，持续到11月份吧。</p><p>电子产品：平板、手表在离职在家时换了，用了5年多的手机，在年底也换了。</p><p>体重：大概是从172到161，数据测量时间基本为早晨起床时称的净重。</p><p>今年下半年感觉就没什么顺心意的事发生，可以说非常糟了，生活真的就像一堆垃圾呢。</p><h2 id="明年规划"><a class="header-anchor" href="#明年规划"></a>明年规划</h2><p>2025！必须得拼，不能再躺平了，平稳是留给晚年的。时光在流逝，年龄不会躺平。</p><p>第一个念头，严格实行 TODO List，设立奖惩机制。</p><h3 id="TODO-List"><a class="header-anchor" href="#TODO-List"></a>TODO List</h3><p>TODO List 不再设立长期目标，TODO 分为待办事项和简易事项。（根据对自己的了解，拖延症不会去处理长期事务的）</p><p>TODO 待办事项：</p><ul><li>概念：表示最近希望做的事，但事项内容不能在一天内完成或者立刻能开始处理的事情。</li><li>“最近”：表示一个月内，每一项建立的事项将会记录创建时间以及完成时间，事项必须在创建后一个月内完成，若未完成则需要备注原因。</li><li>事项内容：凡是自己念头想到的，想要做的。（至于有没有意义，做完了不就知道了）</li></ul><p>TODO 简易事项：表示需要在一周内完成的事项，事项一般可以花几分钟、几小时或者当天能完成。</p><h3 id="奖惩机制"><a class="header-anchor" href="#奖惩机制"></a>奖惩机制</h3><p>第一点！！！奖必须在有足够的积分情况下才能实施！！！</p><p>奖惩机制将采用 iw-mixes 项目的积分服务实现，通过积分制约束自己。</p><p>积分的取值之前都是随意取的，接下来将采用两个维度决定积分值大小。</p><p>维度一，用于决定没有任务时长，偏生活小事的方向，大致介绍如下：</p><ul><li>1分：表示一些基础的生活作息。例如早睡早起、自己做饭而不是偷懒点外卖。</li><li>2分：表示丰富的生活。例如跑步、锻炼、家务等。</li></ul><p>有奖就有罚，对于简易任务，若是有要求但未完成，扣其奖励分的双倍，警示自己要自律。</p><p>维度二，根据任务价值和任务用时做阶梯性取值。（任务价值需要视具体事项决定）</p><table><thead><tr><th style="text-align:center">任务价值/任务用时</th><th style="text-align:center">一星</th><th style="text-align:center">二星</th><th style="text-align:center">三星</th></tr></thead><tbody><tr><td style="text-align:center">1h</td><td style="text-align:center">1分</td><td style="text-align:center">2分</td><td style="text-align:center">3分</td></tr><tr><td style="text-align:center">2h</td><td style="text-align:center">2分</td><td style="text-align:center">4分</td><td style="text-align:center">6分</td></tr><tr><td style="text-align:center">4h</td><td style="text-align:center">3分</td><td style="text-align:center">5分</td><td style="text-align:center">7分</td></tr><tr><td style="text-align:center">1 day</td><td style="text-align:center">6分</td><td style="text-align:center">8分</td><td style="text-align:center">10分</td></tr></tbody></table><p>维度二的任务，一般是计划任务或额外任务，一般没有罚，但如果是周期性任务要求，扣其对应的奖励分。</p><p>说完积分奖励，那么在积分使用上，一般偏向于奖励机制，例如睡个懒觉、点个外卖、出去潇洒一顿等，对于常用的奖励，将设定固定的扣减积分任务，便于后期做统计。</p><h3 id="博客与站点"><a class="header-anchor" href="#博客与站点"></a>博客与站点</h3><p>站点暂时没什么大计划，维护为主。</p><p>博客计划每月至少产出一篇有价值的文章，如果系统化学习某个领域，将把完整知识体系记录在知识库，博客作为总结性文章。</p><h3 id="个人项目-2"><a class="header-anchor" href="#个人项目-2"></a>个人项目</h3><p>继续完善 iw-mixes 项目架构，以新增功能为主，完善为辅，把功能体系先做大。</p><p>iw-mixes-app 改为微信小程序版，重新学习微信小程序开发。</p><p>定个小目标，2025年3月31号之前，发布 1.0.0-release 版。</p><h3 id="生活-2"><a class="header-anchor" href="#生活-2"></a>生活</h3><p>坚持看书！少看电子产品！让看书成为一种习惯，更希望是一种热爱。</p><p>不计划看多少本书，也不规定看什么书，只要是感兴趣的都看，唯一要求就是，在看完或者看的途中，通过笔记（博客）记录下来，看了总得有个印象。</p><p>跑步与健身，计划体重控制在155，跑步一周三次，按照一次10km计算，一个月（四周）则是要求&gt;=100km。</p><h3 id="总结"><a class="header-anchor" href="#总结"></a>总结</h3><ol><li>严格完成 TODO List 。</li><li>任性之前，必须要有足够的积分。</li><li>持续性的博客和知识库产出。</li><li>持续性的迭代 iw-mixes 项目。</li><li>爱上看书，爱上跑步。</li></ol><p>2025，加油！！！</p>]]></content>
    
    
    <summary type="html">2024年的年终总结，以及对2025年的规划。</summary>
    
    
    
    <category term="年终总结" scheme="https://blog.itwray.com/categories/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93/"/>
    
    
  </entry>
  
  <entry>
    <title>Arthas常用命令</title>
    <link href="https://blog.itwray.com/2024/11/14/arthas-command/"/>
    <id>https://blog.itwray.com/2024/11/14/arthas-command/</id>
    <published>2024-11-14T13:13:01.000Z</published>
    <updated>2024-11-20T07:27:20.864Z</updated>
    
    <content type="html"><![CDATA[<p>继上一次<a href="https://blog.itwray.com/2023/09/20/arthas-use/?highlight=arthas">学习使用 Arthas </a>之后，今天特此学习了解下 Arthas 在项目中比较好用的几个命令。</p><h2 id="启动-Arthas"><a class="header-anchor" href="#启动-Arthas"></a>启动 Arthas</h2><p>首先，使用 <code>as.sh</code> 脚本启动 Arthas ，找到需要监控诊断的 Java 进程。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241120135507933.png" alt="image-20241120135507933"></p><p>输入前面的数字索引下标，进入 Java 进程，例如 IwAuthApplication 进程需要输入 3 。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241120135629279.png" alt="image-20241120135629279"></p><p>接下来就可以使用 Arthas 命令操作了，如果不清楚有哪些命令，在不方便查看官方文档的情况下，或者想要知道当前版本的最新命令，可以直接输入 <code>help</code> 有哪些命令。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241120135910056.png" alt="image-20241120135910056"></p><p>知道主命令之后，可以接上参数 <code>-h</code> 了解每个命令的具体使用方法。</p><h2 id="命令列表"><a class="header-anchor" href="#命令列表"></a>命令列表</h2><h3 id="quit"><a class="header-anchor" href="#quit"></a>quit</h3><p>退出当前 Arthas 客户端，其他 Arthas 客户端不受影响。等同于<strong>exit</strong>、<strong>logout</strong>、<strong>q</strong>三个指令。</p><blockquote><p>使用 quit 命令，只是退出当前 Arthas 客户端，Arthas 的服务器端并没有关闭，所做的修改也不会被重置。</p><p>这里所说的修改是指，因为 Arthas 是以 Java agent 方式运行的，它可以修改指定 Java 进程的参数配置等信息，如果 Arthas 服务器端未关闭，配置就不会重置。</p></blockquote><h3 id="stop"><a class="header-anchor" href="#stop"></a>stop</h3><p>关闭 Arthas 服务端，所有 Arthas 客户端全部退出。</p><blockquote><p>关闭 Arthas 服务器之前，会重置掉所有做过的增强类。但是用 redefine 重加载的类内容不会被重置。</p></blockquote><h3 id="jvm"><a class="header-anchor" href="#jvm"></a>jvm</h3><p><code>jvm</code>命令可以查看到当前运行的 Java 进程的 JVM 相关信息。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">jvm</span></span><br><span class="line"> RUNTIME                                  # JVM 运行环境</span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> MACHINE-NAME                     47349@192.168.1.4                                                                </span><br><span class="line"> JVM-START-TIME                   2024-11-20 13:45:29                                                              </span><br><span class="line"> MANAGEMENT-SPEC-VERSION          3.0                                                                              </span><br><span class="line"> SPEC-NAME                        Java Virtual Machine Specification                                               </span><br><span class="line"> SPEC-VENDOR                      Oracle Corporation                                                               </span><br><span class="line"> SPEC-VERSION                     17                                                                               </span><br><span class="line"> VM-NAME                          OpenJDK 64-Bit Server VM                                                         </span><br><span class="line"> VM-VENDOR                        Homebrew                                                                         </span><br><span class="line"> VM-VERSION                       17.0.9+0                                                                         </span><br><span class="line"> INPUT-ARGUMENTS                  -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:57581,suspend=y,server=n    </span><br><span class="line">                                  -XX:TieredStopAtLevel=1                                                          </span><br><span class="line">                                  -Dspring.output.ansi.enabled=always                                              </span><br><span class="line">                                  -Dcom.sun.management.jmxremote                                                   </span><br><span class="line">                                  -Dspring.jmx.enabled=true                                                        </span><br><span class="line">                                  -Dspring.liveBeansView.mbeanDomain                                               </span><br><span class="line">                                  -Dspring.application.admin.enabled=true                                          </span><br><span class="line">                                  -javaagent:/Users/wangfarui/Library/Caches/JetBrains/IntelliJIdea2022.2/captureA </span><br><span class="line">                                  gent/debugger-agent.jar=file:/private/var/folders/wv/0ljm0h256y1_f_fgv2yw62rh000 </span><br><span class="line">                                  0gn/T/capture.props   # 通过这里可以发现，Idea在通过Debug方式启动Java程序时，会嵌入一个javaagent程序                                                         </span><br><span class="line">                                  -Dfile.encoding=UTF-8                                                            </span><br><span class="line"> CLASS-PATH                       []                                        </span><br><span class="line"> BOOT-CLASS-PATH                                                                                                   </span><br><span class="line"> LIBRARY-PATH                     /Users/wangfarui/Library/Java/Extensions:/Library/Java/Extensions:/Network/Libra </span><br><span class="line">                                  ry/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.               </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> CLASS-LOADING                              # ClassLoader                                                                     </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> LOADED-CLASS-COUNT               21095     # 当前类加载数量          </span><br><span class="line"> TOTAL-LOADED-CLASS-COUNT         21095     # 总共的类加载数量          </span><br><span class="line"> UNLOADED-CLASS-COUNT             0         # 已卸载的类数量        </span><br><span class="line"> IS-VERBOSE                       false                                                                            </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> COMPILATION                                                                                                       </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> NAME                             HotSpot 64-Bit Tiered Compilers                                                  </span><br><span class="line"> TOTAL-COMPILE-TIME               2875                                                                             </span><br><span class="line"> [time (ms)]                                                                                                       </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> GARBAGE-COLLECTORS                           # 垃圾收集器                                                                     </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> G1 Young Generation              name : G1 Young Generation                                                       </span><br><span class="line"> [count/time (ms)]                collectionCount : 20       # G1新生代收集次数        </span><br><span class="line">                                  collectionTime : 79        # G1新生代收集时间               </span><br><span class="line"> G1 Old Generation                name : G1 Old Generation                                                         </span><br><span class="line"> [count/time (ms)]                collectionCount : 0        # G1老年代收集次数      </span><br><span class="line">                                  collectionTime : 0         # G1老年代收集时间                                                       </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> MEMORY-MANAGERS                               # 内存管理器                                                                    </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> CodeCacheManager                 CodeCache                                                                        </span><br><span class="line"> Metaspace Manager                Metaspace                                                                        </span><br><span class="line">                                  Compressed Class Space                                                           </span><br><span class="line"> G1 Young Generation              G1 Eden Space                                                                    </span><br><span class="line">                                  G1 Survivor Space                                                                </span><br><span class="line">                                  G1 Old Gen                                                                       </span><br><span class="line"> G1 Old Generation                G1 Eden Space                                                                    </span><br><span class="line">                                  G1 Survivor Space                                                                </span><br><span class="line">                                  G1 Old Gen                                                                       </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> MEMORY                                          # 内存信息                                                                  </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> HEAP-MEMORY-USAGE                init : 268435456(256.0 MiB)                                                      </span><br><span class="line"> [memory in bytes]                used : 115123728(109.8 MiB)    # 堆已使用的内存</span><br><span class="line">                                  committed : 169869312(162.0 MiB)                                                 </span><br><span class="line">                                  max : 4294967296(4.0 GiB)      # 堆最大内存 即 -Xmx         </span><br><span class="line"> NO-HEAP-MEMORY-USAGE             init : 2555904(2.4 MiB)                                                          </span><br><span class="line"> [memory in bytes]                used : 136267744(130.0 MiB)    # 元空间已使用的内存            </span><br><span class="line">                                  committed : 137297920(130.9 MiB)                                                 </span><br><span class="line">                                  max : -1(-1 B)    # -1表示元空间无限大                                                               </span><br><span class="line"> PENDING-FINALIZE-COUNT           0                                                                                </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> OPERATING-SYSTEM                                # 操作系统信息                                                                  </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> OS                               Mac OS X                                                                         </span><br><span class="line"> ARCH                             aarch64                                                                          </span><br><span class="line"> PROCESSORS-COUNT                 8                                                                                </span><br><span class="line"> LOAD-AVERAGE                     6.015625                                                                         </span><br><span class="line"> VERSION                          14.6.1                                                                           </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> THREAD                                                                                                            </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> COUNT                            114            # JVM 当前活跃的线程数           </span><br><span class="line"> DAEMON-COUNT                     61             # JVM 当前活跃的守护线程数        </span><br><span class="line"> PEAK-COUNT                       134            # 从 JVM 启动开始曾经活着的最大线程数         </span><br><span class="line"> STARTED-COUNT                    676            # 从 JVM 启动开始总共启动过的线程次数          </span><br><span class="line"> DEADLOCK-COUNT                   0              # JVM 当前死锁的线程数 </span><br><span class="line">                                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> FILE-DESCRIPTOR                                                                                                   </span><br><span class="line">-------------------------------------------------------------------------------------------------------------------</span><br><span class="line"> MAX-FILE-DESCRIPTOR-COUNT        -1            # JVM 进程最大可以打开的文件描述符数                                                </span><br><span class="line"> OPEN-FILE-DESCRIPTOR-COUNT       -1            # JVM 当前打开的文件描述符数            </span><br><span class="line">                            </span><br></pre></td></tr></table></figure><h3 id="thread"><a class="header-anchor" href="#thread"></a>thread</h3><p>作用：查看当前线程信息，查看线程的堆栈。</p><table><thead><tr><th style="text-align:right">参数名称</th><th style="text-align:left">参数说明</th></tr></thead><tbody><tr><td style="text-align:right"><em>id</em></td><td style="text-align:left">线程 id</td></tr><tr><td style="text-align:right">[n:]</td><td style="text-align:left">指定最忙的前 N 个线程并打印堆栈</td></tr><tr><td style="text-align:right">[b]</td><td style="text-align:left">找出当前阻塞其他线程的线程</td></tr><tr><td style="text-align:right">[i <code>&lt;value&gt;</code>]</td><td style="text-align:left">指定 cpu 使用率统计的采样间隔，单位为毫秒，默认值为 200</td></tr><tr><td style="text-align:right">[--all]</td><td style="text-align:left">显示所有匹配的线程</td></tr></tbody></table><p>直接执行 <code>thread</code> 命令，表示按照 CPU 增量时间降序排列，打印第一页所有线程数据。</p><p><code>thread --state</code> 查看指定状态的线程。例如 <code>thread --state WAITING</code>查询所有等待线程。</p><h3 id="memory"><a class="header-anchor" href="#memory"></a>memory</h3><p>作用：查看 JVM 内存信息。</p><p>直接执行 <code>memory</code> 命令，结果信息如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">memory</span></span><br><span class="line">Memory                                           used             total           max              usage           </span><br><span class="line">heap                                             97M              162M            4096M            2.37%           </span><br><span class="line">g1_survivor_space                                8M               10M             -1               80.16%          </span><br><span class="line">g1_eden_space                                    34M              66M             -1               51.52%          </span><br><span class="line">g1_old_gen                                       55M              86M             4096M            1.34%   </span><br><span class="line"></span><br><span class="line">nonheap                                          132M             133M            -1               99.27%      </span><br><span class="line">metaspace                                        98M              98M             -1               99.36%          </span><br><span class="line">compressed_class_space                           13M              13M             1024M            1.31%           </span><br><span class="line">codecache                                        20M              20M             48M              43.06%          </span><br><span class="line">mapped                                           0K               0K              -                0.00%           </span><br><span class="line">direct                                           86M              86M             -                100.00%         </span><br><span class="line">mapped - &#x27;non-volatile memory&#x27;                   0K               0K              -                0.00%     </span><br></pre></td></tr></table></figure><p>可以看到主要分为 <code>heap</code> 和 <code>nonheap</code> 两块信息。</p><h3 id="stack"><a class="header-anchor" href="#stack"></a>stack</h3><p>作用：输出当前方法被调用的调用路径。</p><blockquote><p>很多时候我们都知道一个方法被执行，但这个方法被执行的路径非常多，或者你根本就不知道这个方法是从那里被执行了，此时你需要的是 stack 命令。</p></blockquote><p><code>stack</code> 是一个监控命令，只能监控 stack 命令执行之后的 JVM 运行状态。它主要是监控具体某个类的某个方法，输出该方法的调用链路。</p><table><thead><tr><th style="text-align:right">参数名称</th><th style="text-align:left">参数说明</th></tr></thead><tbody><tr><td style="text-align:right"><em>class-pattern</em></td><td style="text-align:left">类名表达式匹配</td></tr><tr><td style="text-align:right"><em>method-pattern</em></td><td style="text-align:left">方法名表达式匹配</td></tr><tr><td style="text-align:right"><em>condition-express</em></td><td style="text-align:left">条件表达式</td></tr><tr><td style="text-align:right">[E]</td><td style="text-align:left">开启正则表达式匹配，默认为通配符匹配</td></tr><tr><td style="text-align:right"><code>[n:]</code></td><td style="text-align:left">执行次数限制</td></tr><tr><td style="text-align:right"><code>[m &lt;arg&gt;]</code></td><td style="text-align:left">指定 Class 最大匹配数量，默认值为 50。长格式为<code>[maxMatch &lt;arg&gt;]</code>。</td></tr></tbody></table><p>[E] 是一个观察表达式，主要由 ognl 表达式组成，具体表达式用法可以参考：</p><ul><li>特殊用法请参考：<a href="https://github.com/alibaba/arthas/issues/71">https://github.com/alibaba/arthas/issues/71</a></li><li>OGNL 表达式官网：https://commons.apache.org/dormant/commons-ognl/language-guide.html</li></ul><p>例如，执行 <code>stack com.itwray.iw.auth.service.impl.AuthUserServiceImpl loginByPassword</code>，等待有线程触发执行到 <code>AuthUserServiceImpl#loginByPassword</code> 方法时，Arthas 就会打印调用链路。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">stack com.itwray.iw.auth.service.impl.AuthUserServiceImpl loginByPassword</span></span><br><span class="line">Press Q or Ctrl+C to abort.</span><br><span class="line">Affect(class count: 2 , method count: 2) cost in 187 ms, listenerId: 1</span><br><span class="line">ts=2024-11-20 14:36:19.412;thread_name=http-nio-18001-exec-5;id=122;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@2f29e630</span><br><span class="line">    @com.itwray.iw.auth.service.impl.AuthUserServiceImpl.loginByPassword()</span><br><span class="line">        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)</span><br><span class="line">        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)</span><br><span class="line">        at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)</span><br><span class="line">        at java.lang.reflect.Method.invoke(Method.java:568)</span><br><span class="line">        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351)</span><br><span class="line">        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)</span><br><span class="line">        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)</span><br><span class="line">        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)</span><br><span class="line">        at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)</span><br><span class="line">        at com.itwray.iw.starter.redis.lock.RedisDistributedLockAspect.around(RedisDistributedLockAspect.java:63)</span><br><span class="line">        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)</span><br><span class="line">        at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)</span><br><span class="line">        at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)</span><br><span class="line">        at java.lang.reflect.Method.invoke(Method.java:568)</span><br><span class="line">        at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:637)</span><br><span class="line">        at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:627)</span><br><span class="line">        at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)</span><br><span class="line">        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)</span><br><span class="line">        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)</span><br><span class="line">        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)</span><br><span class="line">        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)</span><br><span class="line">        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:765)</span><br><span class="line">        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:717)</span><br><span class="line">        at com.itwray.iw.auth.service.impl.AuthUserServiceImpl$$SpringCGLIB$$0.loginByPassword(&lt;generated&gt;:-1)</span><br><span class="line">        at com.itwray.iw.auth.controller.AuthLoginController.loginByPassword(AuthLoginController.java:60)</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>需要注意的是，在上面这段命令执行示例中，有一段输出内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Affect(class count: 2 , method count: 2) cost in 187 ms, listenerId: 1</span><br></pre></td></tr></table></figure><p>它表示监听到两个符合条件的类，两个符合条件的方法。然而在项目中我是只写了一个AuthUserServiceImpl#loginByPassword方法的，这是因为 Spring 的@Service 对其进行了增强，生成了一个 AuthUserServiceImpl$$SpringCGLIB$$0.loginByPassword 代理类。</p><h3 id="watch"><a class="header-anchor" href="#watch"></a>watch</h3><p><code>watch</code>命令与<code>stack</code>命令一样，也是监控方法运行的，不过它的主要作用是观察方法的调用情况，例如<code>返回值</code>、<code>抛出异常</code>、<code>入参</code> 。</p><table><thead><tr><th style="text-align:right">参数名称</th><th style="text-align:left">参数说明</th></tr></thead><tbody><tr><td style="text-align:right"><em>class-pattern</em></td><td style="text-align:left">类名表达式匹配</td></tr><tr><td style="text-align:right"><em>method-pattern</em></td><td style="text-align:left">函数名表达式匹配</td></tr><tr><td style="text-align:right"><em>express</em></td><td style="text-align:left">观察表达式，默认值：<code>&#123;params, target, returnObj&#125;</code></td></tr><tr><td style="text-align:right"><em>condition-express</em></td><td style="text-align:left">条件表达式</td></tr><tr><td style="text-align:right">[b]</td><td style="text-align:left">在<strong>函数调用之前</strong>观察</td></tr><tr><td style="text-align:right">[e]</td><td style="text-align:left">在<strong>函数异常之后</strong>观察</td></tr><tr><td style="text-align:right">[s]</td><td style="text-align:left">在<strong>函数返回之后</strong>观察</td></tr><tr><td style="text-align:right">[f]</td><td style="text-align:left">在<strong>函数结束之后</strong>(正常返回和异常返回)观察</td></tr><tr><td style="text-align:right">[E]</td><td style="text-align:left">开启正则表达式匹配，默认为通配符匹配</td></tr><tr><td style="text-align:right">[x:]</td><td style="text-align:left">指定输出结果的属性遍历深度，默认为 1，最大值是 4</td></tr><tr><td style="text-align:right"><code>[m &lt;arg&gt;]</code></td><td style="text-align:left">指定 Class 最大匹配数量，默认值为 50。长格式为<code>[maxMatch &lt;arg&gt;]</code>。</td></tr></tbody></table><p><strong>特别说明</strong>：</p><ul><li>watch 命令定义了 4 个观察事件点，即 <code>-b</code> 函数调用前，<code>-e</code> 函数异常后，<code>-s</code> 函数返回后，<code>-f</code> 函数结束后</li><li>4 个观察事件点 <code>-b</code>、<code>-e</code>、<code>-s</code> 默认关闭，<code>-f</code> 默认打开，当指定观察点被打开后，在相应事件点会对观察表达式进行求值并输出</li><li>这里要注意<code>函数入参</code>和<code>函数出参</code>的区别，有可能在中间被修改导致前后不一致，除了 <code>-b</code> 事件点 <code>params</code> 代表函数入参外，其余事件都代表函数出参</li><li>当使用 <code>-b</code> 时，由于观察事件点是在函数调用前，此时返回值或异常均不存在</li><li>在 watch 命令的结果里，会打印出<code>location</code>信息。<code>location</code>有三种可能值：<code>AtEnter</code>，<code>AtExit</code>，<code>AtExceptionExit</code>。对应函数入口，函数正常 return，函数抛出异常。</li></ul><p><strong>示例：</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">watch com.itwray.iw.auth.service.impl.AuthUserServiceImpl loginByPassword</span></span><br><span class="line">Press Q or Ctrl+C to abort.</span><br><span class="line">Affect(class count: 2 , method count: 2) cost in 117 ms, listenerId: 2</span><br><span class="line">method=com.itwray.iw.auth.service.impl.AuthUserServiceImpl.loginByPassword location=AtExit</span><br><span class="line">ts=2024-11-20 14:47:20.846; [cost=116.9355ms] result=@ArrayList[</span><br><span class="line">    @Object[][isEmpty=false;size=1],</span><br><span class="line">    @AuthUserServiceImpl[com.itwray.iw.auth.service.impl.AuthUserServiceImpl@7aae5a4c],</span><br><span class="line">    @UserInfoVo[UserInfoVo(name=wray, tokenName=iwtoken, tokenValue=0a85a7e9-fed8-4be2-b50a-d6c981b81f98, avatar=https://1.com/img/border-collie-8501579_1920.jpg)],</span><br><span class="line">]</span><br><span class="line">method=com.itwray.iw.auth.service.impl.AuthUserServiceImpl$$SpringCGLIB$$0.loginByPassword location=AtExit</span><br><span class="line">ts=2024-11-20 14:47:20.851; [cost=142.679166ms] result=@ArrayList[</span><br><span class="line">    @Object[][isEmpty=false;size=1],</span><br><span class="line">    @AuthUserServiceImpl$$SpringCGLIB$$0[com.itwray.iw.auth.service.impl.AuthUserServiceImpl@7aae5a4c],</span><br><span class="line">    @UserInfoVo[UserInfoVo(name=wray, tokenName=iwtoken, tokenValue=0a85a7e9-fed8-4be2-b50a-d6c981b81f98, avatar=https://1.com/img/border-collie-8501579_1920.jpg)],</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>注意：同<code>stack</code>命令一样，<code>watch</code>命令也会监听代理方法。</p><h3 id="trace"><a class="header-anchor" href="#trace"></a>trace</h3><p><code>trace</code>和<code>stack</code>、<code>watch</code>命令一样，也是监听方法的，它的主要作用是监听方法的调用链路上每个节点的耗时。</p><table><thead><tr><th style="text-align:right">参数名称</th><th style="text-align:left">参数说明</th></tr></thead><tbody><tr><td style="text-align:right"><em>class-pattern</em></td><td style="text-align:left">类名表达式匹配</td></tr><tr><td style="text-align:right"><em>method-pattern</em></td><td style="text-align:left">方法名表达式匹配</td></tr><tr><td style="text-align:right"><em>condition-express</em></td><td style="text-align:left">条件表达式</td></tr><tr><td style="text-align:right">[E]</td><td style="text-align:left">开启正则表达式匹配，默认为通配符匹配</td></tr><tr><td style="text-align:right"><code>[n:]</code></td><td style="text-align:left">命令执行次数，默认值为 100。</td></tr><tr><td style="text-align:right"><code>#cost</code></td><td style="text-align:left">方法执行耗时</td></tr><tr><td style="text-align:right"><code>[m &lt;arg&gt;]</code></td><td style="text-align:left">指定 Class 最大匹配数量，默认值为 50。长格式为<code>[maxMatch &lt;arg&gt;]</code>。</td></tr></tbody></table><p><strong>示例：</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">trace com.itwray.iw.auth.service.impl.AuthUserServiceImpl loginByPassword</span></span><br><span class="line">Press Q or Ctrl+C to abort.</span><br><span class="line">Affect(class count: 2 , method count: 2) cost in 155 ms, listenerId: 3</span><br><span class="line">`---ts=2024-11-20 14:59:55.762;thread_name=http-nio-18001-exec-2;id=119;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@2f29e630</span><br><span class="line">    `---[120.320417ms] com.itwray.iw.auth.service.impl.AuthUserServiceImpl$$SpringCGLIB$$0:loginByPassword()</span><br><span class="line">        `---[99.89% 120.190292ms ] org.springframework.cglib.proxy.MethodInterceptor:intercept()</span><br><span class="line">            `---[92.07% 110.65475ms ] com.itwray.iw.auth.service.impl.AuthUserServiceImpl:loginByPassword()</span><br><span class="line">                +---[0.06% 0.062209ms ] com.itwray.iw.auth.model.dto.LoginPasswordDto:getUsername() #59</span><br><span class="line">                +---[4.60% 5.094084ms ] com.itwray.iw.auth.dao.AuthUserDao:queryOneByUsername() #59</span><br><span class="line">                +---[0.01% 0.011792ms ] com.itwray.iw.auth.model.dto.LoginPasswordDto:getPassword() #69</span><br><span class="line">                +---[0.01% 0.013333ms ] com.itwray.iw.auth.model.entity.AuthUserEntity:getPassword() #69</span><br><span class="line">                +---[94.27% 104.315167ms ] cn.hutool.crypto.digest.BCrypt:checkpw() #69</span><br><span class="line">                +---[0.01% 0.005542ms ] com.itwray.iw.auth.model.entity.AuthUserEntity:getId() #80</span><br><span class="line">                +---[0.61% 0.676208ms ] com.itwray.iw.starter.redis.RedisUtil:set() #80</span><br><span class="line">                +---[0.05% 0.056ms ] com.itwray.iw.auth.service.impl.AuthUserServiceImpl:setTokenValue() #83</span><br><span class="line">                +---[0.00% 0.003834ms ] com.itwray.iw.auth.model.vo.UserInfoVo:&lt;init&gt;() #86</span><br><span class="line">                +---[0.00% 0.003541ms ] com.itwray.iw.auth.model.entity.AuthUserEntity:getName() #87</span><br><span class="line">                +---[0.00% 0.003333ms ] com.itwray.iw.auth.model.vo.UserInfoVo:setName() #87</span><br><span class="line">                +---[0.00% 0.003042ms ] com.itwray.iw.auth.model.entity.AuthUserEntity:getAvatar() #88</span><br><span class="line">                +---[0.00% 0.002833ms ] com.itwray.iw.auth.model.vo.UserInfoVo:setAvatar() #88</span><br><span class="line">                +---[0.00% 0.003375ms ] com.itwray.iw.auth.model.vo.UserInfoVo:setTokenName() #89</span><br><span class="line">                `---[0.00% 0.002834ms ] com.itwray.iw.auth.model.vo.UserInfoVo:setTokenValue() #90</span><br></pre></td></tr></table></figure><h3 id="classloader"><a class="header-anchor" href="#classloader"></a>classloader</h3><p>作用：将 JVM 中所有的 classloader 的信息统计出来，并可以展示继承树，urls 等。</p><table><thead><tr><th style="text-align:right">参数名称</th><th style="text-align:left">参数说明</th></tr></thead><tbody><tr><td style="text-align:right">[l]</td><td style="text-align:left">按类加载实例进行统计</td></tr><tr><td style="text-align:right">[t]</td><td style="text-align:left">打印所有 ClassLoader 的继承树</td></tr><tr><td style="text-align:right">[a]</td><td style="text-align:left">列出所有 ClassLoader 加载的类，请谨慎使用</td></tr><tr><td style="text-align:right"><code>[c:]</code></td><td style="text-align:left">ClassLoader 的 hashcode</td></tr><tr><td style="text-align:right"><code>[classLoaderClass:]</code></td><td style="text-align:left">指定执行表达式的 ClassLoader 的 class name</td></tr><tr><td style="text-align:right"><code>[c: r:]</code></td><td style="text-align:left">用 ClassLoader 去查找 resource</td></tr><tr><td style="text-align:right"><code>[c: load:]</code></td><td style="text-align:left">用 ClassLoader 去加载指定的类</td></tr></tbody></table><p>直接执行<code>classloader</code>，可以看到所有 classloader 实例数量以及它们各自加载的类数量。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">classloader</span></span><br><span class="line"> name                                                  numberOfInstances  loadedCountTotal                                     </span><br><span class="line"> jdk.internal.loader.ClassLoaders$AppClassLoader       1                  15968                                               </span><br><span class="line"> BootstrapClassLoader                                  1                  5228                </span><br><span class="line"> com.taobao.arthas.agent.ArthasClassloader             1                  2063         </span><br><span class="line"> jdk.internal.loader.ClassLoaders$PlatformClassLoader  1                  154              </span><br><span class="line"> jdk.internal.reflect.DelegatingClassLoader            112                112            </span><br><span class="line"> sun.reflect.misc.MethodUtil                           1                  1               </span><br><span class="line">Affect(row-cnt:6) cost in 49 ms.</span><br></pre></td></tr></table></figure><h3 id="heapdump"><a class="header-anchor" href="#heapdump"></a>heapdump</h3><p>作用：生成 dump 文件。类似于 jmap 命令的 heap dump 功能。</p><p>dump 到指定文件：heapdump arthas-output/dump.hprof    # 注意，这里使用相对路径时，会使用监听的Java进程的运行路径作为根路径。</p><p>只 dump live 对象：heapdump --live /tmp/dump.hprof</p><p>如果有项目运行环境，可以直接在运行环境执行 jps 找到对应的 pid ，再使用 <code>jcmd pid GC.heap_dump /tmp/dump.hprof</code> 的方式生成 dump 文件。注意 <code>jcmd</code> 是JDK工具，要确保运行环境有安装JDK。</p><h2 id="Arthas-实战场景"><a class="header-anchor" href="#Arthas-实战场景"></a>Arthas 实战场景</h2><h3 id="通过arthas怎样排查项目中，哪个对象泄露了，或者占用内存太大"><a class="header-anchor" href="#通过arthas怎样排查项目中，哪个对象泄露了，或者占用内存太大"></a>通过arthas怎样排查项目中，哪个对象泄露了，或者占用内存太大</h3><p>参考地址：https://arthas.aliyun.com/doc/expert/user-question-history13509.html</p>]]></content>
    
    
    <summary type="html">继上一次学习使用 Arthas 之后，今天特此学习了解下 Arthas 在项目中比较好用的几个命令。</summary>
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="Java" scheme="https://blog.itwray.com/tags/Java/"/>
    
    <category term="Arthas" scheme="https://blog.itwray.com/tags/Arthas/"/>
    
  </entry>
  
  <entry>
    <title>记录一次在uni-app中使用echarts的坑</title>
    <link href="https://blog.itwray.com/2024/10/15/echart-uni-20241015/"/>
    <id>https://blog.itwray.com/2024/10/15/echart-uni-20241015/</id>
    <published>2024-10-15T12:26:07.000Z</published>
    <updated>2024-11-20T07:23:33.265Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a class="header-anchor" href="#背景"></a>背景</h2><p>在 uni-app 的内置组件和官方扩展组件中，是没有支持图表的组件的。通过<code>内置组件-画布-canvas</code>的页面内容，可以找到官方文档对图表使用的解释：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241015203614690.png" alt="image-20241015203614690"></p><p>一开始准备直接尝试使用uChart的，看到微信扫一扫就劝退了。反正我只需要基础的图表功能，也不太在乎性能，再加上之前在Vue项目写过echarts代码，对echarts官方的文档说明比较了解，就选择了另一个插件：<a href="https://ext.dcloud.net.cn/plugin?id=4899">echarts</a> 。</p><p>这个插件主要就是为了让 uni-app 能兼容echarts，所有图表相关语法都是直接用echarts的，所以上手就比较简单些。</p><h2 id="开始使用"><a class="header-anchor" href="#开始使用"></a>开始使用</h2><p>在插件文档的代码演示中，找到Vue3版本的示例代码，直接拷贝，运行项目，图表正常展示。</p><p>现在就是需要自定义自己的图表样式了，我做的图表是一个饼图，也没什么样式要求，整个option的结构定义如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  series<span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">&#123;</span></span><br><span class="line">    type<span class="punctuation">:</span> &#x27;pie&#x27;<span class="punctuation">,</span></span><br><span class="line">    data<span class="punctuation">:</span> data<span class="punctuation">,</span></span><br><span class="line">    radius<span class="punctuation">:</span> &#x27;<span class="number">80</span>%&#x27;<span class="punctuation">,</span></span><br><span class="line">    label<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      position<span class="punctuation">:</span> &#x27;inside&#x27;<span class="punctuation">,</span></span><br><span class="line">      formatter<span class="punctuation">:</span> &#x27;<span class="punctuation">&#123;</span>b<span class="punctuation">&#125;</span>\n<span class="punctuation">&#123;</span>d<span class="punctuation">&#125;</span>%&#x27;</span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>所以只需要搞定data数据的异步加载和更新就可以了。</p><p>因为我做的功能是要求饼图能根据页面选择的日期加载指定日期的统计数据，所以饼图是需要刷新变化的，按照echarts的说法，可以直接对chart实例调用<code>setOption</code>方法，将整个option重新赋值就可以了。</p><p>但是，事与愿违，无论我怎么尝试，图表始终展示的都是第一次渲染的data数据。期间通过GPT、Google、echarts官方文档、插件文档等多种途径寻找问题根源，以下是我发起的问题：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line">以下是我在uni-app项目中，一个vue文件关于echarts的部分代码，在交互触发searchData()导致数据变化后，图表并没有发生变化。</span><br><span class="line">&lt;view style=&quot;width:100%; height:750rpx&quot;&gt;&lt;l-echart ref=&quot;chartRef&quot;&gt;&lt;/l-echart&gt;&lt;/view&gt;</span><br><span class="line">const echarts = require(&#x27;../../uni_modules/lime-echart/static/echarts.min&#x27;);</span><br><span class="line">// 支出分类数据。包括图表和分类排行的数据</span><br><span class="line">const categoryData = reactive(&#123;</span><br><span class="line">  chartList: [],</span><br><span class="line">  categoryList: []</span><br><span class="line">&#125;)</span><br><span class="line"></span><br><span class="line">const categoryDataRef = ref(&#123;</span><br><span class="line">  chartList: []  // 确保 chartList 是响应式的</span><br><span class="line">&#125;);</span><br><span class="line">// 图表数据</span><br><span class="line">const chartRef = ref(null)</span><br><span class="line">// 监控 chartList 的变化</span><br><span class="line">watch(() =&gt; categoryDataRef.value.chartList, (newVal, oldVal) =&gt; &#123;</span><br><span class="line">  if (myChart.value) &#123;</span><br><span class="line">    renderChart();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, &#123; deep: true &#125;);  // 确保深度监控</span><br><span class="line">/**</span><br><span class="line"> * 查询数据</span><br><span class="line"> */</span><br><span class="line">function searchData() &#123;</span><br><span class="line">  statistics.pageDto.currentMonth = $&#123;currentMonth.value&#125;-01</span><br><span class="line">  statistics.pageDto.isQueryLastMonth = compareLastMonth.value</span><br><span class="line">  Promise.all([loadTotalStatisticsData(), loadRankStatisticsData(), loadCategoryStatisticsData()])</span><br><span class="line">    .then(([result1, result2, result3]) =&gt; &#123;</span><br><span class="line">      console.log(&quot;进入111&quot;);</span><br><span class="line">      // renderChart()</span><br><span class="line">      categoryDataRef.value.chartList = categoryData.chartList</span><br><span class="line">    &#125;)</span><br><span class="line">    .catch((error) =&gt; &#123;</span><br><span class="line">      console.error(&#x27;请求失败:&#x27;, error);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">const myChart = ref(null)</span><br><span class="line"></span><br><span class="line">function renderChart() &#123;</span><br><span class="line">  console.log(&quot;进入方法&quot;);</span><br><span class="line">  console.log(categoryData.chartList);</span><br><span class="line">  myChart.value.setOption(&#123;</span><br><span class="line">    series: [&#123;</span><br><span class="line">      type: &#x27;pie&#x27;,</span><br><span class="line">      data: categoryDataRef.value.chartList,</span><br><span class="line">      radius: &#x27;80%&#x27;,</span><br><span class="line">      label: &#123;</span><br><span class="line">        position: &#x27;inside&#x27;,</span><br><span class="line">        formatter: &#x27;&#123;b&#125;\n&#123;d&#125;%&#x27;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;, true)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">onReady(() =&gt; &#123;</span><br><span class="line">  // 组件能被调用必须是组件的节点已经被渲染到页面上</span><br><span class="line">  setTimeout(async () =&gt; &#123;</span><br><span class="line">    if (!chartRef.value) return</span><br><span class="line">    myChart.value = await chartRef.value.init(echarts)</span><br><span class="line"></span><br><span class="line">    searchData()</span><br><span class="line">  &#125;, 300)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>GPT的答复中，给出了多方面的检查和调整思路：</p><ol><li><strong>图表实例的初始化与数据更新时机</strong></li></ol><p><code>myChart</code> 可能未在数据变化时正确初始化。如果 <code>searchData</code> 在图表初始化之前触发，数据将不会被正确渲染。</p><p>在 <code>searchData</code> 触发前确保 <code>myChart</code> 已被初始化：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">onReady(() =&gt; &#123;</span><br><span class="line">  setTimeout(async () =&gt; &#123;</span><br><span class="line">    if (!chartRef.value) return;</span><br><span class="line">    myChart.value = await chartRef.value.init(echarts);</span><br><span class="line">    </span><br><span class="line">    // 确保图表实例化后才调用 searchData</span><br><span class="line">    searchData();</span><br><span class="line">  &#125;, 300);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><ol start="2"><li><strong>数据响应式处理</strong></li></ol><p>虽然你使用了 <code>reactive</code> 和 <code>ref</code>，但是在 <code>Promise.all</code> 中直接更新 <code>categoryDataRef.value.chartList</code> 可能没有触发 <code>watch</code>。尝试通过 <code>Vue.set</code> 方式强制数据更新：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">categoryDataRef.value.chartList = [...categoryData.chartList]; // 确保数据发生变化</span><br></pre></td></tr></table></figure><ol start="3"><li><strong><code>watch</code> 的深度监听</strong></li></ol><p><code>watch</code> 中使用了 <code>&#123; deep: true &#125;</code>，但由于 <code>categoryDataRef.value.chartList</code> 是数组，Vue 有时不会捕捉到数组内部的变化。为了确保监控到变化，可以尝试：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">watch(() =&gt; categoryDataRef.value.chartList.slice(), (newVal, oldVal) =&gt; &#123;</span><br><span class="line">  if (myChart.value) &#123;</span><br><span class="line">    renderChart();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;, &#123; deep: true &#125;);</span><br></pre></td></tr></table></figure><p>使用 <code>slice()</code> 方法创建新数组，触发 Vue 的响应式系统。</p><ol start="4"><li><strong><code>setOption</code> 强制更新</strong></li></ol><p>你已经使用了 <code>setOption</code> 的强制更新参数 <code>true</code>，这一点是正确的，可以保证即使数据小幅更新，图表也会重新渲染。</p><p>确保 <code>renderChart</code> 确实在数据变更后被正确调用，检查日志输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">function renderChart() &#123;</span><br><span class="line">  console.log(&quot;进入方法&quot;);</span><br><span class="line">  console.log(categoryDataRef.value.chartList);</span><br><span class="line">  myChart.value.setOption(&#123;</span><br><span class="line">    series: [&#123;</span><br><span class="line">      type: &#x27;pie&#x27;,</span><br><span class="line">      data: categoryDataRef.value.chartList,</span><br><span class="line">      radius: &#x27;80%&#x27;,</span><br><span class="line">      label: &#123;</span><br><span class="line">        position: &#x27;inside&#x27;,</span><br><span class="line">        formatter: &#x27;&#123;b&#125;\n&#123;d&#125;%&#x27;</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;, true);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>总结</strong></p><p>可以尝试以下步骤：</p><ol><li>确保 <code>myChart</code> 初始化在 <code>searchData</code> 调用之前。</li><li>通过 <code>slice()</code> 或者使用 <code>Vue.set</code> 强制触发 <code>watch</code> 的响应式更新。</li><li>在 <code>renderChart</code> 中检查数据是否正确传递给 <code>setOption</code>。</li></ol><p>如果这些调整都没有效果，可以在 <code>renderChart</code> 内部打印日志以进一步调试数据更新的时机与流程。</p><p>---- 分段 ----</p><p>通过上述思路，我首先在 <code>renderChart</code> 方法中，增加了打印 option 的代码：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> currentOption = myChart.<span class="property">value</span>.<span class="title function_">getOption</span>();</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;setOption 后的 series.data:&quot;</span>, currentOption.<span class="property">series</span>[<span class="number">0</span>].<span class="property">data</span>);</span><br></pre></td></tr></table></figure><p>通过打印发现，series的data数据确实没有更新，说明<code>myChart.value.setOption</code>没有生效。</p><p>第1点，图表实例的初始化与数据更新时机，这个其实只要自己代码逻辑清晰，数据更新时机是肯定没问题的，至于实例初始化问题，第一次渲染的数据图表都出来，所以初始化也没有问题。</p><p>第2点，认为数据可能没有触发<code>watch</code>，其实通过日志打印，是可以看到每次数据发生变化后，触发了<code>watch</code>方法的。之前我没有使用监听方法，而是在每次加载数据后手动调用<code>renderChart</code>方法，但总是出现一些莫名其妙的问题，于是就改成监听方式了。</p><p>第3点，<code>watch</code>的深度监听，这个跟第2点一样，是可以忽略的，因为打印了日志，说明能监听到数据变化。</p><p>然后我根据资料解释，一度怀疑是echarts没有触发更新，于是尝试了echarts的<code>clear</code>、<code>dispose</code>、<code>notMerge: true</code>等方法，直到解决问题后才幡然醒悟，属性数据都没有发生变化，echarts图表肯定不会重新渲染，所以不存在第4点说的问题。</p><p>最后，我重整思绪，基本可以确定是<code>myChart.value.setOption</code>这段代码出现了问题，但是实在是想不通这样有什么不对，因为<code>myChart</code>确实就是echarts的对象实例啊，而且第一次加载的数据也渲染出来了。</p><p>整段代码中，其实都很好理解，因为都是vue或者echarts的语法内容，可以确定没问题，问题的关键还是在这个插件上，<code>chartRef.value.init(echarts)</code>方法，顾名思义就是初始化，但我没有看到任何关于这个<code>init</code>方法的解释，于是产生怀疑，<code>init</code>方法返回的对象可能并不是echarts实例对象。</p><p>于是，我又在插件文档上反复浏览，发现了下面这一段内容：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241015211809598.png" alt="image-20241015211809598"></p><p>文档说明<code>init</code>方法还有第二个参数的，第二个参数是回调函数，回调函数的参数才是chart实例。这是其一，还有一个很重要的观察点，既然这个方法都列在一起，说明它们的调用对象是同一个！！！而<code>init</code>方法的调用对象是<code>chartRef.value</code>，那么是不是说明<code>setOption</code>方法的调用对象也是<code>chartRef.value</code>，于是<code>renderChart</code>方法代码改成如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">renderChart</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> option = &#123;</span><br><span class="line">    <span class="attr">series</span>: [&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;pie&#x27;</span>,</span><br><span class="line">      <span class="attr">data</span>: categoryDataRef.<span class="property">value</span>.<span class="property">chartList</span>,</span><br><span class="line">      <span class="attr">radius</span>: <span class="string">&#x27;80%&#x27;</span>,</span><br><span class="line">      <span class="attr">label</span>: &#123;</span><br><span class="line">        <span class="attr">position</span>: <span class="string">&#x27;inside&#x27;</span>,</span><br><span class="line">        <span class="attr">formatter</span>: <span class="string">&#x27;&#123;b&#125;\n&#123;d&#125;%&#x27;</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  chartRef.<span class="property">value</span>.<span class="title function_">setOption</span>(option)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>再次启动项目，图表重新渲染成功！</p><p>所以～关键点就是<code>setOption</code>的调用对象搞错了。。。</p><p>但也留下了一个疑问，为什么第一次渲染时，使用<code>myChart.value.setOption</code>却可以。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#背景&quot;&gt;&lt;/a&gt;背景&lt;/h2&gt;
&lt;p&gt;在 uni-app 的内置组件和官方扩展组件中，是没有支持图表的组件的。通过&lt;code&gt;内置组件-画布-canvas&lt;/code&gt;的页面内容，可以找到官方文档对图表使用的解释：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://itwray.oss-cn-heyuan.al</summary>
      
    
    
    
    <category term="ECharts" scheme="https://blog.itwray.com/categories/ECharts/"/>
    
    
    <category term="ECharts" scheme="https://blog.itwray.com/tags/ECharts/"/>
    
    <category term="uni-app" scheme="https://blog.itwray.com/tags/uni-app/"/>
    
  </entry>
  
  <entry>
    <title>RocketMQ 5.x在SpringBoot中的上手使用过程</title>
    <link href="https://blog.itwray.com/2024/10/13/rocketmq-springboot-simple/"/>
    <id>https://blog.itwray.com/2024/10/13/rocketmq-springboot-simple/</id>
    <published>2024-10-13T05:13:01.000Z</published>
    <updated>2024-11-20T07:24:19.789Z</updated>
    
    <content type="html"><![CDATA[<h2 id="准备环境"><a class="header-anchor" href="#准备环境"></a>准备环境</h2><ul><li>JDK 17</li><li>Spring Boot 3.2.3</li><li>RocketMQ（服务端） 5.3.1</li><li>rocketmq-v5-client-spring-boot-starter（客户端） 2.3.1</li></ul><p>在 SpringBoot 项目中依赖如下配置：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.rocketmq<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>rocketmq-v5-client-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.3.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>如果还未搭建服务端，可以先看<strong>第5节-服务器环境搭建</strong>。</p><h2 id="参数配置"><a class="header-anchor" href="#参数配置"></a>参数配置</h2><p>按照 SpringBoot 的约定习俗，在上手一个新的 <code>spring-boot-starter</code>项目时，想要知道怎么使用它，看它的 AutoConfiguration 就对了。</p><p>在 <code>rocketmq-v5-client-spring-boot</code>中，对应的 AutoConfiguration 类为 <code>RocketMQAutoConfiguration</code>，其类定义部分代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableConfigurationProperties(RocketMQProperties.class)</span></span><br><span class="line"><span class="meta">@Import(&#123;MessageConverterConfiguration.class, ListenerContainerConfiguration.class, ExtTemplateResetConfiguration.class,</span></span><br><span class="line"><span class="meta">        ExtConsumerResetConfiguration.class, RocketMQTransactionConfiguration.class, RocketMQListenerConfiguration.class&#125;)</span></span><br><span class="line"><span class="meta">@AutoConfigureAfter(&#123;MessageConverterConfiguration.class&#125;)</span></span><br><span class="line"><span class="meta">@AutoConfigureBefore(&#123;RocketMQTransactionConfiguration.class&#125;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RocketMQAutoConfiguration</span> <span class="keyword">implements</span> <span class="title class_">ApplicationContextAware</span> &#123;</span><br><span class="line">  <span class="comment">// ... 省略</span></span><br><span class="line">   </span><br><span class="line">    <span class="meta">@Bean(PRODUCER_BUILDER_BEAN_NAME)</span></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean(ProducerBuilderImpl.class)</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(prefix = &quot;rocketmq&quot;, value = &#123;&quot;producer.endpoints&quot;&#125;)</span></span><br><span class="line">    <span class="keyword">public</span> ProducerBuilder <span class="title function_">producerBuilder</span><span class="params">(RocketMQProperties rocketMQProperties)</span> &#123;</span><br><span class="line">      <span class="comment">// ... 省略</span></span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">    <span class="meta">@Bean(SIMPLE_CONSUMER_BUILDER_BEAN_NAME)</span></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean(SimpleConsumerBuilder.class)</span></span><br><span class="line">    <span class="meta">@ConditionalOnProperty(prefix = &quot;rocketmq&quot;, value = &#123;&quot;simple-consumer.endpoints&quot;&#125;)</span></span><br><span class="line">    <span class="keyword">public</span> SimpleConsumerBuilder <span class="title function_">simpleConsumerBuilder</span><span class="params">(RocketMQProperties rocketMQProperties)</span> &#123;</span><br><span class="line">       <span class="comment">// ... 省略</span></span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">    <span class="meta">@Bean(destroyMethod = &quot;destroy&quot;)</span></span><br><span class="line">    <span class="meta">@Conditional(ProducerOrConsumerPropertyCondition.class)</span></span><br><span class="line">    <span class="meta">@ConditionalOnMissingBean(name = ROCKETMQ_TEMPLATE_DEFAULT_GLOBAL_NAME)</span></span><br><span class="line">    <span class="keyword">public</span> RocketMQClientTemplate <span class="title function_">rocketMQClientTemplate</span><span class="params">(RocketMQMessageConverter rocketMQMessageConverter)</span> &#123;</span><br><span class="line">       <span class="comment">// ... 省略</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以发现，在<code>rocketmq-v5-client-spring-boot</code>中，根据 RocketMQ 5.x 在架构上做的改进，使用了 <code>endpoints</code> 来替代传统的 <code>namesrvAddr</code>，以支持更灵活的网络拓扑和云原生架构。<code>endpoints</code> 通常指向 RocketMQ 的 <strong>Broker 或 Nameserver</strong> 地址，用于生产者与 RocketMQ 集群建立连接。<strong>endpoints</strong> 是一个 URL 或 IP 地址（ip:host）列表（使用<code>;</code>分割）。</p><blockquote><p>⚠️注意：在 RocketMQ 5.x 中，现已默认使用gRPC作为通信协议，entpoints更建议指向 <strong>Proxy</strong> 地址，一般默认端口为8081。</p></blockquote><p>因此，现在想要启用默认的生产者（ProducerBuilder），只需要配置<code>rocketmq.producer.endpoints</code>即可。</p><p>想要启用默认的消费者（SimpleConsumerBuilder），只需要配置<code>rocketmq.simple-consumer.endpoints</code>即可。</p><p>而<code>RocketMQClientTemplate</code>则是通过判断当前应用上下文是否含有<code>ProducerBuilder</code>或<code>SimpleConsumerBuilder</code> Bean对象生成而来。它属于<code>rocketmq-v5-client-spring-boot</code>模块下，也就是说它利用了Spring特性，提供了Spring风格的API，方便开发者通过 Spring 的编程模型来进行消息发送和接收。</p><p>既然是原生态的简易使用教程，那么就尽可能在不写多的代码的情况下，实现生产环境中使用MQ。</p><p>因此，本次项目就只配置 <code>rocketmq.producer.endpoints</code> 用于启用默认的生产者，消费者使用Push消费模式，所以配置<code>rocketmq.push-consumer.endpoints</code>。配置如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">rocketmq:</span></span><br><span class="line">  <span class="attr">producer:</span></span><br><span class="line">    <span class="attr">endpoints:</span> <span class="string">localhost:8081</span></span><br><span class="line">  <span class="attr">push-consumer:</span></span><br><span class="line">  <span class="attr">endpoints:</span> <span class="string">localhost:8081</span></span><br></pre></td></tr></table></figure><p><code>topic</code>在代码中指定，不使用<code>rocketmq.producer.topic</code>和<code>rocketmq.push-consumer.topic</code>配置默认的topic。</p><blockquote><p>tips: 在启动客户端服务时，topic需要先创建，否则会启动报错。</p></blockquote><h2 id="生产者生产消息"><a class="header-anchor" href="#生产者生产消息"></a>生产者生产消息</h2><p>生产消息通过SpringBoot自动装配的<code>RocketMQClientTemplate</code>对象实现，发送<code>Message</code>对象，示例代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyService</span> &#123;</span><br><span class="line">  <span class="meta">@Autowired</span></span><br><span class="line">  <span class="keyword">private</span> RocketMQClientTemplate rocketMQClientTemplate;</span><br><span class="line">  </span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendMessage</span><span class="params">()</span> &#123;</span><br><span class="line">      <span class="type">byte</span>[] bytes = <span class="string">&quot;这是一个字符串&quot;</span>.getBytes(StandardCharsets.UTF_8);</span><br><span class="line">      Message&lt;<span class="type">byte</span>[]&gt; message = MessageBuilder.withPayload(bytes).build();</span><br><span class="line">      rocketMQClientTemplate.send(<span class="string">&quot;MyTopic&quot;</span>, message);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>⚠️注意：在 RocketMQ 5.x 中，<code>Message</code>对象已从自定义对象改为<code>spring-messaging</code>包中的<code>Message</code>对象。一般通过<code>MessageBuilder</code>构建，实例对象类型为<code>GenericMessage</code>。</p></blockquote><h2 id="消费者消费消息"><a class="header-anchor" href="#消费者消费消息"></a>消费者消费消息</h2><p>消费者通过<code>@RocketMQMessageListener</code>注解，并实现<code>RocketMQListener</code>接口消费消息，示例代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@RocketMQMessageListener(consumerGroup = &quot;MyTopic-service&quot;, topic = &quot;MyTopic&quot;, tag = &quot;*&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyService</span> <span class="keyword">implements</span> <span class="title class_">RocketMQListener</span> &#123;</span><br><span class="line">  </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> ConsumeResult <span class="title function_">consume</span><span class="params">(MessageView messageView)</span> &#123;</span><br><span class="line">        <span class="comment">// 从 MessageView 中获取 ByteBuffer</span></span><br><span class="line">        <span class="type">ByteBuffer</span> <span class="variable">byteBuffer</span> <span class="operator">=</span> messageView.getBody();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 转换 ByteBuffer 为字节数组</span></span><br><span class="line">        <span class="type">byte</span>[] body = <span class="keyword">new</span> <span class="title class_">byte</span>[byteBuffer.remaining()];</span><br><span class="line">        byteBuffer.get(body);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 处理字节数组，例如转换为字符串</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">messageBody</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(body, StandardCharsets.UTF_8);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;消费消息内容：&quot;</span> + messageBody);</span><br><span class="line">      </span><br><span class="line">      <span class="keyword">return</span> ConsumeResult.SUCCESS;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="服务端环境搭建"><a class="header-anchor" href="#服务端环境搭建"></a>服务端环境搭建</h2><ol><li><p>下载二进制包</p><p>在 <a href="https://rocketmq.apache.org/zh/docs/quickStart/01quickstart">Apache RocketMQ 本地部署 RocketMQ</a> 文档中，可以找到最新的二进制包，位置如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/pic/image-20241014163749567.png" alt="image-20241014163749567"></p><p>如果想保持跟本文相同版本，可以直接点击<a href="https://dist.apache.org/repos/dist/release/rocketmq/5.3.1/rocketmq-all-5.3.1-bin-release.zip">链接</a>下载RocketMQ 5.3.1版本。</p></li><li><p>启动NameServer</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment">### 启动namesrv</span></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">nohup</span> sh bin/mqnamesrv &amp;</span></span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment">### 验证namesrv是否启动成功</span></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">tail</span> -f ~/logs/rocketmqlogs/namesrv.log</span></span><br><span class="line">The Name Server boot success...</span><br></pre></td></tr></table></figure></li><li><p>本地模式启动Broker+Proxy</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment">### 先启动broker</span></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">nohup</span> sh bin/mqbroker -n localhost:9876 --enable-proxy &amp;</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment">### 验证broker是否启动成功, 比如, broker的ip是192.168.1.2 然后名字是broker-a</span></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">tail</span> -f ~/logs/rocketmqlogs/proxy.log</span> </span><br><span class="line">The broker[broker-a,192.169.1.2:10911] boot success...</span><br></pre></td></tr></table></figure><p><code>mqbroker</code>脚本默认会读取 <code>conf/broker.conf</code> 配置用于Broker服务。在 <code>conf/rmq-proxy.json</code> 中是Proxy服务的配置，通过 <code>--enable-proxy</code> 命令启动时，需要加上 <code>-pc conf/rmq-proxy.json</code> 参数指定配置文件位置。</p><p><code>broker.conf</code>的监听端口key为<code>listenPort</code>，管理端口key为<code>brokerAdminPort</code>。</p><p><code>rmq.proxy.json</code>的gRPC请求端口key为<code>grpcServerPort</code>，传统的消息发送和接收请求的端口key为<code>remotingListenPort</code>。</p></li><li><p>关闭服务</p><p>停止Broker：<code>sh bin/mqshutdown broker</code></p><p>停止NameServer：<code>sh bin/mqshutdown namesrv</code></p></li></ol><p>关于RocketMQ的管理命令可以参考<a href="https://rocketmq.apache.org/zh/docs/deploymentOperations/02admintool">Admin Tool</a>。</p>]]></content>
    
    
    <summary type="html">在SpringBoot环境下原生态的集成使用RocketMQ的简易教程。</summary>
    
    
    
    <category term="RocketMQ" scheme="https://blog.itwray.com/categories/RocketMQ/"/>
    
    
    <category term="SpringBoot" scheme="https://blog.itwray.com/tags/SpringBoot/"/>
    
    <category term="RocketMQ" scheme="https://blog.itwray.com/tags/RocketMQ/"/>
    
  </entry>
  
  <entry>
    <title>Fail2Ban使用心得</title>
    <link href="https://blog.itwray.com/2024/09/23/fail2ban-use/"/>
    <id>https://blog.itwray.com/2024/09/23/fail2ban-use/</id>
    <published>2024-09-23T04:23:29.000Z</published>
    <updated>2024-11-20T07:24:52.332Z</updated>
    
    <content type="html"><![CDATA[<h2 id="起因"><a class="header-anchor" href="#起因"></a>起因</h2><p>最近租了两个“肉鸡”服务器用于个人开发学习，因为有公网暴露，频繁被机器人恶意登录，导致经常会出现如下提示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">There were * failed login attempts since the last successful login.</span><br></pre></td></tr></table></figure><p>通过修改 <code>/etc/ssh/sshd_config</code> 禁止root用户密码登录也拦不住，于是就找到一个第三方工具 - Fail2Ban。</p><h2 id="介绍"><a class="header-anchor" href="#介绍"></a>介绍</h2><p>Fail2Ban 是一个开源的入侵防御软件，主要用于防止恶意的暴力破解攻击。</p><p>它通过监控系统日志文件（例如 <code>/var/log/auth.log</code>、<code>/var/log/apache2/error.log</code> 等）中的可疑行为（如重复的登录失败、异常的 IP 请求等），根据预定义的规则识别出恶意行为，然后对恶意 IP 地址采取临时封禁措施。Fail2ban 主要通过修改防火墙规则来实现这一点。</p><h2 id="安装"><a class="header-anchor" href="#安装"></a>安装</h2><p>Linux下执行如下命令即可：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum install -y fail2ban</span><br></pre></td></tr></table></figure><h2 id="配置"><a class="header-anchor" href="#配置"></a>配置</h2><p>Fail2Ban 有一个名为 <code>jail.conf</code> 的主（默认）配置文件，它可以与 <code>jail.local</code> 配置文件共存，<code>jail.local</code> 配置优先级高于 <code>jail.conf</code>。</p><p>建议不要修改 <code>jail.conf</code> 配置文件，推荐将 <code>jail.conf</code> 文件复制到名为 <code>jail.local</code> 的文件中，然后在 <code>jail.local</code> 文件中自定义自己的配置。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local</span><br></pre></td></tr></table></figure><p><code>jail.local</code> 文件中的每个监狱定义由一组 <code>[监狱名称]</code> 标记组成，包含一系列指令来指定监控服务、日志路径、过滤器规则、封禁时长等。</p><h3 id="常用配置字段"><a class="header-anchor" href="#常用配置字段"></a>常用配置字段</h3><ol><li><p><strong>[监狱名称]</strong>每个监狱名称标识 Fail2ban 应监控的服务或日志源。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[sshd]</span><br></pre></td></tr></table></figure><p>该部分用于定义 SSH 服务的监控规则。</p></li><li><p><strong>enabled</strong>该字段用于启用或禁用某个监狱。如果要启用某个监狱，设置为 <code>true</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">enabled = true</span><br></pre></td></tr></table></figure></li><li><p><strong>port</strong>指定监控的端口号或服务名称。可以是具体的端口号，也可以使用协议名称（如 <code>ssh</code>、<code>http</code> 等）。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">port = ssh</span><br></pre></td></tr></table></figure><p>或者指定端口号：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">port = 22</span><br></pre></td></tr></table></figure></li><li><p><strong>filter</strong>该字段指定 Fail2ban 用于分析日志的过滤器文件名。过滤器定义了如何解析日志文件中的特定模式。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">filter = sshd</span><br></pre></td></tr></table></figure><p>这会使用 <code>/etc/fail2ban/filter.d/sshd.conf</code> 中定义的过滤规则。</p></li><li><p><strong>logpath</strong>定义日志文件的路径，Fail2ban 将监控这些文件并根据过滤器规则分析其内容。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">logpath = /var/log/auth.log</span><br></pre></td></tr></table></figure></li><li><p><strong>maxretry</strong>指定在指定时间段内失败的最大次数，超过该次数后 Fail2ban 将封禁对应 IP 地址。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maxretry = 5</span><br></pre></td></tr></table></figure><p>表示在超过 5 次登录失败后触发封禁。</p></li><li><p><strong>bantime</strong>设置封禁的时间（秒为单位）。如果不需要永久封禁，可以设置一个合理的时间长度，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bantime = 3600</span><br></pre></td></tr></table></figure><p>该设置表示封禁时间为 1 小时（3600 秒）。如果希望永久封禁，可以将其设置为负数：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bantime = -1</span><br></pre></td></tr></table></figure></li><li><p><strong>findtime</strong>该字段定义检测攻击行为的时间窗口（单位为秒）。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">findtime = 600</span><br></pre></td></tr></table></figure><p>这表示在 600 秒（10 分钟）内，如果有 <code>maxretry</code> 次失败，则封禁 IP。</p></li><li><p><strong>action</strong>定义在封禁时执行的操作，通常是修改防火墙规则封禁 IP，也可以发送邮件通知等。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">action = iptables[name=SSH, port=ssh, protocol=tcp]</span><br></pre></td></tr></table></figure><p>或者使用默认的 <code>action_</code> 操作模板：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">action = %(action_mw)s</span><br></pre></td></tr></table></figure><p><code>action_mw</code> 表示发送邮件通知管理员，并封禁 IP。</p></li></ol><h3 id="配置示例"><a class="header-anchor" href="#配置示例"></a>配置示例</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">[DEFAULT]</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment"># 默认封禁时间为 1 小时</span></span></span><br><span class="line">bantime = 3600</span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment"># 在 10 分钟内检测到 5 次失败则封禁</span></span></span><br><span class="line">findtime = 600</span><br><span class="line">maxretry = 5</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment"># 发送邮件通知管理员</span></span></span><br><span class="line">destemail = admin@example.com</span><br><span class="line">sender = fail2ban@example.com</span><br><span class="line">action = %(action_mw)s</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash"><span class="comment"># 监控 SSH 服务</span></span></span><br><span class="line">[sshd]</span><br><span class="line">enabled = true</span><br><span class="line">port = ssh</span><br><span class="line">filter = sshd</span><br><span class="line">logpath = /var/log/auth.log</span><br><span class="line">maxretry = 3</span><br><span class="line">bantime = -1</span><br></pre></td></tr></table></figure><p>提示：每次修改 <code>jail.local</code> 文件后，记得重启 Fail2ban 服务以应用更改。</p><h2 id="命令"><a class="header-anchor" href="#命令"></a>命令</h2><ol><li><p>启动 Fail2ban 服务：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl start fail2ban</span><br></pre></td></tr></table></figure></li><li><p>配置开机启动：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl enable fail2ban</span><br></pre></td></tr></table></figure></li><li><p>查看服务状态：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl status fail2ban</span><br></pre></td></tr></table></figure></li><li><p>列出当前所有活动的监狱：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fail2ban-client status</span><br></pre></td></tr></table></figure></li><li><p>查看某个监狱的详细信息（如 SSH）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fail2ban-client status sshd</span><br></pre></td></tr></table></figure></li><li><p>解封 IP 地址：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fail2ban-client unban &lt;IP地址&gt;</span><br></pre></td></tr></table></figure></li><li><p>重启服务</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl restart fail2ban</span><br></pre></td></tr></table></figure></li><li><p>检查配置文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">## 检查配置文件的语法，并输出任何配置错误</span><br><span class="line">sudo fail2ban-client -d</span><br></pre></td></tr></table></figure></li></ol>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;起因&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#起因&quot;&gt;&lt;/a&gt;起因&lt;/h2&gt;
&lt;p&gt;最近租了两个“肉鸡”服务器用于个人开发学习，因为有公网暴露，频繁被机器人恶意登录，导致经常会出现如下提示：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span cl</summary>
      
    
    
    
    <category term="Linux" scheme="https://blog.itwray.com/categories/Linux/"/>
    
    
    <category term="Linux" scheme="https://blog.itwray.com/tags/Linux/"/>
    
    <category term="Fail2Ban" scheme="https://blog.itwray.com/tags/Fail2Ban/"/>
    
  </entry>
  
  <entry>
    <title>SpringBoot集成Redis使用心得</title>
    <link href="https://blog.itwray.com/2024/08/26/spring-boot-redis-use/"/>
    <id>https://blog.itwray.com/2024/08/26/spring-boot-redis-use/</id>
    <published>2024-08-26T07:27:17.000Z</published>
    <updated>2024-11-20T07:25:13.787Z</updated>
    
    <content type="html"><![CDATA[<p>记录一下最近在从零搭建项目时集成Redis的使用心得，主要内容如下：</p><ol><li>SpringBoot如何引入Redis；</li><li>SpringBoot引入Redis依赖后，为什么不能直接注入RedisTemplate；</li><li>SpringBoot中Redis的序列化方式；</li><li>自动装配默认使用的Redis客户端为什么是Lettuce；</li></ol><h2 id="SpringBoot如何引入Redis"><a class="header-anchor" href="#SpringBoot如何引入Redis"></a>SpringBoot如何引入Redis</h2><p>首先，如果作为初次在SpringBoot项目中使用Redis的人，可以按照如下方法查找关于Redis的依赖包。</p><ol><li><p>浏览器打开spring官网，找到SpringBoot项目。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240826160335356.png" alt="image-20240826160335356"></p></li><li><p>点击当前版本（CURRENT）SpringBoot的参考资料（Reference Doc.）。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240826160737190.png" alt="image-20240826160737190"></p></li><li><p>进入文档后，在搜索栏（Search）搜索Redis，一般第一个就是对应的文档目录。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240826161026006.png" alt="image-20240826161026006"></p></li><li><p>根据文档内容，可以了解到SpringBoot提供了一个 <code>spring-boot-starter-data-redis</code> 依赖包用于管理SpringBoot中关于Redis的依赖配置。并且在文档中还有大致的使用说明和示例。</p></li></ol><p>因此，要想使用Redis，只需要在SpringBoot项目中引入如下依赖即可：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p><code>spring-boot-starter-data-redis</code> 依赖的版本号在 <code>spring-boot-dependencies</code> 有依赖管理。</p><p>如果想要开启Redis连接池，则需要依赖 <code>commons-pool2</code> ：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.commons<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>commons-pool2<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>同样的，<code>commons-pool2</code> 依赖的版本号在 <code>spring-boot-dependencies</code> 有依赖管理。</p><h2 id="SpringBoot项目为什么不能直接注入RedisTemplate"><a class="header-anchor" href="#SpringBoot项目为什么不能直接注入RedisTemplate"></a>SpringBoot项目为什么不能直接注入RedisTemplate</h2><p>这个问题有一点歧义，其实SpringBoot项目在引入<code>spring-boot-starter-data-redis</code>依赖后，是可以直接注入的，通过 <code>RedisAutoConfiguration</code> 源码分析可知，它默认注册了两个RedisTemplate类型的Bean。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@AutoConfiguration</span></span><br><span class="line"><span class="meta">@ConditionalOnClass(RedisOperations.class)</span></span><br><span class="line"><span class="meta">@EnableConfigurationProperties(RedisProperties.class)</span></span><br><span class="line"><span class="meta">@Import(&#123; LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class &#125;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisAutoConfiguration</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnMissingBean(RedisConnectionDetails.class)</span></span><br><span class="line">PropertiesRedisConnectionDetails <span class="title function_">redisConnectionDetails</span><span class="params">(RedisProperties properties)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PropertiesRedisConnectionDetails</span>(properties);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnMissingBean(name = &quot;redisTemplate&quot;)</span></span><br><span class="line"><span class="meta">@ConditionalOnSingleCandidate(RedisConnectionFactory.class)</span></span><br><span class="line"><span class="keyword">public</span> RedisTemplate&lt;Object, Object&gt; <span class="title function_">redisTemplate</span><span class="params">(RedisConnectionFactory redisConnectionFactory)</span> &#123;</span><br><span class="line">RedisTemplate&lt;Object, Object&gt; template = <span class="keyword">new</span> <span class="title class_">RedisTemplate</span>&lt;&gt;();</span><br><span class="line">template.setConnectionFactory(redisConnectionFactory);</span><br><span class="line"><span class="keyword">return</span> template;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line"><span class="meta">@ConditionalOnSingleCandidate(RedisConnectionFactory.class)</span></span><br><span class="line"><span class="keyword">public</span> StringRedisTemplate <span class="title function_">stringRedisTemplate</span><span class="params">(RedisConnectionFactory redisConnectionFactory)</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">StringRedisTemplate</span>(redisConnectionFactory);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Spring依赖注入的原理是基于Bean类型+名称确定，通过<code>@Autowried</code>注入唯一RedisTemplate对象时，可以注入下面两种：</p><ul><li>RedisTemplate&lt;Object, Object&gt; redisTemplate;</li><li>StringRedisTemplate stringRedisTemplate;</li></ul><p><code>StringRedisTemplate</code> 继承于<code>RedisTemplate&lt;String, String&gt;</code>，因此从Spring容器获取所有RedisTemplate的Bean对象时，<code>StringRedisTemplate</code>也会在其中。</p><p>细心点可以发现，<code>StringRedisTemplate</code>和<code>RedisTemplate&lt;Object, Object&gt;</code>主要是范型类型不一样，所以这里就涉及到一个知识点。</p><blockquote><p>在Java中，尽管泛型类型在运行时会被擦除，但在 Spring 的上下文中，Bean 的定义还是会包含泛型信息，这样可以在自动装配时进行更加精确的匹配。</p><p>因此，通过<code>@Autowired</code>自动注入Bean时，Spring会尝试匹配 Bean 的类型和泛型参数。</p></blockquote><p>所以，回到问题本身，如果在项目代码中，使用如下方式注入，就会报错：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 报错，因为SpringBoot自动装配注册的Bean为RedisTemplate&lt;Object, Object&gt;类型</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line">RedisTemplate&lt;String, Object&gt; redisTemplate;</span><br></pre></td></tr></table></figure><p>如果想要具备<code>RedisTemplate&lt;String, Object&gt;</code>类型的Bean，就得手动注册一个。</p><h2 id="SpringBoot中Redis的序列化方式"><a class="header-anchor" href="#SpringBoot中Redis的序列化方式"></a>SpringBoot中Redis的序列化方式</h2><p>准确来说，应该是<code>RedisTemplate</code>的序列化方式，在 Spring Data Redis 中，RedisTemplate 使用序列化器（Serializer）将 Java 对象序列化为二进制数据（字节数组），并反序列化为 Java 对象。</p><p>默认情况下，<code>RedisTemplate</code> 会使用 <code>JdkSerializationRedisSerializer</code> 来序列化 <code>key</code> 和 <code>value</code>，这意味着对象会使用 Java 自带的序列化机制（<code>Serializable</code> 接口）进行序列化。</p><p>序列化器的接口为 <code>RedisSerializer</code>，Spring Data Redis中默认实现了几种序列化方式，常见的有：</p><ul><li><p><strong>StringRedisSerializer</strong>：将字符串序列化为字节数组，常用于序列化 <code>key</code>，因为 Redis 的 <code>key</code> 通常是字符串类型。</p></li><li><p><strong>JdkSerializationRedisSerializer</strong>：使用 Java 序列化机制，将对象序列化为字节数组，这也是默认的 <code>RedisTemplate</code> 序列化方式。</p></li><li><p><strong>Jackson2JsonRedisSerializer</strong>：使用 Jackson 库将对象序列化为 JSON 字符串，适合存储和读取 JSON 数据。</p></li><li><p><strong>GenericJackson2JsonRedisSerializer</strong>：类似于 <code>Jackson2JsonRedisSerializer</code>，但更通用，可以处理泛型类型。</p></li><li><p><strong>GenericToStringSerializer</strong>：将对象的 <code>toString()</code> 方法的结果进行序列化，适用于可以通过 <code>toString()</code> 表达的简单对象。</p></li></ul><p>再看SpringBoot自动装配中提供的 <code>StringRedisTemplate</code> Bean对象，就会发现它使用的全部是 <code>StringRedisSerializer</code> 序列化器，它要求调用 <code>RedisTemplate</code> 存储键值对时，数据类型都为 <code>String</code> ，然后在序列化时，直接调用 <code>String</code> 类的<code>getBytes</code>方法，反序列化时则通过<code>new String(bytes)</code>方式。</p><h2 id="自动装配默认使用的Redis客户端为什么是Lettuce"><a class="header-anchor" href="#自动装配默认使用的Redis客户端为什么是Lettuce"></a>自动装配默认使用的Redis客户端为什么是Lettuce</h2><p>Spring Boot Starter Redis 默认使用 Lettuce 作为 Redis 客户端，是基于其性能优势、异步与反应式支持、线程安全性、集群与高可用性支持等多方面的优点。关键原因如下：</p><ol><li>Lettuce 的异步与反应式支持：Lettuce可以让应用程序以非阻塞的方式处理 Redis 操作。</li><li>线程安全：Lettuce 是一个线程安全的 Redis 客户端，允许多个线程共享同一个连接实例进行操作。</li><li>高可用与集群支持：Lettuce 支持 Redis 集群模式和分片（Sharding），并且能够处理主从复制架构中的故障转移情况，确保应用程序在 Redis 节点故障时仍然能够正常运行。</li><li>轻量且无第三方依赖：Lettuce 是一个轻量级的客户端，并且不依赖于 Netty 之外的第三方库。</li></ol><p>再说 Jedis ，它与 Lettuce 相比具有如下限制：</p><ol><li>同步 API：Jedis 是一个传统的同步 Redis 客户端，不支持异步操作，因此在处理高并发请求时，性能可能不如 Lettuce。</li><li>连接池依赖：Jedis 依赖于连接池来管理 Redis 连接，这在一些高并发场景中可能导致性能瓶颈或连接池耗尽的问题。</li></ol><p>因此，Lettuce 更适合现代微服务架构和高并发场景，使其成为 Spring Boot 的首选 Redis 客户端。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;记录一下最近在从零搭建项目时集成Redis的使用心得，主要内容如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SpringBoot如何引入Redis；&lt;/li&gt;
&lt;li&gt;SpringBoot引入Redis依赖后，为什么不能直接注入RedisTemplate；&lt;/li&gt;
&lt;li&gt;SpringBoot中Redis的序列化方式；&lt;/li&gt;
&lt;li&gt;自动装配默认使用的Redis客户端为什么是Lettuce；&lt;/l</summary>
      
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="Java" scheme="https://blog.itwray.com/tags/Java/"/>
    
    <category term="SpringBoot" scheme="https://blog.itwray.com/tags/SpringBoot/"/>
    
    <category term="Redis" scheme="https://blog.itwray.com/tags/Redis/"/>
    
  </entry>
  
  <entry>
    <title>Java-“手撕”Class文件结构</title>
    <link href="https://blog.itwray.com/2024/07/03/java-jvm-readClassFile/"/>
    <id>https://blog.itwray.com/2024/07/03/java-jvm-readClassFile/</id>
    <published>2024-07-03T03:31:38.000Z</published>
    <updated>2024-11-20T07:25:36.971Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a class="header-anchor" href="#前言"></a>前言</h2><p>在上一章节 <a href="https://blog.itwray.com/2024/06/30/java-jvm-classFileStructure/">Java-JVM类文件结构</a> 中描述了Class文件的组成，为了加深影响，这章将进行手动实践，编写一个Java示例文件，对编译生成后的Class文件进行一个一个字节的分析。</p><h2 id="Java文件"><a class="header-anchor" href="#Java文件"></a>Java文件</h2><p>以下是示例文件的Java代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.itwray.study.advance.jvm;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 字节码分析类</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> wray</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@since</span> 2024/7/25</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> num;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">name</span> <span class="operator">=</span> <span class="string">&quot;wray&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">Main</span> <span class="variable">main</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Main</span>();</span><br><span class="line">        main.num++;</span><br><span class="line">        main.print(name + main.num);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">print</span><span class="params">(String arg)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;Hello &quot;</span> + arg);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Class文件"><a class="header-anchor" href="#Class文件"></a>Class文件</h2><p>通过 <code>javac Main.java</code> 命令生成Class文件，文件内容如下（本文使用Sublime Text）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line">cafe babe 0000 0034 003e 0a00 0f00 2507</span><br><span class="line">0026 0a00 0200 2509 0002 0027 0700 280a</span><br><span class="line">0005 0025 0800 290a 0005 002a 0a00 0500</span><br><span class="line">2b0a 0005 002c 0a00 0200 2d09 002e 002f</span><br><span class="line">0800 300a 0031 0032 0700 3301 0003 6e75</span><br><span class="line">6d01 0001 4901 0004 6e61 6d65 0100 124c</span><br><span class="line">6a61 7661 2f6c 616e 672f 5374 7269 6e67</span><br><span class="line">3b01 000d 436f 6e73 7461 6e74 5661 6c75</span><br><span class="line">6501 0006 3c69 6e69 743e 0100 0328 2956</span><br><span class="line">0100 0443 6f64 6501 000f 4c69 6e65 4e75</span><br><span class="line">6d62 6572 5461 626c 6501 0012 4c6f 6361</span><br><span class="line">6c56 6172 6961 626c 6554 6162 6c65 0100</span><br><span class="line">0474 6869 7301 0023 4c63 6f6d 2f69 7477</span><br><span class="line">7261 792f 7374 7564 792f 6164 7661 6e63</span><br><span class="line">652f 6a76 6d2f 4d61 696e 3b01 0004 6d61</span><br><span class="line">696e 0100 1628 5b4c 6a61 7661 2f6c 616e</span><br><span class="line">672f 5374 7269 6e67 3b29 5601 0004 6172</span><br><span class="line">6773 0100 135b 4c6a 6176 612f 6c61 6e67</span><br><span class="line">2f53 7472 696e 673b 0100 0570 7269 6e74</span><br><span class="line">0100 1528 4c6a 6176 612f 6c61 6e67 2f53</span><br><span class="line">7472 696e 673b 2956 0100 0361 7267 0100</span><br><span class="line">0a53 6f75 7263 6546 696c 6501 0009 4d61</span><br><span class="line">696e 2e6a 6176 610c 0015 0016 0100 2163</span><br><span class="line">6f6d 2f69 7477 7261 792f 7374 7564 792f</span><br><span class="line">6164 7661 6e63 652f 6a76 6d2f 4d61 696e</span><br><span class="line">0c00 1000 1101 0017 6a61 7661 2f6c 616e</span><br><span class="line">672f 5374 7269 6e67 4275 696c 6465 7201</span><br><span class="line">0004 7772 6179 0c00 3400 350c 0034 0036</span><br><span class="line">0c00 3700 380c 0020 0021 0700 390c 003a</span><br><span class="line">003b 0100 0648 656c 6c6f 2007 003c 0c00</span><br><span class="line">3d00 2101 0010 6a61 7661 2f6c 616e 672f</span><br><span class="line">4f62 6a65 6374 0100 0661 7070 656e 6401</span><br><span class="line">002d 284c 6a61 7661 2f6c 616e 672f 5374</span><br><span class="line">7269 6e67 3b29 4c6a 6176 612f 6c61 6e67</span><br><span class="line">2f53 7472 696e 6742 7569 6c64 6572 3b01</span><br><span class="line">001c 2849 294c 6a61 7661 2f6c 616e 672f</span><br><span class="line">5374 7269 6e67 4275 696c 6465 723b 0100</span><br><span class="line">0874 6f53 7472 696e 6701 0014 2829 4c6a</span><br><span class="line">6176 612f 6c61 6e67 2f53 7472 696e 673b</span><br><span class="line">0100 106a 6176 612f 6c61 6e67 2f53 7973</span><br><span class="line">7465 6d01 0003 6f75 7401 0015 4c6a 6176</span><br><span class="line">612f 696f 2f50 7269 6e74 5374 7265 616d</span><br><span class="line">3b01 0013 6a61 7661 2f69 6f2f 5072 696e</span><br><span class="line">7453 7472 6561 6d01 0007 7072 696e 746c</span><br><span class="line">6e00 2100 0200 0f00 0000 0200 0200 1000</span><br><span class="line">1100 0000 1a00 1200 1300 0100 1400 0000</span><br><span class="line">0200 0700 0300 0100 1500 1600 0100 1700</span><br><span class="line">0000 2f00 0100 0100 0000 052a b700 01b1</span><br><span class="line">0000 0002 0018 0000 0006 0001 0000 0009</span><br><span class="line">0019 0000 000c 0001 0000 0005 001a 001b</span><br><span class="line">0000 0009 001c 001d 0001 0017 0000 006d</span><br><span class="line">0003 0002 0000 002d bb00 0259 b700 034c</span><br><span class="line">2b59 b400 0404 60b5 0004 2bbb 0005 59b7</span><br><span class="line">0006 1207 b600 082b b400 04b6 0009 b600</span><br><span class="line">0ab7 000b b100 0000 0200 1800 0000 1200</span><br><span class="line">0400 0000 1000 0800 1100 1200 1200 2c00</span><br><span class="line">1300 1900 0000 1600 0200 0000 2d00 1e00</span><br><span class="line">1f00 0000 0800 2500 1c00 1b00 0100 0200</span><br><span class="line">2000 2100 0100 1700 0000 5200 0300 0200</span><br><span class="line">0000 1ab2 000c bb00 0559 b700 0612 0db6</span><br><span class="line">0008 2bb6 0008 b600 0ab6 000e b100 0000</span><br><span class="line">0200 1800 0000 0a00 0200 0000 1600 1900</span><br><span class="line">1700 1900 0000 1600 0200 0000 1a00 1a00</span><br><span class="line">1b00 0000 0000 1a00 2200 1300 0100 0100</span><br><span class="line">2300 0000 0200 24</span><br></pre></td></tr></table></figure><h3 id="魔数"><a class="header-anchor" href="#魔数"></a>魔数</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">u4             magic; <span class="comment">//Class 文件的标志</span></span><br></pre></td></tr></table></figure><p>从魔数开始，魔数是u4的无符号数，对应字节为 <code>cafe babe</code>。</p><h3 id="版本号"><a class="header-anchor" href="#版本号"></a>版本号</h3><pre><code>u2             minor_version;//Class 的小版本号u2             major_version;//Class 的大版本号</code></pre><p>Class的小版本号是u2的无符号数，对应字节为 <code>0000</code> ，表示小版本号为0。</p><p>Class的大版本号是u2的无符号数，对应字节为 <code>0034</code> ，表示大版本号为52，即Java 8版本。</p><h3 id="常量池"><a class="header-anchor" href="#常量池"></a>常量池</h3><pre><code>u2             constant_pool_count;//常量池的数量cp_info        constant_pool[constant_pool_count-1];//常量池</code></pre><p>常量池的数量是u2的无符号数，对应字节为 <code>003e</code> ，对应十进制为62，表示常量池中有61个常量。</p><p>因此，在<code>003e</code>后面的cp_info对应有61个常量，按照常量特性，第一个u1无符号数表示标志位，用于确定常量类型，接下来一个一个的列举出来。</p><ol><li><p><code>0a</code>对应十进制为10，代表 CONSTANT_Methodref_info ，表示类中方法的符号引用，它对应的结构定义为如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730135800638.png" alt="image-20240730135800638"></p><p>那么后面对应的值就是<code>000f</code>和<code>0025</code>，十进制为15和37，分别表示方法返回类型的Class常量索引为15，方法的名称和描述符的常量索引为37。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#1 = Methodref          #15.#37        // java/lang/Object.&quot;&lt;init&gt;&quot;:()V</span><br></pre></td></tr></table></figure></li><li><p><code>07</code>对应十进制为7，代表 CONSTANT_Class_info ，表示类或接口的符号引用，它对应的结构定义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730141438882.png" alt="image-20240730141438882"></p><p>后面对应的值就是<code>0026</code>，十进制为38，表示这个类的全限定名的常量索引为38。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#2 = Class              #38            // com/itwray/study/advance/jvm/Main</span><br></pre></td></tr></table></figure><blockquote><p>可以发现，每个常量的名称，无论是全限定名还是简单名称，到最后都会指向 CONSTANT_Utf8_info 常量，表示字符串的意思。</p></blockquote></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0002</code>和<code>0025</code>，十进制为2和37，分别表示方法返回类型的Class常量索引为2，方法的名称和描述符的常量索引为37。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#3 = Methodref          #2.#37         // com/itwray/study/advance/jvm/Main.&quot;&lt;init&gt;&quot;:()V</span><br></pre></td></tr></table></figure><blockquote><p>通过第一个常量和第三个常量，可以发现它们的名称和描述符指向了同一个常量，说明Class文件中允许多个不同的方法有相同的名称和描述符，只要返回值不同，也是可以在一个Class文件中共存，这点与Java代码的重载（Overload）有点不同。</p></blockquote></li><li><p><code>09</code>对应十进制为9，代表 CONSTANT_Fieldref_info ，表示字段的符号引用，它对应的结构定义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730143401427.png" alt="image-20240730143401427"></p><p>那么后面对应的值就是<code>0002</code>和<code>0027</code>，十进制为2和39，分别表示字段所在的Class常量索引为2，字段的名称和描述符的常量索引为39。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#4 = Fieldref           #2.#39         // com/itwray/study/advance/jvm/Main.num:I</span><br></pre></td></tr></table></figure></li><li><p><code>07</code>同第2个常量的类型一样，代表 CONSTANT_Class_info ，对应的值是<code>0028</code>，十进制为40，表示这个类的全限定名的常量索引为40。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#5 = Class              #40            // java/lang/StringBuilder</span><br></pre></td></tr></table></figure></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0005</code>和<code>0025</code>，十进制为5和37，分别表示方法返回类型的Class常量索引为5，方法的名称和描述符的常量索引为37。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#6 = Methodref          #5.#37         // java/lang/StringBuilder.&quot;&lt;init&gt;&quot;:()V</span><br></pre></td></tr></table></figure></li><li><p><code>08</code>对应十进制为8，代表 CONSTANT_String_info ，表示字符串类型字面量，它的结构定义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730160946175.png" alt="image-20240730160946175"></p><p>对应的值就是<code>0029</code>，十进制为41，表示这个字符串对应的 CONSTANT_Utf8_info 索引为41。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#7 = String             #41            // wray</span><br></pre></td></tr></table></figure></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0005</code>和<code>002a</code>，十进制为5和42，分别表示方法返回类型的Class常量索引为5，方法的名称和描述符的常量索引为42。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#8 = Methodref          #5.#42         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;</span><br></pre></td></tr></table></figure></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0005</code>和<code>002b</code>，十进制为5和43，分别表示方法返回类型的Class常量索引为5，方法的名称和描述符的常量索引为43。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#9 = Methodref          #5.#43         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;</span><br></pre></td></tr></table></figure></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0005</code>和<code>002c</code>，十进制为5和44，分别表示方法返回类型的Class常量索引为5，方法的名称和描述符的常量索引为44。</p></li></ol>   <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#10 = Methodref          #5.#44         // java/lang/StringBuilder.toString:()Ljava/lang/String;</span><br></pre></td></tr></table></figure><ol start="11"><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0002</code>和<code>002d</code>，十进制为2和45，分别表示方法返回类型的Class常量索引为2，方法的名称和描述符的常量索引为45。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#11 = Methodref          #2.#45         // com/itwray/study/advance/jvm/Main.print:(Ljava/lang/String;)V</span><br></pre></td></tr></table></figure></li><li><p><code>09</code>同第4个常量的类型一样，代表 CONSTANT_Fieldref_info ，对应为<code>002e</code>和<code>002f</code>，十进制为46和47，分别表示字段所在的Class常量索引为46，字段的名称和描述符的常量索引为47。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#12 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/PrintStream;</span><br></pre></td></tr></table></figure></li><li><p><code>08</code>同第7个常量的类型一样，代表 CONSTANT_String_info ，对应值为<code>0030</code>，十进制为48，表示这个字符串对应的 CONSTANT_Utf8_info 索引为48。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#13 = String             #48            // Hello</span><br></pre></td></tr></table></figure></li><li><p><code>0a</code>同第1个常量的类型一样，代表 CONSTANT_Methodref_info ，对应为<code>0031</code>和<code>0032</code>，十进制为49和50，分别表示方法返回类型的Class常量索引为49，方法的名称和描述符的常量索引为50。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#14 = Methodref          #49.#50        // java/io/PrintStream.println:(Ljava/lang/String;)V</span><br></pre></td></tr></table></figure></li><li><p><code>07</code>同第2个常量的类型一样，代表 CONSTANT_Class_info ，对应的值是<code>0033</code>，十进制为51，表示这个类的全限定名的常量索引为51。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#15 = Class              #51            // java/lang/Object</span><br></pre></td></tr></table></figure></li><li><p><code>01</code>对应十进制为1，代表 CONSTANT_Utf8_info，表示UTF-8编码的字符串，它的结构定义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730163314160.png" alt="image-20240730163314160"></p><p>对应的length选项值为<code>0003</code>，对应十进制为3，说明bytes选项的无符号数长度为3，即<code>6e756d</code>，对应的字符串为<code>num</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#16 = Utf8               num</span><br></pre></td></tr></table></figure></li><li><p>从第17个常量到第36个常量，以及索引为38、40、41、48、51～61的常量，同第16个常量的类型一样，代表 CONSTANT_Utf8_info，它们对应的字符串如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">#17 = Utf8               I</span><br><span class="line">#18 = Utf8               name</span><br><span class="line">#19 = Utf8               Ljava/lang/String;</span><br><span class="line">#20 = Utf8               ConstantValue</span><br><span class="line">#21 = Utf8               &lt;init&gt;</span><br><span class="line">#22 = Utf8               ()V</span><br><span class="line">#23 = Utf8               Code</span><br><span class="line">#24 = Utf8               LineNumberTable</span><br><span class="line">#25 = Utf8               LocalVariableTable</span><br><span class="line">#26 = Utf8               this</span><br><span class="line">#27 = Utf8               Lcom/itwray/study/advance/jvm/Main;</span><br><span class="line">#28 = Utf8               main</span><br><span class="line">#29 = Utf8               ([Ljava/lang/String;)V</span><br><span class="line">#30 = Utf8               args</span><br><span class="line">#31 = Utf8               [Ljava/lang/String;</span><br><span class="line">#32 = Utf8               print</span><br><span class="line">#33 = Utf8               (Ljava/lang/String;)V</span><br><span class="line">#34 = Utf8               arg</span><br><span class="line">#35 = Utf8               SourceFile</span><br><span class="line">#36 = Utf8               Main.java</span><br><span class="line">#38 = Utf8               com/itwray/study/advance/jvm/Main</span><br><span class="line">#40 = Utf8               java/lang/StringBuilder</span><br><span class="line">#41 = Utf8               wray</span><br><span class="line">#48 = Utf8               Hello</span><br><span class="line">#51 = Utf8               java/lang/Object</span><br><span class="line">#52 = Utf8               append</span><br><span class="line">#53 = Utf8            (Ljava/lang/String;)Ljava/lang/StringBuilder;</span><br><span class="line">#54 = Utf8               (I)Ljava/lang/StringBuilder;</span><br><span class="line">#55 = Utf8               toString</span><br><span class="line">#56 = Utf8               ()Ljava/lang/String;</span><br><span class="line">#57 = Utf8               java/lang/System</span><br><span class="line">#58 = Utf8               out</span><br><span class="line">#59 = Utf8               Ljava/io/PrintStream;</span><br><span class="line">#60 = Utf8               java/io/PrintStream</span><br><span class="line">#61 = Utf8               println</span><br></pre></td></tr></table></figure></li><li><p>第37个常量，对应的字节码位置如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730172445090.png" alt="image-20240730172445090"></p><p><code>0c</code>的十进制为12，代表 CONSTANT_NameAndType_info ，表示字段或方法的部分符号引用，它的结构定义如下：<img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730172707842.png" alt="image-20240730172707842"></p><p>对应的值是<code>0015</code>和<code>0016</code>，十进制为21和22，分别表示对应的 CONSTANT_Utf8_info 常量的索引为21和22。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#37 = NameAndType        #21:#22        // &quot;&lt;init&gt;&quot;:()V</span><br></pre></td></tr></table></figure><blockquote><p>关于如何在字节码文件中快速定位数据所处的字节码位置：在已知该项数据的上一个数据的值的情况下，可以根据数据的类型和值 反算出16进制编码，然后在编辑器中Ctrl + F搜索即可。（最好结合javap -verbose Xxx命令判断，避免找错）</p></blockquote></li><li><p>常量索引为39、42、43、44、45、47、50的常量，均同第37个常量的类型一样，代表 CONSTANT_NameAndType_info ，它们对应的数据如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">#39 = NameAndType        #16:#17        // num:I</span><br><span class="line">#42 = NameAndType        #52:#53        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;</span><br><span class="line">#43 = NameAndType        #52:#54        // append:(I)Ljava/lang/StringBuilder;</span><br><span class="line">#44 = NameAndType        #55:#56        // toString:()Ljava/lang/String;</span><br><span class="line">#45 = NameAndType        #32:#33        // print:(Ljava/lang/String;)V</span><br><span class="line">#47 = NameAndType        #58:#59        // out:Ljava/io/PrintStream;</span><br><span class="line">#50 = NameAndType        #61:#33        // println:(Ljava/lang/String;)V</span><br></pre></td></tr></table></figure></li><li><p>第46个常量同第2个常量的类型一样，代表 CONSTANT_Class_info ，其对应的16进制字节码位置可以按照前面的搜索逻辑查询，16进制结果为39，十进制为57，表示这个类的全限定名的常量索引为57。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#46 = Class              #57            // java/lang/System</span><br></pre></td></tr></table></figure></li><li><p>第49个常量同第2个常量的类型一样，代表 CONSTANT_Class_info ，16进制结果为3c，十进制为60，表示这个类的全限定名的常量索引为60。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#49 = Class              #60            // java/io/PrintStream</span><br></pre></td></tr></table></figure></li></ol><p>至此，61个常量就分析完了。确定常量池最后在字节码文件中结束的位置如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240730174718030.png" alt="image-20240730174718030"></p><p><code>01 0007 7072 696e 746c6e</code>是最后一个 CONSTANT_Utf8_info 常量的字节码内容，对应的字符串为<code>println</code>。</p><h3 id="访问标志"><a class="header-anchor" href="#访问标志"></a>访问标志</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">u2             access_flags;<span class="comment">//Class 的访问标记</span></span><br></pre></td></tr></table></figure><p>访问标志是一个u2无符号数，对应的字节码是<code>0021</code>，通过标志位表查询的结果为：ACC_PUBLIC、ACC_SUPER。</p><h3 id="类索引、父类索引、接口索引集合"><a class="header-anchor" href="#类索引、父类索引、接口索引集合"></a>类索引、父类索引、接口索引集合</h3><pre><code>u2             this_class;//类索引u2             super_class;//父类索引u2             interfaces_count;//接口数量u2             interfaces[interfaces_count];//一个类可以实现多个接口</code></pre><p>类索引是u2无符号数，对应字节码为<code>0002</code>，十进制为2，表示当前类文件的类对应为常量池中索引为2的常量。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#2 = Class              #38            // com/itwray/study/advance/jvm/Main</span><br></pre></td></tr></table></figure><p>父类索引是u2无符号数，对应字节码为<code>000f</code>，十进制为15，表示当前类文件的父类对应为常量池中索引为15的常量。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#15 = Class              #51            // java/lang/Object</span><br></pre></td></tr></table></figure><p>接口索引集合的结构如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             interfaces_count;<span class="comment">//接口数量</span></span><br><span class="line">u2             interfaces[interfaces_count];<span class="comment">//一个类可以实现多个接口</span></span><br></pre></td></tr></table></figure><p>对应字节码为<code>0000</code>，十进制为0，表示当前类文件没有接口。</p><h3 id="字段表集合"><a class="header-anchor" href="#字段表集合"></a>字段表集合</h3><pre><code>u2             fields_count;//字段数量field_info     fields[fields_count];//一个类可以有多个字段</code></pre><p>字段表是先以一个u2无符号数表示字段数量，对应字节码为<code>0002</code>，十进制为2，表示类中有2个字段。</p><p>字段的结构定义固定如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725171340663.png" alt="image-20240725171340663"></p><p>access_flags的标志字典如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725172852283.png" alt="image-20240725172852283"></p><p>接下来一个个字段的分析：</p><ol><li><p>access_flags对应字节码为<code>0002</code>，表示ACC_PRIVATE；</p><p>name_index对应字节码为<code>0010</code>，十进制为16，表示字段的简单名称对应常量池的索引为16，即<code>\#16 = Utf8        num</code>；</p><p>descriptor_index对应字节码为<code>0011</code>，十进制为17，表示字段的描述符对应常量池的索引为17，即<code>\#17 = Utf8        I</code>；</p><p>attributes_count对应字节码为<code>0000</code>，说明该字段没有属性信息，即没有attributes_info。</p><p>通过匹配常量池的索引，该字段为：<code>private int num</code> 。</p></li><li><p>access_flags对应字节码为<code>001a</code>，<code>0010</code>表示ACC_FINAL，<code>000a</code>则是<code>0002</code>和<code>0008</code>的按位或运算结果，所以还表示ACC_PRIVATE、ACC_STATIC。</p><p>name_index对应字节码为<code>0012</code>，十进制为18，表示字段的简单名称对应常量池的索引为18，即<code>\#18 = Utf8        name</code>；</p><p>descriptor_index对应字节码为<code>0013</code>，十进制为19，表示字段的描述符对应常量池的索引为19，即<code>\#19 = Utf8        Ljava/lang/String;</code>；</p><p>attributes_count对应字节码为<code>0001</code>，说明该字段有1个属性信息，根据属性表的结构定义，开始是一个u2无符号数，表示属性名称在常量池中的索引，对应字节码为<code>0014</code>，十进制为20，对应常量池的<code>\#20 = Utf8        ConstantValue</code>。<code>ConstantValue</code>的属性结构如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731151436047.png" alt="image-20240731151436047"></p><p>根据属性结构得出紧接着后面是一个u4无符号数，表述属性的长度，对应字节码<code>0000 0002</code>，表示该属性的长度是2个u1（与属性结构中的第三个u2刚好对应上）。对应的属性长度的字节码为<code>0007</code>，通过<code>ConstantValue</code>标志表示这个字段是一个常量属性，然后通过属性长度的字段找到常量池中索引为7的常量，内容为<code>\#7 = String       #41      // wray</code>，说明这个常量字段的值是一个字符串，字符串内容对应常量池中索引41，即<code>\#41 = Utf8        wray</code>。</p><p>最后通过匹配常量池的索引，该字段为：<code>private static final String name = &quot;wray&quot;</code> 。</p></li></ol><h3 id="方法表集合"><a class="header-anchor" href="#方法表集合"></a>方法表集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             methods_count;<span class="comment">//方法数量</span></span><br><span class="line">method_info    methods[methods_count];<span class="comment">//一个类可以有多个方法</span></span><br></pre></td></tr></table></figure><p>方法表同字段表的结构几乎一样，先是以一个u2无符号数表示方法数量，对应字节码为<code>0003</code>，表示当前类有3个方法。方法表的结构定义与字段表一样：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729160002172.png" alt="image-20240729160002172"></p><p>只是access_flags访问标志有一点区别，访问标志字典如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729160514905.png" alt="image-20240729160514905"></p><p>接下来一个个方法的分析：</p><ol><li><p>access_flags对应字节码为<code>0001</code>，表示ACC_PUBLIC；</p><p>name_index对应字节码为<code>0015</code>，十进制为21，表示字段的简单名称对应常量池的索引为21，即<code>\#21 = Utf8        &lt;init&gt;</code>；</p><p>descriptor_index对应字节码为<code>0016</code>，十进制为22，表示字段的描述符对应常量池的索引为22，即<code>\#22 = Utf8        ()V</code>；</p><p>从上面三个标志可知，这是当前类无参构造函数。</p><p>attributes_count对应字节码为<code>0001</code>，说明该字段有1个属性信息，根据属性表的结构定义，开始是一个u2无符号数，表示属性名称在常量池中的索引，对应字节码为<code>0017</code>，十进制为23，对应常量池的<code>\#23 = Utf8        Code</code>。<code>Code</code>属性的结构定义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731151714048.png" alt="image-20240731151714048"></p><p><code>Code</code>属性较为复杂，接下来一个个类型再分析：</p><ol><li>attribute_name_index 在<code>Code</code>属性中肯定对应的是常量池中的<code>Code</code>常量，也就是属性最开始的u2无符号数，即<code>0017</code>。</li><li>attribute_lenth表示属性值的长度，由于属性名称索引与属性长度一共为6个字节，所以属性值的长度固定为整个属性表长度减去6个字节。对应字节码为<code>0000 002f</code>，十进制为47，所以属性值长度为41。</li><li>max_stack代表操作数栈深度的最大值，</li><li>在方法执行的任意时刻，操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧（Stack Frame）中的操作栈深度。对应字节码为<code>0001</code>。</li><li>max_locals代表了局部变量表所需的存储空间。在这里，max_locals的单位是变量槽（Slot），变量槽是虚拟机为局部变量分配内存所使用的最小单位。对应字节码为<code>0001</code>。</li></ol><blockquote><p>对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型，每个局部变量占用一个变量槽，而double和long这两种64位的数据类型则需要两个变量槽来存放。</p><p>注意，并不是在方法中用了多少个局部变量，就把这些局部变量所占变量槽数量之和作为max_locals的值，操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存，不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用，当代码执行超出一个局部变量的作用域时，这个局部变量所占的变量槽可以被其他局部变量所使用，Javac编译器会根据变量的作用域来分配变量槽给各个变量使用，根据同时生存的最大局部变量数量和类型计算出max_locals的大小。</p></blockquote><ol start="6"><li><p>code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度，code是用于存储字节码指令的一系列字节流。</p><p>code_lenth对应字节码为<code>0000 0005</code>，说明code有5个u1，那么code对应字节码为<code>2ab7 0001 b1</code>。每一个u1对应一个字节码指令，具体指令可以参考《深入理解Java虚拟机》附录C“虚拟机字节码指令表”。</p></li><li><p>exception_table_length和exception_table表示方法的异常表，异常表有自己的表结构定义（从上到下、从左到右）：<img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731154308343.png" alt="image-20240731154308343"></p><p>exception_table_length对应字节码为<code>0000</code>，表示没有异常表，所以exception_table为空。</p></li><li><p>attributes_count和attributes则表示属性表（attribute_info），说明<code>Code</code>属性内部可以包含其他属性，例如<code>LineNumberTable</code>和<code>LocalVariableTable</code>等子属性。attributes_count对应字节码为<code>0002</code>，表示有两个属性表，属性表结构如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731180451803.png" alt="image-20240731180451803"></p><p>接下来一个个属性分析：</p><ol><li><p>attribute_name_index对应字节码为<code>0018</code>，十进制为24，说明该属性的类型在常量池索引为24中，即<code> \#24 = Utf8        LineNumberTable</code>。<code>LineNumberTable</code>属性的结构这里就不在过多分析了。</p><blockquote><p>！！！只需要记住，在属性表结构中通过attribute_lenth确定了长度，attribute_length个u1就是这个属性的总长度，无论这个属性的内部结构怎么变，最后的总长度就是 u2 + u4 + attribute_length个u1。</p></blockquote><p>再看attribute_lenth对应字节码为<code>0000 0006</code>，表示info的长度为6，对应字节码为<code>0001 0000 0009</code>。</p></li><li><p>attribute_name_index对应字节码为<code>0019</code>，十进制为25，说明该属性的类型在常量池索引为25中，即<code>\#25 = Utf8        LocalVariableTable</code>。attribute_lenth对应字节码为<code>0000 000c</code>，表示info的长度为12，对应字节码为<code>0001 0000 0005 001a 001b 0000</code>。</p></li></ol></li></ol></li><li><p>接下来是第二个方法，access_flags对应字节码为<code>0009</code>，对应的字节码标志值为<code>0001</code>和<code>0008</code>，即表示ACC_PUBLIC、ACC_STATIC；</p><p>name_index对应字节码为<code>001c</code>，十进制为28，表示字段的简单名称对应常量池的索引为28，即<code>\#28 = Utf8        main</code>；</p><p>descriptor_index对应字节码为<code>001d</code>，十进制为29，表示字段的描述符对应常量池的索引为29，即<code>\#29 = Utf8        ([Ljava/lang/String;)V</code>；</p><p>从上面三个标志可知，该方法的定义为：<code>public static void main(String[] arg0)</code>。</p><blockquote><p>其中<code>arg0</code>参数名称是不被虚拟机关注的，可以通过方法对应的属性表找到<code>LocalVariableTable</code>属性以确定实际代码中的参数名称。</p></blockquote><p>attributes_count对应字节码为<code>0001</code>，说明该字段有1个属性信息，根据属性表的结构定义，开始是一个u2无符号数，表示属性名称在常量池中的索引，对应字节码为<code>0017</code>，十进制为23，对应常量池的<code>\#23 = Utf8        Code</code>。因为与第一个方法属性一样，就不再展开分析了，按照属性表的通用结构定义直接分析字节码，再次展示一遍属性表的结构定义：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731180451803.png" alt="image-20240731180451803"></p><p>attribute_length对应字节码为<code>0000 006d</code>，十进制为109，说明info有109个u1无符号数，对应字节码如下（灰色选中区域）：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240801103318748.png" alt="image-20240801103318748"></p></li><li><p>第三个方法，access_flags对应字节码为<code>0002</code>，表示ACC_PRIVATE；</p><p>name_index对应字节码为<code>0020</code>，十进制为32，表示字段的简单名称对应常量池的索引为32，即<code>\#32 = Utf8        print</code>；</p><p>descriptor_index对应字节码为<code>0021</code>，十进制为33，表示字段的描述符对应常量池的索引为33，即<code>\#33 = Utf8        (Ljava/lang/String;)V</code>；</p><p>从上面三个标志可知，该方法的定义为：<code>private void print(String arg0)</code>。</p><p>attributes_count对应字节码为<code>0001</code>，说明该字段有1个属性信息，根据属性表的结构定义，开始是一个u2无符号数，表示属性名称在常量池中的索引，对应字节码为<code>0017</code>，十进制为23，对应常量池的<code>\#23 = Utf8        Code</code>。同前两个方法一样，直接看attribute_length对应字节码为<code>00 0000 52</code>，十进制为82，info对应的82个字节码如下（灰色选中区域）：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240801104128103.png" alt="image-20240801104128103"></p></li></ol><h3 id="属性表集合"><a class="header-anchor" href="#属性表集合"></a>属性表集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             attributes_count;<span class="comment">//此类的属性表中的属性数</span></span><br><span class="line">attribute_info attributes[attributes_count];<span class="comment">//属性表集合</span></span><br></pre></td></tr></table></figure><p>首先第一个u2无符号数表示属性表的属性数，对应字节码为<code>0001</code>，表示有一个属性。</p><p>同分析方法表中的属性一样，再次根据attribute_info（属性表）的结构定义分析：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240731180451803.png" alt="image-20240731180451803"></p><p>attribute_name_index对应字节码为<code>0023</code>，十进制为35，对应常量池中索引为35的常量，即<code>\#35 = Utf8        SourceFile</code>。</p><p>接下来是attribute_length，对应字节码为<code>00 0000 02</code>，表示info的长度为2。</p><p>info对应的字节码为<code>00 24</code>。</p><p>至此，这个Class文件分析完毕（完结撒花～）。</p><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>从魔数到最后的属性表集合，一个个u1无符号数分析下来，不得不感慨Class文件结构的紧凑，因为它真的没有任何一个分隔符，但即使文件结构紧凑，仍然提供了很多可扩展的特性，并且通过《Java虚拟机规范》实现了平台无关性、语言无关性。</p><p>而且，这还是从Java初版到至今，仍然维持Class文件结构几乎不变，且功能稳定实用。</p><p>再次佩服Java语言设计者们的智慧。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;
&lt;p&gt;在上一章节 &lt;a href=&quot;https://blog.itwray.com/2024/06/30/java-jvm-classFileStructure/&quot;&gt;Java-JVM类文件结构&lt;/a&gt; 中描述了Class文件的组成，为了加深影响，这章将进行手动实践，编写一个Jav</summary>
      
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="Java" scheme="https://blog.itwray.com/tags/Java/"/>
    
    <category term="JVM" scheme="https://blog.itwray.com/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>Java-JVM类文件结构</title>
    <link href="https://blog.itwray.com/2024/06/30/java-jvm-classFileStructure/"/>
    <id>https://blog.itwray.com/2024/06/30/java-jvm-classFileStructure/</id>
    <published>2024-06-30T02:31:38.000Z</published>
    <updated>2024-11-20T07:25:58.715Z</updated>
    
    <content type="html"><![CDATA[<h2 id="简介"><a class="header-anchor" href="#简介"></a>简介</h2><p>在Java中，被 JVM 可以理解的代码称为<code>字节码</code>，即扩展名为 <code>.class</code> 的文件，这种文件被称为字节码文件，也可以称为类文件或者Class文件。</p><p>Java使用Java编译器（javac）可以将Java代码编译为字节码存储的Class文件，其他语言也可以使用自己的编译器将代码编译成Class文件，例如JRuby通过jrubyc编译器生成、Groovy通过groovyc编译器生成、Kotlin通过kotlinc编译器生成等。</p><p>编译生成后的Class文件被JVM虚拟机执行，不受操作系统平台影响，因此具有“一次编译，到处运行”的特性。</p><p><em><strong>🔔温馨提示</strong>：本章内容偏硬核，基本就是一个硬背字典的概念，建议结合 <a href="https://blog.itwray.com/2024/07/03/java-jvm-readClassFile/">“手撕”Class文件结构</a> 一起阅读，边看边上手最好。</em></p><h2 id="类文件结构"><a class="header-anchor" href="#类文件结构"></a>类文件结构</h2><p>任何一个Class文件都对应着唯一的一个类或接口的定义信息，但是反过来说，类或接口并不一定都得定义在文件里（譬如类或接口也可以动态生成，直接送入类加载器中）。</p><p>Class文件是一组紧凑排列的二进制流，，各个数据项目按照顺序排列，没有分隔符，这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据，没有空隙存在。</p><p>Class文件格式采用一种类似于C语言结构体的伪结构来存储数据，这种伪结构中只有两种数据类型：“无符号数”和“表”。</p><p>无符号数属于基本的数据类型，以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数，无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。</p><p>表是由多个无符号数或者其他表作为数据项构成的复合数据类型，为了便于区分，所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据，整个Class文件本质上也可以视作是一张表，这张表由如下所示的数据项按严格顺序排列构成。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">ClassFile &#123;</span><br><span class="line">    u4             magic; <span class="comment">//Class 文件的标志</span></span><br><span class="line">    u2             minor_version;<span class="comment">//Class 的小版本号</span></span><br><span class="line">    u2             major_version;<span class="comment">//Class 的大版本号</span></span><br><span class="line">    u2             constant_pool_count;<span class="comment">//常量池的数量</span></span><br><span class="line">    cp_info        constant_pool[constant_pool_count-<span class="number">1</span>];<span class="comment">//常量池</span></span><br><span class="line">    u2             access_flags;<span class="comment">//Class 的访问标志</span></span><br><span class="line">    u2             this_class;<span class="comment">//类索引</span></span><br><span class="line">    u2             super_class;<span class="comment">//父类索引</span></span><br><span class="line">    u2             interfaces_count;<span class="comment">//接口数量</span></span><br><span class="line">    u2             interfaces[interfaces_count];<span class="comment">//一个类可以实现多个接口</span></span><br><span class="line">    u2             fields_count;<span class="comment">//字段数量</span></span><br><span class="line">    field_info     fields[fields_count];<span class="comment">//一个类可以有多个字段</span></span><br><span class="line">    u2             methods_count;<span class="comment">//方法数量</span></span><br><span class="line">    method_info    methods[methods_count];<span class="comment">//一个类可以有个多个方法</span></span><br><span class="line">    u2             attributes_count;<span class="comment">//此类的属性表中的属性数</span></span><br><span class="line">    attribute_info attributes[attributes_count];<span class="comment">//属性表集合</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>无论是无符号数还是表，当需要描述同一类型但数量不定的多个数据时，经常会使用一个前置的容量计数器加若干个连续的数据项的形式，这时候称这一系列连续的某一类型的数据为某一类型的“集合”。用一张图可以更加清晰的了解Class文件的组成。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240723151123554.png" alt="image-20240723151123554"></p><h3 id="魔数"><a class="header-anchor" href="#魔数"></a>魔数</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">u4             magic; <span class="comment">//Class 文件的标志</span></span><br></pre></td></tr></table></figure><p>每个Class文件的头4个字节被称为魔数（Magic Number），它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。</p><p>Java 规范规定魔数为固定值：0xCAFEBABE。如果读取的文件不是以这个魔数开头，Java 虚拟机将拒绝加载它。</p><h3 id="Class文件的版本号"><a class="header-anchor" href="#Class文件的版本号"></a>Class文件的版本号</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             minor_version;<span class="comment">//Class 的小版本号</span></span><br><span class="line">u2             major_version;<span class="comment">//Class 的大版本号</span></span><br></pre></td></tr></table></figure><p>紧接着魔数的4个字节存储的是Class文件的版本号：第5和第6个字节是次版本号（MinorVersion），第7和第8个字节是主版本号（Major Version）。</p><p>Java的版本号是从45开始的，JDK 1.1之后的每个JDK大版本发布主版本号向上加1（JDK 1.0～1.1使用了45.0～45.3的版本号），高版本的JDK能向下兼容以前版本的Class文件，但不能运行以后版本的Class文件，因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化，虚拟机也必须拒绝执行超过其版本号的Class文件。</p><h3 id="常量池"><a class="header-anchor" href="#常量池"></a>常量池</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             constant_pool_count;<span class="comment">//常量池的数量</span></span><br><span class="line">cp_info        constant_pool[constant_pool_count-<span class="number">1</span>];<span class="comment">//常量池</span></span><br></pre></td></tr></table></figure><p>常量池可以比喻为Class文件里的资源仓库，它是Class文件结构中与其他项目关联最多的数据，通常也是占用Class文件空间最大的数据项目之一，另外，它还是在Class文件中第一个出现的表类型数据项目。</p><p><strong>常量池计数器是从 1 开始计数的，将第 0 项常量空出来是有特殊考虑的，索引值为 0 代表“不引用任何一个常量池项”</strong>。即如果<code>constant_pool_count</code>的十进制值为10，那么<code>cp_info</code>中实际的常量有9个。</p><p>Class文件结构中只有常量池的容量计数是从1开始，对于其他集合类型，包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同，是从0开始。</p><p>常量池中主要存放两大类常量：<strong>字面量</strong>（Literal）和<strong>符号引用</strong>（Symbolic References）。</p><ul><li>字面量（Literals）：字面量是不变的数据，主要包括数值（如整数、浮点数）和字符串字面量。例如，一个整数100或一个字符串&quot;Hello World&quot;，在源代码中直接赋值，编译后存储在常量池中。</li><li>符号引用（Symbolic References）：符号引用是对类、接口、字段、方法等的引用，它们不是由字面量值给出的，而是通过符号名称（如类名、方法名）和其他额外信息（如类型、签名）来表示。这些引用在类文件中以一种抽象的方式存在，它们在类加载时被虚拟机解析为具体的内存地址。</li></ul><p>常量池中每一种常量都是一个表，这些表都有一个共同特点，就是表结构起始的第一位是u1类型的标志位（tag），代表着当前常量属于哪种常量类型。常见的常量类型如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725161908360.png" alt="image-20240725161908360"></p><p>每一种常量池的表结构也是不一样的，例如CONSTANT_Utf8_info类型的结构如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725162126812.png" alt="image-20240725162126812"></p><p>如果tag位对应十进制为1时，后面紧接着的2个字节是字符串的长度，然后length是字符串的字节长度。</p><p>因为常量类型过多，表结构不一致，并且随着Java迭代升级，常量类型越来越多，所以JDK提供了<code>javap</code>工具用于分析Class文件字节码，使用 <code>javap -verbose Xxx</code> 命令即可查看Xxx.class文件的字节码内容。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725163420489.png" alt="image-20240725163420489"></p><p>如上图所示，Constant pool表示常量池，拿第一个常量进行分析：</p><p>#1表示第一个常量，Methodref表示它的类型（对应标志位为10，描述是“类中方法的符号引用”），#15.#37表示该常量的值，在Methodref类型中表示指向常量Class #15 和 NameAndType #37，后面的 // ... 内容是注释内容，提示用户这个常量的含义是 引用了<code>java/lang/Object</code>类的构造方法<code>&lt;init&gt;</code>，方法签名为<code>()V</code>，即无参数，返回类型为void。</p><blockquote><p>仔细看一下会发现，其中有些常量似乎从来没有在代码中出现过，如“I”“V”“&lt;init&gt;”“LineNumberTable”“LocalVariableTable”等，这些看起来在源代码中不存在的常量是哪里来的？这部分常量的确不来源于Java源代码，它们都是编译器自动生成的，会被后面即将讲到的字段表（field_info）、方法表（method_info）、属性表（attribute_info）所引用，它们将会被用来描述一些不方便使用“固定字节”进行表达的内容，譬如描述方法的返回值是什么，有几个参数，每个参数的类型是什么。因为Java中的“类”是无穷无尽的，无法通过简单的无符号数来描述一个方法用到了什么类，因此在描述方法的这些信息时，需要引用常量表中的符号引用进行表达。</p></blockquote><p>以下是部分常量类型的结构定义：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725164602204.png" alt="image-20240725164602204"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725164710997.png" alt="image-20240725164710997"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725164730738.png" alt="image-20240725164730738"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725164841168.png" alt="image-20240725164841168"></p><h3 id="访问标志"><a class="header-anchor" href="#访问标志"></a>访问标志</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">u2             access_flags;<span class="comment">//Class 的访问标记</span></span><br></pre></td></tr></table></figure><p>在常量池结束之后，紧接着的两个字节代表访问标志，这个标志用于识别一些类或者接口层次的访问信息，包括：这个 Class 是类还是接口，是否为 <code>public</code> 或者 <code>abstract</code> 类型，如果是类的话是否声明为 <code>final</code> 等等。</p><p>具体的标志位以及标志的含义如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725165703996.png" alt="image-20240725165703996"></p><p>访问标志要求类文件中没有使用到的标志位一律为0，然后把使用到的标志值通过<strong>按位或运算</strong>计算得到一个u2长度的十六进制。</p><h3 id="类索引、父类索引、接口索引集合"><a class="header-anchor" href="#类索引、父类索引、接口索引集合"></a>类索引、父类索引、接口索引集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">u2             this_class;<span class="comment">//类索引</span></span><br><span class="line">u2             super_class;<span class="comment">//父类索引</span></span><br><span class="line">u2             interfaces_count;<span class="comment">//接口数量</span></span><br><span class="line">u2             interfaces[interfaces_count];<span class="comment">//一个类可以实现多个接口</span></span><br></pre></td></tr></table></figure><p>类索引（this_class）和父类索引（super_class）都是一个u2类型的数据，而接口索引集合（interfaces）是一组u2类型的数据的集合，Class文件中由这三项数据来确定该类型的继承关系。</p><p>类索引用于确定这个类的全限定名，父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承，所以父类索引只有一个，除了java.lang.Object之外，所有的Java类都有父类，因此除了java.lang.Object外，所有Java类的父类索引都不为0。</p><p>接口索引集合就用来描述这个类实现了哪些接口，这些被实现的接口将按implements关键字（如果这个Class文件表示的是一个接口，则应当是extends关键字）后的接口顺序从左到右排列在接口索引集合中。</p><p>类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量，通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。</p><p>接口索引入口的第一项u2类型的数据为接口计数器（interfaces_count），表示索引表的容量。如果容量为0，则索引集合为0，后面就没有对应的字节。</p><h3 id="字段表集合"><a class="header-anchor" href="#字段表集合"></a>字段表集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             fields_count;<span class="comment">//字段数量</span></span><br><span class="line">field_info     fields[fields_count];<span class="comment">//一个类可以有多个字段</span></span><br></pre></td></tr></table></figure><p>字段表（field_info）用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量，但不包括在方法内部声明的局部变量。</p><p>字段表的结构如下（从上到下、从左到右的顺序）：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725171340663.png" alt="image-20240725171340663"></p><ul><li><p><strong>access_flags:</strong> 字段的作用域（<code>public</code> ,<code>private</code>,<code>protected</code>修饰符），是实例变量还是类变量（<code>static</code>修饰符）,可否被序列化（transient 修饰符）,可变性（final）,可见性（volatile 修饰符，是否强制从主内存读写）。</p><blockquote><p>字段中 access_flags 的取值如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240725172852283.png" alt="image-20240725172852283"></p></blockquote></li><li><p><strong>name_index:</strong> 对常量池的引用，表示的字段的简单名称。</p><blockquote><p>相较于“全限定名”，简单名称就是指没有类型和参数修饰的方法或者字段名称，全限定名则是把类全名中的“.“换成了“/”，为了使连续的多个全限定名之间不产生混淆，在使用时最后一般会加入一个“；”号表示全限定名结束。</p></blockquote></li><li><p><strong>descriptor_index:</strong> 对常量池的引用，表示字段的描述符（同方法的descriptor_index描述符性质一样），描述符的作用是用来描述字段的数据类型、方法的参数列表（包括数量、类型以及顺序）和返回值。</p><blockquote><p>描述符一般用于字段和方法，它有如下一些规则：基本数据类型（byte、char、double、float、int、long、short、boolean）以及代表无返回值的void类型都用一个大写字符来表示，而对象类型则用字符L加对象的全限定名来表示，对于数组类型，每一维度将使用一个前置的“[”字符来描述，如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String；”，一个整型数组“int[]”将被记录成“[I”。</p><p>用描述符来描述方法时，按照先参数列表、后返回值的顺序描述，参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”，方法java.lang.String toString()的描述符为“()Ljava/lang/String；”，方法int indexOf(char[]source，int sourceOffset，int sourceCount，char[]target，int targetOffset，int targetCount，int fromIndex)的描述符为“([CII[CIII)I”。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729154954973.png" alt="image-20240729154954973"></p></blockquote></li><li><p><strong>attributes_count:</strong> 一个字段还会拥有一些额外的属性，attributes_count 存放属性的个数。</p></li><li><p><strong>attributes[attributes_count]:</strong> 存放具体属性具体内容。</p></li></ul><h3 id="方法表集合"><a class="header-anchor" href="#方法表集合"></a>方法表集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             methods_count;<span class="comment">//方法数量</span></span><br><span class="line">method_info    methods[methods_count];<span class="comment">//一个类可以有多个方法</span></span><br></pre></td></tr></table></figure><p>Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式，方法表的结构如同字段表一样，依次包括访问标志（access_flags）、名称索引（name_index）、描述符索引（descriptor_index）、属性表集合（attributes）几项。这些数据项目的含义也与字段表中的非常类似，仅在访问标志和属性表集合的可选项中有所区别。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729160002172.png" alt="image-20240729160002172"></p><p>因为volatile关键字和transient关键字不能修饰方法，所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对，synchronized、native、strictfp和abstract关键字可以修饰方法，方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。对于方法表，所有标志位及其取值可参见如下表。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729160514905.png" alt="image-20240729160514905"></p><p>方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚，而方法里的Java代码经过Javac编译器编译成字节码指令之后，存放在方法属性表集合中一个名为“Code”的属性里面，然后通过属性索引在属性表中查找。</p><p>与字段表集合相对应地，如果父类方法在子类中没有被重写（Override），方法表集合中就不会出现来自父类的方法信息。但同样地，有可能会出现由编译器自动添加的方法，最常见的便是类构造器“&lt;clinit&gt;()”方法和实例构造器“&lt;init&gt;()”方法。</p><p>在Java语言中，要重载（Overload）一个方法，除了要与原方法具有相同的简单名称之外，还要求必须拥有一个与原方法不同的特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合，也正是因为返回值不会包含在特征签名之中，所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式之中，特征签名的范围明显要更大一些，只要描述符不是完全一致的两个方法就可以共存。也就是说，如果两个方法有相同的名称和特征签名，但返回值不同，那么也是可以合法共存于同一个Class文件中的。</p><h3 id="属性表集合"><a class="header-anchor" href="#属性表集合"></a>属性表集合</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">u2             attributes_count;<span class="comment">//此类的属性表中的属性数</span></span><br><span class="line">attribute_info attributes[attributes_count];<span class="comment">//属性表集合</span></span><br></pre></td></tr></table></figure><p>在 Class 文件中，字段表，方法表中都可以携带自己的属性表集合，以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同，属性表集合的限制稍微宽松一些，不再要求各个属性表具有严格的顺序，并且只要不与已有的属性名重复，任何人实现的编译器都可以向属性表中写 入自己定义的属性信息，Java 虚拟机运行时会忽略掉它不认识的属性。</p><p>《Java虚拟机规范》有一些预定属性，要求所有Java虚拟机实现都应该能识别这些属性，部分属性如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729162021636.png" alt="image-20240729162021636"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729162037395.png" alt="image-20240729162037395"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729162107799.png" alt="image-20240729162107799"></p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729162133065.png" alt="image-20240729162133065"></p><p>对于每一个属性，它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示，而属性值的结构则是完全自定义的，只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足下表中所定义的结构。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240729162201397.png" alt="image-20240729162201397"></p><h2 id="参考"><a class="header-anchor" href="#参考"></a>参考</h2><ul><li>《深入理解Java虚拟机》（第三版）</li><li><a href="https://javaguide.cn/java/jvm/class-file-structure.html">https://javaguide.cn/java/jvm/class-file-structure.html</a></li><li><a href="https://coolshell.cn/articles/9229.html">https://coolshell.cn/articles/9229.html</a></li><li><a href="https://javabetter.cn/jvm/class-file-jiegou.html">https://javabetter.cn/jvm/class-file-jiegou.html</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;简介&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#简介&quot;&gt;&lt;/a&gt;简介&lt;/h2&gt;
&lt;p&gt;在Java中，被 JVM 可以理解的代码称为&lt;code&gt;字节码&lt;/code&gt;，即扩展名为 &lt;code&gt;.class&lt;/code&gt; 的文件，这种文件被称为字节码文件，也可以称为类文件或者Class文件。&lt;/p&gt;
&lt;p&gt;Java使用Java编译器（javac）可以将Java代码</summary>
      
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="Java" scheme="https://blog.itwray.com/tags/Java/"/>
    
    <category term="JVM" scheme="https://blog.itwray.com/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>Java-JVM基础</title>
    <link href="https://blog.itwray.com/2024/06/18/java-jvm-basic/"/>
    <id>https://blog.itwray.com/2024/06/18/java-jvm-basic/</id>
    <published>2024-06-18T09:10:14.000Z</published>
    <updated>2024-11-20T07:26:23.577Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a class="header-anchor" href="#前言"></a>前言</h2><p>JVM是Java进阶之路中非常重要的一步，因此写下本文，用一篇文章对JVM知识点做一个总结。</p><p>JVM知识体系比较多，本文将采用“想到什么说什么“的思维编写，个人感觉这样更容易引发学习思考，面对有难度的知识点，可以由浅入深，一点点的了解。</p><p>最后，再大概了解完JVM的所有知识点后，再做一个核心知识点总结，用于整理归纳。</p><h2 id="JVM是什么"><a class="header-anchor" href="#JVM是什么"></a>JVM是什么</h2><p>首先，在听到一个新单词时，不禁会产生疑问，它是什么？所以JVM是什么呢？</p><p>JVM全名 Java Virtual Machine ，中文名 Java虚拟机。顾名思义，它是一个虚拟化的计算机。</p><p>Q：<strong>JVM作为虚拟化机器，能做什么呢？</strong></p><p>它能执行Java字节码，将Java字节码翻译成机器代码，供操作系统执行，实现Java“一次编译，到处运行“的特性。</p><p>Q：<strong>出现了新单词 - Java字节码，什么是Java字节码呢？</strong></p><p>Java字节码（Java Bytecode）是Java编程语言编译后生成的一种中间表示形式。它是与平台无关的<strong>二进制代码</strong>，JVM能够直接理解和执行它。反过来说，Java字节码需要JVM进行解释，才能够被机器直接执行。</p><p>Q：<strong>Java字节码是怎么产生的呢？</strong></p><p>它是由Java源代码（.java文件）经过Java编译器（javac）编译后生成的，生成的文件后缀名为 .class 。</p><blockquote><p>需要注意的是，Java字节码对于JVM来说都应该被叫做 JVM字节码 ，因为.class文件是为了便于JVM识别和执行的。只是因为JVM最初是为运行Java程序而设计的，因此Java语言是第一个也是最广泛使用的编译生成JVM字节码的语言。由于Java的普及和影响力，JVM字节码通常被称为“Java字节码”。虽然JVM最初是为Java设计的，但它已经发展成为一个多语言平台。许多其他编程语言（如Scala、Kotlin、Groovy、Clojure等）也可以编译成JVM字节码并在JVM上运行。因此，用“JVM字节码”这个术语更能反映现代JVM生态系统的多样性和广泛应用。</p></blockquote><p>Q：<strong>既然知道是通过JVM执行的Java字节码，那JVM的执行机制又是怎样的？</strong></p><p>这个问题有点偏面试的问法了，换成下面白话文的形式：</p><p>现在有如下一段代码，它是怎么运行起来的，JVM在执行Java字节码的过程中做了哪些操作？</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Hello</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;Hello&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>开发人员只看源代码的情况下，瞟一眼就知道系统执行后会输出“Hello”语句。</p><p>那么操作系统是如何知道要输出“Hello”语句的，就涉及到Java代码的执行过程了，执行过程一般分为编译期和运行时。</p><p>首先Java源代码（.java文件）经过编译器（javac）生成为Java字节码（.class文件），在 IDEA 上双击Hello.class文件，可以看到如下内容：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// Source code recreated from a .class file by IntelliJ IDEA</span></span><br><span class="line"><span class="comment">// (powered by   decompiler)</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">package</span> com.itwray.study.advance.jvm;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Hello</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Hello</span><span class="params">()</span> &#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;Hello&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过注释可以看出，IDEA 默认使用了 FernFlower 反编译工具将字节码文件反编译为我们看得懂的 Java 源代码。</p><p>那么，真实的.class文件是什么样子的呢，建议使用 Sublime Text 工具直接打开.class文件，打开后的文件内容如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240619165037574.png" alt="image-20240619165037574"></p><p>从右下角的 Binary 可以看出，Sublime Text 识别到.class文件为二进制文件，只不过在展示时将其转换为了十六进制。</p><p>有了字节码文件后，就可以启动JVM运行字节码文件了（启动方式的本质是使用<code>java</code>命令工具）。</p><p>JVM在运行时的执行过程如下：</p><ol><li><strong>类加载</strong>：通过类加载器加载Hello类。</li><li><strong>字节码验证</strong>：验证Hello.class文件的合法性。</li><li><strong>内存分配</strong>：在堆上为Hello类的对象分配内存。</li><li><strong>解释执行</strong>：JVM解释执行Hello的main方法中的字节码指令。</li><li><strong>即时编译</strong>：如果main方法是热点代码，JIT编译器将其编译为本地机器码，提升执行效率。</li><li><strong>输出结果</strong>：调用本地方法（java.io.FileOutputStream#writeBytes），通过JNI与操作系统交互，输出“Hello”到控制台。</li></ol><p>看完JVM的执行机制，突然发现了很多新词汇，在一个一个分析之前，需要先了解了解JVM的组成部分。</p><h2 id="JVM的组成"><a class="header-anchor" href="#JVM的组成"></a>JVM的组成</h2><p>Q：<strong>JVM的组成部分有哪些？</strong></p><p>JVM大致可以分为类加载器、运行时数据区、执行引擎三个部分，下面是它的组成图。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/20240624172938.png" alt="20240624172938"></p><p>其实在执行引擎之后还有两个组成部分，分别是本地方法接口（JNI）和本地方法库（Native Method Libraries）。可以从上图看出，JVM分析字节码文件的执行过程大致就是按照它的组成部分从上往下执行的。</p><p>类加载器是JVM运行Java程序的第一关，主要负责加载类文件（.class文件），如果类文件加载失败，就不会进入到运行时数据区和执行引擎了。类加载器将类文件加载到内存中后，会经过加载、连接、初始化三个主要阶段。</p><p>运行时数据区负责存储类的元数据、对象实例、方法调用信息和线程执行状态等。方法区存储类信息和静态数据，堆存储对象实例，Java虚拟机栈和本地方法栈分别管理方法调用和本地方法调用的状态信息，程序计数器记录当前执行的字节码指令地址。这些区域协同工作，确保Java程序能够高效、正确地执行。</p><p>执行引擎负责执行Java字节码，将Java字节码指令转换为机器指令，并执行这些指令。它的主要职责包括解释执行、即时编译（JIT）、垃圾回收、以及各种优化技术。</p><p>总结：在JVM中，类加载器负责加载类文件并生成<code>Class</code>对象，而为Class对象分配内存空间和初始化是由运行时数据区中的方法区完成的。执行引擎负责解释执行字节码或将其编译为本地机器码并执行。这些组件协同工作，确保类文件被正确加载、分配内存、初始化并执行。</p><h2 id="类加载机制"><a class="header-anchor" href="#类加载机制"></a>类加载机制</h2><p>Java虚拟机把描述类的数据从Class文件加载到内存，并对数据进行校验、转换解析和初始化，最终形成可以被虚拟机直接使用的Java类型，这个过程被称作虚拟机的类加载机制。</p><h3 id="类加载时机"><a class="header-anchor" href="#类加载时机"></a>类加载时机</h3><p>一个类型从被加载到虚拟机内存中开始，到卸载出内存为止，它的整个生命周期将会经历加载（Loading）、验证（Verification）、准备（Preparation）、解析（Resolution）、初始化（Initialization）、使用（Using）和卸载（Unloading）七个阶段，其中验证、准备、解析三个部分统称为连接（Linking）。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240710161054762.png" alt="image-20240710161054762"></p><h3 id="类加载过程"><a class="header-anchor" href="#类加载过程"></a>类加载过程</h3><h4 id="加载"><a class="header-anchor" href="#加载"></a>加载</h4><p>加载阶段负责<strong>将类文件从不同来源（如本地文件系统、网络等）加载到内存中，并生成类的<code>Class</code>对象</strong>。这个阶段包括以下步骤：</p><ul><li><strong>查找并加载类的二进制数据</strong>：类加载器首先在类路径（classpath）中查找类文件。如果找不到，会继续使用其他方式（如网络下载或自定义加载器）查找类文件。</li><li><strong>生成类的<code>Class</code>对象</strong>：将加载的类文件的二进制数据解析为JVM内部数据结构，并创建对应的<code>Class</code>对象。</li></ul><p>Q：<strong>查找类文件的依据是什么？或者说JVM使用什么数据查找的类文件？</strong></p><p>类加载器根据类的全限定名称来查找类文件。全限定名称包括包名和类名，例如：<code>com.example.MyClass</code>。</p><p>类的全限定名称通常映射到文件路径。例如：类<code>com.example.MyClass</code>映射到文件路径<code>com/example/MyClass.class</code>。</p><p>Q：<strong>加载类文件的来源一般有哪些？</strong></p><ul><li>Classpath：<ul><li>本地文件系统：通常类文件会放在本地文件系统的特定目录中，这些目录通过classpath设置。</li><li>JAR文件：类文件可以打包在JAR文件中，通过classpath包含这些JAR文件。</li></ul></li><li>网络：类加载器可以从网络上加载类文件，特别是自定义的类加载器可以从指定的URL或远程服务器上加载类文件。</li><li>内存：类文件可以直接从内存中加载。例如，一些框架会生成类文件的字节码并直接加载到JVM中。</li><li>其他存储：类文件可以存储在数据库中，某些类加载器可以从数据库中读取和加载类文件。</li></ul><p>Q：<strong>类加载器有哪些？</strong></p><p>以Java 8为例，类加载器一般分为4种：（前三个<code>ClassLoader</code>为内置类加载器）</p><ol><li>引导类加载器（Bootstrap ClassLoader）：加载核心Java类库（通常位于&lt;JAVA_HOME&gt;/lib目录下），如java.lang.*包中的类。</li><li>扩展类加载器（Extension ClassLoader）：加载扩展库（通常位于&lt;JAVA_HOME&gt;/lib/ext目录下）的类。</li><li>应用程序类加载器（AppClassLoader）：加载应用程序类路径（classpath）中的类，这是最常用的类加载器。</li><li>自定义类加载器：用户可以定义自己的类加载器，以便从特定位置或以特定方式加载类。</li></ol><p>Q：<strong>怎么确定类文件应该由哪个类加载器加载呢？</strong></p><p><code>ClassLoader</code>采用双亲委派模型搜索类和资源，<code>ClassLoader</code> 实例会在试图亲自查找类或资源之前，将搜索类或资源的任务委托给其父类加载器。委派机制如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240627173246953.png" alt="image-20240627173246953"></p><p>并且，双亲委派模型要求除了顶层的启动类加载器外，其余的类加载器都应有自己的父类加载器。</p><p>Java内置的三个类加载器就是按照 BootstrapClassLoader -&gt; ExtClassLoader -&gt; AppClassLoader 的层级设计的，BootstrapClassLoader作为顶层ClassLoader，是没有父类加载器的。</p><p>如果在代码中获取<code>ExtClassLoader</code>的parent ClassLoader，也会输出为空，因为BootstrapClassLoader是由C++实现的，这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的，所以拿到的结果是 null。</p><p>提示：双亲委派模型并非是Java强制的约束，只是一种官方推荐的方式，在自定义类加载器中，可以重写<code>C lassLoad</code>的<code>loadClass</code>方法改为不采用双亲委派模型的方式。不过为了避免类重复加载以及Java核心API的安全，一般不建议重写<code>loadClass</code>方法，而是重写<code>findClass</code>方法实现自定义类的加载机制。</p><p>Q：<strong>类加载器是怎么解析的类文件的二进制数据呢？</strong></p><p>这个问题涉及到类文件的详细结构，后面单独出章节分析。目前只需要记住类文件的组成结构有如下部分：魔数（Magic Number）、版本号（Version Number）、常量池（Constant Pool）、访问标志（Access Flags）、类索引、父类索引和接口索引集合、字段表（Fields）、方法表（Methods）、属性表（Attributes）。</p><p>并且，这个组成结构的顺序是固定的，下面是一个类文件的字节码结构组成部分：(u后面的数字表示占用的字节数)</p><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">ClassFile</span> &#123;</span><br><span class="line">    <span class="selector-tag">u4</span> <span class="selector-tag">magic</span>;                           <span class="comment">// 魔数</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">minor_version</span>;                   <span class="comment">// 次版本号</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">major_version</span>;                   <span class="comment">// 主版本号</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">constant_pool_count</span>;             <span class="comment">// 常量池计数</span></span><br><span class="line">    <span class="selector-tag">cp_info</span> <span class="selector-tag">constant_pool</span><span class="selector-attr">[constant_pool_count-1]</span>; <span class="comment">// 常量池</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">access_flags</span>;                    <span class="comment">// 访问标志</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">this_class</span>;                      <span class="comment">// 类索引</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">super_class</span>;                     <span class="comment">// 父类索引</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">interfaces_count</span>;                <span class="comment">// 接口计数</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">interfaces</span><span class="selector-attr">[interfaces_count]</span>;    <span class="comment">// 接口索引集合</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">fields_count</span>;                    <span class="comment">// 字段计数</span></span><br><span class="line">    <span class="selector-tag">field_info</span> <span class="selector-tag">fields</span><span class="selector-attr">[fields_count]</span>;    <span class="comment">// 字段表</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">methods_count</span>;                   <span class="comment">// 方法计数</span></span><br><span class="line">    <span class="selector-tag">method_info</span> <span class="selector-tag">methods</span><span class="selector-attr">[methods_count]</span>; <span class="comment">// 方法表</span></span><br><span class="line">    <span class="selector-tag">u2</span> <span class="selector-tag">attributes_count</span>;                <span class="comment">// 属性计数</span></span><br><span class="line">    <span class="selector-tag">attribute_info</span> <span class="selector-tag">attributes</span><span class="selector-attr">[attributes_count]</span>; <span class="comment">// 属性表</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Q：<strong>创建的<code>Class</code>对象被存储到哪了？</strong></p><p>在类加载阶段，生成的<code>Class</code>对象被存放在JVM的<strong>方法区</strong>中。方法区是JVM运行时数据区的一部分，用于存储类的元数据、常量、静态变量和即时编译器编译后的代码等。</p><p>而<code>Class</code>在运行时被实例化的对象则是存放在<strong>堆</strong>中，通过这个对象实例可以访问到类的元数据（即<code>Object#getClass()</code>方法）。</p><h4 id="连接"><a class="header-anchor" href="#连接"></a>连接</h4><p>类连接主要包括三个子步骤：验证（Verification）、准备（Preparation）和解析（Resolution）。这些步骤确保类文件格式正确，分配必要的内存，并将符号引用转换为直接引用。</p><h5 id="验证"><a class="header-anchor" href="#验证"></a>验证</h5><p>验证阶段中<strong>JVM执行了一系列详细的验证规则，以确保类文件的格式和内容符合JVM规范</strong>，从而保证运行时的安全性和稳定性。验证规则主要由四部分组成：</p><ol><li><strong>文件格式验证</strong>：确保类文件的格式正确。<ul><li><strong>魔数</strong>：类文件的前四个字节是否为<code>0xCAFEBABE</code>。</li><li><strong>版本号</strong>：主次版本号是否在当前JVM的处理范围之内。例如，使用JDK8 javac编译的字节码文件，是不能在JAVA7 java命令下运行的。</li><li><strong>常量池</strong>：检查常量池中的每个常量项是否符合类型和格式的要求。例如，<code>CONSTANT_Class_info</code>项的格式是否正确。</li><li><strong>常量池索引</strong>：确保常量池中的索引是有效的，不超出常量池的边界。</li></ul></li><li><strong>元数据验证</strong>：确保类文件的元数据（类的结构信息）符合JVM规范。<ul><li><strong>类声明</strong>：检查类的访问标志（如<code>public</code>, <code>final</code>, <code>abstract</code>等）是否合法，确保不能同时使用互斥的标志。</li><li><strong>父类和接口</strong>：检查类的父类是否存在并且可访问，确保类实现的接口合法。</li><li><strong>字段和方法</strong>：检查字段和方法的声明是否合法，包括访问修饰符、类型、名称等。</li><li><strong>方法签名</strong>：确保方法的签名合法，包括参数和返回值类型是否合法。</li></ul></li><li><strong>字节码验证</strong>：确保类文件中的字节码正确。<ul><li><strong>数据流分析</strong>：<ul><li>检查局部变量和操作数栈的使用是否合法，确保操作数栈的深度不超过最大限制。</li><li>确保所有变量在使用前已经初始化。</li><li>确保方法调用的参数类型和数量正确。</li></ul></li><li><strong>控制流分析</strong>：<ul><li>确保所有的跳转指令跳转到有效的字节码位置，不会跳转到中间的指令或无效的位置。</li><li>确保异常处理块（<code>try-catch-finally</code>）的范围合法，不会跨越方法边界。</li><li>确保方法中的所有路径都正确处理了异常，确保异常处理器的类型和捕获类型匹配。</li></ul></li></ul></li><li><strong>符号引用验证</strong>：确保常量池中的符号引用能够被解析为合法的直接引用。<ul><li><strong>类引用</strong>：检查常量池中的类引用是否合法，确保引用的类存在并且可访问。</li><li><strong>字段引用</strong>：检查常量池中的字段引用是否合法，确保引用的字段在相应的类或接口中存在并且可访问。</li><li><strong>方法引用</strong>：检查常量池中的方法引用是否合法，确保引用的方法在相应的类或接口中存在并且可访问。</li></ul></li></ol><h5 id="准备"><a class="header-anchor" href="#准备"></a>准备</h5><p>准备阶段主要是<strong>为类的所有静态变量分配内存，并将其初始化为默认值</strong>。</p><p><strong>准备阶段与初始化阶段的区别</strong></p><ul><li><strong>准备阶段</strong>：分配静态变量的内存并将其初始化为默认值。这个过程只涉及默认值的设置，不执行任何用户代码（如静态初始化块和静态变量的显式赋值）。</li><li><strong>初始化阶段</strong>：执行类的静态初始化块和静态变量的显式赋值。这个过程是在准备阶段之后进行的，确保所有静态变量已经分配好内存并设置了默认值。</li></ul><p>注意：</p><ul><li>这些内存都是在方法区进行分配的，如果是JDK8及以后，方法区的实现是元空间。</li><li>类变量此时的初始化是指默认值初始化，而不是用户定义的赋值。默认值如下：<ul><li>整数类型（如<code>int</code>、<code>short</code>、<code>byte</code>、<code>long</code>）：默认值为<code>0</code>。</li><li>浮点类型（如<code>float</code>、<code>double</code>）：默认值为<code>0.0</code>。</li><li>字符类型（<code>char</code>）：默认值为<code>\u0000</code>（null字符）。</li><li>布尔类型（<code>boolean</code>）：默认值为<code>false</code>。</li><li>引用类型（如对象引用）：默认值为<code>null</code>。（引用类型包括String、Integer、枚举等）</li></ul></li><li>如果类变量被 final 关键字修饰，那就需要根据变量类型决定其何时被初始化。如果是基本类型和字符串常量，则在准备阶段就会被初始化赋值；如果是引用类型（包括Integer、枚举、自定义类等），则还是在初始化阶段被赋值，准备阶段仍为null。</li></ul><h5 id="解析"><a class="header-anchor" href="#解析"></a>解析</h5><p>解析阶段是<strong>Java虚拟机将常量池内的符号引用替换为直接引用的过程</strong>。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。</p><p><strong>符号引用</strong>：<strong>符号引用以一组符号来描述所引用的目标，符号可以是任何形式的字面量，只要使用时能无歧义地定位到目标即可</strong>。各种虚拟机实现的内存布局可以各不相同，但是它们能接受的符号引用必须都是一致的，因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。</p><p><strong>直接引用</strong>：<strong>直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄</strong>。直接引用是和虚拟机实现的内存布局直接相关的，同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用，那引用的目标必定已经在虚拟机的内存中存在。</p><p>在解析阶段，因为需要将符号引用替换为直接引用，所以在此阶段可能会抛出各种异常，例如：</p><ul><li>ClassNotFoundException：无法找到所需的类或接口。</li><li>NoSuchFieldError：无法找到所需的字段。</li><li>NoSuchMethodError：无法找到所需的方法。</li><li>IllegalAccessError：类或接口、字段、方法解析时，发现不具备访问权限。</li><li>IncompatibleClassChangeError：解析过程中发现类的结构与预期不符。</li></ul><p>Q：<strong>用一句话总结连接过程三个阶段主要做了什么。</strong></p><p>验证阶段确保类文件的字节码是否符合JVM规范，准备阶段就是为类变量分配内存并初始化为默认值的过程，解析阶段是JVM将常量池中的符号引用替换为直接引用的过程。</p><p>Q：<strong>这三个阶段是按照顺序执行的吗？</strong></p><p>从类加载过程来说，是顺序执行的。但对《Java虚拟机规范》而言，加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的，类型的加载过程必须按照这种顺序按部就班地开始，而解析阶段则不一定：它在某些情况下可以在初始化阶段之后再开始，这是为了支持Java语言的运行时绑定特性（也称为动态绑定或晚期绑定）。顺序按部就班地开始表示这些阶段通常都是互相交叉地混合进行的，会在一个阶段执行的过程中调用、激活另一个阶段。</p><p>验证阶段保证了字节码的合法和安全，如果验证失败，整个类加载过程就会被中断，所以验证阶段是第一步。</p><p>准备阶段为静态变量分配内存并设置默认值，解析阶段是将常量池的符号引用替换为直接引用。从语义上来看，两个阶段在部分情况下可以并行执行，但是在JVM的实际实现中，为了保证类加载过程的逻辑清晰和实现简单，所以这两个阶段在实际情况下还是按照顺序执行的。</p><p>而至于为什么要把准备阶段放在解析阶段之前，主要是因为在解析过程中可能需要访问或验证静态变量的类型和内存布局。如果准备阶段未完成，这些信息可能不完整或不可用。所以解析阶段依赖于准备阶段。</p><h4 id="初始化"><a class="header-anchor" href="#初始化"></a>初始化</h4><p>初始化阶段是类加载过程的最后一个步骤，它的<strong>主要任务是执行类的初始化逻辑，即执行类构造器&lt;clinit&gt;()方法的过程</strong>。</p><p>&lt;clinit&gt;()并不是程序中编写的构造方法（实例的构造方法在JVM视角中称为&lt;init&gt;()方法），<strong>它是javac编译器自动收集类中的所有类变量的赋值动作和静态语句块（static {}块）中语句 合并产生的</strong>，编译器收集的顺序是由语句在源文件中出现的顺序决定的。</p><p>静态语句块中只能访问到定义在静态语句块之前的变量，定义在它之后的变量，在前面的静态语句块可以赋值，但是不能访问（不能直接使用变量名访问，但可以使用&lt;类名.变量名&gt;的方式访问）。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        i = <span class="number">0</span>; <span class="comment">// 给变量复制可以正常编译通过</span></span><br><span class="line">        System.out.print(i); <span class="comment">// 这句编译器会提示“非法向前引用”</span></span><br><span class="line">      System.out.println(InitializeDemo.i); <span class="comment">// 这句编译可以通过，并且可执行输出为0</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>&lt;clinit&gt;方法与类的构造方法不同，它不需要显式地调用父类&lt;clinit&gt;()方法，Java虚拟机会保证在子类的&lt;clinit&gt;()方法执行前，父类的&lt;clinit&gt;()方法已经执行完毕。因此在Java虚拟机中第一个被执行的&lt;clinit&gt;()方法的类型肯定是java.lang.Object。</p><p>由于父类的&lt;clinit&gt;()方法先执行，也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。例如如下示例中，输出的结果为2。</p><p>&lt;clinit&gt;()方法对于类或接口不是必需的，如果一个类中没有静态语句块和类变量赋值操作，那么javac编译器可以不为这个类生成&lt;clinit&gt;()方法。</p><p>接口虽然不能定义静态语句块，但可以有变量赋值操作，它属于类变量赋值操作。但接口与类不同，在初始化阶段，子接口不会先执行父接口的&lt;clinit&gt;()方法，只有当父接口定义的变量被使用时，父接口才会被初始化。此外，接口的实现类在初始化时也会执行接口的&lt;clinit&gt;()方法。</p><p>每个类的&lt;clinit&gt;()方法在类加载过程中只会执行一次，它通过对&lt;clinit&gt;()方法加锁实现，确保多线程环境中只会有其中一个线程去执行&lt;clinit&gt;()方法，其他线程都需要阻塞等待，直到活动线程的&lt;clinit&gt;()方法执行完毕。如果在类初始化过程中出现异常，该异常都会被封装成<code>ExceptionInInitializerError</code>异常抛出。</p><p>类的初始化阶段是惰性的，即在首次使用该类时才会触发。触发情况有如下几种：</p><ul><li>当创建类的新实例时（使用<code>new</code>关键字）。</li><li>当访问类的静态字段或调用静态方法时。</li><li>当反射机制调用类的方法时（例如，<code>Class.forName</code>）。</li><li>当初始化一个类的子类时，父类会先被初始化。</li><li>当虚拟机启动时，用户指定的主类会首先被初始化。</li></ul><p>因为初始化是惰性的，也间接说明了<strong>类在经过加载、连接阶段后，并不一定会马上执行初始化阶段</strong>。常见情况有如下几种：</p><ol><li><p>通过反射查询类信息，但不实际使用</p><p>使用反射机制查询类的信息，例如获取类的元数据（字段、方法等），这会触发类的加载和解析，但不会触发初始化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">        Class&lt;?&gt; clazz = Class.forName(<span class="string">&quot;Example&quot;</span>, <span class="literal">false</span>, Test.class.getClassLoader());</span><br><span class="line">        <span class="comment">// 仅查询类信息，不触发初始化</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>仅仅解析类而未实际访问</p><p>某些情况下，JVM在运行过程中可能会预解析类以提高性能，但如果这些类没有被实际使用，则不会进入初始化阶段。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        Class&lt;?&gt; clazz = Example.class;  <span class="comment">// 仅解析类，不触发初始化</span></span><br><span class="line">        System.out.println(<span class="string">&quot;Main method&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>引用常量</p><p>使用类的常量字段（<code>final static</code> 修饰的基本类型或字符串）时，只会触发类的加载和解析，不会触发初始化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Example</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">CONST</span> <span class="operator">=</span> <span class="number">42</span>;</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;Example class static block&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Test</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">value</span> <span class="operator">=</span> Example.CONST;  <span class="comment">// 不会触发初始化</span></span><br><span class="line">        System.out.println(value);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ol><h3 id="类加载器"><a class="header-anchor" href="#类加载器"></a>类加载器</h3><p>Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现，以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”（Class Loader）。</p><h4 id="类与类加载器"><a class="header-anchor" href="#类与类加载器"></a>类与类加载器</h4><p>对于任意一个类，都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性，每一个类加载器，都拥有一个独立的类名称空间。</p><p>换一句话说就是：比较两个类是否“相等”，只有在这两个类是由同一个类加载器加载的前提下才有意义，否则，即使这两个类来源于同一个Class文件，被同一个Java虚拟机加载，只要加载它们的类加载器不同，那这两个类就必定不相等。</p><p>这里所指的“相等”，包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果，也包括了使用instanceof关键字做对象所属关系判定等各种情况。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 类加载器与instanceof关键字演示</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ClassLoaderTest</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">ClassLoader</span> <span class="variable">myLoader</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassLoader</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> Class&lt;?&gt; loadClass(String name) <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="type">String</span> <span class="variable">fileName</span> <span class="operator">=</span> name.substring(name.lastIndexOf(<span class="string">&quot;.&quot;</span>) + <span class="number">1</span>) + <span class="string">&quot;.class&quot;</span>;</span><br><span class="line">                    <span class="type">InputStream</span> <span class="variable">is</span> <span class="operator">=</span> getClass().getResourceAsStream(fileName);</span><br><span class="line">                    <span class="keyword">if</span> (is == <span class="literal">null</span>) &#123;</span><br><span class="line">                        <span class="keyword">return</span> <span class="built_in">super</span>.loadClass(name);</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="type">byte</span>[] b = <span class="keyword">new</span> <span class="title class_">byte</span>[is.available()];</span><br><span class="line">                    is.read(b);</span><br><span class="line">                    <span class="keyword">return</span> defineClass(name, b, <span class="number">0</span>, b.length);</span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">ClassNotFoundException</span>(name);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">        <span class="type">Object</span> <span class="variable">obj</span> <span class="operator">=</span> myLoader.loadClass(<span class="string">&quot;com.itwray.study.advance.jvm.ClassLoaderTest&quot;</span>).newInstance();</span><br><span class="line">        <span class="comment">// 输出结果：class com.itwray.study.advance.jvm.ClassLoaderTest</span></span><br><span class="line">        System.out.println(obj.getClass());</span><br><span class="line">        <span class="comment">// 输出结果：false</span></span><br><span class="line">        System.out.println(obj <span class="keyword">instanceof</span> com.itwray.study.advance.jvm.ClassLoaderTest);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="双亲委派模型"><a class="header-anchor" href="#双亲委派模型"></a>双亲委派模型</h4><p>JVM中内置了三个<code>ClassLoader</code>：</p><ul><li><strong><code>BootstrapClassLoader</code>(启动类加载器)</strong>：最顶层的加载类，由 C++实现，通常表示为 null，并且没有父级，主要用来加载 JDK 内部的核心类库（ <code>%JAVA_HOME%/lib</code>目录下的 <code>rt.jar</code>、<code>resources.jar</code>、<code>charsets.jar</code>等 jar 包和类）以及被 <code>-Xbootclasspath</code>参数指定的路径下的所有类。</li><li><strong><code>ExtensionClassLoader</code>(扩展类加载器)</strong>：主要负责加载 <code>%JAVA_HOME%/lib/ext</code> 目录下的 jar 包和类以及被 <code>java.ext.dirs</code> 系统变量所指定的路径下的所有类。</li><li><strong><code>AppClassLoader</code>(应用程序类加载器)</strong>：面向开发者的加载器，负责加载当前应用 classpath 下的所有 jar 包和类。如果应用程序中没有自定义过自己的类加载器，一般情况下这个就是程序中默认的类加载器。</li></ul><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240627173246953.png" alt="image-20240627173246953"></p><p>上图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型（Parents DelegationModel）”。双亲委派模型要求除了顶层的启动类加载器外，其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承（Inheritance）的关系来实现的，而是通常使用组合（Composition）关系来复用父加载器的代码。</p><p>双亲委派模型的工作过程是：如果一个类加载器收到了类加载的请求，它首先不会自己去尝试加载这个类，而是把这个请求委派给父类加载器去完成，每一个层次的类加载器都是如此，因此所有的加载请求最终都应该传送到最顶层的启动类加载器中，只有当父加载器反馈自己无法完成这个加载请求（它的搜索范围中没有找到所需的类）时，子加载器才会尝试自己去完成加载。</p><p>双亲委派模型的好处是当一个类处于包含多个类加载器的JVM环境下时，可以保证加载出来的都是同一个类。即一个全限定名的类，在一个JVM下加载多次得到的Class类对象是相等的。</p><p>例如，手写一个rt.jar类库下的<code>java.lang.String</code>类，当通过<code>Class.forName()</code>方法加载时，并没有执行手写的String类的static块代码，说明没有加载这个类，而是加载的rt.jar类库下的String类。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> java.lang;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">String</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;custom String static method&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">StringDemo</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">        Class&lt;?&gt; stringClass = Class.forName(<span class="string">&quot;java.lang.String&quot;</span>);</span><br><span class="line">        System.out.println(stringClass.getName());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>双亲委派模型的实现位于java.lang.ClassLoader的loadClass()方法中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 源码来自jdk8</span></span><br><span class="line"><span class="keyword">protected</span> Class&lt;?&gt; loadClass(String name, <span class="type">boolean</span> resolve)</span><br><span class="line">        <span class="keyword">throws</span> ClassNotFoundException</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (getClassLoadingLock(name)) &#123;</span><br><span class="line">        <span class="comment">// First, check if the class has already been loaded</span></span><br><span class="line">        Class&lt;?&gt; c = findLoadedClass(name);</span><br><span class="line">        <span class="keyword">if</span> (c == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="type">long</span> <span class="variable">t0</span> <span class="operator">=</span> System.nanoTime();</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="keyword">if</span> (parent != <span class="literal">null</span>) &#123;</span><br><span class="line">                    c = parent.loadClass(name, <span class="literal">false</span>);</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    c = findBootstrapClassOrNull(name);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">              <span class="comment">// 如果父类加载器抛出ClassNotFoundException，说明父类加载器无法完成加载请求</span></span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (c == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 在父类加载器无法加载时，再调用本身的findClass方法来进行类加载</span></span><br><span class="line">                <span class="type">long</span> <span class="variable">t1</span> <span class="operator">=</span> System.nanoTime();</span><br><span class="line">                c = findClass(name);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// this is the defining class loader; record the stats</span></span><br><span class="line">                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);</span><br><span class="line">                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);</span><br><span class="line">                sun.misc.PerfCounter.getFindClasses().increment();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (resolve) &#123;</span><br><span class="line">            resolveClass(c);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> c;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="模块化下的类加载器"><a class="header-anchor" href="#模块化下的类加载器"></a>模块化下的类加载器</h3><p>JDK 9 之后为了适应模块化的发展，类加载器做了如下变化：</p><ul><li>仍维持三层类加载器和双亲委派的架构，但扩展类加载器被平台类加载器所取代；</li><li>当平台及应用程序类加载器收到类加载请求时，要首先判断该类是否能够归属到某一个系统模块中，如果可以找到这样的归属关系，就要优先委派给负责那个模块的加载器完成加载；</li><li>启动类加载器、平台类加载器、应用程序类加载器全部继承自 <code>java.internal.loader.BuiltinClassLoader</code> ，BuiltinClassLoader 中实现了新的模块化架构下类如何从模块中加载的逻辑，以及模块中资源可访问性的处理。</li></ul><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240710173108681.png" alt="image-20240710173108681"></p><h3 id="总结"><a class="header-anchor" href="#总结"></a>总结</h3><p>JVM类加载机制是Java程序运行的基础，它通过加载、连接（验证、准备、解析）和初始化阶段将类文件动态加载到内存中，并通过双亲委派模型确保了类加载的安全性和一致性。</p><h2 id="运行时数据区"><a class="header-anchor" href="#运行时数据区"></a>运行时数据区</h2><p>运行时数据区隶属于Java 内存区域的一部分，主要讲述Java虚拟机对于内存区域的划分，这些区域有各自的用途，以及创建和销毁的时间，有的区域随着虚拟机进程的启动而一直存在，有些区域则是依赖用户线程的启动和结束而建立和销毁。</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240712152559344.png" alt="image-20240712152559344"></p><p>其中方法区和堆属于所有线程共享的数据区，而虚拟机栈、本地方法栈、程序计数器是线程隔离的数据区，也就是说隔离的数据区保证每个线程下都有对应的虚拟机栈、本地方法栈和程序计数器。</p><h3 id="程序计数器"><a class="header-anchor" href="#程序计数器"></a>程序计数器</h3><p>程序计数器（Program Counter Register）是一块较小的内存空间，它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里，字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令，它是程序控制流的指示器，分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。</p><p>由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的，在任何一个确定的时刻，一个处理器（对于多核处理器来说是一个内核）都只会执行一条线程中的指令。因此，为了线程切换后能恢复到正确的执行位置，每条线程都需要有一个独立的程序计数器，各条线程之间计数器互不影响，独立存储，我们称这类内存区域为“线程私有”的内存。</p><p>如果线程正在执行的是一个Java方法，这个计数器记录的是正在执行的虚拟机字节码指令的地址；如果正在执行的是本地（Native）方法，这个计数器值则应为空（Undefined）。</p><p>此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。</p><h3 id="Java虚拟机栈"><a class="header-anchor" href="#Java虚拟机栈"></a>Java虚拟机栈</h3><p>Java虚拟机栈（Java Virtual Machine Stack）又称为JVM栈，它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型：每个方法被执行的时候，Java虚拟机都会同步创建一个栈帧（Stack Frame）用于存储局部变量表、操作数栈、动态连接、方法出口等信息。</p><p>每一个方法被调用直至执行完毕的过程，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。</p><h3 id="本地方法栈"><a class="header-anchor" href="#本地方法栈"></a>本地方法栈</h3><p>本地方法栈（Native Method Stacks）与虚拟机栈所发挥的作用是非常相似的，其区别只是虚拟机栈为虚拟机执行Java方法（也就是字节码）服务，而本地方法栈则是为虚拟机使用到的本地（Native）方法服务。</p><p>与虚拟机栈一样，本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。</p><h3 id="Java堆"><a class="header-anchor" href="#Java堆"></a>Java堆</h3><p>Java堆（Java Heap）是虚拟机所管理的内存中最大的一块，它是被所有线程共享的一块内存区域，在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例，Java世界里“几乎”所有的对象实例都在这里分配内存。</p><p>Java堆是垃圾收集器管理的内存区域，它可以处于物理上不连续的内存空间中，但在逻辑上它应该被视为是连续的。</p><p>Java堆既可以被实现成固定大小的，也可以是可扩展的，不过当前主流的Java虚拟机都是按照可扩展来实现的（通过参数-Xmx和-Xms设定）。</p><p>如果在Java堆中没有内存完成实例分配，并且堆也无法再扩展时，Java虚拟机将会抛出OutOfMemoryError异常。</p><h3 id="方法区"><a class="header-anchor" href="#方法区"></a>方法区</h3><p>方法区（Method Area）与Java堆一样，是各个线程共享的内存区域，它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。</p><p>JDK 8 以后的方法区实现已经不再是永久代（Permanent Generation）了，而是使用元空间（Metaspace）来实现。</p><p>方法区也是可以存在垃圾收集的行为的，不过这个区域的回收效果一般微乎其微。因此，如果方法区无法满足新的内存分配需求时，同样会抛出 OutOfMemoryError 异常。</p><p>运行时常量池（Runtime Constant Pool）也是方法区的一部分，用于存放常量池表（Constant Pool Table），常量池表中存放了编译期生成的各种符号字面量和符号引用。</p><h2 id="字节码执行引擎"><a class="header-anchor" href="#字节码执行引擎"></a>字节码执行引擎</h2><p>JVM字节码执行引擎是Java虚拟机的核心组件之一，它负责执行已加载到内存中的Java字节码，并将其转换为具体的机器指令以执行程序。执行引擎的主要任务包括解释执行字节码、JIT编译、垃圾回收和线程调度等。</p><h3 id="解释执行字节码"><a class="header-anchor" href="#解释执行字节码"></a>解释执行字节码</h3><p>JVM字节码是一种与平台无关的中间表示形式。解释执行是将字节码逐条转换为相应的机器指令并执行。</p><p>字节码解释器：</p><ul><li>JVM内置的字节码解释器逐条读取字节码指令并执行相应的操作。</li><li>解释执行通常较慢，因为每条指令都需要解析和解释。</li></ul><h3 id="JIT（Just-In-Time）编译"><a class="header-anchor" href="#JIT（Just-In-Time）编译"></a>JIT（Just-In-Time）编译</h3><p>为了提高执行效率，JVM使用即时编译技术，将热点代码（被频繁执行的代码）编译为本地机器码，直接在硬件上运行。</p><ul><li>即时编译器（JIT Compiler）：<ul><li><strong>C1编译器</strong>：注重编译速度，用于编译简单和不太频繁的代码。</li><li><strong>C2编译器</strong>：注重优化性能，用于编译频繁执行的热点代码。</li></ul></li><li><strong>热点探测</strong>：JVM通过计数器统计方法的调用次数或循环次数，以识别热点代码。</li><li><strong>编译优化</strong>：包括内联、循环展开、逃逸分析等，进一步提高执行效率。</li></ul><h3 id="垃圾回收（Garbage-Collection）"><a class="header-anchor" href="#垃圾回收（Garbage-Collection）"></a>垃圾回收（Garbage Collection）</h3><p>JVM自动管理内存分配和回收，执行引擎中的垃圾回收器负责清理不再使用的对象，释放内存。</p><ul><li>垃圾回收算法：<ul><li><strong>标记-清除算法</strong>：标记所有可达对象，清除未标记对象。</li><li><strong>复制算法</strong>：将存活对象复制到新空间，清空旧空间。</li><li><strong>标记-压缩算法</strong>：标记所有可达对象，将存活对象压缩到一端，清除其他空间。</li></ul></li><li>垃圾回收器：<ul><li><strong>Serial GC</strong>：单线程垃圾回收器，适用于小型应用。</li><li><strong>Parallel GC</strong>：多线程垃圾回收器，适用于多核处理器。</li><li><strong>CMS GC</strong>：并发标记-清除垃圾回收器，减少停顿时间。</li><li><strong>G1 GC</strong>：分代垃圾回收器，平衡停顿时间和吞吐量。</li></ul></li></ul><h3 id="线程管理"><a class="header-anchor" href="#线程管理"></a>线程管理</h3><p>JVM执行引擎负责管理Java线程的生命周期，包括线程的创建、调度和销毁。</p><ul><li>线程调度：<ul><li>JVM使用操作系统的线程调度机制来管理Java线程。</li><li>线程的状态包括新建、就绪、运行、阻塞、等待和终止。</li></ul></li><li>同步和并发：<ul><li>JVM提供了关键字<code>synchronized</code>和<code>volatile</code>，以及<code>java.util.concurrent</code>包，支持多线程编程和并发控制。</li></ul></li></ul><h3 id="方法调用和返回"><a class="header-anchor" href="#方法调用和返回"></a>方法调用和返回</h3><p>执行引擎负责处理Java方法的调用和返回，包括静态方法、实例方法、构造方法等。</p><ul><li>方法调用：<ul><li><strong>静态绑定</strong>：在编译时确定调用的方法（如静态方法和私有方法）。</li><li><strong>动态绑定</strong>：在运行时根据对象的实际类型确定调用的方法（如实例方法）。</li></ul></li><li><strong>方法返回</strong>：处理方法的返回值和返回指令，管理方法调用栈帧的创建和销毁。</li></ul><h3 id="异常处理"><a class="header-anchor" href="#异常处理"></a>异常处理</h3><p>执行引擎处理Java程序中的异常，包括捕获和抛出异常。</p><ul><li><strong>异常捕获</strong>：使用<code>try-catch</code>块捕获异常。</li><li><strong>异常抛出</strong>：使用<code>throw</code>语句抛出异常。</li><li><strong>异常处理机制</strong>：遍历调用栈，查找匹配的异常处理器。</li></ul><h3 id="本地方法调用"><a class="header-anchor" href="#本地方法调用"></a>本地方法调用</h3><p>JVM执行引擎通过本地方法接口（JNI）调用本地代码，实现与平台相关的功能。</p><ul><li><strong>JNI（Java Native Interface）</strong>：允许Java程序调用本地C/C++代码。</li><li><strong>本地方法库</strong>：加载和执行本地方法库（如<code>.dll</code>或<code>.so</code>文件）。</li></ul><h3 id="总结-2"><a class="header-anchor" href="#总结-2"></a>总结</h3><p>JVM字节码执行引擎主要包括以下功能：</p><ol><li><strong>解释执行字节码</strong>：逐条解释和执行字节码指令。</li><li><strong>JIT编译</strong>：将热点代码编译为本地机器码，提高执行效率。</li><li><strong>垃圾回收</strong>：自动管理内存，回收不再使用的对象。</li><li><strong>线程管理</strong>：管理Java线程的生命周期和调度。</li><li><strong>方法调用和返回</strong>：处理方法的调用、执行和返回。</li><li><strong>异常处理</strong>：捕获和处理Java异常。</li><li><strong>本地方法调用</strong>：通过JNI调用本地代码。</li></ol><h2 id="参考"><a class="header-anchor" href="#参考"></a>参考</h2><ul><li>《深入理解Java虚拟机》（第三版）</li><li><a href="https://javaguide.cn/java/jvm/class-file-structure.html">https://javaguide.cn/java/jvm/class-file-structure.html</a></li><li><a href="https://coolshell.cn/articles/9229.html">https://coolshell.cn/articles/9229.html</a></li><li><a href="https://javabetter.cn/jvm/class-file-jiegou.html">https://javabetter.cn/jvm/class-file-jiegou.html</a></li></ul>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;
&lt;p&gt;JVM是Java进阶之路中非常重要的一步，因此写下本文，用一篇文章对JVM知识点做一个总结。&lt;/p&gt;
&lt;p&gt;JVM知识体系比较多，本文将采用“想到什么说什么“的思维编写，个人感觉这样更容易引发学习思考，面对有难度的知识点，可以由浅入深，一点点的了解。&lt;/p&gt;
&lt;p&gt;最后，再</summary>
      
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="Java" scheme="https://blog.itwray.com/tags/Java/"/>
    
    <category term="JVM" scheme="https://blog.itwray.com/tags/JVM/"/>
    
  </entry>
  
  <entry>
    <title>再见SpringFox，你好SpringDoc</title>
    <link href="https://blog.itwray.com/2024/03/04/springdoc-hello/"/>
    <id>https://blog.itwray.com/2024/03/04/springdoc-hello/</id>
    <published>2024-03-04T07:35:59.000Z</published>
    <updated>2024-09-27T13:14:17.050Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a class="header-anchor" href="#前言"></a>前言</h2><p>最近项目使用 SpringBoot 3 + Spring 6 搭建，接口文档准备一如既往的使用 Swagger 自动生成，引入 <code>springfox-boot-starter</code> 依赖，配置好相关的 Swagger 配置，结果启动报错，修改配置后启动不报错了，但是访问 swagger-ui/index.html 页面报404。</p><h2 id="问题分析"><a class="header-anchor" href="#问题分析"></a>问题分析</h2><p>一顿分析过后发现，最新的 springfox 3.0.0 （最后一次维护在2020年）仅支持 Spring 5.x，要想使用 springfox，最简单快捷的方式就是降版本，将 SpringBoot 的版本号从 3.x 修改为 2.7.x（或更低版本）。</p><p>新项目就是想体验最新的 SpringBoot 版本功能，这样一搞，岂不是本末倒置了，因此我就尝试了各种方法，企图通过修改 Spring Bean 属性等方式适配 Swagger ，结果发现 Spring 里面的对象属性是一层嵌一层，牵一发而动全身，菜鸡的我只好放弃。</p><p>企图尝试：</p><ol><li>注册 WebMvcConfigurer Bean，重写 <code>configurePathMatch(PathMatchConfigurer configurer)</code> 方法。</li><li>在 PathMatchConfigurer 对象中，修改 patternParser 属性为 null ，再修改 pathMatcher 属性不为空，使得 patternParser 为空并且 preferPathMatcher 等于 true。</li><li>从而会影响 WebMvcConfigurationSupport#initHandlerMapping 方法的条件判断，进而影响 AbstractHandlerMapping 的 patternParser 属性为空，而 pathMatcher 不为空。</li></ol><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304165058399.png" alt="image-20240304165058399"></p><ol start="4"><li>AbstractHandlerMapping 的实例对象为 RequestMappingHandlerMapping ，其 afterPropertiesSet() 方法会根据 patternParser 属性作为条件判断，进而 RequestMappingInfo.BuilderConfiguration config 的 patternParser 属性为空并且 pathMatcher 属性不为空。</li></ol><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304165617376.png" alt="image-20240304165617376"></p><ol start="5"><li>RequestMappingInfo.DefaultBuilder#build() 方法会调用 RequestMappingInfo.BuilderConfiguration#getPatternParserToUse() 方法，该方法会返回上一步配置的 patternParser 属性。从而影响 build() 方法的条件判断，实例化 PatternsRequestCondition 对象，最后在实例化 RequestMappingInfo 对象时，pathPatternsCondition 为空，而 patternsCondition 不为空。</li></ol><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304165953003.png" alt="image-20240304165953003"></p><ol start="6"><li>然后，springfox 的 WebMvcRequestHandler#getPatternsCondition() 方法会拿取 RequestMappingInfo 的 patternsCondition 属性，作为参数实例化 WebMvcPatternsRequestConditionWrapper 对象。</li></ol><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304170619406.png" alt="image-20240304170619406"></p><ol start="7"><li>最后，WebMvcPatternsRequestConditionWrapper#getPatterns() 方法在使用 PatternsRequestCondition 属性时就不会出现空指针情况，程序正常启动。</li></ol><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304170853408.png" alt="image-20240304170853408"></p><h2 id="问题突破"><a class="header-anchor" href="#问题突破"></a>问题突破</h2><p>源码分析告一段落后，我就开始在网络遨游，寻找广大群众的力量，最终在 stackoverflow 上找到了解决方案，也是在此处第一次了解到 springdoc-openapi 。</p><p>先说解决方法的最核心东西，引入以下 maven 依赖。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springdoc<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>springdoc-openapi-starter-webmvc-ui<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.x.x<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>注意</strong>：如果是 Spring Boot 1.x 或 2.x 的项目，springdoc-openapi 需要使用 1.x 的版本。</p><p>stackoverflow 帖子的链接地址：<a href="https://stackoverflow.com/questions/74614369/how-to-run-swagger-3-on-spring-boot-3">How to run Swagger 3 on Spring Boot 3</a></p><p>既然知道了可以使用 springdoc-openapi 解决，那就直接进入官方查看怎么使用。官方地址：<a href="https://springdoc.org/">springdoc.org</a></p><p>映入眼帘的介绍，让人为之心动，支持 SpringBoot 3 和 Swagger-ui ！！！</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304171925138.png" alt="image-20240304171925138"></p><h2 id="使用SpringDoc-Openapi"><a class="header-anchor" href="#使用SpringDoc-Openapi"></a>使用SpringDoc Openapi</h2><p>第一步，引入 maven 依赖。</p><p>第二步，同 Swagger 一样，编写配置类，主要是 <code>GroupedOpenApi</code> 和 <code>OpenAPI</code> 两个类，详见 <a href="https://springdoc.org/#migrating-from-springfox">从 SpringFox 迁移</a> 。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> GroupedOpenApi <span class="title function_">publicApi</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> GroupedOpenApi.builder()</span><br><span class="line">            .group(<span class="string">&quot;springshop-public&quot;</span>)</span><br><span class="line">            .pathsToMatch(<span class="string">&quot;/public/**&quot;</span>)</span><br><span class="line">            .build();</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> GroupedOpenApi <span class="title function_">adminApi</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> GroupedOpenApi.builder()</span><br><span class="line">            .group(<span class="string">&quot;springshop-admin&quot;</span>)</span><br><span class="line">            .pathsToMatch(<span class="string">&quot;/admin/**&quot;</span>)</span><br><span class="line">            .addOpenApiMethodFilter(method -&gt; method.isAnnotationPresent(Admin.class))</span><br><span class="line">            .build();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> OpenAPI <span class="title function_">springShopOpenAPI</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">OpenAPI</span>()</span><br><span class="line">            .info(<span class="keyword">new</span> <span class="title class_">Info</span>().title(<span class="string">&quot;SpringShop API&quot;</span>)</span><br><span class="line">            .description(<span class="string">&quot;Spring shop sample application&quot;</span>)</span><br><span class="line">            .version(<span class="string">&quot;v0.0.1&quot;</span>)</span><br><span class="line">            .license(<span class="keyword">new</span> <span class="title class_">License</span>().name(<span class="string">&quot;Apache 2.0&quot;</span>).url(<span class="string">&quot;http://springdoc.org&quot;</span>)))</span><br><span class="line">            .externalDocs(<span class="keyword">new</span> <span class="title class_">ExternalDocumentation</span>()</span><br><span class="line">            .description(<span class="string">&quot;SpringShop Wiki Documentation&quot;</span>)</span><br><span class="line">            .url(<span class="string">&quot;https://springshop.wiki.github.org/docs&quot;</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果项目仅有一个 <code>GroupedOpenApi</code> 配置类，那么可以直接配置在 application.properties 下，例如：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">springdoc.packagesToScan</span>=<span class="string">package1, package2</span></span><br><span class="line"><span class="attr">springdoc.pathsToMatch</span>=<span class="string">/v1, /api/balance/**</span></span><br></pre></td></tr></table></figure><p>第三步，对于 API Interface 和 API Model ，springdoc-openapi 与 swagger-annotation 略有不同，替换方案如下：</p><p><img src="https://itwray.oss-cn-heyuan.aliyuncs.com/img/image-20240304173127941.png" alt="image-20240304173127941"></p><p>其中 <code>@Schema</code> 注解等同于 Swagger 的 <code>@ApiModel</code> 和 <code>@ApiModelProperty</code> 两个注解，不过在使用 <code>@Schema</code> 时需要注解，注解的 name 属性等同于 <code>@ApiModel</code> 的 value ，注解的 title 属性等同于 <code>@ApiModelProperty</code> 的 value 。</p><p>至此，springdoc-openapi 就配置完毕了，启动项目，访问 http://server:port/context-path/swagger-ui.html 。</p><p>springdoc-openapi 支持自定义访问路径，修改属性配置如下：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># swagger-ui custom path</span></span><br><span class="line"><span class="attr">springdoc.swagger-ui.path</span>=<span class="string">/doc.html</span></span><br></pre></td></tr></table></figure><p>修改后，访问 http://server:port/context-path/doc.html 也是一样的效果，它都会转发为最终的地址 http://server:port/context-path/swagger-ui/index.html 。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;
&lt;p&gt;最近项目使用 SpringBoot 3 + Spring 6 搭建，接口文档准备一如既往的使用 Swagger 自动生成，引入 &lt;code&gt;springfox-boot-starter&lt;/code&gt; 依赖，配置好相关的 Swagger 配置，结果启动报错，修改配置后启动不报</summary>
      
    
    
    
    <category term="Java" scheme="https://blog.itwray.com/categories/Java/"/>
    
    
    <category term="springdoc" scheme="https://blog.itwray.com/tags/springdoc/"/>
    
    <category term="swagger-ui" scheme="https://blog.itwray.com/tags/swagger-ui/"/>
    
  </entry>
  
  <entry>
    <title>Git学习-Git内部原理</title>
    <link href="https://blog.itwray.com/2024/01/23/git-study-10/"/>
    <id>https://blog.itwray.com/2024/01/23/git-study-10/</id>
    <published>2024-01-23T09:02:49.000Z</published>
    <updated>2024-09-27T13:14:24.205Z</updated>
    
    <content type="html"><![CDATA[<h2 id="底层命令与上层命令"><a class="header-anchor" href="#底层命令与上层命令"></a>底层命令与上层命令</h2><p>Git 最初是一套面向版本控制系统的工具集，而不是一个完整的、用户友好的版本控制系统， 所以它还包含了一部分用于完成底层工作的子命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起，抑或藉由脚本调用，来完成工作。 这部分命令一般被称作“底层（plumbing）”命令，而那些更友好的命令则被称作“上层（porcelain）”命令。</p><p>前面的章节中，探讨的都是上层命令，而在本章中，我们将主要面对底层命令。 因为，底层命令得以让你窥探 Git 内部的工作机制，也有助于说明 Git 是如何完成工作的，以及它为何如此运作。 多数底层命令并不面向最终用户：它们更适合作为新工具的组件和自定义脚本的组成部分。</p><p>当在一个新目录或已有目录执行 <code>git init</code> 时，Git 会创建一个 <code>.git</code> 目录。 这个目录包含了几乎所有 Git 存储和操作的东西。 如若想备份或复制一个版本库，只需把这个目录拷贝至另一处即可。 本章探讨的所有内容，均位于这个目录内。 新初始化的 <code>.git</code> 目录的典型结构如下：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">ls</span> -F1</span></span><br><span class="line">config</span><br><span class="line">description</span><br><span class="line">HEAD</span><br><span class="line">hooks/</span><br><span class="line">info/</span><br><span class="line">objects/</span><br><span class="line">refs/</span><br></pre></td></tr></table></figure><p><code>description</code> 文件仅供 GitWeb 程序使用，我们无需关心。 <code>config</code> 文件包含项目特有的配置选项。 <code>info</code> 目录包含一个全局性排除（global exclude）文件， 用以放置那些不希望被记录在 <code>.gitignore</code> 文件中的忽略模式（ignored patterns）。 <code>hooks</code> 目录包含客户端或服务端的钩子脚本（hook scripts）。</p><p>剩下的四个条目很重要：<code>HEAD</code> 文件、（尚待创建的）<code>index</code> 文件，和 <code>objects</code> 目录、<code>refs</code> 目录。 它们都是 Git 的核心组成部分。 <code>objects</code> 目录存储所有数据内容；<code>refs</code> 目录存储指向数据（分支、远程仓库和标签等）的提交对象的指针； <code>HEAD</code> 文件指向目前被检出的分支；<code>index</code> 文件保存暂存区信息。</p><h2 id="Git-对象"><a class="header-anchor" href="#Git-对象"></a>Git 对象</h2><p>Git 是一个内容寻址文件系统，Git 的核心部分是一个简单的键值对数据库（key-value data store）。 你可以向 Git 仓库中插入任意类型的内容，它会返回一个唯一的键，通过该键可以在任意时刻再次取回该内容。</p><p>在一个 Git 版本库（尽量使用刚初始化的新版本库）下，用 <code>git hash-object</code> 创建一个新的数据对象并将它手动存入 Git 数据库中：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;test content&#x27;</span> | git hash-object -w --stdin</span></span><br><span class="line">d670460b4b4aece5915caf5c68d12f560a9fe3e4</span><br></pre></td></tr></table></figure><p>在这种最简单的形式中，<code>git hash-object</code> 会接受你传给它的东西，而它只会返回可以存储在 Git 仓库中的唯一键。 <code>-w</code> 选项会指示该命令不要只返回键，还要将该对象写入数据库中。 最后，<code>--stdin</code> 选项则指示该命令从标准输入读取内容；若不指定此选项，则须在命令尾部给出待存储文件的路径。</p><p>此命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值——一个将待存储的数据外加一个头部信息（header）一起做 SHA-1 校验运算而得的校验和。</p><p>使用 <code> find .git/objects -type f</code> 查看 Git 存储数据。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">find .git/objects -<span class="built_in">type</span> f</span></span><br><span class="line">.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4</span><br></pre></td></tr></table></figure><p>如果你再次查看 <code>objects</code> 目录，那么可以在其中找到一个与新内容对应的文件。 这就是开始时 Git 存储内容的方式——一个文件对应一条内容， 以该内容加上特定头部信息一起的 SHA-1 校验和为文件命名。 校验和的前两个字符用于命名子目录，余下的 38 个字符则用作文件名。</p><p>一旦你将内容存储在了对象数据库中，那么可以通过 <code>cat-file</code> 命令从 Git 那里取回数据。 这个命令简直就是一把剖析 Git 对象的瑞士军刀。 为 <code>cat-file</code> 指定 <code>-p</code> 选项可指示该命令自动判断内容的类型，并为我们显示大致的内容：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4</span></span><br><span class="line">test content</span><br></pre></td></tr></table></figure><p>同样可以将这些操作应用于文件中的内容。 例如，可以对一个文件进行简单的版本控制。 首先，创建一个新文件并将其内容存入数据库：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;version 1&#x27;</span> &gt; test.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git hash-object -w test.txt</span></span><br><span class="line">83baae61804e65cc73a7201a7252750c76066a30</span><br></pre></td></tr></table></figure><p>接着，向文件里写入新内容，并再次将其存入数据库：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;version 2&#x27;</span> &gt; test.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git hash-object -w test.txt</span></span><br><span class="line">1f7a7a472abf3dd9643fd615f6da379c4acb3e3a</span><br></pre></td></tr></table></figure><p>对象数据库记录下了该文件的两个不同版本，当然之前我们存入的第一条内容也还在：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">find .git/objects -<span class="built_in">type</span> f</span></span><br><span class="line">.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a</span><br><span class="line">.git/objects/83/baae61804e65cc73a7201a7252750c76066a30</span><br><span class="line">.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4</span><br></pre></td></tr></table></figure><p>现在可以在删掉 <code>test.txt</code> 的本地副本，然后用 Git 从对象数据库中取回它的第一个版本：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 &gt; test.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> test.txt</span></span><br><span class="line">version 1</span><br></pre></td></tr></table></figure><p>或者第二个版本：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a &gt; test.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> test.txt</span></span><br><span class="line">version 2</span><br></pre></td></tr></table></figure><p>然而，记住文件的每一个版本所对应的 SHA-1 值并不现实；另一个问题是，在这个（简单的版本控制）系统中，文件名并没有被保存——我们仅保存了文件的内容。 上述类型的对象我们称之为 <strong>数据对象（blob object）</strong>。 利用 <code>git cat-file -t</code> 命令，可以让 Git 告诉我们其内部存储的任何对象类型，只要给定该对象的 SHA-1 值：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a</span></span><br><span class="line">blob</span><br></pre></td></tr></table></figure><h3 id="树对象"><a class="header-anchor" href="#树对象"></a>树对象</h3><p>树对象（tree object）解决文件名保存的问题，也允许我们将多个文件组织到一起。 Git 以一种类似于 UNIX 文件系统的方式存储内容，但作了些许简化。 <strong>所有内容均以树对象和数据对象的形式存储</strong>，其中树对象对应了 UNIX 中的目录项，数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录（tree entry），每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针，以及相应的模式、类型、文件名信息。</p><p>使用 <code>git cat-file -p main^&#123;tree&#125;</code> 命令，查看项目 main 分支下最新树对象。</p><p><img src="/2024/01/23/git-study-10/image-20240130152921901.png" alt="image-20240130152921901"></p><p>其中 lib 表示一个指针，其指向的是另一个树对象，通过 <code>git cat-file -p &lt;SHA-1&gt;</code> 查看树对象下的对象结构。</p><p><img src="/2024/01/23/git-study-10/image-20240130153109433.png" alt="image-20240130153109433"></p><p>通过 <code>git update-index</code> 命令，可以直接从底层创建树对象。通常情况下，Git 根据某一时刻暂存区（即 index 区域）所表示的状态创建并记录一个对应的树对象， 如此重复便可依次记录（某个时间段内）一系列的树对象。</p><p>可以通过底层命令 <code>git update-index</code> 为一个单独文件——我们的 test.txt 文件的首个版本——创建一个暂存区。 利用该命令，可以把 <code>test.txt</code> 文件的首个版本人为地加入一个新的暂存区。 必须为上述命令指定 <code>--add</code> 选项，因为此前该文件并不在暂存区中（我们甚至都还没来得及创建一个暂存区呢）； 同样必需的还有 <code>--cacheinfo</code> 选项，因为将要添加的文件位于 Git 数据库中，而不是位于当前目录下。 同时，需要指定文件模式、SHA-1 与文件名：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git update-index --add --cacheinfo 100644 \</span></span><br><span class="line"><span class="language-bash">  83baae61804e65cc73a7201a7252750c76066a30 test.txt</span></span><br></pre></td></tr></table></figure><p>指定的文件模式为 <code>100644</code>，表明这是一个普通文件。 其他选择包括：<code>100755</code>，表示一个可执行文件；<code>120000</code>，表示一个符号链接。 这里的文件模式参考了常见的 UNIX 文件模式，但远没那么灵活——上述三种模式即是 Git 文件（即数据对象）的所有合法模式（当然，还有其他一些模式，但用于目录项和子模块）。</p><p>通过 <code>git write-tree</code> 命令将暂存区内容写入一个树对象。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git write-tree</span></span><br><span class="line">d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579</span></span><br><span class="line">100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt</span><br></pre></td></tr></table></figure><p><code>git write-tree</code> 返回的 SHA-1 值是当前目录下的树对象值，通过 <code>git cat-file</code> 查看树对象，可以发现 test.txt 被添加到当前树对象下了，其 SHA-1 值就是 <code>git update-index</code> 指定的值。</p><p>再次进行操作，添加一个新文件，并对 test.txt 文件做修改后再暂存。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;new file&#x27;</span> &gt; new.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git update-index --add --cacheinfo 100644 \</span></span><br><span class="line"><span class="language-bash">  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git update-index --add new.txt</span></span><br></pre></td></tr></table></figure><p>暂存区现在包含了 <code>test.txt</code> 文件的新版本，和一个新文件：<code>new.txt</code>。 记录下这个目录树（将当前暂存区的状态记录为一个树对象），然后观察它的结构：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git write-tree</span></span><br><span class="line">0155eb4229851634a0f03eb265b69f5a2d56f341</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341</span></span><br><span class="line">100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt</span><br><span class="line">100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt</span><br></pre></td></tr></table></figure><p>我们注意到，新的树对象包含两条文件记录，同时 test.txt 的 SHA-1 值（<code>1f7a7a</code>）是先前值的“第二版”。</p><h3 id="提交对象"><a class="header-anchor" href="#提交对象"></a>提交对象</h3><p>树对象的操作中，会出现不同版本的快照，每个快照会对应不用的 SHA-1 哈希值，在没有提交之前，是无法记录这些快照在什么时刻保存的，以及为什么保存这些快照的。</p><p>通过 <code>git commit-tree</code> 命令创建一个提交对象，为此需要指定一个树对象的 SHA-1 值，以及该提交的父提交对象（如果有的话）。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;commit-tree 1&#x27;</span> | git commit-tree 2ed7</span> </span><br><span class="line">b4f6196421ede1205a6e8affdba8a23b741e762a</span><br></pre></td></tr></table></figure><p>由于创建时间和作者数据不同，你现在会得到一个不同的散列值。 请将本章后续内容中的提交和标签的散列值替换为你自己的校验和。 现在可以通过 <code>git cat-file</code> 命令查看这个新提交对象：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git cat-file -p b4f619</span></span><br><span class="line">tree 2ed706a25355ec1a647e0d54971bcc3426f6cfb6</span><br><span class="line">author Wray &lt;wray20156294@gmail.com&gt; 1706601067 +0800</span><br><span class="line">committer Wray &lt;wray20156294@gmail.com&gt; 1706601067 +0800</span><br><span class="line"></span><br><span class="line">commit-tree 1</span><br></pre></td></tr></table></figure><p>提交对象的格式很简单：它先指定一个顶层树对象，代表当前项目快照； 然后是可能存在的父提交（前面描述的提交对象并不存在任何父提交）； 之后是作者/提交者信息（依据你的 <code>user.name</code> 和 <code>user.email</code> 配置来设定，外加一个时间戳）； 留空一行，最后是提交注释。</p><p>将 <code>2ed7</code> 树对象指定父对象为 <code>f9bba3</code> ，将提交记录挂载到父提交记录上。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">echo</span> <span class="string">&#x27;commit-tree 1&#x27;</span> | git commit-tree 2ed7 -p f9bba34</span></span><br><span class="line">3ffffc279924563d89505e654eb42bb8daa64a60</span><br></pre></td></tr></table></figure><p>通过 <code>git log</code> 查看 <code>3ffffc</code> 下的提交日志。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">log</span> --<span class="built_in">stat</span> 3ffffc</span></span><br><span class="line">commit 3ffffc279924563d89505e654eb42bb8daa64a60</span><br><span class="line">Author: Wray &lt;wray20156294@gmail.com&gt;</span><br><span class="line">Date:   Tue Jan 30 15:54:31 2024 +0800</span><br><span class="line"></span><br><span class="line">    commit-tree 1</span><br><span class="line"></span><br><span class="line"> pro.txt | 1 +</span><br><span class="line"> 1 file changed, 1 insertion(+)</span><br><span class="line"></span><br><span class="line">commit f9bba34686b949ce2e359bdfbdc7c855d32dc811 (HEAD -&gt; main)</span><br><span class="line">Author: Wray &lt;wray20156294@gmail.com&gt;</span><br><span class="line">Date:   Tue Jan 30 15:29:08 2024 +0800</span><br><span class="line"></span><br><span class="line">    new lib dir</span><br><span class="line"></span><br><span class="line"> lib/a         | 1 +</span><br><span class="line"> lib/print.out | 1 +</span><br><span class="line"> test.txt      | 1 +</span><br><span class="line"> 3 files changed, 3 insertions(+)</span><br><span class="line"></span><br><span class="line">commit 46eee871bcaa6acbc3568b2e1af69fa5bca2500c</span><br><span class="line">Author: Wray &lt;wray20156294@gmail.com&gt;</span><br><span class="line">Date:   Tue Jan 23 16:13:14 2024 +0800</span><br><span class="line"></span><br><span class="line">    commit 1</span><br><span class="line"></span><br><span class="line"> README.md | 1 +</span><br><span class="line"> 1 file changed, 1 insertion(+)</span><br></pre></td></tr></table></figure><p>可以发现，在没有借助任何上层命令，仅凭几个底层操作便完成了一个 Git 提交历史的创建。 这就是每次我们运行 <code>git add</code> 和 <code>git commit</code> 命令时，Git 所做的工作实质就是将被改写的文件保存为数据对象， 更新暂存区，记录树对象，最后创建一个指明了顶层树对象和父提交的提交对象。</p><p>这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 <code>.git/objects</code> 目录下。</p><h2 id="Git-引用"><a class="header-anchor" href="#Git-引用"></a>Git 引用</h2><p>如果你对仓库中从一个提交（比如 <code>1a410e</code>）开始往前的历史感兴趣，那么可以运行 <code>git log 1a410e</code> 这样的命令来显示历史，不过你需要记得 <code>1a410e</code> 是你查看历史的起点提交。 如果我们有一个文件来保存 SHA-1 值，而该文件有一个简单的名字， 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。</p><p>在 Git 中，这种简单的名字被称为“引用（references，或简写为 refs）”。 你可以在 <code>.git/refs</code> 目录下找到这类含有 SHA-1 值的文件。 在目前的项目中，这个目录没有包含任何文件，但它包含了一个简单的目录结构：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">find .git/refs</span></span><br><span class="line">.git/refs</span><br><span class="line">.git/refs/heads</span><br><span class="line">.git/refs/tags</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">find .git/refs -<span class="built_in">type</span> f</span></span><br></pre></td></tr></table></figure><p>通过 <code>git update-ref</code> 更新引用，若想在第二个提交上创建一个分支，可以这么做：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git update-ref refs/heads/test cac0ca</span></span><br></pre></td></tr></table></figure><p>这个分支将只包含从第二个提交开始往前追溯的记录：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">log</span> --pretty=oneline <span class="built_in">test</span></span></span><br><span class="line">cac0cab538b970a37ea1e769cbbde608743bc96d second commit</span><br><span class="line">fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit</span><br></pre></td></tr></table></figure><h3 id="HEAD文件"><a class="header-anchor" href="#HEAD文件"></a>HEAD文件</h3><p>现在的问题是，当你执行 <code>git branch &lt;branch&gt;</code> 时，Git 如何知道最新提交的 SHA-1 值呢？ 答案是 HEAD 文件。</p><p>HEAD 文件通常是一个符号引用（symbolic reference），指向目前所在的分支。 所谓符号引用，表示它是一个指向其他引用的指针。</p><h3 id="标签引用"><a class="header-anchor" href="#标签引用"></a>标签引用</h3><p>前面我们刚讨论过 Git 的三种主要的对象类型（<strong>数据对象</strong>、<strong>树对象</strong> 和 <strong>提交对象</strong> ），然而实际上还有第四种。 <strong>标签对象（tag object）</strong> 非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息，以及一个指针。 主要的区别在于，标签对象通常指向一个提交对象，而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象，只不过给这个提交对象加上一个更友好的名字罢了。</p><h3 id="远程引用"><a class="header-anchor" href="#远程引用"></a>远程引用</h3><p>如果你添加了一个远程版本库并对其执行过推送操作，Git 会记录下最近一次推送操作时每一个分支所对应的值，并保存在 <code>refs/remotes</code> 目录下。 例如，你可以添加一个叫做 <code>origin</code> 的远程版本库，然后把 <code>master</code> 分支推送上去：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git remote add origin git@github.com:schacon/simplegit-progit.git</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git push origin master</span></span><br><span class="line">Counting objects: 11, done.</span><br><span class="line">Compressing objects: 100% (5/5), done.</span><br><span class="line">Writing objects: 100% (7/7), 716 bytes, done.</span><br><span class="line">Total 7 (delta 2), reused 4 (delta 1)</span><br><span class="line">To git@github.com:schacon/simplegit-progit.git</span><br><span class="line">  a11bef0..ca82a6d  master -&gt; master</span><br></pre></td></tr></table></figure><p>此时，如果查看 <code>refs/remotes/origin/master</code> 文件，可以发现 <code>origin</code> 远程版本库的 <code>master</code> 分支所对应的 SHA-1 值，就是最近一次与服务器通信时本地 <code>master</code> 分支所对应的 SHA-1 值：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cat</span> .git/refs/remotes/origin/master</span></span><br><span class="line">ca82a6dff817ec66f44342007202690a93763949</span><br></pre></td></tr></table></figure><p>远程引用和分支（位于 <code>refs/heads</code> 目录下的引用）之间最主要的区别在于，远程引用是只读的。 虽然可以 <code>git checkout</code> 到某个远程引用，但是 Git 并不会将 HEAD 引用指向该远程引用。因此，你永远不能通过 <code>commit</code> 命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。</p><h2 id="包文件"><a class="header-anchor" href="#包文件"></a>包文件</h2><p>Git 仓库最初的时候，Git 对同一个文件的不同版本会保留源文件，而不是只完整保存其中一个，再保存另一个对象与之前版本的差异内容。</p><p>但是，Git 会时不时地将多个这些对象打包成一个称为“包文件（packfile）”的二进制文件，以节省空间和提高效率。 当版本库中有太多的松散对象，或者你手动执行 <code>git gc</code> 命令，或者你向远程服务器执行推送时，Git 都会这样做。</p><p>Git 是如何做到这点的？ Git 打包对象时，会查找命名及大小相近的文件，并只保存文件不同版本之间的差异内容。 你可以查看包文件，观察它是如何节省空间的。 <code>git verify-pack</code> 这个底层命令可以让你查看已打包的内容。</p><h2 id="引用规范"><a class="header-anchor" href="#引用规范"></a>引用规范</h2><p>现在想要添加一个远程仓库：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git remote add origin https://github.com/schacon/simplegit-progit</span></span><br></pre></td></tr></table></figure><p>运行上述命令会在你仓库中的 <code>.git/config</code> 文件中添加一个小节， 并在其中指定远程版本库的名称（<code>origin</code>）、URL 和一个用于获取操作的 <strong>引用规范（refspec）</strong>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[remote &quot;origin&quot;]</span></span><br><span class="line"><span class="attr">url</span> = https://github.com/schacon/simplegit-progit</span><br><span class="line"><span class="attr">fetch</span> = +refs/heads/*:refs/remotes/origin/*</span><br></pre></td></tr></table></figure><p>引用规范的格式由一个可选的 <code>+</code> 号和紧随其后的 <code>&lt;src&gt;:&lt;dst&gt;</code> 组成， 其中 <code>&lt;src&gt;</code> 是一个模式（pattern），代表远程版本库中的引用； <code>&lt;dst&gt;</code> 是本地跟踪的远程引用的位置。 <code>+</code> 号告诉 Git 即使在不能快进的情况下也要（强制）更新引用。</p><p>默认情况下，引用规范由 <code>git remote add origin</code> 命令自动生成， Git 获取服务器中 <code>refs/heads/</code> 下面的所有引用，并将它写入到本地的 <code>refs/remotes/origin/</code> 中。 所以，如果服务器上有一个 <code>master</code> 分支，你可以在本地通过下面任意一种方式来访问该分支上的提交记录：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">log</span> origin/master</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">log</span> remotes/origin/master</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">log</span> refs/remotes/origin/master</span></span><br></pre></td></tr></table></figure><p>上面的三个命令作用相同，因为 Git 会把它们都扩展成 <code>refs/remotes/origin/master</code>。</p><p>如果想让 Git 每次只拉取远程的 <code>master</code> 分支，而不是所有分支， 可以把（引用规范的）获取那一行修改为只引用该分支：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fetch = +refs/heads/master:refs/remotes/origin/master</span><br></pre></td></tr></table></figure><p>这仅是针对该远程版本库的 <code>git fetch</code> 操作的默认引用规范。 如果有某些只希望被执行一次的操作，我们也可以在命令行指定引用规范。 若要将远程的 <code>master</code> 分支拉到本地的 <code>origin/mymaster</code> 分支，可以运行：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git fetch origin master:refs/remotes/origin/mymaster</span></span><br></pre></td></tr></table></figure><h2 id="传输协议"><a class="header-anchor" href="#传输协议"></a>传输协议</h2><p>Git 可以通过两种主要的方式在版本库之间传输数据：“哑（dumb）”协议和“智能（smart）”协议。</p><h3 id="哑协议"><a class="header-anchor" href="#哑协议"></a>哑协议</h3><p>如果你正在架设一个基于 HTTP 协议的只读版本库，一般而言这种情况下使用的就是哑协议。 这个协议之所以被称为“哑”协议，是因为在传输过程中，服务端不需要有针对 Git 特有的代码；抓取过程是一系列 HTTP 的 <code>GET</code> 请求，这种情况下，客户端可以推断出服务端 Git 仓库的布局。</p><h3 id="智能协议"><a class="header-anchor" href="#智能协议"></a>智能协议</h3><p>智能协议是更常用的传送数据的方法，但它需要在服务端运行一个进程，而这也是 Git 的智能之处——它可以读取本地数据，理解客户端有什么和需要什么，并为它生成合适的包文件。 总共有两组进程用于传输数据，它们分别负责上传和下载数据。</p><p>为了上传数据至远端，Git 使用 <code>send-pack</code> 和 <code>receive-pack</code> 进程。 运行在客户端上的 <code>send-pack</code> 进程连接到远端运行的 <code>receive-pack</code> 进程。</p><p>当你在下载数据时， <code>fetch-pack</code> 和 <code>upload-pack</code> 进程就起作用了。 客户端启动 <code>fetch-pack</code> 进程，连接至远端的 <code>upload-pack</code> 进程，以协商后续传输的数据。</p><h2 id="维护与数据恢复"><a class="header-anchor" href="#维护与数据恢复"></a>维护与数据恢复</h2><p>有的时候，你需要对仓库进行清理——使它的结构变得更紧凑，或是对导入的仓库进行清理，或是恢复丢失的内容。</p><h3 id="维护"><a class="header-anchor" href="#维护"></a>维护</h3><p>Git 会不定时地自动运行一个叫做 “auto gc” 的命令。 大多数时候，这个命令并不会产生效果。 然而，如果有太多松散对象（不在包文件中的对象）或者太多包文件，Git 会运行一个完整的 <code>git gc</code> 命令。 “gc” 代表垃圾回收，这个命令会做以下事情：收集所有松散对象并将它们放置到包文件中， 将多个包文件合并为一个大的包文件，移除与任何提交都不相关的陈旧对象。</p><p>可以像下面一样手动执行自动垃圾回收：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git gc --auto</span></span><br></pre></td></tr></table></figure><p>就像上面提到的，这个命令通常并不会产生效果。 大约需要 7000 个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。 你可以通过修改 <code>gc.auto</code> 与 <code>gc.autopacklimit</code> 的设置来改动这些数值。</p><h3 id="数据恢复"><a class="header-anchor" href="#数据恢复"></a>数据恢复</h3><p>在你使用 Git 的时候，你可能会意外丢失一次提交。 通常这是因为你强制删除了正在工作的分支，但是最后却发现你还需要这个分支， 亦或者硬重置了一个分支，放弃了你想要的提交。 如果这些事情已经发生，该如何找回你的提交呢？</p><p>最方便，也是最常用的方法，是使用一个名叫 <code>git reflog</code> 的工具。 当你正在工作时，Git 会默默地记录每一次你改变 HEAD 时它的值。 每一次你提交或改变分支，引用日志都会被更新。 引用日志（reflog）也可以通过 <code>git update-ref</code> 命令更新，我们在 <em>Git 引用</em> 有提到使用这个命令而不是是直接将 SHA-1 的值写入引用文件中的原因。</p><h3 id="移除对象"><a class="header-anchor" href="#移除对象"></a>移除对象</h3><p>Git 有很多很棒的功能，但是其中一个特性会导致问题，<code>git clone</code> 会下载整个项目的历史，包括每一个文件的每一个版本。 如果所有的东西都是源代码那么这很好，因为 Git 被高度优化来有效地存储这种数据。 然而，如果某个人在之前向项目添加了一个大小特别大的文件，即使你将这个文件从项目中移除了，每次克隆还是都要强制的下载这个大文件。 之所以会产生这个问题，是因为这个文件在历史中是存在的，它会永远在那里。</p><p><strong>警告：这个操作对提交历史的修改是破坏性的。</strong> 它会从你必须修改或移除一个大文件引用最早的树对象开始重写每一次提交。 如果你在导入仓库后，在任何人开始基于这些提交工作前执行这个操作，那么将不会有任何问题——否则， 你必须通知所有的贡献者他们需要将他们的成果变基到你的新提交上。</p><p>必须使用 <code>git rm --cached</code> 命令来移除文件，而不是通过类似 <code>rm file</code> 的命令——因为你需要从索引中移除它，而不是磁盘中。 还有一个原因是速度—— Git 在运行过滤器时，并不会检出每个修订版本到磁盘中，所以这个过程会非常快。</p><h2 id="环境变量"><a class="header-anchor" href="#环境变量"></a>环境变量</h2><p>Git 总是在一个 <code>bash</code> shell 中运行，并借助一些 shell 环境变量来决定它的运行方式。 有</p><h3 id="全局行为"><a class="header-anchor" href="#全局行为"></a>全局行为</h3><p>像通常的程序一样，Git 的常规行为依赖于环境变量。</p><p><strong><code>GIT_EXEC_PATH</code></strong> 决定 Git 到哪找它的子程序 （像 <code>git-commit</code>, <code>git-diff</code> 等等）。 你可以用 <code>git --exec-path</code> 来查看当前设置。</p><p>通常不会考虑修改 <strong><code>HOME</code></strong> 这个变量（太多其它东西都依赖它），这是 Git 查找全局配置文件的地方。 如果你想要一个包括全局配置的真正的便携版 Git， 你可以在便携版 Git 的 shell 配置中覆盖 <code>HOME</code> 设置。</p><p><strong><code>PREFIX</code></strong> 也类似，除了用于系统级别的配置。 Git 在 <code>$PREFIX/etc/gitconfig</code> 查找此文件。</p><p>如果设置了 <strong><code>GIT_CONFIG_NOSYSTEM</code></strong>，就禁用系统级别的配置文件。 这在系统配置影响了你的命令，而你又无权限修改的时候很有用。</p><p><strong><code>GIT_PAGER</code></strong> 控制在命令行上显示多页输出的程序。 如果这个没有设置，就会用 <code>PAGER</code> 。</p><p><strong><code>GIT_EDITOR</code></strong> 当用户需要编辑一些文本（比如提交信息）时， Git 会启动这个编辑器。 如果没设置，就会用 <code>EDITOR</code> 。</p><h3 id="版本库位置"><a class="header-anchor" href="#版本库位置"></a>版本库位置</h3><p>Git 用了几个变量来确定它如何与当前版本库交互。</p><p><strong><code>GIT_DIR</code></strong> 是 <code>.git</code> 目录的位置。 如果这个没有设置， Git 会按照目录树逐层向上查找 <code>.git</code> 目录，直到到达 <code>~</code> 或 <code>/</code>。</p><p><strong><code>GIT_CEILING_DIRECTORIES</code></strong> 控制查找 <code>.git</code> 目录的行为。 如果你访问加载很慢的目录（如那些磁带机上的或通过网络连接访问的），你可能会想让 Git 早点停止尝试，尤其是 shell 构建时调用了 Git 。</p><p><strong><code>GIT_WORK_TREE</code></strong> 是非空版本库的工作目录的根路径。 如果指定了 <code>--git-dir</code> 或 <code>GIT_DIR</code> 但未指定 <code>--work-tree</code>、<code>GIT_WORK_TREE</code> 或 <code>core.worktree</code>，那么当前工作目录就会视作工作树的顶级目录。</p><p><strong><code>GIT_INDEX_FILE</code></strong> 是索引文件的路径（只有非空版本库有）。</p><p><strong><code>GIT_OBJECT_DIRECTORY</code></strong> 用来指定 <code>.git/objects</code> 目录的位置。</p><p><strong><code>GIT_ALTERNATE_OBJECT_DIRECTORIES</code></strong> 一个冒号分割的列表（格式类似 <code>/dir/one:/dir/two:…</code>）用来告诉 Git 到哪里去找不在 <code>GIT_OBJECT_DIRECTORY</code> 目录中的对象。 如果你有很多项目有相同内容的大文件，这个可以用来避免存储过多备份。</p><h3 id="路径规则"><a class="header-anchor" href="#路径规则"></a>路径规则</h3><p>所谓 “pathspec” 是指你在 Git 中如何指定路径，包括通配符的使用。 它们会在 <code>.gitignore</code> 文件中用到，命令行里也会用到（<code>git add *.c</code>）。</p><p><strong><code>GIT_GLOB_PATHSPECS</code></strong> 和 <strong><code>GIT_NOGLOB_PATHSPECS</code></strong> 控制通配符在路径规则中的默认行为。 如果 <code>GIT_GLOB_PATHSPECS</code> 设置为 1, 通配符表现为通配符（这是默认设置）; 如果 <code>GIT_NOGLOB_PATHSPECS</code> 设置为 1,通配符仅匹配字面。意思是 <code>*.c</code> 只会匹配 <em>文件名是</em> “*.c” 的文件，而不是以 <code>.c</code> 结尾的文件。 你可以在各个路径规范中用 <code>:(glob)</code> 或 <code>:(literal)</code> 开头来覆盖这个配置，如 <code>:(glob)*.c</code> 。</p><p><strong><code>GIT_LITERAL_PATHSPECS</code></strong> 禁用上面的两种行为；通配符将不能用，前缀覆盖也不能用。</p><p><strong><code>GIT_ICASE_PATHSPECS</code></strong> 让所有的路径规范忽略大小写。</p><h3 id="提交"><a class="header-anchor" href="#提交"></a>提交</h3><p>Git 提交对象的创建通常最后是由 <code>git-commit-tree</code> 来完成， <code>git-commit-tree</code> 用这些环境变量作主要的信息源。 仅当这些值不存在才回退到预置的值。</p><p><strong><code>GIT_AUTHOR_NAME</code></strong> 是 “author” 字段的可读名字。</p><p><strong><code>GIT_AUTHOR_EMAIL</code></strong> 是 “author” 字段的邮件。</p><p><strong><code>GIT_AUTHOR_DATE</code></strong> 是 “author” 字段的时间戳。</p><p><strong><code>GIT_COMMITTER_NAME</code></strong> 是 “committer” 字段的可读名字。</p><p><strong><code>GIT_COMMITTER_EMAIL</code></strong> 是 “committer” 字段的邮件。</p><p><strong><code>GIT_COMMITTER_DATE</code></strong> 是 “committer” 字段的时间戳。</p><p>如果 <code>user.email</code> 没有配置， 就会用到 <strong><code>EMAIL</code></strong> 指定的邮件地址。 如果 <em>这个</em> 也没有设置， Git 继续回退使用系统用户和主机名。</p><h3 id="网络"><a class="header-anchor" href="#网络"></a>网络</h3><p>Git 使用 <code>curl</code> 库通过 HTTP 来完成网络操作， 所以 <strong><code>GIT_CURL_VERBOSE</code></strong> 告诉 Git 显示所有由那个库产生的消息。 这跟在命令行执行 <code>curl -v</code> 差不多。</p><p><strong><code>GIT_SSL_NO_VERIFY</code></strong> 告诉 Git 不用验证 SSL 证书。 这在有些时候是需要的， 例如你用一个自己签名的证书通过 HTTPS 来提供 Git 服务， 或者你正在搭建 Git 服务器，还没有安装完全的证书。</p><p>如果 Git 操作在网速低于 <strong><code>GIT_HTTP_LOW_SPEED_LIMIT</code></strong> 字节／秒，并且持续 <strong><code>GIT_HTTP_LOW_SPEED_TIME</code></strong> 秒以上的时间，Git 会终止那个操作。 这些值会覆盖 <code>http.lowSpeedLimit</code> 和 <code>http.lowSpeedTime</code> 配置的值。</p><p><strong><code>GIT_HTTP_USER_AGENT</code></strong> 设置 Git 在通过 HTTP 通讯时用到的 user-agent。 默认值类似于 <code>git/2.0.0</code> 。</p><h3 id="比较和合并"><a class="header-anchor" href="#比较和合并"></a>比较和合并</h3><p><strong><code>GIT_DIFF_OPTS</code></strong> 这个有点起错名字了。 有效值仅支持 <code>-u&lt;n&gt;</code> 或 <code>--unified=&lt;n&gt;</code>，用来控制在 <code>git diff</code> 命令中显示的内容行数。</p><p><strong><code>GIT_EXTERNAL_DIFF</code></strong> 用来覆盖 <code>diff.external</code> 配置的值。 如果设置了这个值， 当执行 <code>git diff</code> 时，Git 会调用该程序。</p><p><strong><code>GIT_DIFF_PATH_COUNTER</code></strong> 和 <strong><code>GIT_DIFF_PATH_TOTAL</code></strong> 对于 <code>GIT_EXTERNAL_DIFF</code> 或 <code>diff.external</code> 指定的程序有用。 前者表示在一系列文件中哪个是被比较的（从 1 开始），后者表示每批文件的总数。</p><p><strong><code>GIT_MERGE_VERBOSITY</code></strong> 控制递归合并策略的输出。 允许的值有下面这些：</p><ul><li>0 什么都不输出，除了可能会有一个错误信息。</li><li>1 只显示冲突。</li><li>2 还显示文件改变。</li><li>3 显示因为没有改变被跳过的文件。</li><li>4 显示处理的所有路径。</li><li>5 显示详细的调试信息。</li></ul><p>默认值是 2。</p><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>本章讨论了很多底层命令，这些命令比我们在本书其余部分学到的高层命令来得更原始，也更简洁。 从底层了解 Git 的工作原理有助于更好地理解 Git 在内部是如何运作的，也方便你能够针对特定的工作流写出自己的工具和脚本。</p><p>作为一套内容寻址文件系统，Git 不仅仅是一个版本控制系统，它同时是一个非常强大且易用的工具。</p>]]></content>
    
    
    <summary type="html">第十章学习 Git 内部工作原理和实现方式。</summary>
    
    
    
    <category term="Git" scheme="https://blog.itwray.com/categories/Git/"/>
    
    
    <category term="Git" scheme="https://blog.itwray.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>Git学习-Git与其他系统</title>
    <link href="https://blog.itwray.com/2024/01/22/git-study-9/"/>
    <id>https://blog.itwray.com/2024/01/22/git-study-9/</id>
    <published>2024-01-22T08:52:51.000Z</published>
    <updated>2024-09-27T13:14:26.927Z</updated>
    
    <content type="html"><![CDATA[<h2 id="作为客户端的-Git"><a class="header-anchor" href="#作为客户端的-Git"></a>作为客户端的 Git</h2><p>在开发中接触到的项目，可能已经使用了其他 VCS ，并且暂时没有迁移到 Git 的打算。可以在本地使用 Git 客户端用作平时版本控制，Git 原生支持对其他 VCS 系统自然对接，例如 Subversion 。</p><p>在 Git 中所有 Subversion 桥接命令的基础命令是 <code>git svn</code>。需要特别注意的是当你使用 <code>git svn</code> 时，就是在与 Subversion 打交道，一个与 Git 完全不同的系统。 尽管 <strong>可以</strong> 在本地新建分支与合并分支，但是你最好还是通过变基你的工作来保证你的历史尽可能是直线，并且避免做类似同时与 Git 远程服务器交互的事情。</p><h2 id="迁移到-Git"><a class="header-anchor" href="#迁移到-Git"></a>迁移到 Git</h2><p>如果你现在有一个正在使用其他 VCS 的代码库，但是你已经决定开始使用 Git，必须通过某种方式将你的项目迁移至 Git。</p><p>如果之前使用 Subversion ，则可以直接涌过 <code>git svn clone</code> 迁移仓库。</p><p>如果之前使用 Mercurial ，则需要使用一个叫作“hg-fast-export”的工具，需要从这里拷贝一份：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git <span class="built_in">clone</span> https://github.com/frej/fast-export.git</span></span><br></pre></td></tr></table></figure><p>转换的第一步就是要先得到想要转换的 Mercurial 仓库的完整克隆：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">hg <span class="built_in">clone</span> &lt;remote repo URL&gt; /tmp/hg-repo</span></span><br></pre></td></tr></table></figure><p>下一步就是创建一个作者映射文件。 Mercurial 对放入到变更集作者字段的内容比 Git 更宽容一些，所以这是一个清理的好机会。 只需要用到 <code>bash</code> 终端下的一行命令：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> /tmp/hg-repo</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">hg <span class="built_in">log</span> | grep user: | <span class="built_in">sort</span> | <span class="built_in">uniq</span> | sed <span class="string">&#x27;s/user: *//&#x27;</span> &gt; ../authors</span></span><br></pre></td></tr></table></figure><p>下一步是创建一个新的 Git 仓库，然后运行导出脚本：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git init /tmp/converted</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash"><span class="built_in">cd</span> /tmp/converted</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">/tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors</span></span><br></pre></td></tr></table></figure><p><code>-r</code> 选项告诉 hg-fast-export 去哪里寻找我们想要转换的 Mercurial 仓库，<code>-A</code> 标记告诉它在哪找到作者映射文件（分支和标签的映射文件分别通过 <code>-B</code> 和 <code>-T</code> 选项来指定）。 这个脚本会分析 Mercurial 变更集然后将它们转换成 Git“fast-import”功能需要的脚本。</p><p>所有 Mercurial 标签都已被转换成 Git 标签，Mercurial 分支与书签都被转换成 Git 分支。 现在已经准备好将仓库推送到新的服务器那边：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git remote add origin git@my-git-server:myrepository.git</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git push origin --all</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>本章参考价值较小，具体细节和实践示例可以看原文：<a href="https://git-scm.com/book/zh/v2/Git-%E4%B8%8E%E5%85%B6%E4%BB%96%E7%B3%BB%E7%BB%9F-%E8%BF%81%E7%A7%BB%E5%88%B0-Git">Git 与其他系统 - 迁移到 Git</a> 。</p>]]></content>
    
    
    <summary type="html">第九章学习 Git 与其他VCS项目的交互。</summary>
    
    
    
    <category term="Git" scheme="https://blog.itwray.com/categories/Git/"/>
    
    
    <category term="Git" scheme="https://blog.itwray.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>Git学习-自定义Git</title>
    <link href="https://blog.itwray.com/2024/01/22/git-study-8/"/>
    <id>https://blog.itwray.com/2024/01/22/git-study-8/</id>
    <published>2024-01-22T06:30:26.000Z</published>
    <updated>2024-09-27T13:14:32.892Z</updated>
    
    <content type="html"><![CDATA[<h2 id="配置-Git"><a class="header-anchor" href="#配置-Git"></a>配置 Git</h2><p>在安装 Git 后，首先要做的事就是配置 Git 的提交者的名称和邮件地址：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global user.name &quot;Wray&quot;</span><br><span class="line">git config --global user.email wray20156294@gmail.com</span><br></pre></td></tr></table></figure><p>现在，将会讲解除了配置 Git 提交者信息以外的其他配置信息。</p><p>首先，快速回忆下：Git 使用一系列配置文件来保存你自定义的行为。 它首先会查找系统级的 <code>/etc/gitconfig</code> 文件，该文件含有系统里每位用户及他们所拥有的仓库的配置值。 如果你传递 <code>--system</code> 选项给 <code>git config</code>，它就会读写该文件。</p><p>接下来 Git 会查找每个用户的 <code>~/.gitconfig</code> 文件（或者 <code>~/.config/git/config</code> 文件）。 你可以传递 <code>--global</code> 选项让 Git 读写该文件。</p><p>最后 Git 会查找你正在操作的仓库所对应的 Git 目录下的配置文件（<code>.git/config</code>）。 这个文件中的值只对该仓库有效，它对应于向 <code>git config</code> 传递 <code>--local</code> 选项。</p><p>以上三个层次中每层的配置（系统、全局、本地）都会覆盖掉上一层次的配置，所以 <code>.git/config</code> 中的值会覆盖掉 <code>/etc/gitconfig</code> 中所对应的值。</p><blockquote><p>Git 的配置文件是纯文本的，所以你可以直接手动编辑这些配置文件，输入合乎语法的值。 但是运行 <code>git config</code> 命令会更简单些。</p></blockquote><h3 id="客户端基础配置"><a class="header-anchor" href="#客户端基础配置"></a>客户端基础配置</h3><p>Git 能够识别的配置项分为两大类：客户端和服务器端。 其中大部分属于客户端配置 —— 可以依你个人的工作偏好进行配置。</p><p>如果想得到本地当前版本的 Git 支持的选项列表，可以运行：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">man git-config</span><br></pre></td></tr></table></figure><p>下面主要讲解平常实用的部分命令。</p><h4 id="core-editor"><a class="header-anchor" href="#core-editor"></a>core.editor</h4><p>默认情况下，Git 会调用你通过环境变量 <code>$VISUAL</code> 或 <code>$EDITOR</code> 设置的文本编辑器， 如果没有设置，默认则会调用 <code>vi</code> 来创建和编辑你的提交以及标签信息。 你可以使用 <code>core.editor</code> 选项来修改默认的编辑器：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global core.editor emacs</span><br></pre></td></tr></table></figure><p>现在，无论你定义了什么终端编辑器，Git 都会调用 Emacs 编辑信息。</p><h4 id="commit-template"><a class="header-anchor" href="#commit-template"></a>commit.template</h4><p>如果把此项指定为你的系统上某个文件的路径，当你提交的时候， Git 会使用该文件的内容作为提交的默认初始化信息。 创建的自定义提交模版中的值可以用来提示自己或他人适当的提交格式和风格。</p><h4 id="core-pager"><a class="header-anchor" href="#core-pager"></a>core.pager</h4><p>该配置项指定 Git 运行诸如 <code>log</code> 和 <code>diff</code> 等命令所使用的分页器。 你可以把它设置成用 <code>more</code> 或者任何你喜欢的分页器（默认用的是 <code>less</code>），当然也可以设置成空字符串，关闭该选项：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.pager <span class="string">&#x27;&#x27;</span></span></span><br></pre></td></tr></table></figure><p>这样不管命令的输出量多少，Git 都会在一页显示所有内容。</p><h4 id="user-signingkey"><a class="header-anchor" href="#user-signingkey"></a>user.signingkey</h4><p>如果你要创建经签署的含附注的标签， 那么把你的 GPG 签署密钥设置为配置项会更好。如下设置你的密钥 ID：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global user.signingkey &lt;gpg-key-id&gt;</span></span><br></pre></td></tr></table></figure><p>现在，你每次运行 <code>git tag</code> 命令时，即可直接签署标签，而无需定义密钥：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git tag -s &lt;tag-name&gt;</span></span><br></pre></td></tr></table></figure><h4 id="core-excludesfile"><a class="header-anchor" href="#core-excludesfile"></a>core.excludesfile</h4><p>你可以在你的项目的 <code>.gitignore</code> 文件里面规定无需纳入 Git 管理的文件的模板，这样它们既不会出现在未跟踪列表， 也不会在你运行 <code>git add</code> 后被暂存。</p><p>不过有些时候，你想要在你所有的版本库中忽略掉某一类文件。 如果你的操作系统是 macOS，很可能就是指 <code>.DS_Store</code>。 如果你把 Emacs 或 Vim 作为首选的编辑器，你肯定知道以 <code>~</code> 结尾的文件名。</p><p>这个配置允许你设置类似于全局生效的 <code>.gitignore</code> 文件。 如果你按照下面的内容创建一个 <code>~/.gitignore_global</code> 文件：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">*~</span><br><span class="line">.*.swp</span><br><span class="line">.DS_Store</span><br></pre></td></tr></table></figure><p>然后运行 <code>git config --global core.excludesfile ~/.gitignore_global</code>，Git 将把那些文件永远地拒之门外。</p><h4 id="help-autocorrect"><a class="header-anchor" href="#help-autocorrect"></a>help.autocorrect</h4><p>假如你打错了一条命令，会显示：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git chekcout master</span></span><br><span class="line">git：&#x27;chekcout&#x27; 不是一个 git 命令。参见 &#x27;git --help&#x27;。</span><br><span class="line"></span><br><span class="line">您指的是这个么？</span><br><span class="line">  checkout</span><br></pre></td></tr></table></figure><p>Git 会尝试猜测你的意图，但是它不会越俎代庖。 如果你把 <code>help.autocorrect</code> 设置成 1，那么只要有一个命令被模糊匹配到了，Git 会自动运行该命令。</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git chekcout master</span></span><br><span class="line">警告：您运行一个不存在的 Git 命令 &#x27;chekcout&#x27;。继续执行假定您要要运行的</span><br><span class="line">是 &#x27;checkout&#x27;</span><br><span class="line">在 0.1 秒钟后自动运行...</span><br></pre></td></tr></table></figure><p>注意提示信息中的“0.1 秒”。<code>help.autocorrect</code> 接受一个代表十分之一秒的整数。 所以如果你把它设置为 50, Git 将在自动执行命令前给你 5 秒的时间改变主意。</p><h4 id="color"><a class="header-anchor" href="#color"></a>color.*</h4><p>Git 会自动着色大部分输出内容，但如果你不喜欢花花绿绿，也可以关掉。 要想关掉 Git 的终端颜色输出，试一下这个：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global color.ui <span class="literal">false</span></span></span><br></pre></td></tr></table></figure><p>这个设置的默认值是 <code>auto</code>，它会着色直接输出到终端的内容；而当内容被重定向到一个管道或文件时，则忽略着色功能。</p><p>你也可以设置成 <code>always</code>，来忽略掉管道和终端的不同，即在任何情况下着色输出。 你很少会这么设置，在大多数场合下，如果你想在被重定向的输出中插入颜色码，可以传递 <code>--color</code> 标志给 Git 命令来强制它这么做。 默认设置就已经能满足大多数情况下的需求了。</p><p>要想具体到哪些命令输出需要被着色以及怎样着色，你需要用到和具体命令有关的颜色配置选项。 它们都能被置为 <code>true</code>、<code>false</code> 或 <code>always</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">color.branch</span><br><span class="line">color.diff</span><br><span class="line">color.interactive</span><br><span class="line">color.status</span><br></pre></td></tr></table></figure><p>另外，以上每个配置项都有子选项，它们可以被用来覆盖其父设置，以达到为输出的各个部分着色的目的。 例如，为了让 <code>diff</code> 的输出信息以蓝色前景、黑色背景和粗体显示，你可以运行</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global color.diff.meta <span class="string">&quot;blue black bold&quot;</span></span></span><br></pre></td></tr></table></figure><p>你能设置的颜色有：<code>normal</code>、<code>black</code>、<code>red</code>、<code>green</code>、<code>yellow</code>、<code>blue</code>、<code>magenta</code>、<code>cyan</code> 或 <code>white</code>。 正如以上例子设置的粗体属性，想要设置字体属性的话，可以选择包括：<code>bold</code>、<code>dim</code>、<code>ul</code>（下划线）、<code>blink</code>、<code>reverse</code>（交换前景色和背景色）。</p><h4 id="core-autocrlf"><a class="header-anchor" href="#core-autocrlf"></a>core.autocrlf</h4><p>假如你正在 Windows 上写程序，而你的同伴用的是其他系统（或相反），你可能会遇到 CRLF 问题。 这是因为 Windows 使用回车（CR）和换行（LF）两个字符来结束一行，而 macOS 和 Linux 只使用换行（LF）一个字符。 虽然这是小问题，但它会极大地扰乱跨平台协作。许多 Windows 上的编辑器会悄悄把行尾的换行字符转换成回车和换行， 或在用户按下 Enter 键时，插入回车和换行两个字符。</p><p>Git 可以在你提交时自动地把回车和换行转换成换行，而在检出代码时把换行转换成回车和换行。 你可以用 <code>core.autocrlf</code> 来打开此项功能。 如果是在 Windows 系统上，把它设置成 <code>true</code>，这样在检出代码时，换行会被转换成回车和换行：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.autocrlf <span class="literal">true</span></span></span><br></pre></td></tr></table></figure><p>如果使用以换行作为行结束符的 Linux 或 macOS，你不需要 Git 在检出文件时进行自动的转换； 然而当一个以回车加换行作为行结束符的文件不小心被引入时，你肯定想让 Git 修正。 你可以把 <code>core.autocrlf</code> 设置成 input 来告诉 Git 在提交时把回车和换行转换成换行，检出时不转换：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.autocrlf input</span></span><br></pre></td></tr></table></figure><p>这样在 Windows 上的检出文件中会保留回车和换行，而在 macOS 和 Linux 上，以及版本库中会保留换行。</p><p>如果你是 Windows 程序员，且正在开发仅运行在 Windows 上的项目，可以设置 <code>false</code> 取消此功能，把回车保留在版本库中：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.autocrlf <span class="literal">false</span></span></span><br></pre></td></tr></table></figure><h4 id="core-whitespace"><a class="header-anchor" href="#core-whitespace"></a>core.whitespace</h4><p>Git 预先设置了一些选项来探测和修正多余空白字符问题。 它提供了六种处理多余空白字符的主要选项 —— 其中三个默认开启，另外三个默认关闭，不过你可以自由地设置它们。</p><p>默认被打开的三个选项是：<code>blank-at-eol</code>，查找行尾的空格；<code>blank-at-eof</code>，盯住文件底部的空行； <code>space-before-tab</code>，警惕行头 tab 前面的空格。</p><p>默认被关闭的三个选项是：<code>indent-with-non-tab</code>，揪出以空格而非 tab 开头的行（你可以用 <code>tabwidth</code> 选项控制它）；<code>tab-in-indent</code>，监视在行头表示缩进的 tab；<code>cr-at-eol</code>，告诉 Git 忽略行尾的回车。</p><p>通过设置 <code>core.whitespace</code>，你可以让 Git 按照你的意图来打开或关闭以逗号分割的选项。 要想关闭某个选项，你可以在输入设置选项时不指定它或在它前面加个 <code>-</code>。 例如，如果你想要打开除<code>space-before-tab</code> 之外的所有选项，那么可以这样 （ <code>trailing-space</code> 涵盖了 <code>blank-at-eol</code> 和 <code>blank-at-eof</code> ）：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.whitespace \</span></span><br><span class="line"><span class="language-bash">    trailing-space,-space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol</span></span><br></pre></td></tr></table></figure><p>你也可以只指定自定义的部分：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global core.whitespace \</span></span><br><span class="line"><span class="language-bash">    -space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol</span></span><br></pre></td></tr></table></figure><h3 id="服务端配置"><a class="header-anchor" href="#服务端配置"></a>服务端配置</h3><h4 id="receive-fsckObjects"><a class="header-anchor" href="#receive-fsckObjects"></a>receive.fsckObjects</h4><p>Git 能够确认每个对象的有效性以及 SHA-1 检验和是否保持一致。 但 Git 不会在每次推送时都这么做。这个操作很耗时间，很有可能会拖慢提交的过程，特别是当库或推送的文件很大的情况下。 如果想在每次推送时都要求 Git 检查一致性，设置 <code>receive.fsckObjects</code> 为 true 来强迫它这么做：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --system receive.fsckObjects <span class="literal">true</span></span></span><br></pre></td></tr></table></figure><p>现在 Git 会在每次推送生效前检查库的完整性，确保没有被有问题的客户端引入破坏性数据。</p><h4 id="receive-denyNonFastForwards"><a class="header-anchor" href="#receive-denyNonFastForwards"></a>receive.denyNonFastForwards</h4><p>如果你变基已经被推送的提交，继而再推送，又或者推送一个提交到远程分支，而这个远程分支当前指向的提交不在该提交的历史中，这样的推送会被拒绝。 这通常是个很好的策略，但有时在变基的过程中，你确信自己需要更新远程分支，可以在 push 命令后加 <code>-f</code> 标志来强制更新（force-update）。</p><p>要禁用这样的强制更新推送（force-pushes），可以设置 <code>receive.denyNonFastForwards</code>：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --system receive.denyNonFastForwards <span class="literal">true</span></span></span><br></pre></td></tr></table></figure><p>稍后我们会提到，用服务器端的接收钩子也能达到同样的目的。 那种方法可以做到更细致的控制，例如禁止某一类用户做非快进（non-fast-forwards）推送。</p><h4 id="receive-denyDeletes"><a class="header-anchor" href="#receive-denyDeletes"></a>receive.denyDeletes</h4><p>有一些方法可以绕过 <code>denyNonFastForwards</code> 策略。其中一种是先删除某个分支，再连同新的引用一起推送回该分支。 把 <code>receive.denyDeletes</code> 设置为 true 可以把这个漏洞补上：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --system receive.denyDeletes <span class="literal">true</span></span></span><br></pre></td></tr></table></figure><p>这样会禁止通过推送删除分支和标签 — 没有用户可以这么做。 要删除远程分支，必须从服务器手动删除引用文件。</p><h2 id="Git-属性"><a class="header-anchor" href="#Git-属性"></a>Git 属性</h2><p>你也可以针对特定的路径配置某些设置项，这样 Git 就只对特定的子目录或子文件集运用它们。 这些基于路径的设置项被称为 Git 属性，可以在你的目录下的 <code>.gitattributes</code> 文件内进行设置（通常是你的项目的根目录）。如果不想让这些属性文件与其它文件一同提交，你也可以在 <code>.git/info/attributes</code> 文件中进行设置。</p><p>通过使用属性，你可以对项目中的文件或目录单独定义不同的合并策略，让 Git 知道怎样比较非文本文件，或者让 Git 在提交或检出前过滤内容。</p><h2 id="Git-钩子"><a class="header-anchor" href="#Git-钩子"></a>Git 钩子</h2><p>和其它版本控制系统一样，Git 能在特定的重要动作发生时触发自定义脚本。 有两组这样的钩子：客户端的和服务器端的。 客户端钩子由诸如提交和合并这样的操作所调用，而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。 你可以随心所欲地运用这些钩子。</p><p>钩子都被存储在 Git 目录下的 <code>hooks</code> 子目录中。 也即绝大部分项目中的 <code>.git/hooks</code> 。 当你用 <code>git init</code> 初始化一个新版本库时，Git 默认会在这个目录中放置一些示例脚本。 这些脚本除了本身可以被调用外，它们还透露了被触发时所传入的参数。 所有的示例都是 shell 脚本，其中一些还混杂了 Perl 代码，不过，任何正确命名的可执行脚本都可以正常使用 —— 你可以用 Ruby 或 Python，或任何你熟悉的语言编写它们。 这些示例的名字都是以 <code>.sample</code> 结尾，如果你想启用它们，得先移除这个后缀。</p><p>把一个正确命名（不带扩展名）且可执行的文件放入 <code>.git</code> 目录下的 <code>hooks</code> 子目录中，即可激活该钩子脚本。 这样一来，它就能被 Git 调用。</p><blockquote><p>需要注意的是，克隆某个版本库时，它的客户端钩子 <strong>并不</strong> 随同复制。 如果需要靠这些脚本来强制维持某种策略，建议你在服务器端实现这一功能。</p></blockquote><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>自定义 Git 章节中，我主要在乎的是使用 Git 客户端配置，因为它跟平常的开发息息相关。 Git 属性和 Git 钩子更偏向于自定义化，一般项目确实用不到，等到真的有需求那天，再专门整理一章实践片，现在主打一个了解，知道有这个东西就行。</p>]]></content>
    
    
    <summary type="html">第八章学习借助 Git 的一些重要的配置方法和钩子机制，来满足自定义的需求。</summary>
    
    
    
    <category term="Git" scheme="https://blog.itwray.com/categories/Git/"/>
    
    
    <category term="Git" scheme="https://blog.itwray.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>Git学习-Git工具</title>
    <link href="https://blog.itwray.com/2024/01/20/git-study-7/"/>
    <id>https://blog.itwray.com/2024/01/20/git-study-7/</id>
    <published>2024-01-20T07:26:38.000Z</published>
    <updated>2024-09-27T13:14:47.759Z</updated>
    
    <content type="html"><![CDATA[<h2 id="Git的修订版本"><a class="header-anchor" href="#Git的修订版本"></a>Git的修订版本</h2><p>Git 每一次 commit 都会生成一个版本号，也成为单个修订版本，它由 40 个字符的完整 SHA-1 散列值组成。</p><p>Git 支持只提供 4 个 SHA-1 字符即可获得对应的那次提交，但前提是在没有冲突的情况下，一般 8 到 10 个字符就已经足够在一个项目中避免 SHA-1 的冲突。</p><blockquote><p>关于 SHA-1 的简短说明</p><p>许多人觉得他们的仓库里有可能出现两个不同的对象其 SHA-1 值相同。 然后呢？</p><p>如果你真的向仓库里提交了一个对象，它跟之前的某个 <strong>不同</strong> 对象的 SHA-1 值相同， Git 会发现该对象的散列值已经存在于仓库里了，于是就会认为该对象被写入，然后直接使用它。 如果之后你想检出那个对象时，你将得到先前那个对象的数据。</p><p>但是这种情况发生的概率十分渺小。 SHA-1 摘要长度是 20 字节，也就是 160 位。 2^80 个随机哈希对象才有 50% 的概率出现一次冲突 （计算冲突机率的公式是 <code>p = (n(n-1)/2) * (1/2^160))</code> ）。 2^80 是 1.2 x 10^24，也就是一亿亿亿，这是地球上沙粒总数的 1200 倍。</p><p>举例说一下怎样才能产生一次 SHA-1 冲突。 如果地球上 65 亿个人类都在编程，每人每秒都在产生等价于整个 Linux 内核历史（650 万个 Git 对象）的代码， 并将之提交到一个巨大的 Git 仓库里面，这样持续两年的时间才会产生足够的对象， 使其拥有 50% 的概率产生一次 SHA-1 对象冲突。</p></blockquote><h2 id="交互式暂存"><a class="header-anchor" href="#交互式暂存"></a>交互式暂存</h2><p>用途：当在修改了大量文件后，希望这些改动能拆分为若干提交而不是混杂在一起成为一个提交时。</p><p>通过 <code>git add -i</code> 进入交互式暂存。</p><p><img src="/2024/01/20/git-study-7/image-20240116171937170.png" alt="image-20240116171937170"></p><p>输入数字下标或者前缀字母即可进入下一步，例如 status ，输入 1 或 s 。</p><p>上述的所有命令解释如下：</p><ul><li>status 表示查看当前 Git 已跟踪且变更过的文件状态。</li><li>update 表示进行暂存操作。（即 git add xxx 操作）</li><li>revert 表示取消暂存操作。 （即 git restore xxx 操作）</li><li>add untracked 表示将未跟踪的文件进行跟踪。</li><li>patch 表示暂存文件的特定部分。</li><li>diff 表示查看已暂存内容的区别。</li><li>quit 表示退出。</li><li>help 表示帮助。</li></ul><p>进入以上命令后，想要返回上一步，只需要不输入任何东西的情况下按回车即可。</p><h2 id="贮藏与清理"><a class="header-anchor" href="#贮藏与清理"></a>贮藏与清理</h2><p>有时，当你在项目的一部分上已经工作一段时间后，所有东西都进入了混乱的状态， 而这时你想要切换到另一个分支做一点别的事情。 问题是，你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。 针对这个问题的答案是 <code>git stash</code> 命令。</p><p><strong>贮藏（stash）会处理工作目录的脏的状态——即跟踪文件的修改与暂存的改动——然后将未完成的修改保存到一个栈上， 而你可以在任何时候重新应用这些改动（甚至在不同的分支上）</strong>。</p><p>贮藏相关命令如下：</p><ul><li><p>git stash：贮藏当前改动的文件。</p><blockquote><p>默认情况下，<code>git stash</code> 只会贮藏已修改和暂存的 <strong>已跟踪</strong> 文件。 如果指定 <code>--include-untracked</code> 或 <code>-u</code> 选项，Git 也会贮藏任何未跟踪文件。 然而，在贮藏中包含未跟踪的文件仍然不会包含明确 <strong>忽略</strong> 的文件。 要额外包含忽略的文件，请使用 <code>--all</code> 或 <code>-a</code> 选项。</p></blockquote></li><li><p>git stash list：查看贮藏记录</p></li><li><p>git stash apply：应用贮藏记录的内容</p><blockquote><p>git stash apply 默认应用最新一条记录，如果想要应用指定记录，可根据记录前的下标修改命令为 git stash apply stash@{n} ，n 表示下标。</p></blockquote></li><li><p>git stash drop：删除贮藏记录</p><blockquote><p>默认删除第一条贮藏记录，也可以跟 apply 一样，指定删除。</p></blockquote></li></ul><p><strong>清理（clean）用于从工作目录中移除未被追踪的文件</strong>。</p><p>清理相关命令如下：</p><ul><li><p>git clean -n：查看将要移除的文件有哪些，并不是真的移除。</p></li><li><p>git clean -f：移除 -n 中显示的将要移除的文件。</p><blockquote><p>-d 选项，表示未追踪的子目录。默认情况下 -n 或 -f 不会扫码移除子目录，需要增加 -d 参数才行。</p><p>-x 选项，表示包含移除与 <code>.gitignore</code> 或其他忽略文件中的模式匹配的文件。</p></blockquote></li></ul><h2 id="签署工作"><a class="header-anchor" href="#签署工作"></a>签署工作</h2><p>Git 虽然是密码级安全的，但它不是万无一失的。 如果你从因特网上的其他人那里拿取工作，并且想要验证提交是不是真正地来自于可信来源， Git 提供了几种通过 GPG 来签署和验证工作的方式。</p><p>GPG 使用方法：</p><ul><li><p>gpg --list-keys：查看 GPG 密钥。</p><blockquote><p>示例如下：</p><p><img src="/2024/01/20/git-study-7/image-20240117174307694.png" alt="image-20240117174307694"></p></blockquote></li><li><p>gpg --gen-key：生成 GPG 密钥。</p></li><li><p>git config --global user.signingkey 0A46826A：配置 GPG 的公钥。</p></li></ul><h2 id="搜索"><a class="header-anchor" href="#搜索"></a>搜索</h2><p>无论仓库里的代码量有多少，你经常需要查找一个函数是在哪里调用或者定义的，或者显示一个方法的变更历史。 Git 提供了两个有用的工具来快速地从它的数据库中浏览代码和提交。</p><p>第一个工具是 <code>git grep &lt;搜索字符串或正则表达式&gt;</code> 命令，主要是用于搜索代码在哪里。</p><p>如果传递 <code>-n</code> 参数，将会输出 Git 找到的匹配行的行号。</p><p>如果传递 <code>-c</code> 参数，输出的信息仅包括那些包含匹配字符串的文件，以及每个文件中包含了多少个匹配。</p><p>如果传递 <code>-p</code> 参数，则会显示搜索字符串的上下文。</p><p>第二个工具是 Git 日志搜索，主要是用于搜索代码是何时存在或引入的。</p><p>通过 <code>git log -S &lt;搜索字符串或正则表达式&gt;</code>命令，搜索代码的提交记录，展示提交日志。</p><p>其中 <code>-S</code> 参数表示显示新增和删除该字符串的提交记录。</p><p>如果传递 <code>--oneline</code> 参数，则主要显示提交的修订版本和提交备注。</p><h2 id="重写历史"><a class="header-anchor" href="#重写历史"></a>重写历史</h2><p>在学习重写历史之前，需要牢记一个开发准则：在满意之前不要推送工作内容。</p><p>Git 的基本原则之一是，由于克隆中有很多工作是本地的，因此你可以 <strong>在本地</strong> 随便重写历史记录。 然而一旦推送了你的工作，那就完全是另一回事了，除非你有充分的理由进行更改，否则应该将推送的工作视为最终结果。 简而言之，在对它感到满意并准备与他人分享之前，应当避免推送你的工作。</p><h3 id="修改最后一次提交"><a class="header-anchor" href="#修改最后一次提交"></a>修改最后一次提交</h3><p>顾名思义，只需要对最后一次提交做修改，此时使用 <code>git commit --amend</code> 命令就好。它会用新的提交版本来替换旧的最后一次提交。</p><p>如果提交信息不需要修改，可以使用 <code>--no-edit</code> 参数。</p><h3 id="修改多个提交信息"><a class="header-anchor" href="#修改多个提交信息"></a>修改多个提交信息</h3><p>Git 本身是没有直接改变历史的工具的，可以使用变基工具来变基这一系列提交，通过交互式变基工具，可以在任何想要修改的提交后停止，然后修改信息、添加文件或做任何想做的事情。</p><p>需要注意的是，<code>git rebase -i &lt;版本区间&gt;</code>命令会将这个区间的所有提交信息进行重写，所以尽量不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本，因而使他人困惑。</p><h3 id="filter-branch"><a class="header-anchor" href="#filter-branch"></a>filter-branch</h3><p>有另一个历史改写的选项，如果想要通过脚本的方式改写大量提交的话可以使用它——例如，全局修改你的邮箱地址或从每一个提交中移除一个文件。 这个命令是 <code>filter-branch</code>，它可以改写历史中大量的提交，除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作，否则你不应当使用它。</p><blockquote><p><code>git filter-branch</code> 有很多陷阱，不再推荐使用它来重写历史。 请考虑使用 <code>git-filter-repo</code>，它是一个 Python 脚本，相比大多数使用 <code>filter-branch</code> 的应用来说，它做得要更好。它的文档和源码可访问 https://github.com/newren/git-filter-repo 获取。</p></blockquote><h4 id="从每一个提交中移除一个文件"><a class="header-anchor" href="#从每一个提交中移除一个文件"></a>从每一个提交中移除一个文件</h4><p>有人粗心地通过 <code>git add .</code> 提交了一个巨大的二进制文件，你想要从所有地方删除。 可能偶然地提交了一个包括一个密码的文件，然而你想要开源项目。 <code>filter-branch</code> 是一个可能会用来擦洗整个提交历史的工具。 为了从整个提交历史中移除一个叫做 <code>passwords.txt</code> 的文件，可以使用 <code>--tree-filter</code> 选项给 <code>filter-branch</code>：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git filter-branch --tree-filter <span class="string">&#x27;rm -f passwords.txt&#x27;</span> HEAD</span></span><br><span class="line">Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)</span><br><span class="line">Ref &#x27;refs/heads/master&#x27; was rewritten</span><br></pre></td></tr></table></figure><p><code>--tree-filter</code> 选项在检出项目的每一个提交后运行指定的命令然后重新提交结果。 在本例中，你从每一个快照中移除了一个叫作 <code>passwords.txt</code> 的文件，无论它是否存在。</p><h4 id="使一个子目录做为新的根目录"><a class="header-anchor" href="#使一个子目录做为新的根目录"></a>使一个子目录做为新的根目录</h4><p>假设已经从另一个源代码控制系统中导入，并且有几个没意义的子目录（<code>trunk</code>、<code>tags</code> 等等）。 如果想要让 <code>trunk</code> 子目录作为每一个提交的新的项目根目录，<code>filter-branch</code> 也可以帮助你那么做：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git filter-branch --subdirectory-filter trunk HEAD</span></span><br><span class="line">Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)</span><br><span class="line">Ref &#x27;refs/heads/master&#x27; was rewritten</span><br></pre></td></tr></table></figure><p>现在新项目根目录是 <code>trunk</code> 子目录了。 Git 会自动移除所有不影响子目录的提交。</p><h4 id="全局修改邮箱地址"><a class="header-anchor" href="#全局修改邮箱地址"></a>全局修改邮箱地址</h4><p>另一个常见的情形是在你开始工作时忘记运行 <code>git config</code> 来设置你的名字与邮箱地址， 或者你想要开源一个项目并且修改所有你的工作邮箱地址为你的个人邮箱地址。 任何情形下，你也可以通过 <code>filter-branch</code> 来一次性修改多个提交中的邮箱地址。 需要小心的是只修改你自己的邮箱地址，所以你使用 <code>--commit-filter</code>：</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git filter-branch --commit-filter <span class="string">&#x27;</span></span></span><br><span class="line">        if [ &quot;$GIT_AUTHOR_EMAIL&quot; = &quot;123@qq.com&quot; ];</span><br><span class="line">        then</span><br><span class="line">                GIT_AUTHOR_NAME=&quot;Wray&quot;;</span><br><span class="line">                GIT_AUTHOR_EMAIL=&quot;wray20156294@gmail.com&quot;;</span><br><span class="line">                git commit-tree &quot;$@&quot;;</span><br><span class="line">        else</span><br><span class="line">                git commit-tree &quot;$@&quot;;</span><br><span class="line">        fi&#x27; HEAD</span><br></pre></td></tr></table></figure><p>这会遍历并重写每一个提交来包含你的新邮箱地址。 因为提交包含了它们父提交的 SHA-1 校验和，这个命令会修改你的历史中的每一个提交的 SHA-1 校验和， 而不仅仅只是那些匹配邮箱地址的提交。</p><p>如果提交记录已经推送到远程仓库，需要使用 <code>git push origin branch_name</code>命令更新。</p><p>使用它来修改提交者（作者和提交者）信息可能导致与远程仓库的历史不一致，从而导致推送被拒绝。这是因为修改历史会改变每个提交的 SHA-1 校验和，而远程仓库已经有了不同的历史。</p><p>因此建议使用<code>git push --force-with-lease origin branch_name</code> 方式安全的强制推送，它会检查远程分支是否和你的本地分支一致。如果远程分支的历史已经被其他人修改，推送会被拒绝。</p><h2 id="重置揭密"><a class="header-anchor" href="#重置揭密"></a>重置揭密</h2><p>主要讲解 <code>git reset</code> 命令的强大之处，以及与 <code>git checkout</code> 的区别。</p><p>更多详情在：<a href="https://blog.itwray.com/2023/11/22/git-commands-reset/">https://blog.itwray.com/2023/11/22/git-commands-reset/</a></p><h2 id="高级合并"><a class="header-anchor" href="#高级合并"></a>高级合并</h2><p>Git 的哲学是聪明地决定无歧义的合并方案，但是如果有冲突，它不会尝试智能地自动解决它。</p><p>所以，在合并前，建议将正在做的工作，要么提交到一个临时分支要么储藏（stasg）它。</p><p>如果已经执行了 <code>git merge</code> 合并操作并出现了合并冲突，可以通过 <code>git merge --abort</code> 来简单地退出合并。</p><p>如果这个不想要的合并提交只存在于你的本地仓库中，最简单且最好的解决方案是移动分支到你想要它指向的地方。 大多数情况下，如果你在错误的 <code>git merge</code> 后运行 <code>git reset --hard HEAD~</code>。</p><h2 id="Rerere"><a class="header-anchor" href="#Rerere"></a>Rerere</h2><p><code>git rerere</code> 功能是一个隐藏的功能。 正如它的名字“重用记录的解决方案（reuse recorded resolution）”所示，它允许你让 Git 记住解决一个块冲突的方法， 这样在下一次看到相同冲突时，Git 可以为你自动地解决它。</p><p>有几种情形下这个功能会非常有用。 在文档中提到的一个例子是想要保证一个长期分支会干净地合并，但是又不想要一串中间的合并提交弄乱你的提交历史。 将 <code>rerere</code> 功能开启后，你可以试着偶尔合并，解决冲突，然后退出合并。 如果你持续这样做，那么最终的合并会很容易，因为 <code>rerere</code> 可以为你自动做所有的事情。</p><h2 id="Git-调试"><a class="header-anchor" href="#Git-调试"></a>Git 调试</h2><p>使用 <code>git blame</code> 标注文件，可以知道一个文件的每一行具体在何时引入的，显示每行最后一次修改的提交记录。</p><p>例如：<code>git blame -L 69,82 Makefile</code> 表示用 <code>git blame</code> 确定了 Linux 内核源码顶层的 <code>Makefile</code> 中每一行分别来自哪个提交和提交者， 此外用 <code>-L</code> 选项还可以将标注的输出限制为该文件中的第 69 行到第 82 行。</p><p>使用<code>git bisect</code> 命令会对提交历史进行二分查找，以找到是哪一个提交引入了问题。</p><h2 id="子模块"><a class="header-anchor" href="#子模块"></a>子模块</h2><p>子模块的应用场景如下：某个工作中的项目需要包含并使用另一个项目。 也许是第三方库，或者你独立开发的，用于多个父项目的库。 现在问题来了：你想要把它们当做两个独立的项目，同时又想在一个项目中使用另一个。</p><p>首先，确定自己的主项目，然后在主项目的仓库下通过<code>git submodule add</code> 命令后面加上想要跟踪的项目的相对或绝对 URL 来添加新的子模块。</p><p>例如，在 <code>git-study</code> 项目下添加 <code>git-study-submodule</code>子项目：</p><p><code>git submodule add git@github.com:wangfarui/git-study-submodule.git</code></p><p>查看<code>git-study</code>项目目录结构：</p><p><img src="/2024/01/20/git-study-7/image-20240119110628042.png" alt="image-20240119110628042"></p><p>首先应当注意到新的 <code>.gitmodules</code> 文件。 该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射：</p><p><img src="/2024/01/20/git-study-7/image-20240119110652184.png" alt="image-20240119110652184"></p><p>如果有多个子模块，该文件中就会有多条记录。 要重点注意的是，该文件也像 <code>.gitignore</code> 文件一样受到（通过）版本控制。 它会和该项目的其他部分一同被拉取推送。 这就是克隆该项目的人知道去哪获得子模块的原因。</p><p>添加子模块后，<code>git status</code>查看主项目仓库状态：</p><p><img src="/2024/01/20/git-study-7/image-20240119110832251.png" alt="image-20240119110832251"></p><p>可以发现子模块的项目内容已经被跟踪到主项目中，执行<code>git commit</code>将其提交到主项目中，再执行<code>git push</code>将主项目推送到远程仓库。</p><p>在 GitHub 下，可以看到 GitHub 会自动识别到子项目，项目链接到子项目地址。</p><p><img src="/2024/01/20/git-study-7/image-20240119111301145.png" alt="image-20240119111301145"></p><p>但是！！！需要注意链接地址：</p><p>https://github.com/wangfarui/git-study-submodule/tree/ea6b9f6afef267dc897376df3b98b879aa5ec0fb</p><p>可以再看看<code>git-study-submodule</code>项目的真正 Git 仓库地址：</p><p>https://github.com/wangfarui/git-study-submodule</p><p>可以发现从主项目进入的子项目显示的是主项目的版本，所以如果直接在子项目做提交变更，主项目是感知不到的，需要手动刷新并推送。</p><p>由此可见，子项目是独立存在的，只不过主项目将其引入进来，可以直接使用源码。同理，子项目的开发工作可以是独立项目开发，也可以是直接在主项目的子模块下开发。在子模块下执行 <code>git remote show origin</code> 可以看到当前指向的 Git 远程仓库为子项目的仓库地址。</p><p><img src="/2024/01/20/git-study-7/image-20240119111717930.png" alt="image-20240119111717930"></p><p>在子项目下做编辑并提交操作后，在主项目通过<code>git add</code>跟踪子模块，再提交和推送后，子模块的远程仓库才会同步更新。</p><p>如果子项目已经独立开发并更新到远程仓库了，可以使用 <code>git submodule update --remote git-study-submodule</code> 命令拉取最新信息，此时 <code>git-study-submodule</code> 子项目的内容已经与远程仓库同步了，不需要再 merge 。</p><p>更新完子项目后，主项目需要通过<code>git add</code>跟踪子模块，再提交和推送后，主项目下的子项目版本才会同步变更。</p><h2 id="打包"><a class="header-anchor" href="#打包"></a>打包</h2><p>Git 可以将它的数据“打包”到一个文件中。 这在许多场景中都很有用。 有可能你的网络中断了，但你又希望将你的提交传给你的合作者们。 可能你不在办公网中并且出于安全考虑没有给你接入内网的权限。 可能你的无线、有线网卡坏掉了。 可能你现在没有共享服务器的权限，你又希望通过邮件将更新发送给别人， 却不希望通过 <code>format-patch</code> 的方式传输 40 个提交。</p><p>这些情况下 <code>git bundle</code> 就会很有用。 <code>bundle</code> 命令会将 <code>git push</code> 命令所传输的所有内容打包成一个二进制文件， 你可以将这个文件通过邮件或者闪存传给其他人，然后解包到其他的仓库中。</p><h2 id="替换"><a class="header-anchor" href="#替换"></a>替换</h2><p>Git 对象数据库中的对象是不可改变的， 然而 Git 提供了一种有趣的方式来用其他对象 <strong>假装</strong> 替换数据库中的 Git 对象。</p><p><code>replace</code> 命令可以让你在 Git 中指定 <strong>某个对象</strong> 并告诉 Git：“每次遇到这个 Git 对象时，假装它是 <strong>其它对象</strong>”。 在你用一个不同的提交替换历史中的一个提交而不想以 <code>git filter-branch</code> 之类的方式重建完整的历史时，这会非常有用。</p><h2 id="凭证存储"><a class="header-anchor" href="#凭证存储"></a>凭证存储</h2><p>如果你使用的是 SSH 方式连接远端，并且设置了一个没有口令的密钥，这样就可以在不输入用户名和密码的情况下安全地传输数据。 然而，这对 HTTP 协议来说是不可能的 —— 每一个连接都是需要用户名和密码的。 这在使用双重认证的情况下会更麻烦，因为你需要输入一个随机生成并且毫无规律的 token 作为密码。</p><p>Git 拥有一个凭证系统来处理这个事情。 下面有一些 Git 的选项：</p><ul><li>默认所有都不缓存。 每一次连接都会询问你的用户名和密码。</li><li>“cache” 模式会将凭证存放在内存中一段时间。 密码永远不会被存储在磁盘中，并且在15分钟后从内存中清除。</li><li>“store” 模式会将凭证用明文的形式存放在磁盘中，并且永不过期。 这意味着除非你修改了你在 Git 服务器上的密码，否则你永远不需要再次输入你的凭证信息。 这种方式的缺点是你的密码是用明文的方式存放在你的 home 目录下。</li><li>如果你使用的是 Mac，Git 还有一种 “osxkeychain” 模式，它会将凭证缓存到你系统用户的钥匙串中。 这种方式将凭证存放在磁盘中，并且永不过期，但是是被加密的，这种加密方式与存放 HTTPS 凭证以及 Safari 的自动填写是相同的。</li><li>如果你使用的是 Windows，你可以安装一个叫做 “Git Credential Manager for Windows” 的辅助工具。 这和上面说的 “osxkeychain” 十分类似，但是是使用 Windows Credential Store 来控制敏感信息。 可以在 https://github.com/Microsoft/Git-Credential-Manager-for-Windows 下载。</li></ul><p>你可以设置 Git 的配置来选择上述的一种方式</p><figure class="highlight console"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">git config --global credential.helper cache</span></span><br></pre></td></tr></table></figure><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>Git 工具非常之多，有些工具在日常开发中可能基本用不到，但有些工具还是非常实用的。这里主要列举一下我觉得比较有用的：贮藏（git stash）、重写最后一次提交（git commit --amend）、重置揭密（git reset）、搜索（git grep）。</p>]]></content>
    
    
    <summary type="html">第七章学习 Git 工具，这些工具的也是 Git 的重要一环，包含有修订版本、贮藏、重置等十分强大的功能，这些功能在日常操作中不一定经常使用，但在特殊情况下确实非常受用的。</summary>
    
    
    
    <category term="Git" scheme="https://blog.itwray.com/categories/Git/"/>
    
    
    <category term="Git" scheme="https://blog.itwray.com/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>工作小结-接入钉钉机器人</title>
    <link href="https://blog.itwray.com/2024/01/13/work-report-dingtalk-robot/"/>
    <id>https://blog.itwray.com/2024/01/13/work-report-dingtalk-robot/</id>
    <published>2024-01-13T08:09:33.000Z</published>
    <updated>2024-09-27T13:14:50.076Z</updated>
    
    <content type="html"><![CDATA[<h2 id="钉钉机器人介绍"><a class="header-anchor" href="#钉钉机器人介绍"></a>钉钉机器人介绍</h2><p>官方地址：<a href="https://open.dingtalk.com/document/robots/custom-robot-access">https://open.dingtalk.com/document/robots/custom-robot-access</a></p><p>企业内部有较多系统支撑着公司的核心业务流程，譬如CRM系统、交易系统、监控报警系统等等。通过钉钉的自定义机器人，可以将这些系统事件同步到钉钉的聊天群。</p><h2 id="接入方式"><a class="header-anchor" href="#接入方式"></a>接入方式</h2><p>接入钉钉机器人比较简单，分为两步步骤：</p><ol><li>在钉钉群聊中，添加并配置机器人。</li><li>基于钉钉机器人的 Webhook 地址发起 HTTP POST 请求，即可实现给该钉钉群发送消息。</li></ol><p>发送的消息内容是一个 JSON 对象，按照钉钉给定的消息类型和数据格式进行发送。</p><p>当前自定义机器人支持文本 (text)、链接 (link)、markdown(markdown)、ActionCard、FeedCard消息类型。</p><blockquote><p>钉钉提供了SDK接入方式，通过如下依赖实现。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.aliyun<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>alibaba-dingtalk-service-sdk<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure></blockquote><h2 id="使用方法"><a class="header-anchor" href="#使用方法"></a>使用方法</h2><p>钉钉机器人发送消息可能是一个非常常用的操作，大多数情况下，发送的消息内容结构是固定的，所以在项目中对钉钉机器人发送消息做了一个封装。</p><p>首先，看一下如果不封装代码，直接使用 Java 代码发送消息的示例代码。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OriginalDemo</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        <span class="comment">// 定义消息内容</span></span><br><span class="line">        Map&lt;String, String&gt; msg = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        msg.put(<span class="string">&quot;msgtype&quot;</span>, <span class="string">&quot;text&quot;</span>);</span><br><span class="line">        msg.put(<span class="string">&quot;text&quot;</span>, JSONUtil.toJsonStr(<span class="keyword">new</span> <span class="title class_">Text</span>(<span class="string">&quot;我就是我, 是不一样的烟火&quot;</span>)));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 发送消息</span></span><br><span class="line">        <span class="type">HttpResponse</span> <span class="variable">httpResponse</span> <span class="operator">=</span> HttpUtil.createPost(<span class="string">&quot;https://oapi.dingtalk.com/robot/send?access_token=566cc69da782ec******&quot;</span>)</span><br><span class="line">                .body(JSONUtil.toJsonStr(msg))</span><br><span class="line">                .execute();</span><br><span class="line"></span><br><span class="line">        System.out.println(httpResponse);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Data</span></span><br><span class="line">    <span class="meta">@AllArgsConstructor</span></span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Text</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> String content;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个示例中，发送的钉钉消息是一串文本，需要先定义消息类型，再通过HTTP工具发送。</p><p>这其中除了文本消息内容是可变的，其实都是固定的，所以如果通过封装后的工具发送，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DingTalkHelper.sendMessage(<span class="string">&quot;我就是我, 是不一样的烟火&quot;</span>);</span><br></pre></td></tr></table></figure><br><p>完整的项目示例代码：<a href="https://github.com/wangfarui/work-report/tree/main/dingtalk-rebot">https://github.com/wangfarui/work-report/tree/main/dingtalk-rebot</a></p><h2 id="实现封装"><a class="header-anchor" href="#实现封装"></a>实现封装</h2><p>工作项目上，因为使用了 Apollo 作为动态属性配置，所以钉钉机器人封装中也用到了它。</p><h3 id="参数配置"><a class="header-anchor" href="#参数配置"></a>参数配置</h3><p>首先，确定钉钉机器人需要的属性，通过钉钉文档的介绍，机器人发送消息至少需要客户端地址，如果配置了授权和加密，则还需要 token 和 secret ，参数如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.client-url:https://oapi.dingtalk.com/robot/send&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> String clientUrl;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.access-token:&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> String accessToken;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.secret:&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> String secret;</span><br></pre></td></tr></table></figure><p>然后，钉钉机器人发送的消息有时是不必须的，例如用于发送系统告警信息时，在内网进行开发联调时，不希望打印一堆异常信息到钉钉群里，就增加了个参数用于控制钉钉机器人的开启和关闭（ enabled ）。</p><p>此外，结合钉钉机器人支持@功能，实现了动态@指定人或所有人功能，通过手机号绑定。参数如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 开启钉钉告警功能</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.enabled:false&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> enabled;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 群@指定人手机号</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.at.atMobiles:&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> List&lt;String&gt; atMobiles;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 群@所有人</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.at.atAll:false&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> atAll;</span><br></pre></td></tr></table></figure><p>最后，在钉钉机器人用于发送系统异常消息时，有时希望忽略某些接口的告警，有时希望只开启某些接口的告警，所以配置了忽略告警url和指定告警url两个参数：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 忽略的告警url</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.ignoreUrls:&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; ignoreUrls;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 指定的告警url</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Value(&quot;$&#123;dingTalk.warnUrls:&#125;&quot;)</span></span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; warnUrls;</span><br></pre></td></tr></table></figure><h3 id="发送消息"><a class="header-anchor" href="#发送消息"></a>发送消息</h3><p>为了减轻项目的依赖项，因此没有接入SDK方式，发送消息仍然采用的是HTTP方式。</p><p>为了保证钉钉机器人发送消息功能不影响业务功能的正常进行，因此将发送消息功能独立到新的线程，并对发送结果的异常消息做日志记录。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 发送钉钉消息</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> request  机器人消息对象</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> canApply 是否需要发送钉钉机器人消息</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">send</span><span class="params">(<span class="keyword">final</span> DingTalkSendRequest request, <span class="type">boolean</span> canApply)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (canApply) &#123;</span><br><span class="line">        <span class="keyword">if</span> (properties.isAtAll()) &#123;</span><br><span class="line">            request.setAtAll(properties.isAtAll());</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (CollectionUtils.isNotEmpty(properties.getAtMobiles())) &#123;</span><br><span class="line">            request.setAtMobiles(properties.getAtMobiles());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="type">boolean</span> <span class="variable">completed</span> <span class="operator">=</span> request.completeRequestParam();</span><br><span class="line">        <span class="keyword">if</span> (!completed) &#123;</span><br><span class="line">            log.warn(<span class="string">&quot;[DingTalkClient][send]钉钉消息请求对象数据异常, request:&#123;&#125;&quot;</span>, JSON.toJSONString(request));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">String</span> <span class="variable">requestUrl</span> <span class="operator">=</span> properties.getRequestUrl();</span><br><span class="line">        EXECUTOR_SERVICE.execute(() -&gt; &#123;</span><br><span class="line">            <span class="type">HttpResponse</span> <span class="variable">httpResponse</span> <span class="operator">=</span> HttpUtil.createPost(requestUrl)</span><br><span class="line">                    .body(JSON.toJSONString(request))</span><br><span class="line">                    .charset(StandardCharsets.UTF_8)</span><br><span class="line">                    .execute();</span><br><span class="line">            <span class="keyword">if</span> (httpResponse == <span class="literal">null</span>) &#123;</span><br><span class="line">                log.warn(<span class="string">&quot;[DingTalkClient][send]发送钉钉消息异常, request:&#123;&#125;&quot;</span>, JSON.toJSONString(request));</span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!httpResponse.isOk()) &#123;</span><br><span class="line">                log.warn(<span class="string">&quot;[DingTalkClient][send]发送钉钉消息失败, request:&#123;&#125;, response:&#123;&#125;&quot;</span>, JSON.toJSONString(request), JSON.toJSONString(httpResponse));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过发送消息方法的入参可以看出，此功能主要依赖于 <code>DingTalkSendRequest</code> 对象，此对象的属性是严格按照钉钉文档的参数名进行设定的，避免JSON序列化时的二次包装。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DingTalkSendRequest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 用于钉钉入参</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">private</span> String msgtype;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 钉钉消息类型</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="keyword">private</span> DingTalkMsgType dingTalkMsgType;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 钉钉消息需要 @ 的对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">private</span> AT at;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 钉钉消息类型为 markdown 的内容</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">private</span> Markdown markdown;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 钉钉消息类型为 text 的内容</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">private</span> Text text;</span><br><span class="line">  </span><br><span class="line">  <span class="meta">@Setter</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">AT</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> List&lt;String&gt; atMobiles;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> List&lt;String&gt; atUserIds;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> <span class="type">boolean</span> isAtAll;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Markdown</span> &#123;</span><br><span class="line">        <span class="meta">@Setter</span></span><br><span class="line">        <span class="meta">@Getter</span></span><br><span class="line">        <span class="keyword">private</span> String title;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Setter</span></span><br><span class="line">        <span class="meta">@Getter</span></span><br><span class="line">        <span class="keyword">private</span> String text;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/**</span></span><br><span class="line"><span class="comment">         * 非钉钉消息的数据格式</span></span><br><span class="line"><span class="comment">         */</span></span><br><span class="line">        <span class="keyword">private</span> Map&lt;String, String&gt; content;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addContent</span><span class="params">(String key, String value)</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.content == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="built_in">this</span>.content = <span class="keyword">new</span> <span class="title class_">LinkedHashMap</span>&lt;&gt;();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">this</span>.content.put(key, value);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">formatContentToText</span><span class="params">()</span> &#123;</span><br><span class="line">            <span class="type">StringBuilder</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>();</span><br><span class="line">            <span class="keyword">for</span> (Map.Entry&lt;String, String&gt; entry : <span class="built_in">this</span>.content.entrySet()) &#123;</span><br><span class="line">                sb.append(entry.getKey()).append(<span class="string">&quot;：&quot;</span>).append(entry.getValue()).append(<span class="string">&quot;\n\n&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">this</span>.text = sb.toString();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="meta">@Getter</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Text</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> String content;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="系统异常消息"><a class="header-anchor" href="#系统异常消息"></a>系统异常消息</h3><p>通过 spring-web 的 <code>@RestControllerAdvice</code> + <code>@ExceptionHandler</code> 注解拦截指定异常，对需要的异常发送消息到钉钉。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">sendDingTalkMessage</span><span class="params">(Throwable e, HttpServletRequest httpServletRequest)</span> &#123;</span><br><span class="line">    <span class="comment">// 初始化钉钉告警对象 并配置告警标题</span></span><br><span class="line">    <span class="type">DingTalkSendRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DingTalkSendRequest</span>();</span><br><span class="line">    request.setMarkdownTitle(e <span class="keyword">instanceof</span> BizException ? <span class="string">&quot;业务告警&quot;</span> : <span class="string">&quot;系统告警&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 配置基础告警信息</span></span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;【告警环境】&quot;</span>, <span class="built_in">this</span>.envStr);</span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;【traceId】&quot;</span>, MDC.get(<span class="string">&quot;traceId&quot;</span>));</span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;【租户id】&quot;</span>, UserUtils.getTenantId().toString());</span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;【告警时间】&quot;</span>, DateUtil.now());</span><br><span class="line">    <span class="comment">// 配置http请求告警信息</span></span><br><span class="line">    <span class="keyword">if</span> (httpServletRequest != <span class="literal">null</span>) &#123;</span><br><span class="line">        request.addMarkdownContent(<span class="string">&quot;【异常接口】&quot;</span>, httpServletRequest.getRequestURI());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 异常堆栈信息</span></span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;【异常堆栈】&quot;</span>, ExceptionUtils.exceptionStackTraceText(e, <span class="number">1000</span>));</span><br><span class="line"></span><br><span class="line">    DingTalkHelper.send(request);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="二次封装"><a class="header-anchor" href="#二次封装"></a>二次封装</h3><p>如果直接使用封装的 <code>DingTalkClient</code> 对象，调用 <code>send</code> 方法发送消息，对于开发人员来说还是比较麻烦的，还需要自定义消息对象等。</p><p>因此，根据业务项目场景需求，二次封装了一个抽象类，开发人员可以直接静态调用发送消息功能，实现普通消息和异常消息的发送。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">sendMessage</span><span class="params">(String message)</span> &#123;</span><br><span class="line">    <span class="type">DingTalkSendRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DingTalkSendRequest</span>();</span><br><span class="line">    request.setTextContent(message);</span><br><span class="line">    getDingTalkClient().send(request);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">sendException</span><span class="params">(String message, Throwable e)</span> &#123;</span><br><span class="line">    <span class="type">DingTalkSendRequest</span> <span class="variable">request</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DingTalkSendRequest</span>();</span><br><span class="line">    request.setMarkdownTitle(<span class="string">&quot;自定义异常&quot;</span>);</span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;异常内容&quot;</span>, message);</span><br><span class="line">    request.addMarkdownContent(<span class="string">&quot;异常信息&quot;</span>, ExceptionUtils.exceptionStackTraceText(e));</span><br><span class="line">    getDingTalkClient().send(request);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>钉钉机器人发送消息这个功能，钉钉官方支持的是比较简单的，因此主要是看消息内容的JSON数据，通过JSON格式化确定消息内容的复杂性。所以本次工作上的封装主要是针对消息内容做了一个优化，然后结合项目需求开发一些特殊规定，例如什么环境需要@什么人、什么接口需要屏蔽、什么异常是必须要发送消息的等等。</p>]]></content>
    
    
    <summary type="html">2024-01-13工作小结，整理钉钉机器人API的使用和封装。</summary>
    
    
    
    <category term="工作" scheme="https://blog.itwray.com/categories/%E5%B7%A5%E4%BD%9C/"/>
    
    
    <category term="工作小结" scheme="https://blog.itwray.com/tags/%E5%B7%A5%E4%BD%9C%E5%B0%8F%E7%BB%93/"/>
    
  </entry>
  
  <entry>
    <title>工作小结-MyBatis-Plus填充策略</title>
    <link href="https://blog.itwray.com/2024/01/10/work-report-mybatisplus-fill/"/>
    <id>https://blog.itwray.com/2024/01/10/work-report-mybatisplus-fill/</id>
    <published>2024-01-10T09:12:19.000Z</published>
    <updated>2024-09-27T13:14:53.594Z</updated>
    
    <content type="html"><![CDATA[<h2 id="MyBatis-Plus简介"><a class="header-anchor" href="#MyBatis-Plus简介"></a>MyBatis-Plus简介</h2><p>官方地址：<a href="https://baomidou.com/">https://baomidou.com/</a></p><p>MyBatis-Plus (opens new window)（简称 MP）是一个 MyBatis (opens new window)的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。</p><p>而本次工作内容就是使用了它的<a href="https://baomidou.com/pages/4c6bcf/">自动填充功能</a>，实现 Entity 对象属性值的自动填充。</p><h2 id="自动填充功能"><a class="header-anchor" href="#自动填充功能"></a>自动填充功能</h2><p>实现功能：在插入或更新数据时，自动插入或更新指定字段的值。例如一些特殊字段：创建时间、创建人、更新时间、更新人等。</p><p>实现原理主要分为两步：</p><ol><li>实现元对象处理器接口：com.baomidou.mybatisplus.core.handlers.MetaObjectHandler</li><li>@TableField 注解标记字段的填充策略。</li></ol><p>注意事项：</p><p><img src="/2024/01/10/work-report-mybatisplus-fill/image-20240112152613998.png" alt="image-20240112152613998"></p><p>关于注意事项第一点：填充原理是直接给 Entity 的属性设置值，理解起来就是，填充原理是基于 Entity 实例对象的，所以通过 Mapper SQL 语句方式、或者通过 Lambda 表达式方式都是不可行的。</p><p>关于注意事项最后一点：解释当需要通过 Mapper SQL 语句方式填充时，必须按照它的约定配置，一要求方法参数对象是 Entity 对象，二要求方法参数类型需要按照约定定义别名。</p><h2 id="使用方法"><a class="header-anchor" href="#使用方法"></a>使用方法</h2><p>在 Spring Boot 环境下，定义实体对象、Mapper对象、Service对象。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@TableName(&quot;user&quot;)</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TableId(type = IdType.ASSIGN_ID)</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String email;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TableField(fill = FieldFill.INSERT)</span></span><br><span class="line">    <span class="keyword">private</span> Date createTime;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TableField(fill = FieldFill.INSERT)</span></span><br><span class="line">    <span class="keyword">private</span> Long createById;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TableField(fill = FieldFill.UPDATE)</span></span><br><span class="line">    <span class="keyword">private</span> Date updateTime;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Mapper</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserMapper</span> <span class="keyword">extends</span> <span class="title class_">BaseMapper</span>&lt;User&gt; &#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceImpl</span> <span class="keyword">extends</span> <span class="title class_">ServiceImpl</span>&lt;UserMapper, User&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">save</span><span class="params">(String name, Integer age, String email)</span> &#123;</span><br><span class="line">        <span class="comment">// 初始化User对象</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.setName(name);</span><br><span class="line">        user.setAge(age);</span><br><span class="line">        user.setEmail(email);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 保存</span></span><br><span class="line">        <span class="built_in">this</span>.save(user);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 查询</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.getById(user.getId());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">updateByLambda</span><span class="params">(Long id, Integer age)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.lambdaUpdate()</span><br><span class="line">                .eq(User::getId, id)</span><br><span class="line">                .set(User::getAge, age)</span><br><span class="line">                .update();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.getById(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">updateByMethod</span><span class="params">(Long id, Integer age)</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">        user.setId(id);</span><br><span class="line">        user.setAge(age);</span><br><span class="line">        <span class="built_in">this</span>.updateById(user);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">this</span>.getById(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后，实现 <code>MetaObjectHandler</code> 接口，定义 MyBatis Plus 填充策略。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyMetaObjectHandler</span> <span class="keyword">implements</span> <span class="title class_">MetaObjectHandler</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">insertFill</span><span class="params">(MetaObject metaObject)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.strictInsertFill(metaObject, <span class="string">&quot;createTime&quot;</span>, Date.class, <span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line">        <span class="built_in">this</span>.strictInsertFill(metaObject, <span class="string">&quot;createById&quot;</span>, Long.class, System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">updateFill</span><span class="params">(MetaObject metaObject)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.strictUpdateFill(metaObject, <span class="string">&quot;updateTime&quot;</span>, Date.class, <span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后，编写单元测试类，调用 Service 对象的方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserServiceTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserServiceImpl userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSaveUser</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.save(<span class="string">&quot;wray&quot;</span>, <span class="number">18</span>, <span class="string">&quot;wray20156294@gmail.com&quot;</span>);</span><br><span class="line">        System.out.println(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testUpdateByLambda</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.updateByLambda(<span class="number">1745704644463579138L</span>, <span class="number">19</span>);</span><br><span class="line">        System.out.println(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testUpdateByMethod</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userService.updateByMethod(<span class="number">1745704644463579138L</span>, <span class="number">20</span>);</span><br><span class="line">        System.out.println(user);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试发现，在调用 <code>userService.updateByLambda</code> 方法时，基于 Lambda 表达式修改的数据，填充策略没有生效，证实了注意事项的第一点。</p><br><p>完整项目示例代码：<a href="https://github.com/wangfarui/work-report/tree/main/mybatis-plus-fill">https://github.com/wangfarui/work-report/tree/main/mybatis-plus-fill</a></p><h2 id="关于-TableField-注解标记字段填充策略的疑问"><a class="header-anchor" href="#关于-TableField-注解标记字段填充策略的疑问"></a>关于 @TableField 注解标记字段填充策略的疑问</h2><p>自动填充功能的两步实现原理中，第二步要求指定字段标记填充策略。一开始我认为有点搞繁琐了，因为既然已经在第一步配置填充策略时指定了填充字段的名称，为何还要再标记说明一遍。</p><p>这就得先搞清楚 MyBatis-Plus 是如何实现此功能的了，假如没有第二步，现在有一个 Entity 对象需要保存，填充策略自动填充值是填充的属性值，其实并没有直接给实体对象赋值。等到后面拼接保存方法的sql语句时，判断实体对象是否存在相同字段名称的属性，然后拼接sql语句，实现字段自动填充。</p><p>上述是指实体对象确实需要自动填充，假如某个表也有相同字段，但是它不需要自动填充，如果没有第二步标记指定，就得把业务表是否填充的判断逻辑放到 MyBatis-Plus 的 MetaObjectHandler 填充方法中了，这样一个为了简化代码的自动填充功能反而变得复杂了。</p><p>因此，MyBatis-Plus 在实现自动填充功能时，不仅需要判断实体对象是否存在相同字段名称的属性，还要判断该属性是否被标记。</p><h2 id="总结"><a class="header-anchor" href="#总结"></a>总结</h2><p>MyBatis-Plus 的自动填充功能，在业务项目下还是很实用的，业务项目的数据表基本上都要求要有创建信息、更新信息等，这些基础数据在业务代码中会频繁出现且代码内容完全相同，使用填充功能还可以避免大量重复代码。</p>]]></content>
    
    
    <summary type="html">2024-01-10工作小结，了解MyBatis-Plus填充策略的使用和工作原理。</summary>
    
    
    
    <category term="工作" scheme="https://blog.itwray.com/categories/%E5%B7%A5%E4%BD%9C/"/>
    
    
    <category term="工作小结" scheme="https://blog.itwray.com/tags/%E5%B7%A5%E4%BD%9C%E5%B0%8F%E7%BB%93/"/>
    
    <category term="MyBatis-Plus" scheme="https://blog.itwray.com/tags/MyBatis-Plus/"/>
    
  </entry>
  
  <entry>
    <title>工作小结-ShedLock的使用</title>
    <link href="https://blog.itwray.com/2024/01/08/work-report-shedlock/"/>
    <id>https://blog.itwray.com/2024/01/08/work-report-shedlock/</id>
    <published>2024-01-08T07:17:21.000Z</published>
    <updated>2024-09-27T13:14:56.343Z</updated>
    
    <content type="html"><![CDATA[<h2 id="ShedLock简介"><a class="header-anchor" href="#ShedLock简介"></a>ShedLock简介</h2><p>ShedLock 保证任务最多同时执行一次，当一个节点获取到任务并开始执行，那么其他节点不会等待，而是直接跳过。</p><p>它不是一个分布式调度程序，更像是一个分布式锁，在分布式锁的基础上对调度任务做了扩展，使得分布式服务下的定时任务看起来像分布式调度任务一样。</p><p>此外，ShedLock 的锁是基于时间的，并且 ShedLock 在乐观上是假设所有节点上的时钟是同步的。</p><p>ShedLock 的Github地址：<a href="https://github.com/lukas-krecan/ShedLock">https://github.com/lukas-krecan/ShedLock</a></p><h2 id="ShedLock组成"><a class="header-anchor" href="#ShedLock组成"></a>ShedLock组成</h2><p>ShedLock 分为三部分：</p><ul><li>Core 核心：提供锁机制。</li><li>Integration 集成：使用 Spring AOP、Micronaut AOP 或手写代码与业务项目代码集成。</li><li>Lock Provider 锁提供程序：使用 SQL 数据库、Mongo、Redis 等外部进程提供锁。</li></ul><h2 id="ShedLock用法"><a class="header-anchor" href="#ShedLock用法"></a>ShedLock用法</h2><blockquote><p>ShedLock 基于 Spring 实现时，由于 Spring 6 的JDK依赖因素，所以 ShedLock 的5.x版本需要 JDK &gt; 17 ，否则需要使用 ShedLock 4.x 版本。</p></blockquote><p>接下来，以 Spring 项目（并非Spring Boot项目）为例，JDK 版本为 17 ，Spring 版本为 6.1.1 。</p><p>示例项目代码地址：<a href="https://github.com/wangfarui/work-report/tree/main/shedlock-schedule">https://github.com/wangfarui/work-report/tree/main/shedlock-schedule</a></p><h3 id="启用并配置-Scheduled-locking"><a class="header-anchor" href="#启用并配置-Scheduled-locking"></a>启用并配置 Scheduled locking</h3><p>首先需要添加 ShedLock 依赖。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>net.javacrumbs.shedlock<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>shedlock-spring<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.10.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>net.javacrumbs.shedlock<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>shedlock-provider-redis-spring<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.10.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后，<code>shedlock-provider-redis-spring</code> 中依赖于 <code>spring-data-redis</code>，其中 redis 的驱动器是可选的，默认有 jedis 和 lettuce 两种。示例项目使用 jedis，所以还需要添加 jedis 的依赖。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>redis.clients<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jedis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.1.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>接下来，开始配置 ShedLock 。第一步注入<code>LockProvider</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> LockProvider <span class="title function_">lockProvider</span><span class="params">(RedisConnectionFactory connectionFactory)</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RedisLockProvider</span>(connectionFactory, <span class="string">&quot;local&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过上诉代码可以发现，<code>LockProvider</code>基于 Redis 的实现类 <code>RedisLockProvider</code> 在实例化时还需要一个 <code>RedisConnectionFactory</code> 对象。第二步需要注入 jedis 的 ConnectionFactory：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> JedisConnectionFactory <span class="title function_">jedisConnectionFactory</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">RedisStandaloneConfiguration</span> <span class="variable">configuration</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisStandaloneConfiguration</span>(<span class="string">&quot;localhost&quot;</span>, <span class="number">6379</span>);</span><br><span class="line">    configuration.setPassword(<span class="string">&quot;&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">JedisConnectionFactory</span>(configuration);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置完 ShedLock 的 <code>LockProvider</code> 之后，在配置类上标记 <code>@EnableSchedulerLock</code>以启用 <code>@SchedulerLock</code> 注解的锁功能，标记<code>@EnableScheduling</code>以启用<code>@Scheduled</code>注解的调度功能。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableScheduling</span></span><br><span class="line"><span class="meta">@EnableSchedulerLock(defaultLockAtMostFor = &quot;10m&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MySpringConfiguration</span> &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="配置定时任务"><a class="header-anchor" href="#配置定时任务"></a>配置定时任务</h3><p>通过 <code>@Scheduled</code> 标记Bean对象下的方法为一个定时任务，再通过 <code>@SchedulerLock</code> 使得该任务在分布式服务下同时最多执行一次。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">@Scheduled(cron = &quot;0/5 * * * * *&quot;)</span><br><span class="line">@SchedulerLock(name = &quot;scheduledTaskName&quot;)</span><br><span class="line">public void scheduledTask() &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="ShedLock总结"><a class="header-anchor" href="#ShedLock总结"></a>ShedLock总结</h2><p>使用 ShedLock 大致分为三步：</p><ol><li>提供一个 <code>LockProvider</code> 。</li><li>标记 <code>@EnableSchedulerLock</code> 注解以启用<code>@SchedulerLock</code>的注解功能。</li><li>标记 <code>@SchedulerLock</code> 注解以启用锁功能。</li></ol><p>以上操作步骤都是按照 ShedLock 默认配置来进行的，如果需要启用高级用法，例如希望<code>@SchedulerLock</code>注解只作用于<code>@Scheduled</code>注解下，可以指定<code>@EnableSchedulerLock</code> 注解的 <code>interceptMode()</code> 属性为 InterceptMode.PROXY_SCHEDULER 。</p>]]></content>
    
    
    <summary type="html">2024-01-08工作小结，了解ShedLock的使用和工作原理。</summary>
    
    
    
    <category term="工作" scheme="https://blog.itwray.com/categories/%E5%B7%A5%E4%BD%9C/"/>
    
    
    <category term="工作小结" scheme="https://blog.itwray.com/tags/%E5%B7%A5%E4%BD%9C%E5%B0%8F%E7%BB%93/"/>
    
    <category term="ShedLock" scheme="https://blog.itwray.com/tags/ShedLock/"/>
    
  </entry>
  
</feed>
