<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>宋柏成的博客</title><description>AI 转型中的工程实践者，记录智能应用、模型工具链与产品化探索</description><link>https://songbaicheng.cc.cd/</link><language>zh_CN</language><item><title>Readne</title><link>https://songbaicheng.cc.cd/posts/readne/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/readne/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;开发相关知识&lt;/h1&gt;
&lt;p&gt;很多人都觉得开发只是写写代码，其实不然。开发是一个综合性的过程，需要掌握的知识面很广，需要具备的能力也很多。&lt;/p&gt;
&lt;p&gt;如果每天的工作都只是在接需求实现需求，那只能说是在做开发，而不是在开发，当你脱离了你目前的项目之后你会发现你并不会有自己的对项目的整体把握和领悟，在接触到一个新的项目后仍然是像拿到一个全新的东西而不是一个模型的另一个版本。&lt;/p&gt;
&lt;p&gt;所以，开发是一个需要不断学习的过程，需要不断总结的过程，需要不断思考的过程。&lt;/p&gt;
</content:encoded></item><item><title>Arrays Materices</title><link>https://songbaicheng.cc.cd/posts/arrays-materices/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/arrays-materices/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数组和特殊矩阵&lt;/h1&gt;
&lt;p&gt;矩阵在数据结构中考虑的是如何用最小的内存空间来存储同样的一组数据，所以我们应该把精力放在如何将矩阵更有效的存储在内存中，并能更方便的提取矩阵中的元素。通常矩阵在计算机语言中借助数组进行存储，我们首先认识一下数组的存储。&lt;/p&gt;
&lt;h2&gt;数组&lt;/h2&gt;
&lt;p&gt;数组是由 n（n &amp;gt;= 1）个相同类型的数据元素构成的有限序列，每个数据元素称为一个数组元素，每个元素在 n 个线性关系中的序号称为该元素的下标，下标的取值范围称为数组的边界。数组是线性表的推广，一维数组可视为一个线性表，二维数组可视为其元素也是定长线性表的线性表，以此类推。并且数组一点定义，其维数和维界就不会改变，因此，除结构的初始化和销毁操作外，数组只会有存取和修改元素的操作。&lt;/p&gt;
&lt;p&gt;以一维数组 A[0……n-1] 为例，其存储的结构关系为 &lt;code&gt;LOC(ai) = LOC(a0) + i * L (0&amp;lt;= i &amp;lt; n)&lt;/code&gt;，其中 L 是每个数组元素所占的存储单元。对于多维数组，有按行优先和按列优先两种映射方法。以二维数组为例，其基本思想是：先行后列，先存储行号较小的元素，行号相等先存储列号较小的元素，设二维数组的行下标与列下标的范围分别为[0, h1]与[1, h2]，则关系存储关系式为 &lt;code&gt;LOC(ai,j) = LOC(A0,0) + [i * (h2 + 1) + j] * L&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/arrays-materices/arrays-row.jpg&quot; alt=&quot;二维数组按行优先顺序存储&quot; title=&quot;二维数组按行优先顺序存储&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同理以列优先方式存储时，得出的存储结构关系式为 &lt;code&gt;LOC(ai,j) = LOC(A0,0) + [j * (h1 + 1) + j] * L&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/arrays-materices/arrays-column.jpg&quot; alt=&quot;二维数组按列优先顺序存储&quot; title=&quot;二维数组按列优先顺序存储&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;特殊矩阵&lt;/h2&gt;
&lt;p&gt;矩阵中有许多相同矩阵元素或者零元素的被称为特殊矩阵。为了节约存储空间，多个值相同的元素分配一个存储空间，或者对零元素不分配存储空间，所以要找出特殊矩阵中值相同的矩阵元素的分布规律，把那些呈现规律性分布、值相同的多个矩阵元素压缩到一个存储空间中。&lt;/p&gt;
&lt;h3&gt;对称矩阵&lt;/h3&gt;
&lt;p&gt;若对一个 n 阶矩阵 A 中的任意一个元素都有 &lt;code&gt;ai,j = aj,i&lt;/code&gt;，则称其为对称矩阵。其中的元素可以分为三个部分，上半区域、主对角线和下半区域，对于 n 阶对称矩阵，上三角区的的所有元素和下三角区的所有元素都是相同的，所以我们根据这个特性可以找到其对应规律：当在下三角区，即 i &amp;gt;= j 的时候 &lt;code&gt;(i(i -1)) / 2 + j - 1&lt;/code&gt;，在上三角区，即 i &amp;lt; j 的时候 &lt;code&gt;(j(j - 1)) / 2 + i - 1&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;三角矩阵&lt;/h3&gt;
&lt;p&gt;分为上三角矩阵和下三角矩阵，其中另外一半的元素全部都是一个元素，我们在对称矩阵的基础上，把另一半的元素放在另一半存储完的最后，在下三角矩阵中，即 i &amp;gt;= j 的时候 &lt;code&gt;(i(i - 1)) / 2 + j - 1&lt;/code&gt;，在 i &amp;lt; j 的时候 &lt;code&gt;(n(n - 1)) / 2&lt;/code&gt;；在上三角矩阵中，即 i &amp;lt;= j 的时候 &lt;code&gt;((i -1)(2n - j + 2)) / 2 + j - j&lt;/code&gt;，在 i &amp;lt; j 的时候 &lt;code&gt;(n(n - 1)) / 2&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;三对角矩阵&lt;/h3&gt;
&lt;p&gt;对角矩阵也叫袋状矩阵，对于 n 阶矩阵 A 中的任意一个元素 ai,j，当 |i - j| &amp;gt; 1 时，ai,j = 0 (1 &amp;lt;= i, j &amp;lt;= n)，则称为三对角矩阵。三对角矩阵将三条对角线上的元素按照行优先方式存放在一维数组中，将 a1,1 放入数组第一个元元素中，则在数组中的下标符合 &lt;code&gt;k = 2i + j - 3&lt;/code&gt;。如果我们知道了 k 值也可以反推，&lt;code&gt;i = （k + 1）/ 3 + 1&lt;/code&gt;，顺带就可以求出 j 的值。&lt;/p&gt;
&lt;h2&gt;稀疏矩阵&lt;/h2&gt;
&lt;p&gt;矩阵中非零元素的个数 t，相对矩阵总元素 s 来说相对非常少，即 s &amp;gt;&amp;gt; t 的矩阵被称为稀疏矩阵，至于这个到底是远大于多少并没有明确的指标，可凭借个人主观因素判断。&lt;/p&gt;
&lt;p&gt;若采用常规的方法存储稀疏矩阵，则相当浪费空间，因为对于稀疏矩阵我们仅存储非零元素。因此将非零元素及相对应的行和列构成一个三元组（行标、列标、值）。此方法虽然可以节省了空间，但是也失去了随机存取的特性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/arrays-materices/sparse-matrix.jpg&quot; alt=&quot;稀疏矩阵以及对应的三元组&quot; title=&quot;稀疏矩阵以及对应的三元组&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Code Trick</title><link>https://songbaicheng.cc.cd/posts/code-trick/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/code-trick/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;&lt;/h1&gt;
</content:encoded></item><item><title>Concurrency</title><link>https://songbaicheng.cc.cd/posts/concurrency/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/concurrency/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;并发&lt;/h1&gt;
&lt;h2&gt;Java 并发机制基础&lt;/h2&gt;
&lt;p&gt;提到 Java 的编译过程，我们都会想到 .java 文件到 .class 字节码文件再到汇编指令到 CPU 内执行，而 Java 的并发机制正是依赖了 JVM 的实现和 CPU 的指令，让我们从 volatile 和 synchronized 这两个关键字来配合理解一下并发的原理。&lt;/p&gt;
&lt;h3&gt;volatile&lt;/h3&gt;
&lt;p&gt;都说 volatile 是轻量的 synchronized，是因为它在多线程处理中过程中保证了共享变量的可见性，即一个线程在修改一个共享变量时，另一个线程可以读到这个变量值。所以在 volatile 使用恰当的情况下不会引起线程上下文的切换和调度，相比于 synchronized 成本更低。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;《&lt;em&gt;Java 语言规范 第三版&lt;/em&gt;》对 volatile 的定义：Java 编程语言允许线程访问共享变量，为了确保共享变量能别准确和一致的更新，线程应确保能通过排他锁单独获得这个变量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;让我们聊一下它的底层原理：Java 在对使用了 volatile 的变量进行写操作的时候，JVM 会向处理器发一条 Lock 前缀的指令，将这个变量所存在的缓冲行[^one]的数据写回到系统内存。虽然被写回到内存，但是其他处理器缓存已经读取的值还是旧的，这里就得用得到缓存一致性协议了，每个处理器会嗅探总线上传播的数据检查自己的数据是否时最新的，当发现自己的缓存行地址被修改的时候，就会将当前缓存行状态设置为无效并重新从内存中读取到处理器缓存中来。&lt;/p&gt;
&lt;h3&gt;synchronized&lt;/h3&gt;
&lt;p&gt;synchronized 作为多线程并发中的元老级角色也被称作重量级锁，虽然在日后的优化中可能已经没有那么“重”了。了解锁我们得先知道什么是锁。在 Java 中每个对象都可以是一个锁，具体有以下三种锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于普通同步方法，锁的是当前的实例对象。&lt;/li&gt;
&lt;li&gt;对于静态同步方法，锁的是当前类的 Class 对象。&lt;/li&gt;
&lt;li&gt;对于同步方法块，锁的是 Synchronized 括号里的配置对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当一个线程试图访问同步代码块的时候，他必须先获得锁，退出和异常的时候必须释放锁。而具体的得到和释放锁的过程需要依赖 monitorenter 和 monitorexit 两个指令配合完成，JVM 会保证每个 monitorenter 必须有对应的 monitorexit ，它们在编译时会插入到代码块的开始和异常或者结束位置，任何对象都有一个 monitor 与之对应，并且一个 monitor 在被持有后将处于锁定状态，所以线程对锁的获取和释放就是对 monitor 的所有权的获取和释放。&lt;/p&gt;
&lt;p&gt;在我们就近的 JDK 版本里，为了解决对锁的获得和释放待来的性能损耗，引入了“偏向锁”和“轻量级锁”。这就得提到锁的状态了，锁一共有四种状态，级别由低到高分别是：&lt;strong&gt;无状态锁、偏向锁、轻量级锁、重量级锁&lt;/strong&gt;。这几个状态会随着竞争情况而升级，并且锁只能升级不能降级，这种做法是为了提高获得锁和释放锁的效率，至于如果做到的我们得先了解这几种锁的状态才能展开。&lt;/p&gt;
&lt;h4&gt;偏向锁&lt;/h4&gt;
&lt;p&gt;在大多数情况下，锁并不会存在多线程的竞争，而且总是由同一线程多次获得，所以就引入偏向锁的概念。当一个线程访问同步块并获取锁的时候，会在对当头和栈帧[^two]的锁记录里存储偏向锁的线程ID，以后该线程在进入和退出同步块的时候不需要进行 CAS[^three] 操作来加锁和解锁，只要测试对象头中是否是存储着当前线程的偏向锁即可。偏向锁提供了一种竞争出现才会释放的锁机制，当竞争出现的时候，首先会停止偏向锁的线程，然后检测持有偏向锁的线程是否活着，如果不活动则将对象头设置成无锁状态，如果活着则拥有偏向锁的栈会被执行，最后唤醒暂停的线程。&lt;/p&gt;
&lt;h4&gt;轻量级锁&lt;/h4&gt;
&lt;p&gt;当一个线程获取轻量级锁时，JVM 会先在对象头中存储锁记录的指针，然后使用 CAS 指令尝试将对象的锁记录指针替换为指向当前线程的指针。如果 CAS 成功，表示当前线程获取了锁，可以继续执行，否则说明有竞争发生。在有竞争的情况下，如果其他线程也尝试获取同一个对象的轻量级锁，JVM 会将锁升级为重量级锁，当锁处于这个状态下，其他线程获取锁就会处于阻塞状态，一直等到只有锁的线程释放锁再唤醒这些阻塞线程进行新一轮争夺锁之战，因为锁不可降级的特性在，那么在释放锁时可以直接将锁的状态改为未锁定状态，无需进行额外的处理。这样就减少了释放锁的开销，提高了效率。&lt;/p&gt;
&lt;h4&gt;总结&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;锁&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;使用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;偏向锁&lt;/td&gt;
&lt;td&gt;加锁和解锁不需要额外消耗&lt;/td&gt;
&lt;td&gt;锁竞争会带来额外的锁撤销的消耗&lt;/td&gt;
&lt;td&gt;适用于一个线程访问同步块场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;轻量级锁&lt;/td&gt;
&lt;td&gt;竞争线程不会阻塞，提高了程序的响应速度&lt;/td&gt;
&lt;td&gt;始终得不到锁的线程会自旋消耗 CPU&lt;/td&gt;
&lt;td&gt;追求响应时间，执行速度快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重量级锁&lt;/td&gt;
&lt;td&gt;线程不会自旋，不会消耗 CPU&lt;/td&gt;
&lt;td&gt;线程阻塞，相应时间慢&lt;/td&gt;
&lt;td&gt;追求吞吐量&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;原子操作[^four]&lt;/h3&gt;
&lt;p&gt;处理器能保证从系统内存中读取和写入一个字节是原子的，而复杂的内存操作需要搭配处理器提供的总线锁定和缓存锁来保证其原子性。Java 通过&lt;strong&gt;锁&lt;/strong&gt;和&lt;strong&gt;循环CAS&lt;/strong&gt;来实现原子操作，从 JDK1.5 开始，并发包中就出现了 AtomicBoolean、AtomicInteger 等原子类将当前值加一减一。不过用 CAS 实现原子操作也是存在问题的，ABA 问题、循环时间开销大和只能保证一个变量的原子操作等。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;Java 大部分的容器和框架都是依赖 volatile 和原子操作，这对展开并发编程很有帮助。&lt;/p&gt;
&lt;h2&gt;Java 内存模型&lt;/h2&gt;
&lt;p&gt;在并发编程中有两个重要的问题：线程之间如何通讯及线程之间如何同步。Java 的并发采用的是共享内存模型，即线程之间共享程序的公共状态，通过写-读内存中的公共状态进行隐式通讯。&lt;/p&gt;
&lt;h3&gt;内存模型的抽象结构&lt;/h3&gt;
&lt;p&gt;在 Java 中，所有的实例域、静态域和数组元素都存在堆内存中，堆内存在线程间共享，而局部变量、方法定义参数和异常处理器参数不会在线程之间共享，也不会有可见性问题。Java 线程之间的通讯由 Java 内存模型（JMM）控制，由 JMM 决定一个线程对共享变量的写入何时对另一个线程可见，从抽象的角度来讲，线程之间的共享变量存储在主内存，而每个线程都有本地内存，本地内存存放的共享变量的副本，示意图如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/basic/concurrency/jmm.svg&quot; alt=&quot;Java 内存模型抽象结构图&quot; title=&quot;Java 内存模型抽象结构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从图中来看，如果线程A和线程B之间通讯需要线程A先将本地内存A中更新的变量刷新到主内存去，然后线程B到住内存去读取线程A之前已经更新的共享变量。所以这个步骤的实质就是线程A向线程B发送消息，而且这个通信过程必须经过主内存。&lt;/p&gt;
&lt;h3&gt;指令重排&lt;/h3&gt;
&lt;p&gt;在执行程序的过程中，为了提高性能，编译器和处理器常常会对指令进行重新排序，排序方式有以下三种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编译器优化的重新排序：编译器在不改变单线程程序语意的情况下可以进行语句的执行顺序。&lt;/li&gt;
&lt;li&gt;指令级并行的重新排序：根据指令集并行技术（ILP）来将多条指令重叠执行，如果不存在数据依赖，处理器可以改变语句对机器指令的执行顺序。&lt;/li&gt;
&lt;li&gt;内存系统的重新排序：由于处理器使用缓存和读写缓冲区，使得加载和存储操作看上去可能是乱序执行。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为这些重排序可能造成多线程程序出现内存可见性的问题，JMM 的的处理器重排规则会要求 Java 编译器在生成指令序列的时候插入特定类型的内存屏障（Memory Barriers，也被称为 Memeory Fence），JMM 属于语言级别的内存模型，它确保在不同编译器和不同处理器平台上通过禁止重排序来提供一致的内存可见性保证。&lt;/p&gt;
&lt;p&gt;[^one]: 缓冲行（cache line）缓存中可以分配的最小单位。
[^two]: 栈帧（Stack Frame）支持虚拟机进行方法调用和方法执行的数据结构，在当前线程中，每执行一个方法就会往栈中插入一个栈帧。
[^three]: CAS（compare and swap）判断内存某个位置的值是否为预期值，如果是则更改为新的值，这个过程是原子操作。
[^four]: 原子操作（atomic operations）不可中断的一个或者一系列指令。&lt;/p&gt;
</content:encoded></item><item><title>Database</title><link>https://songbaicheng.cc.cd/posts/database/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/database/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数据库&lt;/h1&gt;
&lt;p&gt;相信一个合格的程序员是必须对数据库这项技术要达到熟练的地步吧，之前可能觉得要和数据库交互的往往只有后端程序员，可是后来随着各类数据库的发展，很多数据库也可以配合前端直接使用，所有掌握 SQL 和数据库进行交互也是程序员必备的技能。&lt;/p&gt;
&lt;p&gt;听我一个朋友谈起，其实程序员很大一份就是在和数据打交道，而对于数据库中数据的处理来说，尽管大家都是在用 SQL ，但是在不同人的手里能拿到不同的结果集，相比于其他各种出现的 cron 表达式、正则表达式，SQL 更能在一些函数和表的组合方面体现你对数据管理的天赋。&lt;/p&gt;
&lt;p&gt;虽然工作中能用到的数据库的知识点都只是停留在对数据的操作，但是在面试或者一些表设计中，数据库的底层结构和高级用法显得尤为重要，这些我们也需要在空余时间了解一些。下面我会分享一些我觉得值得一读的和数据库有关的书。&lt;/p&gt;
&lt;h2&gt;MySQL&lt;/h2&gt;
&lt;p&gt;如果是平时使用和找工作大部分公司在用的话，MySQL 在 CN 的使用率肯定是名列前茅，如果只是想快速了解或复习语法的话，推荐这本 &lt;strong&gt;&lt;em&gt;《MySQL必知必会》&lt;/em&gt;&lt;/strong&gt;，薄薄一本，定位和 &lt;strong&gt;&lt;em&gt;《剑指Offer》&lt;/em&gt;&lt;/strong&gt; 一样，都是最实用的语法，用来入门非常不错。&lt;/p&gt;
&lt;p&gt;![MySQL必知必会](/assets/images/resource/books/mysql-crash-course.png &quot;MySQL必知必会&quot; =350x500)&lt;/p&gt;
</content:encoded></item><item><title>Ddd</title><link>https://songbaicheng.cc.cd/posts/ddd/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/ddd/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;DDD 领域驱动设计&lt;/h1&gt;
&lt;h2&gt;关于 DDD&lt;/h2&gt;
&lt;p&gt;领域驱动设计（Domain-Driven Design，DDD）是一种软件设计方法，是一种思想，它以领域为核心，将软件系统拆分为不同的领域，并在不同的领域中建立模型和规则。通过这种方式，可以提高软件系统的可维护性和可扩展性，并减少软件系统中的错误。&lt;/p&gt;
&lt;p&gt;在我们以前的后端项目的开发过程当中，软件架构的设计往往是在项目开始之初就确立的，可是在实际的项目开发当中，我们往往发现，架构设计往往是在项目后期才逐渐完善起来的，这往往是因为项目初期对业务的理解不足，或者对软件架构的设计不够清晰，导致架构设计不够合理，最终导致软件系统的可维护性和可扩展性越来越差，而这个时期再开始考虑项目拆分的时候就会因为 MVC 架构各种复杂的调用关系而难以下手。&lt;/p&gt;
&lt;p&gt;DDD 的出现就是为了解决这个问题，它将软件系统拆分为不同的领域，并在不同的领域中建立模型和规则，从而提高软件系统的可维护性和可扩展性，并减少软件系统中的错误，刚接触到一个新的领域可以引用事件风暴的方式。事件风暴（Event Storming）是一种由 Alberto Brandolini 在 2013 年提出的工作坊方法，旨在帮助团队在短时间内对一个复杂领域进行建模和理解，帮助跨职能团队通过讨论和协作，快速发现和识别领域中的关键事件和业务流程。&lt;/p&gt;
&lt;h2&gt;事件风暴&lt;/h2&gt;
&lt;p&gt;事件风暴的核心思想是通过可视化事件流来揭示业务领域中的重要行为和互动。以下是事件风暴的一些关键特点和步骤：&lt;/p&gt;
&lt;h3&gt;关键特点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;高度协作：事件风暴强调团队成员之间的互动和协作，通常包括开发人员、业务专家、产品经理等不同角色。&lt;/li&gt;
&lt;li&gt;快速迭代：通过快速生成和调整事件模型，团队能够在短时间内捕捉到领域的复杂性。&lt;/li&gt;
&lt;li&gt;可视化：利用便利贴和白板等工具，将事件以可视化的方式展示出来，便于讨论和理解。&lt;/li&gt;
&lt;li&gt;领域驱动：专注于业务领域中的实际事件和流程，而非技术实现细节。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;准备工作：确定讨论的主题和范围，邀请相关领域的专家和团队成员，准备好便利贴、白板或大张纸。&lt;/li&gt;
&lt;li&gt;标识事件：在白板上记录领域中的关键业务事件，每个事件用一张便利贴表示，事件通常以过去时态动词短语描述（例如，“订单已创建”）。&lt;/li&gt;
&lt;li&gt;排列事件：将事件按照时间顺序排列，展示业务流程的先后顺序。&lt;/li&gt;
&lt;li&gt;识别命令：识别触发这些事件的命令或操作（例如，“创建订单”），并将其添加到模型中。&lt;/li&gt;
&lt;li&gt;添加参与者：识别执行这些命令的参与者（角色或系统），并将其添加到模型中。&lt;/li&gt;
&lt;li&gt;讨论和细化：通过团队讨论，细化和调整事件模型，添加更多的细节和上下文。&lt;/li&gt;
&lt;li&gt;识别聚合根和界限上下文：在详细讨论中，识别出领域中的聚合根和界限上下文，帮助进行更细化的领域建模。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过事件风暴，团队可以在一个开放和互动的环境中，高效地探索和理解复杂的业务领域，进而为后续的设计和开发工作打下坚实的基础。&lt;/p&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;领域（Domain）：领域是软件系统所要解决的问题域，它包含了软件系统所需要处理的业务逻辑和规则，在理解的基础上，我们可以把它看作是一种边界。&lt;/li&gt;
&lt;li&gt;模型（Model）：模型是领域中的实体、值对象和领域服务，它们是领域中最重要的概念，它们定义了领域中的业务逻辑和规则。&lt;/li&gt;
&lt;li&gt;领域服务（Domain Service）：领域服务是领域中的辅助服务，它们提供了一些通用的功能，例如查询、验证等。&lt;/li&gt;
&lt;li&gt;聚合（Aggregate）：聚合是领域中的一个概念，它将相关的实体和值对象组合在一起，形成一个完整的业务单元。&lt;/li&gt;
&lt;li&gt;仓库（Repository）：仓库是领域中的一个概念，它负责存储和检索领域中的实体和值对象。&lt;/li&gt;
&lt;li&gt;领域事件（Domain Event）：领域事件是领域中的一个概念，它表示领域中的一个事件，例如订单创建、订单支付等。&lt;/li&gt;
&lt;li&gt;领域服务（Domain Service）：领域服务是领域中的一个概念，它提供了一些通用的功能，例如查询、验证等，它只负责组装场景而不提供实现，实现交给领域实体自己来做。&lt;/li&gt;
&lt;li&gt;充血模型（Rich Domain Model）：充血模型是一种面向对象的设计思想，它将领域中的业务逻辑和规则封装在在模型实体中。&lt;/li&gt;
&lt;li&gt;贫血模型（Anemic Domain Model）：贫血模型是一种面向对象的设计思想，它将领域中的业务逻辑和规则封装在领域模型中，而不是在模型实体中。&lt;/li&gt;
&lt;li&gt;工厂（Factory）：工厂是领域中的一个概念，它负责创建领域中的实体和值对象。&lt;/li&gt;
&lt;li&gt;值对象（Value Object）：值对象是领域中的一个概念，它表示领域中的一个不可变的数据对象。&lt;/li&gt;
&lt;li&gt;实体（Entity）：实体是领域中的一个概念，它表示领域中的一个可变的数据对象。&lt;/li&gt;
&lt;li&gt;聚合根（Aggregate Root）：聚合根是领域中的一个概念，它表示领域中的一个聚合的根实体。&lt;/li&gt;
&lt;li&gt;防腐层（Anti-Corruption Layer）：防腐层是领域中的一个概念，它负责封装领域模型和外部系统之间的交互，从而减少对外部系统的依赖。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;借鉴思想&lt;/h2&gt;
&lt;p&gt;DDD 应该一直遵循高内聚低耦合的目标，可以借鉴以下原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;单一职责原则：在领域模型中，每个模型都应该只有一个职责，即应该只有一个原因导致模型发生变化。&lt;/li&gt;
&lt;li&gt;开闭原则：在领域模型中，应该尽量使用抽象和接口，而不是具体的实现，这样可以提高模型的可扩展性和可维护&lt;/li&gt;
&lt;li&gt;依赖倒置原则：在领域模型中，应该尽量使用抽象和接口，而不是具体的实现，这样可以提高模型的可扩展性和可&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四层架构&lt;/h2&gt;
&lt;p&gt;在 DDD 中，通常会使用四层架构来组织代码。这四层架构分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;领域层（Domain Layer）：领域层是领域模型的核心，它包含了领域中的业务逻辑和规则。&lt;/li&gt;
&lt;li&gt;应用层（Application Layer）：应用层负责协调领域层和基础设施层之间的交互，它负责处理应用程序的输入和输出。&lt;/li&gt;
&lt;li&gt;基础设施层（Infrastructure Layer）：基础设施层负责提供基础的服务和设施，例如数据库、缓存等。&lt;/li&gt;
&lt;li&gt;表示层（Presentation Layer）：表示层负责处理用户界面和用户交互，它负责将应用程序的输出展示给用户。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Deep Learning</title><link>https://songbaicheng.cc.cd/posts/deep-learning/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/deep-learning/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;深度学习&lt;/h1&gt;
&lt;p&gt;身处 AI 的新时代浪潮里，必须奋力投身其中，才能紧跟时代步伐。作为计算机专业的普通人，我能深刻的感受到 AI 对当下生活的影响和未来无限的可能，虽然自己才疏学浅，没有在 AI 领域周边和基础领域上有任何的造诣，但我仍然选择开始步入这个全新的领域开始自己的新一轮的人生学习探索，在这里我将列出我一步步从零到一的学习过程中的优秀书籍，共勉。&lt;/p&gt;
&lt;h2&gt;《Deep Learning from Scratch》——基于Python的理论与实现&lt;/h2&gt;
&lt;p&gt;![Deep Learning from Scratch](/assets/images/resource/books/deep-learning-from-scratch.png &quot;Deep Learning from Scratch&quot; =350x500)&lt;/p&gt;
&lt;p&gt;大名鼎鼎的“鱼书”，从零开始编写可实际运行的程序，一边写源码一边思考，并且不使用任何现有的深度学习框架，尽可能使用最基础的数学知识和 Python 库，从零讲解深度学习核心问题的数学原理，从零创建一个经典的深度学习网络。&lt;/p&gt;
</content:encoded></item><item><title>Disjoint Set Union</title><link>https://songbaicheng.cc.cd/posts/disjoint-set-union/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/disjoint-set-union/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;并查集&lt;/h1&gt;
</content:encoded></item><item><title>Redis</title><link>https://songbaicheng.cc.cd/posts/redis/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/redis/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Redis&lt;/h1&gt;
&lt;p&gt;Redis (Remote Dictionary Server) 是一个开源的内存数据结构存储，用作数据库、缓存和消息代理。它支持多种数据结构，如字符串（strings）、哈希（hashes）、列表（lists）、集合（sets）以及有序集合（sorted sets）。因为具有丰富的功能和高性能，现在几乎主流的 Web 项目都已经绑定了 Redis 作为缓存组件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Redis 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/persistence/redis/redis-cube.svg
link: https://redis.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;特点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;内存存储：所有数据都保存在内存中，读写速度非常快，非常适合需要快速响应的场景。&lt;/li&gt;
&lt;li&gt;持久化：Redis支持将数据持久化到磁盘，可以通过快照（snapshot）和AOF（Append-Only File）两种方式进行。&lt;/li&gt;
&lt;li&gt;高可用性和分布式：通过 Redis Sentinel 实现高可用，通过Redis Cluster 实现数据分布和负载均衡。&lt;/li&gt;
&lt;li&gt;丰富的数据类型：支持多种数据结构，便于解决复杂的数据存储和操作问题。&lt;/li&gt;
&lt;li&gt;Lua 脚本：支持 Lua 脚本，可以实现复杂的原子操作。&lt;/li&gt;
&lt;li&gt;事务支持：支持事务，保证一系列操作的原子性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;为什么 Redis 那么快&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;基于内存存储：Redis的所有数据都保存在内存中，读写速度非常快。&lt;/li&gt;
&lt;li&gt;多路 I/O 复用：Redis 使用多路 I/O 复用技术，可以同时处理多个客户端的请求，提高并发处理能力。&lt;/li&gt;
&lt;li&gt;高效的数据结构：Redis 支持多种数据结构，这些数据结构都进行了优化，可以快速处理。&lt;/li&gt;
&lt;li&gt;Lua脚本：Redis支持 Lua 脚本，可以实现复杂的原子操作。&lt;/li&gt;
&lt;li&gt;语言实现：C 语言实现，性能很高。&lt;/li&gt;
&lt;li&gt;单线程模型：单线程无法充分利用多核，但另一方面，它避免了多线程的频繁上下文切换以及锁等同步机制的开销。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;为什么选择单线程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;避免过多的上下文切换开销：在多线程调度过程中，需要在 CPU 之间切换线程上下文，并且上下文切换涉及一系列寄存器替换、程序堆栈重置，甚至包括程序计数器、堆栈指针和程序状态字等快速表项的退休。因为单个进程内的多个线程共享进程地址空间，线程上下文要比进程上下文小得多，在跨进程调度的情况下，需要切换整个进程地址空间。&lt;/li&gt;
&lt;li&gt;避免同步机制的开销：如果Redis选择多线程模型，因为 Redis 是一个数据库，不可避免地涉及底层数据同步问题，这必然会引入一些同步机制，如锁。我们知道Redis不仅提供简单的键值数据结构，还提供列表、集合、哈希等丰富的数据结构。不同的数据结构对于同步访问的锁定具有不同的粒度，这可能会在数据操作期间引入大量的锁定和解锁开销，增加了程序的复杂性并降低了性能。&lt;/li&gt;
&lt;li&gt;简单和可维护性：简单且可维护的代码必然是Redis在早期的核心准则之一，引入多线程不可避免地导致了代码复杂性的增加和可维护性的降低。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Redis 真的是单线程的吗？&lt;/h3&gt;
&lt;p&gt;需要看版本来说，对于网络模型来说，Redis 在 v6.0 之前一直是单线程的，在 v6.0 之后正式在网络模型中实现I/O多线程；对于整个 Redis 中，在 v4.0 版本中就引入多线程进行异步任务。
Redis在v4.0版本中引入了多线程来执行一些异步操作，主要用于非常耗时的命令。通过将这些命令的执行设置为异步，可以避免阻塞单线程事件循环。&lt;/p&gt;
&lt;p&gt;我们知道 Redis 的 DEL 命令用于删除一个或多个键的存储值，它是一个阻塞命令。在大多数情况下，要删除的键不会存储太多值，最多几十个或几百个对象，因此可以快速执行。但如果要删除具有数百万个对象的非常大的键值对，则此命令可能会阻塞至少几秒钟，由于事件循环是单线程的，它会阻塞随后的其他事件，从而降低吞吐量。&lt;/p&gt;
&lt;h3&gt;为什么要给缓存数据设置过期时间&lt;/h3&gt;
&lt;p&gt;内存是有限且珍贵的，如果不对缓存数据设置过期时间，那内存占用就会一直增长，最终可能会导致 OOM 问题。通过设置合理的过期时间，Redis 会自动删除暂时不需要的数据，为新的缓存数据腾出空间。
过期时间除了有助于缓解内存的消耗，很多时候，我们的业务场景就是需要某个数据只在某一时间段内存在，比如我们的短信验证码可能只在 1 分钟内有效，用户登录的 Token 可能只在 1 天内有效，这些情况都需要设置过期时间来实现业务功能。&lt;/p&gt;
&lt;h3&gt;Redis 如何判断数据是否过期&lt;/h3&gt;
&lt;p&gt;Redis 通过一个叫做过期字典（可以看作是 hash 表）来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键)，过期字典的值是一个 long long 类型的整数，这个整数保存了 key 所指向的数据库键的过期时间（毫秒精度的 UNIX 时间戳）。 在查询一个 key 的时候，Redis 首先检查该 key 是否存在于过期字典中（时间复杂度为 O(1)），如果不在就直接返回，在的话需要判断一下这个 key 是否过期，过期直接删除 key 然后返回 null。&lt;/p&gt;
&lt;h3&gt;Redis 删除策略&lt;/h3&gt;
&lt;p&gt;常用的过期数据的删除策略就下面这几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;惰性删除&lt;/strong&gt;：只会在取出/查询 key 的时候才对数据进行过期检查。这种方式对 CPU 最友好，但是可能会造成太多过期 key 没有被删除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定期删除&lt;/strong&gt;：周期性地随机从设置了过期时间的 key 中抽查一批，然后逐个检查这些 key 是否过期，过期就删除 key。相比于惰性删除，定期删除对内存更友好，对 CPU 不太友好。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟队列&lt;/strong&gt;：把设置过期时间的 key 放到一个延迟队列里，到期之后就删除 key。这种方式可以保证每个过期 key 都能被删除，但维护延迟队列太麻烦，队列本身也要占用资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定时删除&lt;/strong&gt;：每个设置了过期时间的 key 都会在设置的时间到达时立即被删除。这种方法可以确保内存中不会有过期的键，但是它对 CPU 的压力最大，因为它需要为每个键都设置一个定时器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis 采用的是 定期删除+惰性/懒汉式删除 结合的策略，这也是大部分缓存框架的选择。定期删除对内存更加友好，惰性删除对 CPU 更加友好。Redis 的定期删除过程是随机的（周期性地随机从设置了过期时间的 key 中抽查一批），所以并不保证所有过期键都会被立即删除。
另外，定期删除还会受到执行时间和过期 key 的比例的影响，执行时间已经超过了阈值，那么就中断这一次定期删除循环，以避免使用过多的 CPU 时间；如果这一批过期的 key 比例超过一个比例，就会重复执行此删除流程，以更积极地清理过期 key。相应地，如果过期的 key 比例低于这个比例，就会中断这一次定期删除循环，避免做过多的工作而获得很少的内存回收。&lt;/p&gt;
&lt;h3&gt;大量 key 集中过期问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;尽量避免 key 集中过期，在设置键的过期时间时尽量随机一点。&lt;/li&gt;
&lt;li&gt;对过期的 key 开启 lazyfree 机制（修改 redis.conf 中的 &lt;code&gt;lazyfree-lazy-expire&lt;/code&gt; 参数即可），这样会在后台异步删除过期的 key，不会阻塞主线程的运行。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;理论场景&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;缓存（Caching）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;对需要频繁访问的数据进行缓存，如用户信息、商品信息等。&lt;/li&gt;
&lt;li&gt;极大提高数据的读取速度，减轻数据库负载。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;会话存储（Session Store）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：在Web应用中，将用户会话信息存储在Redis中，如登录状态、购物车等。&lt;/li&gt;
&lt;li&gt;好处：读取速度快，支持持久化，可以实现分布式会话管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;实时数据分析（Real-time Analytics）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：用于实时统计和分析，如网站的访问量统计、实时排名等。&lt;/li&gt;
&lt;li&gt;好处：通过内存操作实现快速数据处理和统计。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;消息队列（Message Queue）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：利用Redis的列表（List）或发布/订阅（Pub/Sub）机制实现消息队列，进行异步任务处理。&lt;/li&gt;
&lt;li&gt;好处：简单易用，适合中小规模的消息队列需求。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;排行榜（Leaderboard）和计数器（Counting）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：实现各种排行榜功能，如游戏排名、积分榜等。&lt;/li&gt;
&lt;li&gt;好处：通过有序集合（sorted set）快速实现排名和分数统计。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;分布式锁（Distributed Lock）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：在分布式系统中实现锁机制，确保同一资源不会被多个进程同时修改。&lt;/li&gt;
&lt;li&gt;好处：利用Redis的原子操作，实现简单有效的分布式锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;地理信息存储和查询（Geospatial Information Storage and Query）：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：存储和查询地理位置数据，如定位服务、地图应用等。&lt;/li&gt;
&lt;li&gt;好处：通过 geo 命令集快速实现地理位置的存储和半径查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;阅读量/浏览量&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;场景：配合 String 类型的 incr 原子增加操作，每次访问链接自动加一。&lt;/li&gt;
&lt;li&gt;好处：实现简单，读写速度快。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实际场景&lt;/h2&gt;
&lt;h3&gt;缓存&lt;/h3&gt;
&lt;p&gt;目前系统的数据访问通常设计有有三种方式，第一种是简单的请求直接访问数据库，如果是在数据量和并发量很小的情况下，这种访问方式是可行的。但是当数据量很大，并发量很高时，这种访问方式就会导致数据库压力过大，从而影响系统的性能。&lt;/p&gt;
&lt;p&gt;在这种情况之上就增加了缓存来缓解数据库的压力的第二种方式，请求进入先在缓存中查询数据信息，如果缓存中不存在再去访问数据库数据。这里的方案的实现也有两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据结构实现的代码层面的缓存，无论是自定义 Map 存储要缓存的数据，还是使用 Guava 等第三方缓存框架，这种方案的缺点是只能单机使用，并且占用内存会严重些。&lt;/li&gt;
&lt;li&gt;使用 Redis 作为缓存，天然支持分布式，并且灵活的使用数据结构可以达到优秀的效果，缺点是技术门槛和服务器成本较高。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三种方式是在缓存的基础上增加布隆过滤器降低无效数据的访问来防止缓存穿透，这种方案的缺点是布隆过滤器需要维护，并且布隆过滤器的误判率需要控制。&lt;/p&gt;
&lt;h3&gt;布隆过滤器&lt;/h3&gt;
&lt;p&gt;略&lt;/p&gt;
&lt;h3&gt;Redis 与数据库读写一致性&lt;/h3&gt;
&lt;p&gt;这些设计的前提是访问数据库之前先访问 Redis，如果 Redis 不存在再访问数据库。&lt;/p&gt;
&lt;h4&gt;1. 先写 MySQL，再写 Redis&lt;/h4&gt;
&lt;p&gt;数据库更新后更新 Redis。在高并发的情况下，如图会出现同时修改数据库和 Redis，导致数据不一致的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/persistence/sql/mr.png&quot; alt=&quot;先写 MySQL，再写 Redis&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2. 先写 Redis，再写 MySQL&lt;/h4&gt;
&lt;h4&gt;3. 先删除 Redis，再写 MySQL&lt;/h4&gt;
&lt;h4&gt;4. 先写 MySQL，再删除 Redis&lt;/h4&gt;
&lt;p&gt;上面的方式都会存在不太能接受的数据不一致的情况，而此方案只存在第一次不一致的情况，对于不是强一致性的业务（秒杀、库存服务），可以采用此方案。&lt;/p&gt;
&lt;h4&gt;5. 先删除 Redis，再写 MySQL，再删除 Redis&lt;/h4&gt;
&lt;h4&gt;6. 先写 MySQL，通过 Binlog，异步更新 Redis&lt;/h4&gt;
&lt;h3&gt;分布式锁&lt;/h3&gt;
</content:encoded></item><item><title>Ads</title><link>https://songbaicheng.cc.cd/posts/ads/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/ads/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数据结构和算法&lt;/h1&gt;
&lt;p&gt;一提到数据结构和算法就不的不让我想到上学时计算机的四大天书：&lt;strong&gt;&lt;em&gt;《数据结构》&lt;/em&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;em&gt;《计算机组成与设计》&lt;/em&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;em&gt;《计算机操作系统》&lt;/em&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;em&gt;《计算机网络》&lt;/em&gt;&lt;/strong&gt;。当时根本不知道这几本书的分量，要不然也不会到现在才后悔莫及。其实每次提到数据结构和算法它们都是共同出现的，因为算法大部分都是基于大量的数据结构组合而成的，想要掌握好算法就必须在数据结构上下好功夫。&lt;/p&gt;
&lt;h2&gt;《Hello 算法》&lt;/h2&gt;
&lt;p&gt;相信大家在求职冲刺阶段都会认识一本叫做 &lt;strong&gt;&lt;em&gt;《剑指offer》&lt;/em&gt;&lt;/strong&gt; 的书籍，相信其中循序渐进的经典算法例题会让你在算法方面鼓足干劲和勇气，而这本 &lt;strong&gt;&lt;em&gt;《Hello 算法》&lt;/em&gt;&lt;/strong&gt; 是这个系列有一本重量级作品，收到行业内多位大佬推荐的一本数据结构与算法入门书。&lt;/p&gt;
&lt;p&gt;![Hello 算法](/assets/images/resource/books/hello-algo.png &quot;Hello 算法&quot; =350x500)&lt;/p&gt;
&lt;p&gt;并且相对于枯燥的纸质书籍，Github 上还配套在线阅读网站，重点和难点知识将主要通过动画和图解形式展示，并且仓库源代码附有测试样例，可一键运行，对新手非常友好。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: 《Hello 算法》项目 Github 官网
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/github-logo.svg
link: https://github.com/krahets/hello-algo
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Arthas</title><link>https://songbaicheng.cc.cd/posts/arthas/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/arthas/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Arthas（阿尔萨斯)&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;Arthas 是阿里提供的一款线上监控诊断产品，通过全局视角实时查看应用 load、内存、gc、线程的状态信息，并能在不修改应用代码的情况下，对业务问题进行诊断，包括查看方法调用的出入参、异常，监测方法执行耗时，类加载信息等，大大提升线上问题排查效率。&lt;/p&gt;
&lt;p&gt;当你遇到以下类似问题而束手无策时，Arthas可以帮助你解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个类从哪个 jar 包加载的？为什么会报各种类相关的 Exception？&lt;/li&gt;
&lt;li&gt;我改的代码为什么没有执行到？难道是我没 commit？分支搞错了？&lt;/li&gt;
&lt;li&gt;遇到问题无法在线上 debug，难道只能通过加日志再重新发布吗？&lt;/li&gt;
&lt;li&gt;线上遇到某个用户的数据处理有问题，但线上同样无法 debug，线下无法重现！&lt;/li&gt;
&lt;li&gt;是否有一个全局视角来查看系统的运行状况？&lt;/li&gt;
&lt;li&gt;有什么办法可以监控到 JVM 的实时运行状态？&lt;/li&gt;
&lt;li&gt;怎么快速定位应用的热点，生成火焰图？&lt;/li&gt;
&lt;li&gt;怎样直接从 JVM 内查找某个类的实例？&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;title: Arthas 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/arthas/arthas.png
link: https://arthas.aliyun.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;下载&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 在线的环境下下载 arthas-boot 包进行启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 如果离线或者条件允许的情况下可以去 Github 下载完整安装包
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Arthas Github 网站发行版
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/github-logo.svg
link: https://github.com/alibaba/arthas/releases
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动&lt;/h3&gt;
&lt;p&gt;找到 arthas-boot.jar 包并执行 &lt;code&gt;java -jar arthas-boot.jar&lt;/code&gt; 命令启动 Arthas，这里需要注意，启动 Arthas 必须保证当前环境中有运行的 java 进程，否则会自动退出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-break-out.png&quot; alt=&quot;Arthas 启动时退出&quot; title=&quot;Arthas 启动时退出&quot; /&gt;&lt;/p&gt;
&lt;p&gt;启动成功后，紧随其后就会生成当前用户下启动的 java 程序，选择序号后执行就 attach（粘连） 到了目标进程，在进入目标进程后，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-start.png&quot; alt=&quot;Arthas 启动成功并 attach 到目标进程&quot; title=&quot;Arthas 启动成功并 attach 到目标进程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中我们可以看到 Arthas 会打开一个端口为 3658 的 client，我们之后的操作也可以选择在浏览器界面进行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-broswer.png&quot; alt=&quot;Arthas 浏览器界面&quot; title=&quot;Arthas 浏览器界面&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;退出&lt;/h3&gt;
&lt;p&gt;如果只是退出当前的连接，可以用 quit 或者 exit 命令。Attach 到目标进程上的 arthas 还会继续运行，端口会保持开放，下次连接时可以直接连接上。如果想完全退出 arthas，可以执行stop命令。&lt;/p&gt;
&lt;h2&gt;常用场景&lt;/h2&gt;
&lt;h3&gt;查找执行方法&lt;/h3&gt;
&lt;p&gt;当我们遇到一个非常耗时的方法的时候，或者遇到代码问题需要定位的时候，我们可以使用 trace 命令来通过一步步缩小范围来找到耗时最长的方法，如果你并不熟悉代码或者不清楚具体在那个包下，只需要从最外层的包名开始查看，可以用 * 来代替不确定的路径，在多次看到红色最长时间的方法后就能明确具体方法了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-trace.png&quot; alt=&quot;查看方法耗时&quot; title=&quot;查看方法耗时&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;查看源码解决问题&lt;/h3&gt;
&lt;p&gt;如果我们已经知晓了问题代码的位置，可是源码我们又看不到，我们就可以用 jad 命令来反编译类名或者方法名来查看源码。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-jad.png&quot; alt=&quot;反编译源码&quot; title=&quot;反编译源码&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;查看某些方法出参入参&lt;/h3&gt;
&lt;p&gt;如果我们想排查方法调用时一些方法的入参和出参，我们可以使用 watch 命令，如果是多层的结构，我们可以用 -x 【n】来指定查看解析数据的层数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/arthas/arthas-watch.png&quot; alt=&quot;监听方法参数&quot; title=&quot;监听方法参数&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Ant</title><link>https://songbaicheng.cc.cd/posts/ant/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/ant/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;打包工具-Ant&lt;/h1&gt;
&lt;h2&gt;对ant的理解&lt;/h2&gt;
&lt;p&gt;Apache Ant，在我看来称他为时代弃子也不为过，我现在都还依稀记得当时学习ant时看到的一篇文章里有那么一句话：使用 ant 作为构建工具对程序员来说是一种挑战。&lt;/p&gt;
&lt;p&gt;对我来说，认识到ant还是因为刚步入社会的公司里总有些老旧的纯 Java 项目依然苟延残喘，第一次看见项目目录里的 build.xml 的我那时候还没有认识到问题的严重性，直到自己负责这些项目打包上线的时候，我才发现不知不觉，我已经和它打了两年的交道了。说句题外话，其实相对于我这个年龄段的程序员来说，xml文件就已经逐渐被 yml 所取代了，虽然 xml 在公司项目中更容易维护，但是体积过大和比较繁琐的标签和属性确实让人头大，不过对我来说，拥抱 yaml 的原因只有优雅二字而已。言归正传，想学会ant最主要的就是学会读懂构建脚本，先列举一个简单的Java项目打 jar 包的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project name=&quot;myproject&quot; default=&quot;build&quot;&amp;gt;

  &amp;lt;!-- 定义属性 --&amp;gt;
  &amp;lt;property name=&quot;src.dir&quot; value=&quot;src&quot;/&amp;gt;
  &amp;lt;property name=&quot;build.dir&quot; value=&quot;build&quot;/&amp;gt;
  &amp;lt;property name=&quot;dist.dir&quot; value=&quot;dist&quot;/&amp;gt;
  &amp;lt;property name=&quot;main-class&quot; value=&quot;com.example.MyApp&quot;/&amp;gt;


  &amp;lt;!-- 定义任务 --&amp;gt;
  &amp;lt;target name=&quot;clean&quot;&amp;gt;
    &amp;lt;delete dir=&quot;${build.dir}&quot;/&amp;gt;
    &amp;lt;delete dir=&quot;${dist.dir}&quot;/&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name=&quot;compile&quot; depends=&quot;clean&quot;&amp;gt;
    &amp;lt;mkdir dir=&quot;${build.dir}&quot;/&amp;gt;
    &amp;lt;javac srcdir=&quot;${src.dir}&quot; destdir=&quot;${build.dir}&quot;/&amp;gt;
  &amp;lt;/target&amp;gt;

  &amp;lt;target name=&quot;build&quot; depends=&quot;compile&quot;&amp;gt;
    &amp;lt;mkdir dir=&quot;${dist.dir}&quot;/&amp;gt;
    &amp;lt;jar destfile=&quot;${dist.dir}/myapp.jar&quot; basedir=&quot;${build.dir}&quot;&amp;gt;
      &amp;lt;manifest&amp;gt;
        &amp;lt;attribute name=&quot;Main-Class&quot; value=&quot;${main-class}&quot;/&amp;gt;
      &amp;lt;/manifest&amp;gt;
    &amp;lt;/jar&amp;gt;
  &amp;lt;/target&amp;gt;

&amp;lt;/project&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实这个脚本的内容就挺“Java”的：定义属性，执行任务，所以整个脚本最重要的就是其中的一个个&lt;code&gt;&amp;lt;target&amp;gt;&amp;lt;/target&amp;gt;&lt;/code&gt;标签中的任务，这里的 target 任务和 Gradle 里的 task 十分相像，里面的步骤也是根据一个个的命令标签从上到下执行的，正如这里的打 jar 包的操作，就是使用了&lt;code&gt;&amp;lt;jar&amp;gt;&amp;lt;/jar&amp;gt;&lt;/code&gt;标签来实现的，所以相比于Maven和Gradle来说，ant并不是一无是处，跨平台、简单易用、强大的任务库和可拓展性这些优点放在现在就是一句话，成熟！可是陈旧的设计注定它不能拥有依赖管理、自动化测试、增量构建这些功能，这也注定了它逐渐被 Maven 和 Gradle 所取代，并逐渐被视为过时的技术。&lt;/p&gt;
&lt;h2&gt;工作中遇到的Ant打包需求&lt;/h2&gt;
&lt;p&gt;low 归 low，活还是得照干，下面是工作上项目打包的一些需求记录。&lt;/p&gt;
&lt;h3&gt;构建war包和jar包中的Manifest文件添加打包Git分支明细&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;target name=&quot;set-version&quot;&amp;gt;
  &amp;lt;exec executable=&quot;git&quot; outputproperty=&quot;git.branch&quot;&amp;gt;
    &amp;lt;arg value=&quot;rev-parse&quot; /&amp;gt;
    &amp;lt;arg value=&quot;--abbrev-ref&quot; /&amp;gt;
    &amp;lt;arg value=&quot;HEAD&quot; /&amp;gt;
  &amp;lt;/exec&amp;gt;

  &amp;lt;manifest file=&quot;MANIFEST.MF&quot;&amp;gt;
    &amp;lt;attribute name=&quot;Git-Branch&quot; value=&quot;${git.branch}&quot; /&amp;gt;
  &amp;lt;/manifest&amp;gt;
&amp;lt;/target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;脚本中逻辑判断&lt;/h3&gt;
&lt;p&gt;ant 中并不存在 if 标签来进行逻辑判断，如果想实现是否执行的步骤需要将逻辑抽离到一个 target 后使用 antcall 标签来调用，根据 target 标签中的 if 属性来决定是否执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;target name=&quot;step1&quot;&amp;gt;
  ....
  &amp;lt;ant call target=&quot;step2&quot;&amp;gt;
&amp;lt;/target&amp;gt;

&amp;lt;target name=&quot;step2&quot; if=${flag}&amp;gt;
  ....
&amp;lt;/target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>1Panel</title><link>https://songbaicheng.cc.cd/posts/1panel/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/1panel/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1Panel&lt;/h1&gt;
&lt;h2&gt;聊聊运维面板&lt;/h2&gt;
&lt;p&gt;在很多内网的服务器中，需要手工输入命令安装各类软件，操作起来费时费力并且容易出错，非常考验运维人员的基本功，而面向一些云服务器来说，我只是需要安装一些数据库或者运行环境，这个时候如果使用运维面板就可以一件安装，简直不要太方便。之前听过的运维面板只有宝塔，不过最近一款开源并且号称新一代的 Linux 服务器运维管理面板 1Panel 横空出世，正好手头也有服务器就看看面板工具是不是真的那么好用。如果想了解更详细的面板介绍可以点击下面链接跳转官网查看。&lt;/p&gt;
&lt;p&gt;::: card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: 1Panel 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/1panel/1Panel.png
link: https://1panel.cn/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: 宝塔官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/1panel/bt.svg
link: https://www.bt.cn/new/index.html
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;环境准备和下载&lt;/h3&gt;
&lt;p&gt;这里我准备了一台阿里云的服务器，下载前注意一下服务器用户是否有足够权限，默认是安装在 /opt 目录下的，而且像很多这种下载脚本执行的安装方式在下载安装的时候会产生文件，所以个人还是推荐创建一个新文件夹再进行下载。执行下面命令即可下载最新安装脚本并自动进行安装。&lt;/p&gt;
&lt;p&gt;::: code-tabs
@tab RedHat / CentOS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh &amp;amp;&amp;amp; sh quick_start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Ubuntu&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh &amp;amp;&amp;amp; sudo bash quick_start.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;启动展示&lt;/h3&gt;
&lt;p&gt;安装的话会有一些初始化操作的步骤，一步一步完成即可，安装成功后，界面会出现访问服务的网址，如果你手速够快或者一些特殊情况看不到了，可以执行  &lt;code&gt;1pctl user-info&lt;/code&gt; 进行查看，然后使用 ip + port 的方式就可以直接跳转登录界面了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/1panel/safe-entrance.png&quot; alt=&quot;暂无权限访问&quot; title=&quot;暂无权限访问&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果出现上面界面，是因为新版本增加了一个安全入口登录的限制，这个就需要用 &lt;code&gt;1pctl user-info&lt;/code&gt; 这个命令查看 entrance 这个属性跟在端口后才能进行访问。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/1panel/index.png&quot; alt=&quot;登录成功&quot; title=&quot;登录成功&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
</content:encoded></item><item><title>Environment Variable Configuration</title><link>https://songbaicheng.cc.cd/posts/environment-variable-configuration/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/environment-variable-configuration/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;配置转存到环境变量&lt;/h1&gt;
&lt;p&gt;由于最近上线总是出现问题，而上线规范还要求“一包到底”，这就导致如果上线包出错会导致重新准备包将重新走一遍上线流程，而仅仅是修改配置的话这样反而显得得不偿失，所以沟通后决定将生产包中的配置和代码分离开来，每次上包只包含代码，这样配置抽离存放就有两种方案，一种是放在云平台读取，另一种就是放在环境变量中。&lt;/p&gt;
&lt;p&gt;如果是放在微服务中来讲，将配置放在 Nacos 或 Spring Cloud Config 上是为了方便集中管理，而且使用这种框架也可以动态更新配置，最根本的原因还是为了多环境和多实例的支持，让多台环境不用重复配置相同的配置。可现在像一些 Spring MVC 的老项目完全没有这种需求，基本都是打 war 包双活部署，而且并没有这种云配置的框架支持，所以我们决定把 .properties 中的配置迁移到环境变量读取。&lt;/p&gt;
&lt;h2&gt;实操&lt;/h2&gt;
&lt;p&gt;如果 Spring MVC 项目一切用法都规范的话，大部分 .properties 中的配置读取都是在 xml 中用 ${} 占位符读取出来的，至于加载 .properties 则是用下面这种方式读取配置的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;bean class=&quot;org.springframework.beans.factory.config.PropertyPlaceholderConfigurer&quot;&amp;gt;
    &amp;lt;property name=&quot;location&quot; value=&quot;classpath:config.properties&quot; /&amp;gt;
&amp;lt;/bean&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们全部将 .properties 中的变量添加到环境变量中后，把项目中的 .properties 全部删除并且把上面读取的 bean 也删除掉后启动，发现项目从环境变量中读取的 value 全部都是 String 类型，当你一些端口一类的变量需要其他类型的时候，在转换的时候会出现问题，这个问题的解决方法就是加一个空的上面占位符所读取的类，让 Spring 这个类帮我们做类型转换即可。&lt;/p&gt;
&lt;p&gt;如果有些项目并不规范，有些配置是调用 &lt;code&gt;ClassLoader()&lt;/code&gt; 读取文件获取的配置，这些就要用到 &lt;code&gt;System.getenv()&lt;/code&gt; 方法获取到环境变量的 Map ，然后匹配赋值即可。不过这个方法有个弊端，无论是 Windows 还是 Liunx ，这个方法只能获取到全局（root）的变量，获取不到个人用户下的环境变量，所以在移动环境变量的时候尽量配置到全局变量中去。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然成功把配置和代码从包中分离出来了，但是只能说是符合当前公司的使用场景，至于是不是最优解还尚且未知，希望以后可以学习到其他更优实践。&lt;/p&gt;
</content:encoded></item><item><title>Encode</title><link>https://songbaicheng.cc.cd/posts/encode/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/encode/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;字符编码&lt;/h1&gt;
&lt;h2&gt;字符编码的作用&lt;/h2&gt;
&lt;p&gt;计算机内部，所有信息最终都是二进制的数字，而计算机内部存储的字符都是通过编码表映射出来的，为了能让我们熟知的字符正常显示在屏幕上，我们需要做以下两件事情：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;给所有的字符一个独一无二的数字编号，做一个数字编号到汉字的 mapping 关系（即字符集）&lt;/li&gt;
&lt;li&gt;把这个数字编号能用0和1表示出来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们通常所说的 Unicode，其实只做了第1件事情，并且是给全世界所有语言的所有文字或字母一个独一无二的数字编码，这样只要设计一种机制做第2件事情来表示 Unicode，就可以显示全球范围内任何文字了。Unicode具体对所有语言的每个字母、文字的数字编号可以从其官方网站 Unicode 编码表 查询。&lt;/p&gt;
&lt;h2&gt;常见的字符编码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root((字符编码类型))
  ((Unicode))
  ((ASCII))
  ((Latin1))
  ((GB2312))
  ((GBK))
  ((GB18030))
  ((UTF8))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ASCII&lt;/h3&gt;
&lt;p&gt;ASCII 编码，全称是 American Standard Code for Information Interchange，美国信息交换标准代码。&lt;/p&gt;
&lt;p&gt;ASCII编码每个字母或符号占1byte（8bits），并且8bits的最高位是0，因此ASCII能编码的字母和符号只有128个。有一些编码把8bits最高位为1的后128个值也编码上，使得1byte可以表示256个值，但是这属于扩展的ASCII，并非标准ASCII。通常所说的标准ASCII只有前128个值！&lt;/p&gt;
&lt;p&gt;ASCII编码几乎被世界上所有编码所兼容（UTF16和UTF32是个例外），因此如果一个文本文档里面的内容全都由ASCII里面的字母或符号构成，那么不管你如何展示该文档的内容，都不可能出现乱码的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/development/encode/ascii.png&quot; alt=&quot;ASCII编码&quot; title=&quot;ASCII编码&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Latin1（又名ISO-8859-1编码）&lt;/h3&gt;
&lt;p&gt;Latin1 在 ASCII 基础上又充分利用了后面那128个值，赋予他们一些泰语、希腊语等字母或符号，将1个字节的256个值全部占满了。 值得一提的是 MySQL 选它做默认编码。&lt;/p&gt;
&lt;h3&gt;GB2312&lt;/h3&gt;
&lt;p&gt;GB 全称 GuoBiao（国标），GBK 全称 GuoBiaoKuozhan（国标扩展）。GB18030 编码兼容 GBK，GBK 兼容 GB2312，其实这三种编码有着非常深厚的渊源，我们放在一起进行介绍。&lt;/p&gt;
&lt;p&gt;GB2312 是最早一版的中文编码，每个字占据 2bytes。由于要和 ASCII 兼容，那这 2bytes 最高位不可以为0了（否则和ASCII会有冲突）。在 GB2312 中收录了 6763 个简体汉字以及 682 个特殊符号，已经囊括了生活中最常用的所有汉字。&lt;/p&gt;
&lt;h3&gt;GBK&lt;/h3&gt;
&lt;p&gt;由于 GB2312 只有 6763 个汉字，于是 GBK 中在保证不和 GB2312、ASCII 冲突的前提下，也用每个字占据 2bytes 的方式又编码了许多汉字。经过 GBK 编码后，可以表示的汉字达到了 20902 个，另有 984 个汉语标点符号、部首等。值得注意的是这 20902 个汉字还包含了繁体字，但是该繁体字与台湾Big5编码不兼容，因为同一个繁体字很可能在 GBK 和 Big5 中数字编码是不一样的。&lt;/p&gt;
&lt;h3&gt;GB18030&lt;/h3&gt;
&lt;p&gt;然而，GBK的两万多字也已经无法满足我们的需求了，还有更多可能你自己从来没见过的汉字需要编码。&lt;/p&gt;
&lt;p&gt;这时候显然只用2bytes表示一个字已经不够用了（2bytes最多只有65536种组合，然而为了和ASCII兼容，最高位不能为0就已经直接淘汰了一半的组合，只剩下3万多种组合无法满足全部汉字要求）。因此GB18030多出来的汉字使用4bytes编码。当然，为了兼容GBK，这个四字节的前两位显然不能与GBK冲突（实操中发现后两位也并没有和GBK冲突）。&lt;/p&gt;
&lt;p&gt;我国在2000年和2005年分别颁布的两次GB18030编码，其中2005年的是在2000年基础上进一步补充。至此，GB18030编码的中文文件已经有七万多个汉字了，甚至包含了少数民族文字。&lt;/p&gt;
&lt;h3&gt;UTF8&lt;/h3&gt;
&lt;p&gt;UTF8 全称 Unicode Transformation Format - 8-bit，是一种变长的编码方式。UTF8 可以表示出世界上所有的文字！伟大无需多言。&lt;/p&gt;
</content:encoded></item><item><title>Docker</title><link>https://songbaicheng.cc.cd/posts/docker/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/docker/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Docker&lt;/h1&gt;
&lt;p&gt;05 年之前，项目的部署都依赖物理隔离，而到了08年之后几乎就已经大量采用虚拟化技术，通过硬件和软件的支持物理架构的重新整合运用，再发展到18年至今，云计算开始发力。&lt;/p&gt;
&lt;p&gt;在云计算的推动下，虚拟化技术是大势所趋，Docker 是作为运维工程师及后端开发人员都应该了解的技术，简化环境搭建、节省开支、持续交付、部署和部署。&lt;/p&gt;
&lt;p&gt;学习后我们再也不用受各种安装环境时提示安装失败的折磨，并且在微服务的项目中我们所开发的项目也都已经部署在容器，这是我们面向云原生的怀抱、学习 k8s 之前必须要学会的知识点。&lt;/p&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;Docker 包括三个基本概念:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像（Image）：相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。&lt;/li&gt;
&lt;li&gt;容器（Container）：镜像和容器的关系，就像是面向对象程序设计中的类和实例一样，镜像是静态的定义，容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。&lt;/li&gt;
&lt;li&gt;仓库（Repository）：仓库可看成一个代码控制中心，用来保存镜像。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;下载并安装&lt;/h3&gt;
&lt;p&gt;根据官方文档下载指定操作系统的安装包安装即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Docker 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/docker/docker.png
link: https://www.docker.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;加速器&lt;/h3&gt;
&lt;p&gt;默认的统一镜像仓库是 DockerHub ，不过国内从 DockerHub 拉取镜像有时会遇到困难，此时可以配置镜像加速器。Docker 官方和国内很多云服务商都提供了国内加速器服务，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;科大镜像：https://docker.mirrors.ustc.edu.cn/&lt;/li&gt;
&lt;li&gt;网易：https://hub-mirror.c.163.com/&lt;/li&gt;
&lt;li&gt;阿里云：https://&amp;lt;你的ID&amp;gt;.mirror.aliyuncs.com(需要登录个人阿里云控制台查看)&lt;/li&gt;
&lt;li&gt;七牛云加速器：https://reg-mirror.qiniu.com&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;配置加速器&lt;/h3&gt;
&lt;p&gt;如果是可视化的界面在 Docker 的 Docker Engine 中的 &lt;code&gt;registry-mirrors&lt;/code&gt; 中添加加速器地址即可；对于使用 systemd 的系统，请在 /etc/docker/daemon.json 中写入如下内容（如果文件不存在请新建该文件）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;registry-mirrors&quot;:[&quot;https://reg-mirror.qiniu.com/&quot;]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;检查加速器是否生效&lt;/h3&gt;
&lt;p&gt;检查加速器是否生效配置加速器之后，如果拉取镜像仍然十分缓慢，请手动检查加速器配置是否生效，在命令行执行 &lt;code&gt;docker info&lt;/code&gt;，如果从结果中看到了如下内容，说明配置成功。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker info
Registry Mirrors:
    https://reg-mirror.qiniu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;感觉所有编程技术的第一课都是 Hello World ，Docker 也不例外，这里我们拿一个输出 Hello World 的 Ubuntu 镜像来作为我们运行的第一个例子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;runoob@runoob:~$ docker run ubuntu:15.10 /bin/echo &quot;Hello World&quot;
Hello World
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其实学过 shell 脚本的人不难看出，其实这里的输出其实就是相当于 &lt;code&gt;echo &apos;Hello World&apos;&lt;/code&gt;，而其他命令的作用就是帮我们运行一个 Ubuntu 镜像，使它变成一个可执行的容器然后来运行我们的命令，&lt;code&gt;docker run [container name]&lt;/code&gt; 这个命令就是运行指定容器的命令，而这个命令也可以指定很多参数，比如；&lt;/p&gt;
&lt;p&gt;-t: 在新容器内指定一个伪终端或终端。
-i: 允许你对容器内的标准输入 (STDIN) 进行交互。&lt;/p&gt;
&lt;p&gt;一般我们使用这两个命令就是在第一次启动容器的时候直接进入到容器内部进行编辑，当然如果你第一次只是想运行出一个镜像并不想和产生的容器产生交互，就可以使用后台模式启动，即 &lt;code&gt;-d&lt;/code&gt; 参数，使用 &lt;code&gt;-d&lt;/code&gt; 参数运行后的容器会返回一串 container id，当然我们也可以用 &lt;code&gt;docker ps&lt;/code&gt; 来查看正在运行的容器。&lt;/p&gt;
&lt;p&gt;如果我们想要停止一些容器，就可以先用 &lt;code&gt;docker ps&lt;/code&gt; 来查看启动容器的 id，然后用 &lt;code&gt;docker stop [container id]&lt;/code&gt; 来停止容器，停止后的容器再使用 &lt;code&gt;docker ps&lt;/code&gt; 已经查看不到了，当然并不是被删除了，你可以使用 &lt;code&gt;docker ps -a&lt;/code&gt; 查看全部创建的容器。&lt;/p&gt;
&lt;h2&gt;容器命令&lt;/h2&gt;
&lt;h2&gt;镜像分层&lt;/h2&gt;
&lt;h2&gt;私服库&lt;/h2&gt;
&lt;h2&gt;数据卷&lt;/h2&gt;
&lt;h2&gt;DockerFile&lt;/h2&gt;
&lt;p&gt;虚悬镜像&lt;/p&gt;
&lt;h2&gt;容器网络&lt;/h2&gt;
&lt;h2&gt;容器编排&lt;/h2&gt;
&lt;h2&gt;可视化工具&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;title: portainer 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/docker/portainer.svg
link: https://www.portainer.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;CIG 监控&lt;/h2&gt;
</content:encoded></item><item><title>Chain</title><link>https://songbaicheng.cc.cd/posts/chain/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/chain/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;责任链模式&lt;/h1&gt;
&lt;h2&gt;什么是责任链模式&lt;/h2&gt;
&lt;p&gt;责任链模式（Chain of Responsibility Pattern）它允许你构建一个对象链，每个对象都持有对下一个对象的引用，从而形成一条链。每个对象在收到请求后，可以选择处理请求或将请求传递给链中的下一个对象。
这种模式的核心思想是解耦发送者和接收者，让多个对象都有机会处理请求，而不需要显式指定接收者。请求会沿着链传递，直到有一个对象处理它为止。&lt;/p&gt;
&lt;h3&gt;角色&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Handler（处理者）： 定义一个处理请求的接口，通常包含一个处理请求的方法，并持有对下一个处理者的引用。处理者可以决定是否处理请求，或者将请求传递给链中的下一个处理者。&lt;/li&gt;
&lt;li&gt;ConcreteHandler（具体处理者）： 实现处理者接口，在收到请求时判断自己是否能够处理，如果可以处理则处理请求，否则将请求传递给链中的下一个处理者。&lt;/li&gt;
&lt;li&gt;Client（客户端）： 创建责任链，并向链的第一个处理者发送请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;解耦发送者和接收者： 发送者不需要知道接收者的具体信息，只需要将请求发送到链的第一个处理者即可。&lt;/li&gt;
&lt;li&gt;灵活性和可扩展性： 可以动态地添加、移除或重新排序处理者，以满足不同的需求。&lt;/li&gt;
&lt;li&gt;单一职责原则： 每个处理者只负责处理特定类型的请求，符合单一职责原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;如果没有处理者能够处理请求，请求可能会到达链的末端而未被处理。&lt;/li&gt;
&lt;li&gt;性能问题： 如果责任链过长或者处理者之间的调用关系复杂，可能会影响性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;场景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;事件处理系统
责任链模式在 GUI 框架和其他事件处理系统中广泛使用。例如，在一个图形用户界面中，一个按钮点击事件可以被多个处理器处理（如按钮本身、父容器、窗口等），直到有一个处理器处理了该事件。&lt;/li&gt;
&lt;li&gt;日志记录系统
日志记录系统可以使用责任链模式，不同的日志级别（如 DEBUG、INFO、WARN、ERROR）可以有不同的处理器。日志请求沿着责任链传递，不同级别的日志可以由不同的处理器处理，或者由同一个处理器处理但以不同的方式记录。&lt;/li&gt;
&lt;li&gt;请求处理管道
在 web 应用程序中，常常有一系列的过滤器（如安全过滤器、日志过滤器、压缩过滤器等）对请求进行处理。每个过滤器都是一个处理器，请求沿着过滤器链传递，每个过滤器可以选择处理请求或将其传递到链中的下一个过滤器。&lt;/li&gt;
&lt;li&gt;审批工作流系统
在审批工作流系统中，不同级别的审批者可以形成一个责任链。请求从一个审批者传递到下一个审批者，直到请求被批准或拒绝。例如，一个请假申请可以从部门经理传递到人力资源经理，再到总经理，每个级别的审批者都有机会处理该请求。&lt;/li&gt;
&lt;li&gt;权限管理系统
在权限管理系统中，可以使用责任链模式来检查用户权限。权限检查请求沿着责任链传递，直到有一个处理器确认用户具有足够的权限或到达链的末尾。如果没有处理器处理该请求，则表示用户没有足够的权限。&lt;/li&gt;
&lt;li&gt;数据处理管道
数据处理管道可以由一系列的数据处理步骤组成，每个步骤都是一个处理器。例如，在数据清洗过程中，可以有多个步骤（如去重、格式化、数据校验等）依次处理数据，每个步骤可以选择处理数据或将其传递到下一个处理器。&lt;/li&gt;
&lt;li&gt;命令处理系统
在命令处理系统中，可以使用责任链模式来处理命令。不同的处理器可以处理不同类型的命令，请求沿着责任链传递，直到有一个处理器处理该命令。例如，在一个文本编辑器中，不同的命令（如复制、粘贴、撤销等）可以由不同的处理器处理。&lt;/li&gt;
&lt;li&gt;异常处理
在异常处理系统中，可以使用责任链模式来处理不同类型的异常。异常请求沿着责任链传递，直到有一个处理器处理该异常。例如，在一个 web 应用程序中，可以有一系列的异常处理器来处理不同类型的异常（如数据库异常、网络异常、业务逻辑异常等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之符合多层处理流程的功能都可以用责任链模式来应对。&lt;/p&gt;
&lt;h2&gt;应用场景&lt;/h2&gt;
&lt;h3&gt;用责任链实现请求内容校验&lt;/h3&gt;
&lt;p&gt;用户注册的场景下，我们在创建新用户之前需要经过对字段合法性、用户是否已注册、用户黑名单校验等步骤，如果只是将步骤分为不同的方法或者都写在同一个方法里难免会造成一个大类，维护起来十分困难，这里使用责任链的模式进行拆分。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;抽象处理者
这里我们抽象处理者需要有处理方法和分类标识，处理方法当然是我们责任链每个环节的的具体实现，而分类标识则用来分类处理者种类从而实现多种处理者并存。这里我们还继承 Spring 的 Ordered 来实现每种责任链中处理者的执行排序。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.core.Ordered;

/**
 * @description: 抽象业务责任链
 **/
public interface AbstractChainHandler&amp;lt;T&amp;gt; extends Ordered {

    /**
     * 执行责任链逻辑
     *
     * @param requestParam 责任链执行入参
     */
    void handler(T requestParam);

    /**
     * 责任链组件标识
     */
    String mark();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;具体处理者
这里我们分别实现校验注册字段的三个具体实现类，首先定于用户注册责任链的分类。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @description: 用户责任链处理者
 **/
public interface UserRegisterCreateChainFilter &amp;lt;T extends UserRegisterReqVo&amp;gt; extends AbstractChainHandler&amp;lt;UserRegisterReqVo&amp;gt; {

    @Override
    default String mark() {
        return UserChainMarkEnum.USER_REGISTER_FILTER.name();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了注册责任链接口后，我们只需要继承这个接口实现具体的校验逻辑和顺序即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @description: 用户注册参数必填检验
 **/
@Component
public class UserRegisterParamNotNullChainHandler implements UserRegisterCreateChainFilter&amp;lt;UserRegisterReqVo&amp;gt; {

    @Override
    public void handler(UserRegisterReqVo requestParam) {
        if (Objects.isNull(requestParam.getUsername())) {
            throw new ClientException(UserRegisterErrorCodeEnum.USER_NAME_NOTNULL);
        } else if (Objects.isNull(requestParam.getPassword())) {
            throw new ClientException(UserRegisterErrorCodeEnum.PASSWORD_NOTNULL);
        } else if (Objects.isNull(requestParam.getTelephone())) {
            throw new ClientException(UserRegisterErrorCodeEnum.PHONE_NOTNULL);
        } else if (Objects.isNull(requestParam.getIdType())) {
            throw new ClientException(UserRegisterErrorCodeEnum.ID_TYPE_NOTNULL);
        } else if (Objects.isNull(requestParam.getIdCard())) {
            throw new ClientException(UserRegisterErrorCodeEnum.ID_CARD_NOTNULL);
        } else if (Objects.isNull(requestParam.getMail())) {
            throw new ClientException(UserRegisterErrorCodeEnum.MAIL_NOTNULL);
        } else if (Objects.isNull(requestParam.getRealName())) {
            throw new ClientException(UserRegisterErrorCodeEnum.REAL_NAME_NOTNULL);
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

/**
 * @description: 用户注册用户名唯一检验
 **/
@Component
@RequiredArgsConstructor
public class UserRegisterHasUsernameChainHandler implements UserRegisterCreateChainFilter&amp;lt;UserRegisterReqVo&amp;gt; {

    private final UserLoginService userLoginService;

    @Override
    public void handler(UserRegisterReqVo requestParam) {
        if (userLoginService.hasUsername(requestParam.getUsername())) {
            throw new ClientException(UserRegisterErrorCodeEnum.USERNAME_REGISTERED);
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

/**
 * @description: 用户注册检查证件号是否多次注销
 **/
@Component
@RequiredArgsConstructor
public class UserRegisterCheckDeletionChainHandler implements UserRegisterCreateChainFilter&amp;lt;UserRegisterReqVo&amp;gt; {

    private final UserInfoService userInfoService;

    @Override
    public void handler(UserRegisterReqVo requestParam) {
        Integer userDeletionNum = userInfoService.queryUserDeletionNum(requestParam.getIdType(), requestParam.getIdCard());
        if (userDeletionNum &amp;gt;= 5) {
            throw new ClientException(&quot;证件号多次注销账号已被加入黑名单&quot;);
        }
    }

    @Override
    public int getOrder() {
        return 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建责任链
我们使用 Spring 的容器夹在组件的方式创建责任链，我们使用 Spring 提供的 CommandLineRunner 接口实现项目启动时获取所有继承我们抽象处理者的类并且按照 mark 分类和 Ordered 顺序来进行排序。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @param &amp;lt;T&amp;gt; 请求参数的类型
 * @description: 抽象责任链上下文
 */
public class AbstractChainContext&amp;lt;T&amp;gt; implements CommandLineRunner {

    private final Map&amp;lt;String, List&amp;lt;AbstractChainHandler&amp;gt;&amp;gt; abstractChainHandlerContainer = new HashMap&amp;lt;&amp;gt;();

    @Override
    public void run(String... args) {
        // 获取所有具体执行者组件
        Map&amp;lt;String, AbstractChainHandler&amp;gt; chainFilterMap = ApplicationContextHolder.getBeansOfType(AbstractChainHandler.class);

        // 根据责任链组件标识将组件分类
        chainFilterMap.values().forEach(bean -&amp;gt; {
            abstractChainHandlerContainer
                    .computeIfAbsent(bean.mark(), k -&amp;gt; new ArrayList&amp;lt;&amp;gt;())
                    .add(bean);
        });

        // 按照组件 order 优先级进行排序
        abstractChainHandlerContainer.replaceAll((mark, handlers) -&amp;gt;
                handlers.stream()
                        .sorted(Comparator.comparing(Ordered::getOrder))
                        .collect(Collectors.toList())
        );
    }

    /**
     * 责任链组件执行
     *
     * @param mark         责任链组件标识
     * @param requestParam 请求参数
     */
    public void handler(String mark, T requestParam) {
        List&amp;lt;AbstractChainHandler&amp;gt; abstractChainHandlers = abstractChainHandlerContainer.get(mark);
        if (CollectionUtils.isEmpty(abstractChainHandlers)) {
            throw new RuntimeException(String.format(&quot;[%s] 责任链标识未定义。&quot;, mark));
        }
        abstractChainHandlers.forEach(each -&amp;gt; each.handler(requestParam));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用责任链&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class)
@Override
public UserRegisterRespVo register(UserRegisterReqVo requestParam) {
    // 登录责任链
    abstractChainContext.handler(UserChainMarkEnum.USER_REGISTER_FILTER.name(), requestParam);
    // 其他逻辑省略
   
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Git</title><link>https://songbaicheng.cc.cd/posts/git/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/git/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Git 开发规范&lt;/h1&gt;
&lt;h2&gt;糟糕的 SVN 仓库&lt;/h2&gt;
&lt;p&gt;之前的 SVN 仓库听说是接手的上家公司的原因才保留至今的，如今在甲方要求全面推行 Git 仓库和 DevOps 平台的浪潮下，终于我们要开始使用 Git 开发了。&lt;/p&gt;
&lt;p&gt;其实在 SVN 的日子里，刚开始接触的时候确实有点痛啊，因为 SVN 目录在设计的的时候就模仿了 Git 多分支的结构，这里其实我一直就有个问题，我接触的两家公司都是用过 SVN 的，但是从来不会出现使用分支的用法，尽管 SVN 是支持分支的，所以这让我一度认为 SVN 不支持分支。因此我们为了实现 Git 的效果，就把 dev、ver、prd 创造了三个不同的目录，但是文件夹之间没有任何联系，仅仅是代码的三份存档，每次开发完毕合并的时候需要使用 BCopmare 工具对比合并到上一层文件夹中，所以每次开发上线简直是地狱体验。&lt;/p&gt;
&lt;p&gt;其实 SVN 个人看来如果公司没有私有云盘或者公共文件服务器的话，也是一个存储文件库的替代方法，如果是协作开发，Git 的分布式多分支更适合团队协作。&lt;/p&gt;
&lt;p&gt;当我们决定迁移到 Git 仓库的时候，我自然是义不容辞，结合自己之前公司和自己使用的经验在新公司发扬光大，现在想想当时确实是激情澎湃，一天就整出了一份我们自己使用的 Git 开发规约，因为确实项目太多了，自己能力再大也不会说一个人就把整个仓库都给迁移了，况且很多项目自己没有接触了解过，所以这份规约里面包含了分支开发的定义、迁移步骤、工作场景等部分，足够当时我们团队平时开发使用了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Git分支开发规约&lt;/h2&gt;
&lt;h3&gt;Git分支含义&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;master/main（线上分支）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一个项目只能有一个master分支&lt;/li&gt;
&lt;li&gt;master分支并不是一个特殊的分支，它和其他分支完全没有区别，只是默认创建后大多数人懒得去改动它。&lt;/li&gt;
&lt;li&gt;master分支上的代码应该和线上代码始终保持一致&lt;/li&gt;
&lt;li&gt;master分支应该是保护分支，不可直接push，更不允许被删除&lt;/li&gt;
&lt;li&gt;每次上线后都需要添加tag，建议用 &lt;em&gt;上线日期&lt;/em&gt; 命名，用于存档和回滚&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;develop（开发分支）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;一个项目只能有一个develop分支&lt;/li&gt;
&lt;li&gt;基于master分支创建，作为主开发分支，保存当前最新开发成果的稳定分支&lt;/li&gt;
&lt;li&gt;为保证分支稳定可用，建议只能合并测试后可运行的的稳定分支，不允许直接在该分支做功能开发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;feature/topic（功能分支）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;作为新功能或新特性开发分支，是我们最经常交互的分支&lt;/li&gt;
&lt;li&gt;命名建议使用姓名和功能介绍组成，见名知意，知道是谁负责开发和大致开发内容，如 &lt;em&gt;feature/sbc_addMqTranfer&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;feature分支属于临时分支，可以只存在于本地仓库，功能完成并合并之后可选删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;release（预上线分支）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;release分支主要用于上线前的各种测试和部署，需要基于本次上线将所有功能整合&lt;/li&gt;
&lt;li&gt;命名建议根据上线日期决定，如 &lt;em&gt;release/20230413&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;release分支属于临时分支，上线后master标记tag后可选删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bugfix/hotfix（线上bug紧急修复分支）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;如果是紧急问题，以master分支为基线，修复后直接部署。当问题修复完成后，需要和并到master和develop分支&lt;/li&gt;
&lt;li&gt;命名建议使用解决人、上线日期和bug内容决定，如 &lt;em&gt;hotfix/sbc_20230413_fixQueueFull&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;hotfix分支属于临时分支，bug修复上线后可选删除&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Git仓库的创建和使用&lt;/h3&gt;
&lt;h4&gt;仓库的创建&lt;/h4&gt;
&lt;p&gt;根据现有的上线模式，抛弃之前svn的目录分层来区分多版本的结构，新Git仓库将使用单套代码多环境配置的模式，舍弃ver和branch文件下的未上线功能代码和备份，只保留tag下与线上环境一致的代码作为新Git仓库的开始。如果svn舍弃的代码中有未来准备上线的代码，则需要根据之后的开发规则重新手动合并到新的Git仓库中来。迁移或创建Git仓库步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在码云中在对应模块中创建工程仓库。一定要创建.gitignore文件并根据项目开发中IDE或编译过程中产生的杂余文件作对应的筛除，保持项目的干净整洁。建议创建README.md文件，其中编写项目启动和部署信息和项目简介，方便项目未来的对接和开发。项目创建完成后，中心仓库应该只有master分支，其中包含.gitignore和README.md两个文件。&lt;/li&gt;
&lt;li&gt;开始搭建项目。将工程从码云clone到本地，开始在master分支构建代码，如果是从svn迁移项目，则需要从最新的tag目录下将代码拷贝过来。准备多环境配置并完善打包脚本，实现多环境打包。在master分支构建到可以胜任开发运行和各环境打包上线的基础上，就可以push到中心仓库进行行下一步操作了。&lt;/li&gt;
&lt;li&gt;基于master分支新建develop分支，并将他们都设为保护分支，不允许push代码，只允许合并操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;仓库的使用&lt;/h4&gt;
&lt;p&gt;到这里Git仓库就已经迁移完成了，之后master分支和develop分支大部分时间都将保持完全相同的状态。当然构建仓库的方式有很多种，码云也提了创建仓库时候选择多分支的选项，但是构造的结构也是大同小异，所以希望读者都够清晰理解每个分支的作用和意义，促进开发的效率和默契。
接下来就是最关键的部分了，根据工程开发参与者的数量和便利我们可以有很多种开发流程，我们把常见的几种流程都过一遍：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果是极少的人开发，并且沟通便利、代码量也较少，这种情况你就算只使用master一个主分支开发都可以，只需要保证每次提交前拉去代码在本地处理好冲突再push即可。既然只有master一条分支，上线部署只需要从master创建tag发布即可。&lt;/li&gt;
&lt;li&gt;如果是十人左右的小组并且项目处于有大量需求需要开发的起步阶段，大家如果都在develop分支开发测试，那可能当你想测试的时候就会发现代码中有其他人提交不全的代码，导致项目启动不起来或者功能紊乱，这种情况就需要每个人根据功能从develop分支中拉去一条属于自己的分支，如 &lt;em&gt;feature/sbc_addMqTranfer&lt;/em&gt; 来完成自己的功能开发，开发测试完成后合并到develop分支，保证develop分支是最新功能的稳定分支。在保证了develop分支稳定可用的基础上遇到上线，我们只需要从所需功能完善的develop分上创建对应的release分支进行部署测试，如果develop还有功能尚未完成，则后续开发完成后再提交到develop分支再合并到release分支上，如果release分支测试出现问题，则可以删除release分支，在develop分支上重新解决后问题后再创建新的release分支测试即可。等到release分支已经完全测试完毕，则将release分支合并到master分支上并创建tag，一次上线流程就此完成。&lt;/li&gt;
&lt;li&gt;如果目前处于维护阶段的项目，需求较少也不固定并且不强制必须上线的情况，我们可以保证master和develop与生产一致保持一致，如果有明确的上线日期，如四月十三日，我们可以先从develop创建 &lt;em&gt;release/20230413&lt;/em&gt; 分支，再从该release分支下创建对应开发功能的feature分支进行开发，开发完毕后合并到release分支等待上线。如果不知道准确的上线日期，则先从develop分支上拉取feature分支开发，等上线日期确定后再创建release分支并合并进去。至于和第2种的区别在于，我们并不着急将release分支合并到develop和master分支上，因为避免临时不上或者上线失败回退而导致污染了develop和master，在上线成功之后再将release分支合并到devlop和master并标记tag。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;当然每个公司和团队的Git流程多少都会存在区别，但都是对于项目上线流程的妥协，所以无论流程是如何的，只要我们能保证代码上线的正常流转，就是好的流程。&lt;/h2&gt;
&lt;h3&gt;常见场景处理步骤&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;四月十三日上线需求&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 master 创建 &lt;em&gt;release/20230413&lt;/em&gt; 分支。&lt;/li&gt;
&lt;li&gt;从 release/20230413 分支创建 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支进行开发。&lt;/li&gt;
&lt;li&gt;开发完成后提交到中心仓库并创建从 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支到 &lt;em&gt;release/20230413&lt;/em&gt; 分支的pull request 请求进行审核，审核不通过则继续在 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支上更改，之后重新提交pull request。&lt;/li&gt;
&lt;li&gt;审批通过后合并分支并删除 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支，从 release/20230413 分支上准备上线介质。&lt;/li&gt;
&lt;li&gt;待上线完成之后，管理员将 &lt;em&gt;release/20230413&lt;/em&gt; 依次合并到develop和master分支上，并在 master 打上20230413的taf，删除 &lt;em&gt;release/20230413&lt;/em&gt; 分支。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;未知时间上线需求&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 develop 创建 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支进行开发。&lt;/li&gt;
&lt;li&gt;等待确定上线日期后，从 master 创建 &lt;em&gt;release/20230427&lt;/em&gt; 分支。&lt;/li&gt;
&lt;li&gt;创建 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 到 &lt;em&gt;release/20230427&lt;/em&gt; 的 pull request请求进行审核，之后就与第一种情况相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;你和其他人一起开发四月十三日上线的任务&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从develop创建 &lt;em&gt;release/20230413&lt;/em&gt; 分支。&lt;/li&gt;
&lt;li&gt;从 &lt;em&gt;release/20230413&lt;/em&gt; 创建 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支开发。&lt;/li&gt;
&lt;li&gt;开发完成单独测试完成后，每个都需要发起到 &lt;em&gt;release/20230413&lt;/em&gt; 分支的 pull request 审核，但是后合并的人可能就需要解决两个人冲突的代码部分。冲突解决之后合并，接下来也是和第一种情况相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;很早一个需求闲置了好几次上线，而这次需要四月十三日上线&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;假如之前从 master 分支创建的 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 功能落后了好几个上线版本，首先创建 &lt;em&gt;release/20230413&lt;/em&gt; 分支。&lt;/li&gt;
&lt;li&gt;从 &lt;em&gt;release/20230413&lt;/em&gt; 创建 &lt;em&gt;feature/sbc_deleteProduct&lt;/em&gt; 分支，因为 &lt;em&gt;release/20230413&lt;/em&gt; 和 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支源头都develop分支创建，Git存在快照记录这两条分支前后文件变动，所以直接先将 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; merge合并到 &lt;em&gt;feature/sbc_deleteProduct&lt;/em&gt; 上并删除 &lt;em&gt;feature/sbc_addFunction&lt;/em&gt; 分支，之后继续在 &lt;em&gt;feature/sbc_deleteProduct&lt;/em&gt; 上开发。&lt;/li&gt;
&lt;li&gt;开发完成后创建到 &lt;em&gt;release/20230413&lt;/em&gt; 分支的pull request 评审，之后就和第一种情况相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;讲在最后&lt;/h3&gt;
&lt;p&gt;虽然长远存在的分支只有master和develop分支，但是现在的设计迟早会因为各种挑战和原因而使仓库变得越来越笨重和复杂，所以规范存在的意义是当新的问题出现的时候，我们能以一个相对规范的状态转换到下一个规范中来，而不是完全的废弃重构，这将由我们未来一起努力。&lt;/p&gt;
&lt;p&gt;&amp;lt;style&amp;gt;
hr:nth-of-type(1) {
border-image: linear-gradient(to right, #F00, #0F0 20%, #00F 80%, #000) 1 !important;
}
&amp;lt;/style&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Daily Tools</title><link>https://songbaicheng.cc.cd/posts/daily-tools/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/daily-tools/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;日常工具（Mac 版）&lt;/h1&gt;
&lt;p&gt;刚从 Windows 换到 Mac 时，确实对这个安装软件晕得很，时间久了其实也觉得差不多，无非也是商店和官网两个下载途径，但是在苹果有了自家芯片之后，很多软件不支持这种 arm 架构，运行 X86 版本的话需要编译一版再运行，还是比较消耗算力的，有的时候甚至会出现发热或者疯狂占内存的情况，所以下载的时候除非不得已还是要下载苹果芯片的版本，查看自己目前软件是什么版的可以在 &lt;em&gt;系统信息&lt;/em&gt; 这个自带的软件上查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/resource/tools/daily-tools/system-info.png&quot; alt=&quot;软件版本&quot; title=&quot;版本信息&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;DeepL&lt;/h2&gt;
&lt;p&gt;一个号称全世界最准确的翻译软件，界面也很简洁，我最种草的还是因为他可以实现离线翻译。它也支持 Word 、 PDF 和 PPT 等整个文件的翻译，免费版也足够用了，如果你还没有用的惯的翻译软件可以尝试一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: DeepL 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/daily-tools/DeepL_Logo_darkBlue_v2.svg
link: https://www.deepl.com/translator
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;腾讯柠檬 &amp;amp; RunCat&lt;/h2&gt;
&lt;p&gt;还记得当时那几款带 Touch Bar 的 MacBook Pro 在一些特殊场景确实是效率利器，结果很多人直接在上面养宠物，在你码字休息间隙可以摸摸它、喂喂食，好家伙，我直接就是一个很感兴趣啊，最近不是卸载掉了腾讯的柠檬清理，当然不是说这个不是好软件，主要是我这丐版确实撑不起太多东西，仔细想想依赖柠檬清理有系统监视、系统开机项、软件卸载、文件清理什么的还是很方便的，心里作用吧，如果你们想搞一个可以搞一个试试，当然如果财力雄厚，看大家更多的还是推荐 CleanMyMac X，我倒是没用过。之后就是 RunCat 这个了，如果你有的时候看不见自己的电脑有么有在认真工作，它压力大不大就觉得少点东西，你可以试试这个 RunCat，见名知意，就是根据你的 CPU 的速度来调节奔跑速度的一个菜单栏小猫，还是挺解压的一个东西，算是如果不想看冰冷的数字的一种替代方式吧。&lt;/p&gt;
&lt;p&gt;::: card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: 腾讯柠檬官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/daily-tools/lemon.png
link: https://lemon.qq.com
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: RunCat 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/daily-tools/runcat.png
link: https://kyome.io/runcat/index.html?lang=en
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;Better365&lt;/h2&gt;
&lt;p&gt;这位更是我自己使用软件中的重量型选手，致力于 macOS 优秀 APP 开发的宁波上官科技有限公司 Better365 团队，旗下众多产品线都是我的常客。我来列举几个他们我个人十分推荐的软件，它们都能在官网的产品列表中找到。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Better365 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/daily-tools/better365.ico
link: https://www.better365.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;XApp&lt;/h3&gt;
&lt;p&gt;XApp 可以帮你找到应用程序的缓存、配置等文件，一键卸载，干净、零残留，清理 Mac 的好帮手。虽然自己还没有卸载很多软件的机会，但是尝试下来确实可以显示很多隐藏的文件，强迫症患者福音，也算是 CleanMyMac X 的一部分平替吧。&lt;/p&gt;
&lt;h3&gt;iShot&lt;/h3&gt;
&lt;p&gt;iShot 堪称 macOS上 功能最为全面的截图、录屏、OCR、截图翻译工具，截图、长截图、多窗口截图、全屏带壳截图、延时截图、标注、贴图、取色、录屏、录音、OCR、截图翻译......众多丰富功能满足你各种需求，尤其是在 QQ 截图的很多功能在 Mac 上不支持以后，这绝对是使用最频繁的神器。&lt;/p&gt;
&lt;h3&gt;FastZip&lt;/h3&gt;
&lt;p&gt;苹果是自带解压缩功能的，但是鉴于有些分卷压缩的特殊场景，还是需要一个压缩软件的，我在 Windows 下是用 Bandizip 的，可在 Mac 上也是变成了收费软件，所以这里干脆推荐一个系列啦，功能该有的都有，也都不差。&lt;/p&gt;
&lt;h3&gt;State&lt;/h3&gt;
&lt;p&gt;监控软件谁不喜欢呢，虽然我自认为监控软件都是徒增系统压力，可是确确实实不能让任务栏闲着，而且展示效果上也是比&lt;code&gt;腾讯柠檬&lt;/code&gt;略胜一筹。&lt;/p&gt;
&lt;h3&gt;超级右键&lt;/h3&gt;
&lt;p&gt;还有高手！Mac 上最强大的右键菜单效率工具！我不认为大家习惯 Windows 后可以拒绝直接右键菜单带来的便利化操作，强大无须多言。&lt;/p&gt;
&lt;h2&gt;Sublime Text&lt;/h2&gt;
&lt;p&gt;相信大家都会觉得系统自带的文本编辑器的功能有点单调，尤其是在复制一些带有格式的文字，更是显得力不从心，虽然之前自己一直在用 VS Code 来作为文本编辑的主力，但是奈何它实在是太重了，很多时候有种杀鸡焉用牛刀的感觉，尤其是很多情况打开它重新打开上次意外关闭的项目，所以还是觉得本地应该有个轻量级的文本编辑器方便平时简单使用，当然这个方向竞品还是很的，因为平时工作还是 Windows 系统居多，所以大部分都是 NotePad++ 或者 UE 比较多，但是一些特殊的原因还是最后选择了 Sublime Text，虽然是付费软件，但是可以无限期的试用，在功能和使用逻辑上比较对味，如果你也有同样的需求，可以同样试一试。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Sublime Text 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/sublime_text.png
link: http://www.sublimetext.com
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Neat Download Manager&lt;/h2&gt;
&lt;p&gt;号称最快的浏览器下载器，最高支持 32 线程同时下载，解决浏览器下载大文件慢的问题。安装后可以自动检测本地浏览器，一键添加浏览器插件，十分方便。不过在 Safari 需要打开开发者选项中的配置才可以使用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/resource/tools/development-tools/safari-ndm.png&quot; alt=&quot;Safari 注意事项&quot; title=&quot;Safari 注意事项&quot; /&gt;
&lt;img src=&quot;/assets/images/resource/tools/development-tools/safari-dev.png&quot; alt=&quot;Safari 配置&quot; title=&quot;Safari 配置&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Neat Download Manager 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/neatdownloadmanager.png
link: https://www.neatdownloadmanager.com/index.php/en/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;VMware Fusion&lt;/h2&gt;
&lt;p&gt;虚拟机软件软件相信大家肯定都不陌生，无论你是想同时体验不同的系统还是说需要临时使用其他的系统，恰巧你的电脑内存和存储又都很充裕，虚拟机一定是一个不错的选择。&lt;/p&gt;
&lt;p&gt;最近 VMware 退出了全新的 VMware Fusion 13，支持 macOS Monterey 和 Windows 11，并且支持 M1 芯片，同时支持 macOS 13 Ventura，并且支持个人申请永久激活码，尽管体验上稍逊一筹，但是至少已经解决燃眉之急了。&lt;/p&gt;
&lt;p&gt;最近因为沉迷《泰拉瑞亚》用虚拟机运行测试了一下游戏效果，记过差强人意。就算是本机运行支持 M 系芯片的 Java 版的 Minecraft，一段时间下来也是发热严重，果然玩游戏还的是 Windows。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: VMware Fusion 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/daily-tools/vmware-logo-grey.svg
link: https://www.vmware.com/products/fusion.html
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Hadoop</title><link>https://songbaicheng.cc.cd/posts/hadoop/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/hadoop/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Apache Hadoop&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;Hadoop 是一个开源的分布式计算和存储框架，由 Apache 基金会开发和维护。
Hadoop 为庞大的计算机集群提供可靠的、可伸缩的应用层计算和存储支持，它允许使用简单的编程模型跨计算机群集分布式处理大型数据集，并且支持在单台计算机到几千台计算机之间进行扩展。
Hadoop 使用 Java 开发，所以可以在多种不同硬件平台的计算机上部署和使用。其核心部件包括分布式文件系统 (Hadoop DFS，HDFS) 和 分布式计算框架（MapReduce）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Apache Hadoop 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/big-data/hadoop/hadoop.png
link: https://hadoop.apache.org
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基础概念&lt;/h2&gt;
&lt;p&gt;HDFS（分布式文件系统）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Namenode（名称节点）
&lt;ul&gt;
&lt;li&gt;功能：Namenode是HDFS的核心，它管理文件系统的元数据，例如文件和目录的结构、文件块的位置等。&lt;/li&gt;
&lt;li&gt;职责：负责客户端请求的处理，如打开、关闭、重命名文件和目录等。它还决定文件块（block）存储在哪些Datanode上。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Datanode（数据节点）
&lt;ul&gt;
&lt;li&gt;功能：Datanode负责实际的数据存储，存储HDFS中的文件块。&lt;/li&gt;
&lt;li&gt;职责：定期向Namenode报告它们存储的块信息，并处理来自客户端的读写请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Secondary Namenode（次级名称节点）
&lt;ul&gt;
&lt;li&gt;功能：辅助Namenode管理元数据。&lt;/li&gt;
&lt;li&gt;职责：定期与Namenode同步，以减轻Namenode的工作负担，但它并不是Namenode的热备份。如果Namenode故障，Secondary Namenode无法立即接管工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MapReduce（分布式计算框架）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JobTracker是MapReduce 1.x中的单点故障。在YARN中，它的功能被ResourceManager和ApplicationMaster替代。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;JobTracker
&lt;ul&gt;
&lt;li&gt;功能：负责协调和管理MapReduce作业的执行。&lt;/li&gt;
&lt;li&gt;职责：将作业分成多个任务（tasks），并将这些任务分配给集群中的TaskTracker节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TaskTracker
&lt;ul&gt;
&lt;li&gt;功能：负责执行JobTracker分配的具体任务。&lt;/li&gt;
&lt;li&gt;职责：报告任务执行的进展情况，并处理任务失败的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;YARN（资源管理和作业调度）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hadoop 2.x及以上版本引入了YARN来改进资源管理和作业调度&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;ResourceManager
&lt;ul&gt;
&lt;li&gt;功能：集群的资源管理和调度。&lt;/li&gt;
&lt;li&gt;职责：管理所有节点的资源，分配资源给不同的应用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;NodeManager
&lt;ul&gt;
&lt;li&gt;功能：负责管理单个节点上的资源和任务。&lt;/li&gt;
&lt;li&gt;职责：报告节点的资源使用情况，管理节点上的容器（containers）并监控任务执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ApplicationMaster
&lt;ul&gt;
&lt;li&gt;功能：负责应用程序的管理。&lt;/li&gt;
&lt;li&gt;职责：为每个应用程序（如MapReduce作业）启动一个ApplicationMaster，它负责申请资源并监控任务的执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重点知识&lt;/h2&gt;
&lt;h3&gt;HDFS 读写流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建文件：
1.1 HDFSclient向HDFS写数据先调用DistributedFileSystem.create()；
1.2 RPC调用namenode的create()方法，会在HDFS目录树中指定路径，添加新文件；并将操作记录在edits.log中namenode的create()方法执行完后，返回一个FSDataOutPutStream，他是DFSOutPutStream的包装类；&lt;/li&gt;
&lt;li&gt;建立数据流管道pipeline
2.1 client调用DFSOutPutStream.write()写数据（先写文件的第一个块，暂时称为blk1）；
2.2 DFSOutputStream通过RPC调用namenode的addBlock，向namenode申请一个空的数据块block；
2.3 addBlock返回一个LocatedBlock对象，此对象包含当前blk要存储哪三个datanode信息，比如dn1，dn2，dn3；
2.4 客户端根据位置信息建立数据流管道；&lt;/li&gt;
&lt;li&gt;向数据流管道写入当前块的数据
1、 写数据时，先将数据写入一个检验块chunk中，写满512字节后，对此chunk计算校验和chunksum值（4字节）；
2、 然后将chunk和对应的校验写入packet中，一个packet是64kb；
3、 随着源源不断的带校验chunk写入packet，当packet写满之后将其写入dataqueue队列中；
4、 packet从队列中取出，沿着pipeline发送到dn1，再从dn1发送到dn2，dn2发送到dn3；
5、 同时，这个packet也会保存一份到一个确认队列ackqueue中；
6、 packet到达最后一个datanode即nd3之后会做检验，然后将检验沿结果逆着pipeline方向传回客户端，具体检验结果从dn3传到dn2，dn2做检验，dn2传到dn1，dn1做检验，结果再传回客户端；
7、 客户端根据校验结果，如果“成功”，则将保存在ackqueue中的packet删除，如果失败则将packet取出重新放回到dataqueue末尾，等待沿pipeline再次传输；
8、 如此将block中一个数据的一个个packet发送出去当block发送完毕，即dn1，dn2，dn3都接收了blk1的副本，那么三个datanode分别RPC调用namenode的blockReceivedAndDeleted()，namenode会更新内存中block与datanode的对应关系（比如dn1上多了个blk1）；&lt;/li&gt;
&lt;li&gt;关闭三个datanode构建的pipeline，且文件还有下一个块的时候，再从4开始直到全部文件写完
4.1、 最终，调用DFSOutputStream的close()；
4.2、 客户端调用namenode的complete()，告知namenode文件传输完成；&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Find</title><link>https://songbaicheng.cc.cd/posts/find/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/find/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;查找&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root(查找)
    线性结构
      顺序查找
      折半查找
      分块查找
    树形结构
      二叉排序树
      二叉平衡树
      红黑树
      B树、B+树
    散列结构
      散列表
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查找的基本概念&lt;/h2&gt;
&lt;p&gt;在数据集合中寻找满足某各种条件的数据元素的过程称为查找。&lt;/p&gt;
&lt;p&gt;用于查找的数据集合称为查找表，它由同一类型的数据元素组成，可以是一个数组或链表等数据结构。对查找表的操作一般有四种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询某个特定的数据元素是否在查找表中。&lt;/li&gt;
&lt;li&gt;检索满足条件的的某个特定的数据元素的各种属性。&lt;/li&gt;
&lt;li&gt;在擦杭州啊表中插入一个数据元素。&lt;/li&gt;
&lt;li&gt;从查找表中删除某个数据元素。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若一个查找表的操作只涉及上述操作1和2，则无须动态的改变查找表，此类查找表称为静态查找表，与此对应，需要动态的插入或删除的查找表称为动态查找表。&lt;/p&gt;
&lt;p&gt;对应静态查找表的查找方法有顺序查找、折半查找、散列查找；适合动态查找表的查找方式有二叉排序树的查找、散列查找等。&lt;/p&gt;
&lt;p&gt;数据元素中唯一标识该元素的某个数据项的值，使用基于关键字的查找，查找结果应该是唯一的。&lt;/p&gt;
&lt;p&gt;在查找过程中，一次查找的长度是指需要比较的关键字次数，而平均查找长度则是所有查找过程中惊醒关键字的比较次数的平均值，平均查找长度是衡量查找算法效率的最主要的指标。&lt;/p&gt;
&lt;h2&gt;顺序查找&lt;/h2&gt;
&lt;p&gt;顺序查找又称为线性查找，它对顺序表和链表都是适用的。对于顺序表，可通过数组下标递增来顺序扫描每个元素；对于链表，可通过指针 next 来一次扫描每个元素。顺序查找通常分为对一般的无序线性表的顺序查找和对按关键字有序的线性表的顺序查找，如果是一般线性表的顺序查找即通过“哨兵”从一端开始查找到表的另一端，如果“哨兵”找到关键字则跳出循环，查找成功；如果到另一端也没有找到关键字则查找失败，成功的ASL为 (n+1)/2。如果是有序表的顺序查找，则不需要再比较到另一端就能返回查找失败的信息，从而降低顺序查找失败的平均查找长度，所以其 ASL 会好一些。&lt;/p&gt;
&lt;p&gt;顺序查询的好处是对数据元素的存储没有要求，无论是顺序和链式存储皆可，记录按关键字有序也均可，缺点就是如果 n 过大平均查找长度较大，效率低；&lt;/p&gt;
&lt;h2&gt;折半查找（二分查找）&lt;/h2&gt;
&lt;p&gt;折半查找仅适用于有序的顺序表。基本思想是首先将给定 key 与表中中间位置的元素比较，若相等，则查找成功，返回该元素的存储位置；若不等，则所需查找的元素只能在中间元素以外的前半部分和后半部分，然后再缩小的范围内继续进行同样的查找，如此重复直到找到为止，或确定表中没有所需要查找的元素，则查找不成功。&lt;/p&gt;
&lt;p&gt;::: normal-demo 二分查找(Java)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T binarySearch(T[] array, T target) {
    int left = 0;
    int right = array.length - 1;

    while (left &amp;lt;= right) {
        int mid = left + (right - left) / 2;
        int result = target.compareTo(array[mid]);

        if (result == 0) {
            return target;
        } else if (result &amp;lt; 0) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }

    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;折半查找的过程可以用二叉树来描述，称为判定树。树中的圆形结点表示一个记录，结点中的值为该记录的关键字值；树最下面叶结点表示不成功的查找条件。从判定树可以看出，查找成功时的查找长度为从根结点到目的结点的路径上的结点树，而查找不成功时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数。显然，判定树是一颗平衡二叉树。所以二分查找的平均查找长度为ASL=log~2~(n+1)-1。&lt;/p&gt;
&lt;p&gt;因为折半查找需要方便的定位查找区域，所以它要求线性表必须具有随机存取的特性。因此，该查找方法仅适用于顺序存储结构，不适合于链式存储结构，且要求元素按关键字有序排列。&lt;/p&gt;
&lt;h2&gt;分块查找&lt;/h2&gt;
&lt;p&gt;分块查找又称为索引顺序查找，它吸取了顺序查找和折半查找各自的优点，既有动态结构，又适于快速查找。&lt;/p&gt;
&lt;p&gt;分块查找的基本思想：将查找表分为若干子块。块内的元素可以无序，但块间的元素是有序的，即第一个块中的最大关键小于第二块中的所有记录的关键字，以此类推。再建立一个索引表，索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址，索引表按关键字有序排列。&lt;/p&gt;
&lt;p&gt;分块查找的过程分为两步：第一步是在索引表中确定待查记录所在的块，可以顺序查找或折半查找索引表；第二步是在块内顺序查找。&lt;/p&gt;
&lt;p&gt;将长度为n的查找表均匀的分为b块，每块有s个记录，平均查找长度ASL=(s^2+2s+n)/2s。&lt;/p&gt;
&lt;h2&gt;树型查找&lt;/h2&gt;
&lt;h3&gt;二叉排序树（BST）&lt;/h3&gt;
&lt;p&gt;二叉排序树有称为二叉查找树，它可以是一颗空树，它具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若左子树非空，则左子树上所有结点的值均小于根结点的值。&lt;/li&gt;
&lt;li&gt;若右子树非空，则右子树上所有的结点的值均大于根结点的值。&lt;/li&gt;
&lt;li&gt;左右子树分别也是一颗二叉排序树。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;查找&lt;/h4&gt;
&lt;p&gt;二叉排序树的查找是从根结点开始，沿着某个分支逐层向下比较的过程。若二叉排序树非空，先将给定值和根结点关键字比较，若相同则返回成功，若不相同则在根结点的左子树寻找，否则在右子树寻找。&lt;/p&gt;
&lt;h4&gt;插入&lt;/h4&gt;
&lt;p&gt;二叉排序树是一种动态树表，其特点是树的结构通常不是一次生成的，而是在查找的过程中，当树中不存在关键字值等于给定值时在进行插入的。过程如下：若原二叉树为空，则直接插入，否则，若小于根结点值，则插入到左子树，否则插入到右子树。&lt;/p&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;p&gt;二叉排序树中删除一个结点的时候，不能把以该结点为根的子树上的结点全部删除，必须先把被删除结点从存储二叉树排序树的链表上摘下，将因删除结点而断开的二叉链表重新链接起来，同时确保二叉排序树的性质不会丢失。删除操作按照下面三种情况来处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若被删除的结点 z 是叶结点，则直接删除，不会破坏二叉树的性质。&lt;/li&gt;
&lt;li&gt;若结点 z 只有一颗左子树或者右子树，则让 z 的子树成为 z 父结点的子树来代替 z 的位置即可。&lt;/li&gt;
&lt;li&gt;若结点 z 既有左子树又有右子树，则令 z 的直接后继（直接前驱）代替 z，然后从二叉排序树删去找个直接后继（直接前驱），这样就转换成了第一或第二种情况。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;效率分析&lt;/h4&gt;
&lt;p&gt;二叉排序树的查找效率主要取决于树的高度，若是平衡二叉树，则平均查找长度就是 O(log~2~n)，如果是有序的单枝树，则平均查找长度就会变成 O(n)。&lt;/p&gt;
&lt;h3&gt;平衡二叉树（AVL）&lt;/h3&gt;
&lt;p&gt;为了避免树的高度增长过快，降低二叉排序树的性能，规定在插入和删除结点时，要保证任意结点的左右子树高度差的绝对值不超过 1，将这样的二叉树成为平衡二叉树。&lt;/p&gt;
&lt;h4&gt;插入&lt;/h4&gt;
&lt;p&gt;基本思想：每当在二叉排序树中插入或删除一个结点的时候，首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。若导致了不平衡则先找到插入路径上离插入结点最近的平衡因子的绝对值大于 1 的结点 A，然后再对 A 为根的子树保持二叉排序树特性的前提下调整各个结点的位置关系。&lt;/p&gt;
&lt;h4&gt;删除&lt;/h4&gt;
&lt;h4&gt;查找&lt;/h4&gt;
&lt;h3&gt;红黑树&lt;/h3&gt;
&lt;p&gt;为了保持 AVL 树的平衡性，插入和删除后非常频繁的调整全树整体拓扑结构代价太大，为此放宽条件成为红黑树：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个结点或是红色或是黑色。&lt;/li&gt;
&lt;li&gt;根结点是黑色的。&lt;/li&gt;
&lt;li&gt;叶结点都是黑色的。&lt;/li&gt;
&lt;li&gt;不存在两个相邻的红结点。&lt;/li&gt;
&lt;li&gt;对每个结点，从该结点到任意一个叶结点的简单路径上，所含黑结点的数量相同。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;B树和B+树&lt;/h3&gt;
&lt;h2&gt;散列表&lt;/h2&gt;
</content:encoded></item><item><title>Gateway</title><link>https://songbaicheng.cc.cd/posts/gateway/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/gateway/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Gateway&lt;/h1&gt;
&lt;h2&gt;浅聊微服务网关&lt;/h2&gt;
&lt;p&gt;网关是一个通用的概念，它在计算机网络中指的是在不同网络之间进行连接、转发和控制流量的设备或软件。而微服务网关我们通用的理解是统一对外暴露可共享的服务 API 的功能，一般这些微服务网关都与服务注册中心相配合使用，这里我们要谈的 Spring Cloud Gateway 是基于 Spring Boot 和 Spring WebFlux 构建的网关框架。它提供了一种简单、轻量级的方式来处理路由、过滤和负载均衡。Spring Cloud Gateway 还支持动态路由、断路器、限流等功能，并与 Spring Cloud 生态系统无缝集成，具体详情见其官网文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Gateway 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/spring-initializr.svg
link: https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;项目依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-gateway&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-cloud-starter-loadbalancer&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;简单的网关实例配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 启用了服务发现功能
          lower-case-service-id: true # 将服务ID转换为小写形式
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以上配置的网关服务就可以发现服务注册中心的其他服务并进行相应的定位和路由设置，在请求该网关服务后加上实例名称和接口即可实现点对点通讯。&lt;/p&gt;
</content:encoded></item><item><title>Cmds</title><link>https://songbaicheng.cc.cd/posts/cmds/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/cmds/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;重构接收价格前置项目&lt;/h1&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;没想到，重构这个项目的原因居然是公司开始不再使用 Tomcat 部署项目，想把之前的 war 包启动的项目都换成 jar 包部署。之前的前置项目大部分都都是“远古时期”流传下来的“毒瘤”，纯 Java 项目，各种 Thread 满天飞，规范什么的就更别提了，为了追赶上线进度，我们引用 tomcat-embed 模块实现 Java 内置 Tomcat 启动，核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) throws Exception {
        String webappDirLocation = &quot;src/main/webapp&quot;;
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.addWebapp(&quot;/&quot;, new File(webappDirLocation).getAbsolutePath());
        tomcat.start();
        tomcat.getServer().await();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然这样可以脱离 Tomcat 部署，但是肯定不是长远之计，所以重构的任务就落到了我的身上，其实在我看来这些老项目还是应该尽早维护，趁着了解业务的人都还在和项目并不复杂的时候，尽早规范统一起来才是正途。虽然这些前置项目平时都不是我负责，但是多多少少上线和联调的时候也都接触过，并说不上陌生，而且业务也不负责，话不多说直接开始。&lt;/p&gt;
&lt;h2&gt;重构思路&lt;/h2&gt;
&lt;h3&gt;框架选型&lt;/h3&gt;
&lt;p&gt;价格接收转发前置项目，这是一个接收交易所价格并转发给多个后台的纯后台项目，几乎没有什么逻辑处理，就是是充当透传的作用，因为数据压力不大，所以我们采用双活的 Spring Boot 来选择重构部署。由于只是转发价格，并不作存储作用，所以我们并不会牵扯到数据持久化的问题，不会用到数据库。往后台项目推送价格有使用 MQ 交互的，也有用 Socket 通信的，MQ 这里我们只能使用公司在用的 IBM MQ，Socket 我们就继续原来使用了 Java 原生的 Socket 类实现。置于其他的知识点就放在下面一一介绍了。&lt;/p&gt;
&lt;h3&gt;业务梳理&lt;/h3&gt;
&lt;p&gt;业务流程就是在工作日时将所需要的产品价格从交易所获取并转发给报价平台、K线平台和日终价格三个后台项目。&lt;/p&gt;
&lt;p&gt;与交易所建立通讯之后，通过接口会一直受到价格，并且连接建立之后并不会主动断开，除非遇到网络等特殊情况，如果发生断联则触发重连策略。而所收到的价格并不是随时并且全部都发送，因为有些产品的价格是我们不需要的，但是又不确定对方发送的产品价格是固定的，所以我们将自己定义的工作日和所需产品价格。而所需数据的后台项目有新有旧，所以对接价格的接口实现还需要和后台一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start=&amp;gt;start: 启动项目
one=&amp;gt;operation: 尝试与交易所建立连接
isConn=&amp;gt;condition: 是否建立连接
isSend=&amp;gt;condition: 价格是否需要发送
transfer=&amp;gt;operation: 对接后台准备发送
sub=&amp;gt;subroutine: 后台程序

start-&amp;gt;one
one(right)-&amp;gt;isConn
isConn(yes, right)-&amp;gt;isSend
isConn(no, top)-&amp;gt;one
isSend(yes, right)-&amp;gt;transfer
transfer(right)-&amp;gt;sub
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;方案设计&lt;/h2&gt;
&lt;h3&gt;线程池方案&lt;/h3&gt;
&lt;p&gt;项目中需要使用多个线程监听价格队列的变化，所以我们统一使用 Spring 的 &lt;code&gt;ThreadPoolTaskExecutor&lt;/code&gt; 来创建线程池进行管理，根据系统需求规定好核心线程数、最大线程数、任务队列长度和线程最大空闲时间等关键参数，作为 Bean 注入到 Spring 容器后，通过实现 &lt;code&gt;ApplicationRunner&lt;/code&gt; 中的 &lt;code&gt;run&lt;/code&gt; 方法在项目启动的时候从容器中拿到线程池来 execute 各个业务线程。&lt;/p&gt;
&lt;p&gt;因为业务线程有依赖关系，需要考虑到线程的启动顺序，比如说价格发送线程必须在价格接收线程启动完成后再启动，我们这里采用 Java 提供的 &lt;code&gt;CountDownLatch&lt;/code&gt; 线程计数器来解决，当每条价格发送线程启动后执行计数器减一的操作，在计数器为零再放行价格接收线程开启。&lt;/p&gt;
&lt;h3&gt;价格的接收与筛选&lt;/h3&gt;
&lt;p&gt;从交易所获取的价格虽然密集的，但是好在并不会频繁更新，而且由于我们只需要工作日接收价格并且只关心我们在意的币种，所以我们将工作日和需求币种维护到 yaml 中，如果是接收的价格并没有在我们的需求范围内则直接抛弃。&lt;/p&gt;
&lt;p&gt;集成交易所的 sdk 后，通过唯一的身份.cfg文件与交易所建立 Socket 长链接，交易所返回的价格存储到本地阻塞队列中，因为要求价格的实时性，所以当主队列价格超过20条时清空队列。&lt;/p&gt;
&lt;h3&gt;价格的处理与转发&lt;/h3&gt;
&lt;p&gt;本项目唯一麻烦的点就是价格的处理与转发，因为需要往多个后台进行实时转发，并且每个后台对价格的格式、种类和接收方式也有要求，所以为了解耦，我们借助 Spring 的 Event 事件来处理每个平台价格的转发，也就是观察者模式。&lt;/p&gt;
&lt;p&gt;首先，为了解决实时性，我们用 while 一直从总价格队列中 take 拿取价格到我们针对每个平台初始化的多个阻塞队列来区分价格，并将这些队列注册到 Spring 容器中，通过新建线程来监听容器每个后台价格队列的变化，当总价格队列根据价格类型和币种存储将价格分发到后台队列中时，线程监听到价格并将价格借助 Event 的参数传递发布出去，监听每个 Event 的观察者收到参数后对报文进行封装并发送。&lt;/p&gt;
&lt;h2&gt;代码实现&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;因为代码都在内网，这里采用伪代码的方式进行展示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;线程池&lt;/h3&gt;
&lt;p&gt;::: normal-demo 线程池代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// application.yaml
tread-pool:
  core-pool-size: 9
  max-pool-size: 10
  queue-capacity: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ThreadPoolConfig
@Getter
@Setter
@Configuration
@ConfigurationProperties(&quot;tread-pool&quot;)
public class ThreadPoolConfig {
  /**
   * 核心线程数
   */
  private int corePoolSize;
    /**
   * 核心线程数
   */
  private int maxPoolSize;
  /**
   * 任务队列长度
   */
  private int queueCapacity;
  /**
   * 线程最大空闲时间
   */
  private int keepAliveSeconds;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// AppConfig
@Configuration
public class AppConfig {
  
  @Resource
  private ThreadPoolConfig threadPoolConfig;

  @Bean
  public ThreadPoolExecutor appThreadPool() {
    ThreadPooolTaskExexutor threadPooolTaskExexutor = new ThreadPooolTaskExexutor();
    threadPooolTaskExexutor.setCorePoolSize(threadPoolConfig.getCorePoolSize());
    threadPooolTaskExexutor.setMaxPoolSize(threadPoolConfig.getMaxPoolSize());
    threadPooolTaskExexutor.setQueueCapacity(threadPoolConfig.getQueueCapacity());
    threadPooolTaskExexutor.setThreadNamePrefix(&quot;appThreadPool-&quot;);
    threadPooolTaskExexutor.initialize();
    return threadPooolTaskExexutor;
  }

  @Bean(&quot;pricesQueue&quot;)
  public LinkedBlockingQueue&amp;lt;CmdsPriceDto&amp;gt; pricesQueue() {
    return new LinkedBlockingQueue&amp;lt;&amp;gt;();
  }
  
  …………
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// applicationRunner
@Component
@RequiredArgsConstructor
public class applicationRunner implements ApplicationRunner {

  private final ThreadPoolExecutor appThreadPool;
  private final CountDownLatch countDownLatch;

  @Override
  public void run(ApplicationArguments args) throws InterruptedException, ConfigError {
    // 启动价格发送线程
    appThreadPool.execute();
    // 等待价格处理线程发送完成
    countDownLatch.await;
    // 连接交易所线程启动

  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;价格处理与转发&lt;/h3&gt;
&lt;p&gt;::: normal-demo 价格处理与转发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// event
@Getter
public class PriceEvent extends ApplicationEvent {

  private final List&amp;lt;CmdsPriceDto&amp;gt; cmsPriceDtos;

  public priceEvent(Object source, List&amp;lt;CmdsPriceDto&amp;gt; cmdsPriceDtos) {
    super(source);
    this.cmdsPriceDtos = cmdsPriceDtos;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Component
public class PriceListener implements ApplicationListener&amp;lt;PriceEvent&amp;gt; {

  @Overide
  public void onApplicationEvent(PriceEvent enent) {
    // 处理价格并发送
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Dynamic Proxy</title><link>https://songbaicheng.cc.cd/posts/dynamic-proxy/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/dynamic-proxy/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;动态代理&lt;/h1&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;今天在复习微服务的时候看到 OpenFeign 使用动态代理集成调用 Ribbon 来实现负载均衡，出于兴趣我就想简单瞥一眼到底是如何调用的，可是结果非常出乎意料，搜索到的结果是：在 OpenFeign 中，Ribbon 的集成是通过使用 Feign 的 Client 接口来实现的。可能那个作者单纯是想延伸一下动态代理这一块的知识，至于到底如何调用的先放在下次再谈，但是动态代理这个知识点必须该梳理梳理了，无论是背面试题还是源码当中它都无处不在，什么 jdk 实现和 cjlib 实现，今天都得给我整明白。&lt;/p&gt;
&lt;h2&gt;走进动态代理&lt;/h2&gt;
&lt;h3&gt;什么是动态代理&lt;/h3&gt;
&lt;p&gt;Java 的动态代理是一种运行时生成代理对象的机制，允许在运行时创建代理类及其实例，以实现对目标对象的代理操作。简单来说就是它提供了一种灵活的方式来在不修改目标对象源代码的情况下，对其方法进行增强或添加额外的逻辑。有动态代理就有静态代理，见名知意，静态代理就是代码中事先定义好的，在编译的时候就已经确定的，在灵活性上肯定就是略逊一筹了。动态代理在需要在运行时对对象进行控制、增强或拦截的场景中非常有用，使用的场景有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AOP（面向切面编程）：动态代理可以用于实现横切关注点的模块化，例如日志记录、性能监控、事务管理等。通过在目标方法执行前后插入额外的逻辑，可以实现对目标方法的增强。&lt;/li&gt;
&lt;li&gt;延迟加载（Lazy Loading）：动态代理可以延迟加载对象，当需要访问对象时才进行实例化，可以提高系统的性能和资源利用率。代理对象可以在真正需要对象时才创建，从而避免了不必要的对象创建和初始化过程。&lt;/li&gt;
&lt;li&gt;远程代理（Remote Proxy）：动态代理可以用于远程方法调用，通过代理对象在本地调用方法，实际执行的是远程的对象的方法。这种方式可以隐藏远程调用的细节，提供更简洁的调用方式。&lt;/li&gt;
&lt;li&gt;安全控制：动态代理可以用于实现安全控制，例如权限验证、身份认证等。代理对象可以在调用目标方法之前进行权限检查，只有符合要求的用户才能访问目标方法。&lt;/li&gt;
&lt;li&gt;缓存管理：动态代理可以用于实现缓存管理，通过在代理对象中添加缓存逻辑，可以在访问某个方法时先检查缓存，如果缓存中存在结果，则直接返回结果，避免重复计算。&lt;/li&gt;
&lt;li&gt;日志记录：动态代理可以用于实现日志记录，通过在代理对象中添加日志记录逻辑，可以记录方法的调用信息、参数和返回值，方便系统的跟踪和调试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们遇到以上这些情况的时候怎么知道这是在使用动态代理呢，那我们就需要知道如何实现动态代理了。&lt;/p&gt;
&lt;h3&gt;如何实现动态代理&lt;/h3&gt;
&lt;p&gt;在Java中，有两种常见的实现方式用于实现动态代理：基于接口的动态代理和基于类的动态代理。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;基于接口的动态代理：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;基于Java的java.lang.reflect.Proxy类实现。&lt;/li&gt;
&lt;li&gt;要求目标对象实现一个或多个接口。&lt;/li&gt;
&lt;li&gt;代理类是在运行时动态生成的，基于接口生成代理类，因此代理类只能代理接口中定义的方法。&lt;/li&gt;
&lt;li&gt;代理对象通过实现InvocationHandler接口来处理被代理方法的调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;基于类的动态代理：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;基于第三方库如CGLIB（Code Generation Library）实现。&lt;/li&gt;
&lt;li&gt;不要求目标对象实现接口，可以代理普通的类。&lt;/li&gt;
&lt;li&gt;代理类是通过继承目标类来生成的，因此代理类可以代理目标类中的所有方法，包括非公共的方法。&lt;/li&gt;
&lt;li&gt;代理对象通过继承目标类并重写方法来实现对被代理方法的调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两种实现方式的区别在于代理对象的生成方式和代理的范围。基于接口的动态代理要求目标对象实现接口，生成的代理类只能代理接口中的方法；而基于类的动态代理不要求目标对象实现接口，生成的代理类可以代理目标类中的所有方法，包括非公共的方法。&lt;/p&gt;
&lt;p&gt;总的来说，基于接口的动态代理适用于那些已经实现了接口的目标对象；而基于类的动态代理适用于那些没有实现接口的目标对象，或者需要代理非公共方法的情况。&lt;/p&gt;
&lt;h3&gt;基于接口实现动态代理&lt;/h3&gt;
&lt;h4&gt;示例思路&lt;/h4&gt;
&lt;p&gt;我们定义了一个 UserService 接口和其实现类 UserServiceImpl。然后，创建了一个 UserServiceProxy 类作为代理对象的处理器，并实现了 InvocationHandler 接口。在 invoke() 方法中，我们可以在方法执行前后添加额外的逻辑。最后，使用 Proxy 类的 newProxyInstance() 方法创建代理对象并执行目标方法。&lt;/p&gt;
&lt;h4&gt;实例代码&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;定义接口和实现类：首先，需要定义一个接口，该接口是目标对象和代理对象共同实现的接口。假设我们有一个简单的接口 UserService，包含了一些用户操作的方法，然后简单实现该接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public interface UserService {
    void addUser(String username);
    void deleteUser(String username);
    void updateUser(String username);
    void getUser(String username);
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println(&quot;Adding user: &quot; + username);
    }

    @Override
    public void deleteUser(String username) {
        System.out.println(&quot;Deleting user: &quot; + username);
    }

    @Override
    public void updateUser(String username) {
        System.out.println(&quot;Updating user: &quot; + username);
    }

    @Override
    public void getUser(String username) {
        System.out.println(&quot;Getting user: &quot; + username);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现 InvocationHandler 接口：创建一个实现 InvocationHandler 接口的类，该类负责处理代理对象的方法调用。在该类中，你可以定义在目标方法执行前后需要执行的逻辑。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserServiceProxy implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在方法执行前添加额外逻辑
        System.out.println(&quot;Before method: &quot; + method.getName());

        // 调用目标对象的方法
        Object result = method.invoke(target, args);

        // 在方法执行后添加额外逻辑
        System.out.println(&quot;After method: &quot; + method.getName());

        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建代理对象：使用 java.lang.reflect.Proxy 类的 newProxyInstance() 方法创建代理对象。该方法接受三个参数：类加载器，目标对象实现的接口数组，和 InvocationHandler 对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        // 创建目标对象
        UserService userService = new UserServiceImpl();

        // 创建 InvocationHandler 对象
        UserServiceProxy handler = new UserServiceProxy(userService);

        // 创建代理对象
        UserService proxy = (UserService) Proxy.newProxyInstance(
                userService.getClass().getClassLoader(),
                userService.getClass().getInterfaces(),
                handler);

        // 通过代理对象调用方法
        proxy.addUser(&quot;John Doe&quot;);
        proxy.deleteUser(&quot;John Doe&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于类实现动态代理&lt;/h3&gt;
&lt;h4&gt;示例思路&lt;/h4&gt;
&lt;p&gt;通过Enhancer类创建了一个代理类，将MyInterceptor作为拦截器。当调用代理对象的方法时，拦截器的intercept方法将被调用，你可以在该方法中添加适当的逻辑来丰富目标功能。&lt;/p&gt;
&lt;h4&gt;实例代码&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;引入 CGLib 依赖&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;dependencies {
    // 其他依赖项...
    implementation &apos;cglib:cglib:3.3.0&apos;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;!-- 其他依赖项... --&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;cglib&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;cglib&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;3.3.0&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建一个被代理的类，无需实现任何接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class MyClass {
    public void doSomething() {
        System.out.println(&quot;Doing something&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现一个MethodInterceptor接口的类，该接口定义了一个intercept方法，在该方法中定义了代理类的行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 在调用被代理类方法前执行一些操作
        System.out.println(&quot;Before method invocation&quot;);

        // 调用被代理类的方法
        Object result = proxy.invokeSuper(obj, args);

        // 在调用被代理类方法后执行一些操作
        System.out.println(&quot;After method invocation&quot;);

        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用CGLib的Enhancer类来创建代理对象。Enhancer类提供了一种方便的方式来生成代理类的子类，并将拦截逻辑应用到被代理类的方法上。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import net.sf.cglib.proxy.Enhancer;

public class Main {
    public static void main(String[] args) {
        // 创建Enhancer实例
        Enhancer enhancer = new Enhancer();
        
        // 设置被代理类的父类
        enhancer.setSuperclass(MyClass.class);
        
        // 设置拦截器
        enhancer.setCallback(new MyInterceptor());
        
        // 创建代理对象
        MyClass proxy = (MyClass) enhancer.create();
        
        // 调用代理对象的方法
        proxy.doSomething();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总结和延伸&lt;/h3&gt;
&lt;p&gt;示例中代理对象通过反射的方式调用目标对象的方法，并在方法执行前后执行了额外的逻辑，实现了动态代理的动态性。我们可以根据这两个 demo 在各个场景中做延伸，比如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;日志记录：你可以添加日志记录的逻辑，记录方法的调用信息、参数和返回值。这样可以实现在不修改目标对象的代码的情况下，对方法的调用进行日志记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;性能监控：你可以添加性能监控的逻辑，包括记录方法的执行时间、计数器等。通过动态代理，你可以在方法调用前后测量和监控方法的性能指标。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缓存管理：你可以添加缓存管理的逻辑，检查缓存中是否存在方法调用的结果。如果缓存中存在结果，则直接返回缓存的结果，避免重复计算。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;事务管理：你可以添加事务管理的逻辑，包括在方法执行前开启事务、在方法执行后提交或回滚事务。通过动态代理，可以实现对方法的事务控制，确保方法的执行在事务的上下文中进行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全控制：你可以添加安全控制的逻辑，包括对方法的权限验证、身份认证等。通过动态代理，可以在方法调用前对用户进行权限检查，只有符合要求的用户才能访问目标方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Java</title><link>https://songbaicheng.cc.cd/posts/java/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/java/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 系列&lt;/h1&gt;
&lt;p&gt;目前 Java 赛道处于一个爆满的时代，况且计算机这个行业在当下这个社会也是充满了功利的气息，各个行业的人疯狂涌入计算机，业内的人嘴上说着计算机跌落神坛可是却在疯狂内卷，确实目前看来就算计算机行业身处寒冬，这也只是和之前几年相比吧，当时可能学习完微服务之后，找的工作可能直奔“尔康”，而现在可能微服务也只能是你找到工作的门槛，当然身边的人也有从 Java 转行到 Python 和 Golang 的。在各种新语言层出不穷的年代，Java 的就业地位不知道还能撑多久，不过就目前而言，Java 社区的活跃程度和所包含的业务领域依然是领先的地位，所以学习 Java 可能并不适合作为第一门语言，也不一定是能作为找到工作的保证，但是一定是你学习过程中能找到资源和解决问题方法最顺利的一种。&lt;/p&gt;
&lt;h3&gt;Java 基础&lt;/h3&gt;
&lt;p&gt;一提到 Java 基础，难免绕不开每个语言的圣经，要知道计算机不比其他行业，大部分的学习资料都来自互联网，而能靠其内容质量击败视频、电子书和技术文章成为圣经的绝对是传奇著作，而 Java 的圣经绝对就是下面这本 &lt;strong&gt;&lt;em&gt;《Thinking in Java》&lt;/em&gt;&lt;/strong&gt;(Java 编程思想)。&lt;/p&gt;
&lt;p&gt;![Thinking in java](/assets/images/resource/books/thinking-in-java.png &quot;Thinking in java&quot; =350x500)&lt;/p&gt;
&lt;p&gt;这本书我大学期间看过大半，至于为什么没有读完，是因为这本书确实细，细到令你发指，主打一本工具书的定位，而且其内容也是略显晦涩，当时我也是走马观花，只读了个大概就跑去B站“深造”了。&lt;/p&gt;
&lt;p&gt;这本书最新是到第四版，发行于2007年，据现在已经过去16个年头了，里面的示例大部分都还在基于 jdk6 ，目前 jdk20 都已经问世了，所以说就互联网的发展速度，你要是还去看这本“历史典籍”，确实有点追不上时代了，虽然 Java 业内流传着 “你发任你发，我用 Java8” 的口号，但是就目前的使用情况来看 jdk17 这个长期维护版本才是新宠。如果放在十年前有人给你推荐这本书，确实是厉害，五年前推荐，还说得过去，要是现在推荐，我只能说一个6。&lt;/p&gt;
&lt;p&gt;最近 Java 之父退休前推荐了一本书，就是下面这本 &lt;strong&gt;&lt;em&gt;《Effective Java》&lt;/em&gt;&lt;/strong&gt; （第三版），目前已经包含了较新版本的 Java 语法和特性，里面提供了 90 多条经验法则教你写出更安全、更有扩展性和更健壮的 Java 代码。&lt;/p&gt;
&lt;p&gt;![Effective java](/assets/images/resource/books/effective-java.jpg &quot;Thinking in java&quot; =350x500)&lt;/p&gt;
</content:encoded></item><item><title>Image Hosting Service</title><link>https://songbaicheng.cc.cd/posts/image-hosting-service/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/image-hosting-service/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;个人图床&lt;/h1&gt;
&lt;h2&gt;项目介绍&lt;/h2&gt;
&lt;p&gt;Telegraph-Image，一个免费图片托管解决方案，Flickr/imgur替代品。使用 Cloudflare Pages 和 Telegraph。&lt;/p&gt;
&lt;p&gt;:::card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Telegraph-Image 项目Github官网
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/github-logo.svg
link: https://github.com/cf-pages/Telegraph-Image
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Cloudflare 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/techniques/image-hosting-service/cloudflare.png
link: https://www.cloudflare.com/zh-cn/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;优缺点&lt;/h2&gt;
&lt;h3&gt;优点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;无限空间。&lt;/li&gt;
&lt;li&gt;开源免费。&lt;/li&gt;
&lt;li&gt;部署简单，只需要根据 README 文件几分钟就可以成功。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;图片大小最大为5MB。&lt;/li&gt;
&lt;li&gt;由于使用Cloudflare的网络，图片的加载速度在某些地区可能得不到保证，有时候会因为网络问题导致图片上传问题。&lt;/li&gt;
&lt;li&gt;Cloudflare Function免费版每日限制100,000个请求，超过可能需要选择购买 Cloudflare Function 的付费套餐。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;项目功能&lt;/h2&gt;
&lt;h3&gt;图片审查&lt;/h3&gt;
&lt;p&gt;搭配 moderatecontent 提供免费图片审查，开启图片审查后，因为审查需要时间，首次的图片加载将会变得缓慢，之后的图片加载由于存在缓存，并不会受到影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: moderatecontent 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/techniques/image-hosting-service/moderatecontent-logo.png
link: https://moderatecontent.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;图片管理功能&lt;/h3&gt;
&lt;p&gt;图床管理后台，提供统计图片总数量、关键字搜索、图片黑白名单。&lt;/p&gt;
</content:encoded></item><item><title>Development Tools</title><link>https://songbaicheng.cc.cd/posts/development-tools/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/development-tools/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;开发工具&lt;/h1&gt;
&lt;p&gt;工欲善其事，必先利其器。其实好用的工具从来不用推荐，工具的下载量就证明了它可以胜任好自己的本职工作，下面我整理一下我自己在用的不错的开发工具给大家参考一下。&lt;/p&gt;
&lt;h2&gt;IntelliJ IDEA&lt;/h2&gt;
&lt;p&gt;吃饭的家伙，一提到 IDEA 我就不得不说一个事情，不是吧不是吧，这都2023年了还有人在用 \b\w{7}\b 做开发吧（狗头保命）。使用正版需要订购 JetBrains 的许可证，因为是订阅型的，意味着你需要按照订阅期限支付费用，今年居然比去年更贵了！工具要想用的好，配置插件少不了，我把我认为平时好用的和最常用的配置贴出来，供交流讨论啦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: IntelliJ IDEA 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/IntelliJ IDEA.svg
link: https://www.jetbrains.com/idea/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置推荐&lt;/h3&gt;
&lt;p&gt;配置的话就是见仁见智了，因为每个人有每个人的习惯，虽然很多时候有些设置确实可以提高效率，但是有些人习惯了之前自己的一些方式就觉得还是保持原状效率更高，所以这里我整理一下自己常用的一些可以提高效率的设置供大家参考。&lt;/p&gt;
&lt;p&gt;这里我就按照 preferences 界面从上到下罗列了，首先字体大小什么的这就是按照自己的显示器或者窗口习惯自己决定，但是字体一定是推荐 JetBrains Mono，这是 JetBrains 公司设计的更适合开发人员使用的字体，在阅读体验和符号区分度上下了很大功夫，这套字体在从 v2019.3 开始这套字体就随 JetBrains IDE 一起提供了，如果用起来的一定要用起来。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一个推荐的配置是自动导入包的配置，虽然平时也有快捷键清理多余的包，但是哪有自动清理来的方便快捷，打开这个设置需要勾选下面两个勾选框。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![auto import](/assets/images/resource/tools/development-tools/auto-import.png &quot;auto import&quot; =650x500)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;其次就是方法间增加分割线，因为发现很多人方法间不换行导致可读性很差，这个时候分割线就显得尤为重要了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![method split line](/assets/images/resource/tools/development-tools/method-split-line.png &quot;method split line&quot; =650x500)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有时候因为换显示器或者一些奇怪分辨率的情况下总是感觉整个字体不大不小看着闹挺，到底是用 12、14 还是 16 直接强迫症犯了，这个时候打开下面这个配置，直接用滑轮搭配 command/ctrl 直接换到你喜欢的大小。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![鼠标滑轮控制字体大小](/assets/images/resource/tools/development-tools/font-size-mouse.png &quot;鼠标滑轮控制字体大小&quot; =650x500)&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;还有一个优化展示的小设置，很多情况下我们会同时打开很多 tab 标签而导致我们我们一行标签栏并不能完全展示，这就导致一些标签就会隐藏掉，在我们查找的时候非常麻烦，如果我们配置下面这个设置后就可以多行展示标签方便我们查看了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;![多行标签展示](/assets/images/resource/tools/development-tools/mult-tab.png &quot;多行标签展示&quot; =650x500)&lt;/p&gt;
&lt;h3&gt;插件推荐&lt;/h3&gt;
&lt;h4&gt;主题推荐&lt;/h4&gt;
&lt;p&gt;相信大部分的程序员还是喜欢黑色的主题吧，虽然很大一部分原因还是护眼，但是黑色真的很高级炫酷啊。被人熟知的就是下面这个经典的 One Dark 主题了。&lt;/p&gt;
&lt;p&gt;![One Dark theme](/assets/images/resource/tools/development-tools/one-dark-theme.png &quot;One Dark theme&quot; =650x500)&lt;/p&gt;
&lt;p&gt;不过最近我的新宠还是 Dracula，深色背景下有紫色的高级感。&lt;/p&gt;
&lt;p&gt;![Dracula Theme](/assets/images/resource/tools/development-tools/dracula-theme.png &quot;Dracula Theme&quot; =650x500)&lt;/p&gt;
&lt;p&gt;值得一提的是这些主题在主流 IDE 都是支持的，如果喜欢就都去试试吧。&lt;/p&gt;
&lt;h4&gt;Rainbow Brackets 彩虹括号&lt;/h4&gt;
&lt;p&gt;一开始觉得花里胡哨没什么用，但是当你括号不能对齐的时候就是真香了。&lt;/p&gt;
&lt;p&gt;![Rainbow Brackets](/assets/images/resource/tools/development-tools/rainbow-brackets.png &quot;Rainbow Brackets&quot; =650x500)&lt;/p&gt;
&lt;h4&gt;Alibaba Java Coding Guidelines 阿里巴巴代码规范&lt;/h4&gt;
&lt;p&gt;idea 自带的代码提示可以帮你优化一些简单代码，让代码看上去可以变得更优雅，可是代码规范还是主观性太强，虽然相信大部分 Java 开发都看过 &lt;em&gt;《阿里巴巴Java开发手册》&lt;/em&gt;，但是真正能在写代码的时候约束自己的还是的靠插件。&lt;/p&gt;
&lt;p&gt;![Alibaba Java Coding Guidelines](/assets/images/resource/tools/development-tools/alibaba-java-coding-guidelines.png &quot;Alibaba Java Coding Guidelines&quot; =650x500)&lt;/p&gt;
&lt;h4&gt;CodeGeeX 智能编程助手&lt;/h4&gt;
&lt;p&gt;清华大学与智谱 AI 联合开发的免费代码自动生成工具，周围的朋友都已经在用 GitHub Copilot 了，鉴于收费并且访问问题自己就果断使用 CodeGeex 这款插件了，平常使用中提示功能没有太大问题，一些自带的小功能比较适合国人的开发习惯，也是十分推荐。&lt;/p&gt;
&lt;p&gt;![CodeGeex](/assets/images/resource/tools/development-tools/codeGeeX.png &quot;CodeGeeX&quot; =650x500)&lt;/p&gt;
&lt;h2&gt;Visual Studio Code&lt;/h2&gt;
&lt;p&gt;地表最强 IDE ，具目前的数据来看，全世界受访的程序员中有 81% 的人都在使用，当然我现在就是在用它码字，它依托丰富的插件库可以支持你在开发中遇到的大多数的场景，现在它在我的手里也是“身兼数职”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Visual Studio Code 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/Visual Studio Code.png
link: https://code.visualstudio.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DBeaver Community&lt;/h2&gt;
&lt;p&gt;一个免费的跨平台数据库工具，其 UI 也是十分对味，并且在多数据库支持和数据的导入导出功能上也是十分优秀，并且还开源免费，简直吊打 Navicat。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: DBeaver Community 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/beaver-head.png
link: https://dbeaver.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Chat2DB&lt;/h2&gt;
&lt;p&gt;集成了AI和BI报表功能的新一代数据库管理系统，分析了 Navicat、DBever、DataGrip的优缺点，既可以支持网页版，又支持跨端，又通用的前端完美方案。集成了AIGC的能力，能够将自然语言转换为SQL，也可以将SQL转换为自然语言，可以给出研发人员SQL的优化建议，极大的提升人员的效率，是AI时代数据库研发人员的利器，未来即使不懂SQL的运营业务也可以使用快速查询业务数据、生成报表能力。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Chat2DB 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/chat2db.png
link: http://sqlgpt.cn/zh
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Apifox/Apipost&lt;/h2&gt;
&lt;p&gt;集文档、调试、Mock、测试于一身的API 一体化协作平台，感觉大部分开发者大部分都在使用 Swagger 作为 API 文档协同工具，个人感觉核心的优点还是在每次修改参数后并不需要重新编辑接口就可以自动更新，并且还有测试功能，这已经可以满足开发人员大部分的需求了，可是当你需要更加喜爱丰富的功能的话，比如说出一个手册、Mock等复杂功能还是力不从心，所以 Apifox 和 Apipost 两款国产软件成功问世，主打一个 Postman + Swagger + Mock + Jmeter 的设计理念。个人两个用下来感觉功能差别不大，具体想体验哪一个就看个人喜好了。&lt;/p&gt;
&lt;p&gt;::: card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Apifox 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/apifox.png
link: https://apifox.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Apipost 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/apipost.png
link: https://www.apipost.cn/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;RedisInsight&lt;/h2&gt;
&lt;p&gt;Redis 的可视化工具有很多，但是早起的大部分 UI 都很丑，可视化和没可视化一样，后来像 Redis Desktop Manager 这种简约多彩的几款突出重围，但是美中不足的是要收费，后来 Redis 官方自己推出了自家的 RedisInsight 后，无论是操作逻辑还是 UI 界面都是更胜一筹，我想应该目前应该没有人还在用第三方的了吧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: RedisInsight 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/redisinsigh.svg
link: https://redis.com/redis-enterprise/redis-insight/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tabby&lt;/h2&gt;
&lt;p&gt;这个工具也是最近在社区里火起来的，打着 “A terminal for a more modern age” 的大旗，那就让我康康你有多 modern 。其实我自己是卸载过一次的，当时还是刚用 Mac 不久，几乎找教程都是在用 iTerm2 + Oh My Zsh 这一套，而且第一次用 Tabby 真是没用明白，可是最近又刷到了它，就好奇又下载下来用了，虽然内置的也是用 zsh 的界面，无非就是 iterm 的换壳版，而且还不支持一些系统状态的展示，但是它在 SSH Client 方面可太香了，直接就是 terminal 和 XShell 的合体版本，而且界面真的干爽，建议一上来还是用中文的熟悉一下设置，要不然也会像我第一次一样错过好工具。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Tabby 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/tabby.svg
link: https://tabby.sh/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Warp&lt;/h2&gt;
&lt;p&gt;相对于上面的 Tabby，这个工具号称21世纪的终端工具，与传统终端的操作逻辑有些许不同，不仅用文本框代替了命令行输入，并且新增了 block 块的功能，更加方便我们平时的查看和复用命令。对于我个人理解，如果你是使用终端的小白或者不想安装 oh my zsh 折腾样式的朋友，这是一个非常适合你的工具。&lt;/p&gt;
&lt;p&gt;值得一提的是不仅仅在操作逻辑上，Warp 主打的功能就是与 Ai 相结合，内置了 Ai 助手并且提供命令提示，不需要像之前引入智能提示和历史提示的插件，不过目前每日提问数量应该是有限制的，不过已经可以应付一般人的正常使用强度了。&lt;/p&gt;
&lt;p&gt;当然也是有局限的，目前此工具只支持 macOS，并且会获取用户数据，如果你对个人隐私十分重视那就不推荐使用了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Warp 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/Warp.svg
link: https://www.warp.dev/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OrbStack&lt;/h2&gt;
&lt;p&gt;其实当你在使用 Docker Desktop 的时候你会发现体验非常之差，OrbStack 的出现就是 为了解决 macOS 上的 Docker Desktop 原本就是饱受诟病，慢、重、资源消耗巨的问题，在 Mac OS 上低成本的运行容器和 Linux。不过注意 OrbStack 不支持Windows 和Linux，只支持macOS&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: OrbStack 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/orbStack.png
link: https://orbstack.dev
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;WireShark&lt;/h2&gt;
&lt;p&gt;网络分析的工具很多，相比于 Fiddler 、Charles 来说 WireShark 更专业，功能更强大，并且可以免费使用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: WireShark 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/tools/development-tools/wireshark-logo.png
link: https://www.wireshark.org
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Internal Network Penetration</title><link>https://songbaicheng.cc.cd/posts/internal-network-penetration/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/internal-network-penetration/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;内网穿透&lt;/h1&gt;
&lt;p&gt;虽然在测试一些局域网联机的方式重新用到了内网穿透的工具，但是值得一提的是在一些特定的场合下内网穿透工具还是十分好用的，陆陆续续用了一些工具，最好用的还是 Ngrok ，并不是国内搜索到的无 token 的版本，下面这使用方式和展示效果更直观简洁。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: ngrok 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/techniques/internal-network-penetration/favicon.ico
link: https://dashboard.ngrok.com
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java21</title><link>https://songbaicheng.cc.cd/posts/java21/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/java21/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 21 新特性&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root((Java 21 新特性))
  序列集合
  虚拟线程
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;序列集合&lt;/h2&gt;
&lt;h2&gt;虚拟线程&lt;/h2&gt;
</content:encoded></item><item><title>Layout</title><link>https://songbaicheng.cc.cd/posts/layout/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/layout/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Layout 布局&lt;/h1&gt;
&lt;h2&gt;走进 Layout 布局&lt;/h2&gt;
&lt;p&gt;早期的布局的即把界面看作分为东南西北中五块个模块，其中每个模块的相对位置又可以分流式布局、自适应布局等，到现在网页项目逐渐趋于模块化后，现在认识到的布局更多的是指嵌套 Header、Sider、Content、Footer 和 Main 组件的界面样式，我们可以在 Element UI 的组件中看到这几个部分的排列演示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/frontend/basic/layout/layout.png&quot; alt=&quot;常见页面布局&quot; title=&quot;常见页面布局&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在真正落地布局之前我们还应该需要很多基础知识来支撑 Layout 的实现，提到布局最重要的当然是 CSS 样式，在目前 CSS 有很多样式来增加其规范性和可读性，这里我们结合 Element UI 描述一下 CSS 的 BEM 架构。&lt;/p&gt;
&lt;h2&gt;BEM 架构&lt;/h2&gt;
&lt;p&gt;我们知道 CSS 只有一个作用域，无论你通过什么选择器去操作样式，一旦你声明一个选择器，它就是全局的，一不小心可能就会影响到其他元素，代码的维护性很差，而且 CSS 代码的可读性也不高，虽然目前的前端框架像 Vue 会有 scoped 组件样式的功能，但一些全局样式还是需要有一个规范来整理。&lt;/p&gt;
&lt;p&gt;结合目前的 Layout 布局的组件化思想，我们把界面模块化后借助结构将样式进行规整，结合这种思想，由 Yandex 团队提出的一种 CSS 命名方法论 BEM，也就是一种命名规范。它把 CSS 样式分为三层：block、element 和 modifier，分别是块层、元素层和修饰层。其书写原则就是使用 &lt;code&gt;__&lt;/code&gt; 将块名称和元素名称分开，用 &lt;code&gt;--&lt;/code&gt; 分隔元素名称和修饰符，经典写法为 &lt;code&gt;block-name__element-name--modifier-name--modifier-value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当然很多人觉得 BEM 规范的双下划线和破折号太长或者奇怪，往往大家都是接受其思想而通过短横线来代替，如 Element UI 中的样式命名规范：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/frontend/basic/layout/bem.png&quot; alt=&quot;Element 样式&quot; title=&quot;Element 样式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而我们又知道我们在样式中嵌套会自动追加上父选择器的类名，这样就会破坏 BEM 的命名规范，所以基础的 CSS 语法已经不满足我们去实现这种规范，这里我们需要借助一些 CSS 拓展语言来实现，这里我们选择 Sass。&lt;/p&gt;
&lt;h2&gt;Sass&lt;/h2&gt;
&lt;p&gt;作为自称世界上最成熟、最稳定、最强大的专业级CSS扩展语言，Sass 拥有更多的功能和特性，如果想详细了解 Sass 可以点击下面卡片去官网学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Sass 中文官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/basic/layout/sass.png
link: https://www.sass.hk
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们只介绍 Sass 中 @at-root 的用法，只需要在全局样式中使用 @at-root 就可以使嵌套的格式变成非嵌套，更好的符合 BEM 规范。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/frontend/basic/layout/sass-@at-root.png&quot; alt=&quot;@at-root 用法&quot; title=&quot;@at-root 用法&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Exception</title><link>https://songbaicheng.cc.cd/posts/exception/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/exception/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 异常&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root((Java 异常))
    Throwable
    Error
    Exception
    受检异常
    非受检异常
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Java 异常简介&lt;/h2&gt;
&lt;p&gt;Java 异常是 Java 提供的一种识别及响应错误的一致性机制。Java 异常机制可以使程序中异常处理代码和正常业务代码分离，保证程序代码更加优雅，并提高程序健壮性。&lt;/p&gt;
&lt;h3&gt;Throwable&lt;/h3&gt;
&lt;p&gt;Throwable 是 Java 语言中所有错误与异常的超类，其中包含两个子类：Error（错误）和 Exception（异常），它们通常用于指示发生了异常情况。Throwable 包含了其线程创建时线程执行堆栈的快照，它提供了 printStackTrace() 等接口用于获取堆栈跟踪数据等信息。&lt;/p&gt;
&lt;h3&gt;Error&lt;/h3&gt;
&lt;p&gt;Error 类及其子类是程序中无法处理的错误，表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError（虚拟机运行错误）、NoClassDefFoundError（类定义错误、 OutOfMemoryError：内存不足错误、StackOverflowError：栈溢出等错误。此类错误发生时，JVM 将终止线程。&lt;/p&gt;
&lt;p&gt;这些错误是不受检异常，非代码性错误。因此，当此类错误发生时，应用程序不应该去处理此类错误。按照 Java 惯例，我们是不应该实现任何新的 Error 子类的！&lt;/p&gt;
&lt;h3&gt;Exception&lt;/h3&gt;
&lt;p&gt;程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类：运行时异常和编译时异常。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行时异常：RuntimeException 类及其子类，表示 JVM 在运行期间可能出现的异常。&lt;/li&gt;
&lt;li&gt;编译时异常：Exception 中除 RuntimeException 及其子类之外的异常。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见异常处理方式&lt;/h2&gt;
&lt;h3&gt;直接抛出异常&lt;/h3&gt;
&lt;p&gt;通常，应该捕获那些知道如何处理的异常，将不知道如何处理的异常继续传递下去。传递异常可以在方法签名处使用 throws 关键字声明可能会抛出的异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void readFile(String filePath) throws IOException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = new BufferedReader(new FileReader(file));
    while((result = reader.readLine())!=null) {
        System.out.println(result);
    }
    reader.close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;封装异常再抛出&lt;/h3&gt;
&lt;p&gt;有时我们会从 catch 中抛出一个异常，目的是为了改变异常的类型。多用于在多系统集成时，当某个子系统故障，异常类型可能有多种，可以用统一的异常类型向外暴露，不需暴露太多内部异常细节。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void readFile(String filePath) throws MyException {
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException(&quot;read file failed.&quot;);
        ex.initCause(e);
        throw ex;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;捕获异常&lt;/h3&gt;
&lt;p&gt;在一个 try-catch 语句块中可以捕获多个异常类型，并对不同类型的异常做出不同的处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void readFile(String filePath) {
    try {
        // code
    } catch (FileNotFoundException | UnknownHostException e) {
        // handle FileNotFoundException or UnknownHostException
    } catch (IOException e){
        // handle IOException
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义异常&lt;/h3&gt;
&lt;p&gt;习惯上，定义一个异常类应包含两个构造函数，一个无参构造函数和一个带有详细描述信息的构造函数（Throwable 的 toString 方法会打印这些详细信息，调试时很有用）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;try-catch-finally&lt;/h3&gt;
&lt;p&gt;当方法中发生异常，异常处之后的代码不会再执行，如果之前获取了一些本地资源需要释放，则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码，显得代码比较繁琐，finally 语句可以解决这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void readFile(String filePath) throws MyException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = null;
    try {
        reader = new BufferedReader(new FileReader(file));
        while((result = reader.readLine())!=null) {
        System.out.println(result);
    }
    } catch (IOException e) {
        System.out.println(&quot;readFile method catch block.&quot;);
        MyException ex = new MyException(&quot;read file failed.&quot;);
        ex.initCause(e);
        throw ex;
    } finally {
        System.out.println(&quot;readFile method finally block.&quot;);
        if (null != reader) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用该方法时，读取文件时若发生异常，代码会进入 catch 代码块，之后进入 finally 代码块；若读取文件时未发生异常，则会跳过 catch 代码块直接进入 finally 代码块，所以无论代码中是否发生异常，fianlly 中的代码都会执行。&lt;/p&gt;
&lt;h3&gt;try-with-resource&lt;/h3&gt;
&lt;p&gt;上面例子中，finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放，自动释放的资源需要是实现了 AutoCloseable 接口的类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private  static void tryWithResourceTest(){
    try (Scanner scanner = new Scanner(new FileInputStream(&quot;c:/abc&quot;),&quot;UTF-8&quot;)){
        // code
    } catch (IOException e){
        // handle exception
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;try 代码块退出时，会自动调用 scanner.close 方法，和把 scanner.close 方法放在 finally 代码块中不同的是，若 scanner.close 抛出异常，则会被抑制，抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常，如果想要获取被抑制的异常列表，可以调用 getSuppressed 方法来获取。&lt;/p&gt;
</content:encoded></item><item><title>Jenkins Start</title><link>https://songbaicheng.cc.cd/posts/jenkins-start/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/jenkins-start/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;初始 Jenkins&lt;/h1&gt;
&lt;h2&gt;快速搭建&lt;/h2&gt;
&lt;p&gt;个人理解主节点的 Jenkins 因为不推荐参与构建任务，再加性能损耗小、系统调用权限更直接、复杂度降低等优点，所以应该是安装在宿主机上的，但是目前主流的方案都因为隔离性、可移植性、易于管理和更新的原因都选择容器部署，那我们也就选择容器的方式进行搭建。&lt;/p&gt;
&lt;p&gt;便于管理和配置复用，我们这里采用 Docker Compose 的容器编排方式创建 Jenkins 容器。我们在工作目录中创建下列 Dockerfile 和 Docker Compose。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   # 选择自己的容器镜像
FROM jenkins/jenkins

# 设置环境变量，也可以省略，因为不推荐使用自带的 JDK
ENV JAVA_OPTS=&quot;-Xms512m -Xmx1024m&quot;

# 安装必要的插件，当然可以选择启动容器后手动安装
COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN /usr/local/bin/install-plugins.sh $(cat /usr/share/jenkins/ref/plugins.txt)

# 用户和权限配置（如有必要，根据实际需求调整）
USER root
RUN chown -R jenkins:jenkins /var/jenkins_home/

# 切换容器登录账户
USER jenkins
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3&apos;
services:
  jenkins:
    build:
      context: ./jenkins
      dockerfile: Dockerfile
    container_name: jenkins
    restart: always  # 在生产环境中通常希望容器崩溃后自动重启
    ports:
      - &quot;8080:8080&quot;  # 映射Jenkins Web UI端口到宿主机
      - &quot;50000:50000&quot;  # 映射Jenkins Agent端口
    volumes:
      - jenkins_data:/var/jenkins_home  # 数据持久化，保存在宿主机的数据卷中
      - /var/run/docker.sock:/var/run/docker.sock  # 如果要在Jenkins中执行Docker命令，需要映射Docker守护进程socket
      - /path/to/your/config:/usr/share/jenkins/ref/init.groovy.d  # 若有自定义初始化脚本
      - /usr/local/maven:/usr/local/maven # 自定义 Maven
      - /usr/local/java:/usr/local/java # 自定义 Java
      - /usr/local/node:/usr/local/node # 自定义 Node
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 docker-compose.yml 目录下执行 &lt;code&gt;docker-compose up -d&lt;/code&gt; 即可创建容器，可以用 &lt;code&gt;docker ps&lt;/code&gt; 查看是否启动成功，如果成功则立即执行 &lt;code&gt;docker logs jenkins&lt;/code&gt; 来显示管理员初始化密码，如果没有看到也可以执行 &lt;code&gt;docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword&lt;/code&gt; 进入容器内查看初始化密码。&lt;/p&gt;
&lt;h2&gt;基础配置&lt;/h2&gt;
</content:encoded></item><item><title>Hugging Face</title><link>https://songbaicheng.cc.cd/posts/hugging-face/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/hugging-face/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Hugging Face&lt;/h1&gt;
&lt;p&gt;学习 NLP 最优先级要学习的就是 Hugging Face，它提供了可以轻松地下载并且训练先进的预训练模型的 API 和工具。&lt;/p&gt;
&lt;p&gt;我们要学习 Transformers 的模型、任务和设计理念，还有就是配置（configuration）、模型（model）、分词器（tokenizer）和流水线（pipeline）这几个最重要的类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Hugging Face 官网
desc: 点击跳转 Hugging Face 查看详细内容
logo: /assets/images/ai/llm/hugging-face/huggingface_logo.svg
link: https://huggingface.co/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;在开始之前，确保你已经安装了所有必要的库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install transformers datasets evaluate accelerate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你还需要安装喜欢的机器学习框架：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install torch
// 或者
pip install tensorflow
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;预处理数据&lt;/h2&gt;
&lt;h3&gt;自然语言处理&lt;/h3&gt;
&lt;p&gt;处理文本数据的主要工具是 Tokenizer。Tokenizer根据一组规则将文本拆分为tokens。然后将这些tokens转换为数字，然后转换为张量，成为模型的输入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained(&quot;google-bert/bert-base-cased&quot;)

encoded_input = tokenizer(&quot;Do not meddle in the affairs of wizards, for they are subtle and quick to anger.&quot;)
print(encoded_input)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们一起来看一下输出的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&apos;input_ids&apos;: [101, 2091, 1136, 1143, 13002, 1107, 1103, 5707, 1104, 16678, 1116, 117, 1111, 1152, 1132, 11515, 1105, 3613, 1106, 4470, 119, 102],
  &apos;token_type_ids&apos;: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  &apos;attention_mask&apos;: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 input_ids 是与句子中每个token对应的索引。attention_mask 指示是否应该关注一个token。token_type_ids 在存在多个序列时标识一个token属于哪个序列。&lt;/p&gt;
&lt;p&gt;同样的我们可以解码返回你的输入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tokenizer.decode(encoded_input[&apos;input_ids&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是多个句子一样通过上述方法进行处理，并且可以增加 padding 和 truncation 参数进行填充与截断，最后，tokenizer可以返回实际输入到模型的张量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;batch_sentences = [
    &quot;But what about second breakfast?&quot;,
    &quot;Don&apos;t think he knows about second breakfast, Pip.&quot;,
    &quot;What about elevensies?&quot;,
]
encoded_inputs = tokenizer(batch_sentences, padding=True, truncation=True， return_tensors=&quot;pt&quot;)
print(encoded_inputs)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;音频&lt;/h3&gt;
&lt;p&gt;对于音频任务，您需要feature extractor来准备您的数据集以供模型使用。feature extractor旨在从原始音频数据中提取特征，并将它们转换为张量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from datasets import load_dataset, Audio
from transformers import AutoFeatureExtractor

feature_extractor = AutoFeatureExtractor.from_pretrained(&quot;facebook/wav2vec2-base&quot;)
dataset = load_dataset(&quot;PolyAI/minds14&quot;, name=&quot;en-US&quot;, split=&quot;train&quot;)

def preprocess_function(examples):
    audio_arrays = [x[&quot;array&quot;] for x in examples[&quot;audio&quot;]]
    inputs = feature_extractor(
        audio_arrays,
        sampling_rate=16000,
        padding=True,
        max_length=100000,
        truncation=True,
    )
    return inputs

processed_dataset = preprocess_function(dataset[:5])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在样本长度是相同的，并且与指定的最大长度匹配。您现在可以将经过处理的数据集传递给模型了！&lt;/p&gt;
&lt;h3&gt;计算机视觉&lt;/h3&gt;
&lt;p&gt;图像预处理包括多个步骤将图像转换为模型期望输入的格式。这些步骤包括但不限于调整大小、标准化、颜色通道校正以及将图像转换为张量。&lt;/p&gt;
&lt;h3&gt;多模态&lt;/h3&gt;
&lt;p&gt;对于文本，使用分词器(Tokenizer)将文本转换为一系列标记(tokens)，并创建tokens的数字表示，将它们组合成张量。
对于语音和音频，使用特征提取器(Feature extractor)从音频波形中提取顺序特征并将其转换为张量。
图像输入使用图像处理器(ImageProcessor)将图像转换为张量。
多模态输入，使用处理器(Processor)结合了Tokenizer和ImageProcessor或Processor。&lt;/p&gt;
&lt;h2&gt;微调预训练模型&lt;/h2&gt;
&lt;p&gt;使用预训练模型有许多显著的好处。它降低了计算成本，减少了碳排放，同时允许您使用最先进的模型，而无需从头开始训练一个。
Transformers 提供了涉及各种任务的成千上万的预训练模型。当您使用预训练模型时，您需要在与任务相关的数据集上训练该模型。这种操作被称为微调。&lt;/p&gt;
</content:encoded></item><item><title>Java17</title><link>https://songbaicheng.cc.cd/posts/java17/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/java17/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 17 新特性&lt;/h1&gt;
&lt;p&gt;Java 17 是Java平台的一个重要版本，于2021年9月14日正式发布，与Java 8相比，Java 17带来了更多的新特性和改进，尤其是在性能和安全性方面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root((Java 17 新特性))
  文本块
  switch 表达式
  Record
  密封类 sealed class
  instanceof 模式匹配
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文本块&lt;/h2&gt;
&lt;p&gt;在 17 版本之前定义 JSON 字符串方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void lowVersion() {
    String text = &quot;{\n&quot; +
        &quot;  \&quot;name\&quot;: \&quot;小黑说Java\&quot;,\n&quot; +
        &quot;  \&quot;age\&quot;: 18,\n&quot; +
        &quot;  \&quot;address\&quot;: \&quot;北京市西城区\&quot;\n&quot; +
        &quot;}&quot;;
    System.out.println(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 17 版本之后，可以使用文本块来定义 JSON 字符串，通过三个双引号可以定义一个文本块，并且结束的三个双引号不能和开始的在同一行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private void highVersion() {
    String text = &quot;&quot;&quot;
            {
              &quot;name&quot;: &quot;小黑说Java&quot;,
              &quot;age&quot;: 18,
              &quot;address&quot;: &quot;北京市西城区&quot;
            }
            &quot;&quot;&quot;;
    System.out.println(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;switch 表达式&lt;/h2&gt;
&lt;p&gt;在 17 版本之前，switch 语句中只能使用 break 来终止，否则会继续执行下一个 case 语句。在 17 版本之后，switch 语句中可以使用 yield 来返回一个值，并且不需要 break 来终止，并且支持箭头函数写法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void withYield(Fruit fruit) {
    String text = switch (fruit) {
        case APPLE, PEAR -&amp;gt; {
            System.out.println(&quot;给的水果是: &quot; + fruit);
            yield &quot;普通水果&quot;;
        }
        case MANGO, AVOCADO -&amp;gt; &quot;进口水果&quot;;
        default -&amp;gt; &quot;未知水果&quot;;
    };
    System.out.println(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Record&lt;/h2&gt;
&lt;p&gt;Record类的主要特点是它只包含一些只读的成员变量（这些变量在Record类中被自动声明为final）以及一个或多个构造函数。Record类的目标是简化创建不可变类的过程，并解决Java中语义模型不一致的问题，有以下特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不可变性：Record类中的所有成员变量都是final的，这意味着一旦对象被创建，其状态（即数据）就不能被改变。这种特性为开发者带来了许多好处，如更简单的并发编程模型和更好的数据一致性保证。&lt;/li&gt;
&lt;li&gt;简洁的语法：使用Record类，开发者无需手动编写getters、equals()、hashCode()和toString()方法。编译器会自动为Record类生成这些方法，从而减少了代码的冗余，提高了代码的可读性和清晰度。&lt;/li&gt;
&lt;li&gt;不支持继承：Record类是final的，因此不能被其他类继承。这意味着Record类提供了一种简洁的方式来定义不可变的数据结构，同时避免了继承可能带来的复杂性和不确定性。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public record Employee(String name, int age, String department) {
    // 这里不需要编写任何方法，因为Record会自动为我们生成
}

// 使用
Employee alice = new Employee(&quot;Alice&quot;, 25, &quot;HR&quot;);
System.out.println(alice); // 输出类似于 Employee[name=Alice, age=25, department=HR]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;密封类 sealed class&lt;/h2&gt;
&lt;p&gt;sealed class 允许你限制一个类或接口的子类，只有指定的类或接口才能继承。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public sealed class Fruit permits Apple, Pear {
}
public final class Apple extends Fruit {
}
public final class Pear extends Fruit {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;instanceof 模式匹配&lt;/h2&gt;
&lt;p&gt;instanceof 模式匹配允许开发者使用 instanceof 运算符来检查一个对象是否属于某个类型，并返回一个布尔值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static void oldStyle(Object o) {
    if (o instanceof Furit) {
        Furit furit = (GrapeClass) o;
        System.out.println(&quot;This furit is :&quot; + furit.getName);
    }
}

// 使用后
private static void newStyle(Object o) {
    if (o instanceof Furit furit) {
        System.out.println(&quot;This furit is :&quot; + furit.getName);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java11</title><link>https://songbaicheng.cc.cd/posts/java11/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/java11/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 11 新特性&lt;/h1&gt;
&lt;p&gt;Java 11 是继 Java 8 之后的第一个 LTS 长期支持功能版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root((Java 11 新特性))
  标准HttpClient
  免编译启动
  增强String的API
  集合转换为数组
  Predicate 接口
  var 变量
  嵌套类
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;标准HttpClient&lt;/h2&gt;
&lt;p&gt;Java 9 中引入了增强的 HttpClient API 作为实验性功能。在 Java 11 中，现在 HttpClient 是一个标准。建议使用 Apache Http Client API 等其他 HTTP Client API 代替。它的功能非常丰富，现在基于 Java 的应用程序可以在不使用任何外部依赖的情况下发出 HTTP 请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class APITester {
   public static void main(String[] args) {
      HttpClient httpClient = HttpClient.newBuilder()
         .version(HttpClient.Version.HTTP_2)
         .connectTimeout(Duration.ofSeconds(10))
         .build(); 
         try {
            HttpRequest request = HttpRequest.newBuilder()
            .GET()
            .uri(URI.create(&quot;https://www.baidu.com&quot;))
            .build();                              
            HttpResponse&amp;lt;String&amp;gt; response = httpClient.send(request,
            HttpResponse.BodyHandlers.ofString()); 

         System.out.println(&quot;Status code: &quot; + response.statusCode());                            
         System.out.println(&quot;Headers: &quot; + response.headers().allValues(&quot;content-type&quot;));
         System.out.println(&quot;Body: &quot; + response.body());
      } catch (IOException | InterruptedException e) {
         e.printStackTrace();
      }
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;免编译启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ java Test.java
Hello World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;增强 String 的 API&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;String.repeat(int) ： 重复给定次数的字符串。返回连接的字符串。
String.isBlank() ：检查字符串是否为空或只有空格。
String.strip() ： 删除前导和尾随空格。
String.stripLeading() ： 删除前导空格。
String.stripTrailing() ： 删除尾随空格。
String.lines() ： 返回多行字符串的行流。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;集合转换为数组&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;

public class APITester {
   public static void main(String[] args) {		
      List&amp;lt;String&amp;gt; namesList = Arrays.asList(&quot;Joe&quot;, &quot;Julie&quot;);
      
      // Old way
      String[] names = namesList.toArray(new String[namesList.size()]);
      
      // New way
      names = namesList.toArray(String[]::new);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Predicate 接口&lt;/h2&gt;
&lt;p&gt;Java 11 向 Predicate 接口引入了新方法 not() 来否定类似于 negate 方法的现有谓词。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class APITester {
   public static void main(String[] args) {		
      List&amp;lt;String&amp;gt; tutorialsList = Arrays.asList(&quot;Java&quot;, &quot;\n&quot;, &quot;HTML&quot;, &quot; &quot;);

      List&amp;lt;String&amp;gt; tutorials = tutorialsList.stream()
         .filter(Predicate.not(String::isBlank))
         .collect(Collectors.toList());

      tutorials.forEach(tutorial -&amp;gt; System.out.println(tutorial));
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;var 变量&lt;/h2&gt;
&lt;p&gt;Java 11 允许在 lambda 表达式中使用 var，它可用于将修饰符应用于局部变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@interface NonNull {}

public class APITester {
   public static void main(String[] args) {		
      List&amp;lt;String&amp;gt; tutorialsList = Arrays.asList(&quot;Java&quot;, &quot;HTML&quot;);

      String tutorials = tutorialsList.stream()
         .map((@NonNull var tutorial) -&amp;gt; tutorial.toUpperCase())
         .collect(Collectors.joining(&quot;, &quot;));

      System.out.println(tutorials);
   }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Java8</title><link>https://songbaicheng.cc.cd/posts/java8/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/java8/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java 8 新特性&lt;/h1&gt;
&lt;p&gt;Java 8 发布于 2014 年，成为 Java 程序员最喜欢且热衷的版本之一，Java 8
引入了很多新特性，并且很多数据结构的底层实现都发生了变化，接下来我们罗列我们日常开发中常用的新特性来分享一下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root((Java 8 新特性))
  Interface
  Optional
  Lambda 表达式
  函数式接口
  方法引用
  Stream API
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: oracle JDK 8 介绍
desc: 点击跳转官网查看详细内容
logo: /icon/oracle.svg
link: https://www.oracle.com/java/technologies/javase/8-whats-new.html
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Interface&lt;/h2&gt;
&lt;p&gt;Java 8 中的接口新增默认方法（default）和静态方法（static）,default 修饰的方法，是普通实例方法，可以用 this 调用，可以被子类继承、重写，而
static 修饰的方法，使用上和一般类静态方法一样，但它不能被子类继承，只能用 Interface 调用。这样看来我们的接口和抽象类就越来越像了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface InterfaceNew {
    static void sm() {
        System.out.println(&quot;interface提供的方式实现&quot;);
    }
    static void sm2() {
        System.out.println(&quot;interface提供的方式实现&quot;);
    }

    default void def() {
        System.out.println(&quot;interface default方法&quot;);
    }
    default void def2() {
        System.out.println(&quot;interface default2方法&quot;);
    }
    //须要实现类重写
    void f();
}

public interface InterfaceNew1 {
    default void def() {
        System.out.println(&quot;InterfaceNew1 default方法&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有一个类既实现了 InterfaceNew 接口又实现了 InterfaceNew1接口，它们都有def()，并且 InterfaceNew 接口和
InterfaceNew1接口没有继承关系的话，这时就必须重写def()。不然的话，编译的时候就会报错。&lt;/p&gt;
&lt;h2&gt;functional interface 函数式接口&lt;/h2&gt;
&lt;p&gt;函数式接口（Functional Interface）是 Java 8 中引入的一种特殊的接口，由 &lt;code&gt;@FunctionalInterface&lt;/code&gt; 注解修饰，它只有一个抽象方法。函数式接口可以用于
Lambda 表达式和方法引用，使得代码更加简洁和易读。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;值得注意的是只要符合函数式接口的定义就是函数式接口，与是否有 &lt;code&gt;@FunctionalInterface&lt;/code&gt; 注解无关，注解只是在编译时起到强制规范定义的作用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lambda 表达式&lt;/h2&gt;
&lt;p&gt;说完函数式接口就不得不提到 Lambda 这位重量级选手了，Lambda 表达式是 Java 8 中引入的一种新的语法，它允许你以更简洁的方式编写匿名函数。Lambda
表达式可以用于函数式接口，使得代码更加简洁和易读。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Runnable 接口&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 之前
new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(&quot;The runable now is using!&quot;);
            }
}).start();

// Lambda
new Thread(() -&amp;gt; System.out.println(&quot;It&apos;s a lambda function!&quot;)).start();
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Comparator 接口&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 之前
List&amp;lt;Integer&amp;gt; strings = Arrays.asList(1, 2, 3);

Collections.sort(strings, new Comparator&amp;lt;Integer&amp;gt;() {
@Override
public int compare(Integer o1, Integer o2) {
    return o1 - o2;}
});

// Lambda
Collections.sort(strings, (Integer o1, Integer o2) -&amp;gt; o1 - o2);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;方法调用
Java 8 允许使用 :: 关键字来传递方法或者构造函数引用，无论如何，表达式返回的类型必须是 functional-interface ，下面用
Mybatis Plus 的 Wrapper 来展示。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;LambdaUpdateWrapper&amp;lt;TUser&amp;gt; updateWrapper = Wrappers.lambdaUpdate(TUser.class)
        .eq(TUser::getUsername, username)
        .set(TUser::getRealName, userBasicInfo.getRealName())
        .set(TUser::getIdType, userBasicInfo.getIdType())
        .set(TUser::getIdCard, userBasicInfo.getIdCard());
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Optional&lt;/h2&gt;
&lt;p&gt;Java 8中的 Optional 类可以在以下情况下使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当你不确定一个值是否存在时，可以使用 Optional 来封装这个值，避免在运行时出现 NullPointerException 异常。&lt;/li&gt;
&lt;li&gt;当你需要返回一个可能为空的值时，可以使用 Optional 来代替空指针。这样，你可以避免在代码中使用 null 值，并且可以更加优雅地处理可能为空的情况。&lt;/li&gt;
&lt;li&gt;当你需要对一个可能为空的值进行操作时，可以使用 Optional 提供的 map、filter 等方法来避免空指针异常的出现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无论何时何地，Optional 类可以帮助你更好地处理可能为空的值，使得你的代码更加健壮、优雅。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建一个 Optional
Optional&amp;lt;String&amp;gt; optional1 = Optional.of(&quot;hello&quot;);
Optional&amp;lt;String&amp;gt; optional2 = Optional.ofNullable(null);
Optional&amp;lt;String&amp;gt; optional3 = Optional.empty();

// 判断一个 Optional 是否为空
Optional&amp;lt;String&amp;gt; optional = Optional.of(&quot;hello&quot;);
if (optional.isPresent()) {
    System.out.println(&quot;Optional is not empty&quot;);
} else {
    System.out.println(&quot;Optional is empty&quot;);
}

// get()：如果该 Optional 不为空则返回该对象，否则抛出 NullPointerException 异常。
Optional&amp;lt;String&amp;gt; optional = Optional.of(&quot;hello&quot;);
String value = optional.get(); // 输出hello

// orElse()：如果该 Optional 不为空则返回该对象，否则返回指定的默认值。
Optional&amp;lt;String&amp;gt; optional = Optional.ofNullable(null);
String value = optional.orElse(&quot;fallback value&quot;); // 输出fallback value

// orElseGet()：如果该 Optional 不为空则返回该对象，否则调用指定的 Supplier 获取默认值。
Optional&amp;lt;String&amp;gt; optional = Optional.ofNullable(null);
String value = optional.orElseGet(() -&amp;gt; &quot;fallback value&quot;); // 输出fallback value

// map()：如果有值，则对其执行调用映射函数得到返回值。如果返回值不为 null，则创建包含映射返回值的 Optional 作为 map 方法返回值，否则返回空 Optional。
Optional&amp;lt;String&amp;gt; optional = Optional.of(&quot;hello&quot;);
Optional&amp;lt;String&amp;gt; value = optional.map(str -&amp;gt; str.toUpperCase()); // 输出HELLO

// filter()：如果值存在，并且这个值匹配给定的条件，返回一个 Optional 用以描述这个值，否则返回一个空的 Optional。
Optional&amp;lt;String&amp;gt; optional = Optional.of(&quot;hello&quot;);
Optional&amp;lt;String&amp;gt; value = optional.filter(str -&amp;gt; str.startsWith(&quot;he&quot;)); // 输出hello

// orElseThrow()：如果该 Optional 不为空则返回对象，否则抛出指定的异常。
Optional&amp;lt;String&amp;gt; optional = Optional.ofNullable(null);
String value = optional.orElseThrow(() -&amp;gt; new RuntimeException(&quot;fallback value&quot;)); // 抛出RuntimeException
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Date/Time API&lt;/h2&gt;
&lt;p&gt;随着 JDK 1.5 走来的 java.util.Date 、java.util.Calendar 、java.util.GregoiranCalendar 和 java.text.SimpleDateFormat 相信大家已经烂熟于心了，其中的弊端也是十分头疼：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;非线程安全：java.util.Date 并不是线程安全的。开发者在使用这个类时必须自己处理多线程并发问题。&lt;/li&gt;
&lt;li&gt;设计不佳 ：一方面日期和日期格式化分布在多个包中。另一方面，java.util.Date 的默认日期，年竟然是从 1900 开始，月从 1 开始，日从 0 开始，没有统一性。而且 Date 类也缺少直接操作日期的相关方法。&lt;/li&gt;
&lt;li&gt;时区处理困难：因为设计不佳，开发人员不得不编写大量代码来处理时区问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;本地时间&lt;/h3&gt;
&lt;p&gt;Java 8 为处理本地的日期时间提供了三个类 LocalDate 、LocalTime 和 LocalDateTime。分别用于处理 本地日期、本地时间 和 本地日期时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 返回当前的日期时间，默认使用的是操作系统的时区。
LocalDateTime currentTime = LocalDateTime.now();

// 时间切换
LocalDateTime currentTime = LocalDateTime.now();
LocalDate date1 = currentTime.toLocalDate();
LocalTime time1 = currentTime.toLocalTime();

// 当前月份
Month month = currentTime.getMonth();
// 当前月中的第几天
int day = currentTime.getDayOfMonth();
// 当前秒数
int seconds = currentTime.getSecond();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;时区时间&lt;/h3&gt;
&lt;p&gt;ZonedDateTime 和 LocalDateTime 类似，几乎有着相同的 API。从某些方面说，ZonedLocalTime 如果不传递时区信息，那么它会默认使用操作系统的时区，这样，结果其实和 LocalDateTime 是类似的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 返回当前的日期时间
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime datetime = ZonedDateTime.parse(&quot;2012-10-10T21:58:00+08:00&quot;);

// 切换本地时间
LocalDate date = now.toLocalDate();
LocalTime time = now.toLocalTime();

// 获取当前时区
ZoneId currentZone = ZoneId.systemDefault();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;格式化&lt;/h3&gt;
&lt;p&gt;Java 8 重新创建了一个 java.time.format 包，新增 DateTimeFormatter、DateTimeFormatterBuilder、FormatStyle 用于格式化日期时间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 格式化本地时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;yyyy/MM/dd H:m:s&quot;);
String text = now.format(formatter);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Leetcode</title><link>https://songbaicheng.cc.cd/posts/leetcode/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/leetcode/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;LeetCode&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;想起来就做一题吧！&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;遍历&lt;/h2&gt;
&lt;h3&gt;3099. 哈沙德数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【3099】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/harshad-number/solutions/2832414/shu-zi-bian-li-by-songbaicheng-w1ci
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;83. 删除排序链表中的重复元素&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【83】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/remove-duplicates-from-sorted-list/solutions/2832429/lian-biao-bian-li-by-songbaicheng-5kjm
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3033. 修改矩阵&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【3033】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/modify-the-matrix/solutions/2832513/bian-li-er-jie-shu-zu-by-songbaicheng-fcci
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2974. 最小数字游戏&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【2974】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/minimum-number-game/solutions/2841156/bian-li-shu-zu-by-songbaicheng-6wmc
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;前缀和&lt;/h2&gt;
&lt;h3&gt;724. 寻找数组的中心下标&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【724】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/find-pivot-index/solutions/2835129/qian-zhui-he-by-songbaicheng-y6xh
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;双指针&lt;/h2&gt;
&lt;h3&gt;21. 合并两个有序链表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【21】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/merge-two-sorted-lists/solutions/2834528/jing-dian-shuang-zhi-zhen-by-songbaichen-0igt
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;递归&lt;/h2&gt;
&lt;h3&gt;94. 二叉树的中序遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【94】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/binary-tree-inorder-traversal/solutions/2836175/shen-du-you-xian-bian-li-by-songbaicheng-3orf
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;100. 相同的树&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【100】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/same-tree/solutions/2838042/shen-du-you-xian-bian-li-by-songbaicheng-k88d
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;110. 平衡二叉树&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【110】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/balanced-binary-tree/solutions/2842372/di-gui-by-songbaicheng-4n7i
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;112. 路径总和&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【112】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/path-sum/solutions/2845079/shen-du-you-xian-bian-li-by-songbaicheng-c95k
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;栈&lt;/h2&gt;
&lt;h3&gt;20. 有效的括号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;title: LeetCode 题库【20】
desc: 点击跳转题解页查看详细内容
logo: /assets/common-icon/leetcode-dark-cn.svg
link: https://leetcode.cn/problems/valid-parentheses/solutions/2887726/jing-dian-zhan-wen-ti-by-songbaicheng-4yn8
color:&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Mkcert</title><link>https://songbaicheng.cc.cd/posts/mkcert/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/mkcert/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;mkcert&lt;/h1&gt;
&lt;p&gt;mkcert是由 Filippo Valsorda 开发的一款免费开源工具，专门用于生成受信任的本地 SSL/TLS 证书。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: mkcert 官网
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/github-logo.svg
link: https://github.com/FiloSottile/mkcert
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Lock</title><link>https://songbaicheng.cc.cd/posts/lock/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/lock/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;锁&lt;/h1&gt;
&lt;h2&gt;锁的概念&lt;/h2&gt;
&lt;p&gt;锁是计算机协调多个进程或线程并发访问某一资源的机制，它主要用于同步并发访问共享资源的多个线程或进程，以防止数据不一致或竞争条件的发生，锁的使用场景有以下几种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;多线程编程：在多线程环境中，多个线程可能同时访问和修改共享数据。使用锁可以确保在同一时间只有一个线程可以访问和修改数据，从而避免数据不一致和竞争条件。&lt;/li&gt;
&lt;li&gt;并发编程：在并发编程中，多个进程可能同时访问共享文件、数据库连接等资源。使用锁可以确保同一时间只有一个进程可以访问资源，防止数据损坏或不一致。&lt;/li&gt;
&lt;li&gt;分布式系统：在分布式系统中，多个节点可能同时访问和修改共享资源。使用分布式锁可以确保同一时间只有一个节点可以访问和修改资源，实现数据的最终一致性。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;锁的类型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root((锁的类型))
    共享锁
    排他锁
    悲观锁
    乐观锁
    行锁
    表锁
    自旋锁
    公平锁
    非公平锁
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;共享锁&lt;/h3&gt;
&lt;p&gt;共享锁（Shared Lock，通常简称为S锁）是数据库管理系统（DBMS）中的一种锁机制，用于控制对共享资源的并发访问。当一个事务需要读取某个资源时，它会获取该资源的共享锁，以确保在事务结束之前，其他事务不能修改该资源，但可以同时获取该资源的共享锁来读取它。这允许多个事务同时读取同一资源而不会相互干扰。&lt;/p&gt;
&lt;p&gt;然而，如果某个事务已经持有了一个资源的共享锁，并且另一个事务想要修改这个资源（即获取排他锁或独占锁），那么第二个事务将被阻塞，直到第一个事务释放其共享锁。同样地，如果一个事务持有排他锁，其他事务既不能获取该资源的共享锁也不能获取排他锁。&lt;/p&gt;
&lt;h3&gt;排他锁&lt;/h3&gt;
&lt;p&gt;排他锁（Exclusive Lock），通常简称为X锁，是数据库管理系统（DBMS）中用于控制并发访问的一种锁机制。当一个事务需要对某个资源进行修改（如UPDATE、DELETE操作）时，它会请求获取该资源的排他锁。一旦获得了排他锁，该事务就可以独占性地访问这个资源，而其他事务则无法再获取该资源的任何锁（包括共享锁和排他锁），直到第一个事务释放了排他锁。&lt;/p&gt;
&lt;p&gt;排他锁的主要目的是确保在数据被修改的过程中，其他事务不会对其进行读取或修改，从而避免了脏读（Dirty Read）、不可重复读（Non-repeatable Read）和幻读（Phantom Read）等并发问题。&lt;/p&gt;
&lt;p&gt;在数据库操作中，排他锁通常是自动获取的。当事务执行DML（数据操纵语言）语句时，数据库管理系统会根据需要自动在相应的资源上加上排他锁。例如，在MySQL中，当事务执行UPDATE或DELETE操作时，它会自动在受影响的行上加上排他锁。&lt;/p&gt;
&lt;p&gt;需要注意的是，排他锁不仅会影响其他事务对同一资源的访问，还可能导致死锁（Deadlock）的发生。死锁是指两个或更多的事务在执行过程中，因争夺资源而造成的一种互相等待的现象，若无外力作用，它们都将无法向前推进。因此，在使用排他锁时，需要谨慎考虑并发访问的需求和可能的并发问题，并采取相应的策略来避免死锁的发生。&lt;/p&gt;
&lt;h3&gt;悲观锁&lt;/h3&gt;
&lt;p&gt;悲观锁（Pessimistic Lock）是一种并发控制机制，它基于对数据被外界（包括本系统当前的其他事务，以及来自外部系统的事务处理）修改持保守态度的策略，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;悲观锁具有强烈的独占和排他特性。&lt;/li&gt;
&lt;li&gt;它假设最坏的情况，即数据在处理过程中很可能会被其他事务修改。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，在整个数据处理过程中，悲观锁会将数据处于锁定状态，以确保数据的完整性和一致性。悲观锁的实现通常依靠数据库提供的锁机制。这是因为只有数据库层提供的锁机制才能真正保证数据访问的排他性。 在数据库中，悲观锁可以通过SQL语句（如SELECT ... FOR UPDATE）来实现，这会将检索到的数据行加锁，防止其他事务对其进行修改。 在Java等编程语言中，悲观锁的实现可能涉及使用同步块（synchronized blocks）或Lock接口等机制。&lt;/p&gt;
&lt;p&gt;悲观锁适用于写操作频繁、并发冲突严重的场景。由于它总是假设最坏的情况，因此能够有效地避免数据的不一致性和脏读等问题。然而，过度使用悲观锁可能会导致性能下降和死锁等问题。因此，在选择是否使用悲观锁时需要根据具体的业务场景和需求进行权衡。&lt;/p&gt;
&lt;h3&gt;乐观锁&lt;/h3&gt;
&lt;p&gt;乐观锁（Optimistic Lock）是一种并发控制机制，它基于对数据被外界修改持保守态度的策略， 认为最坏的情况不会发生，乐观锁不是数据库自带的，需要我们自己去实现，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;乐观锁假设在大多数情况下，多个线程或事务之间不会发生冲突。&lt;/li&gt;
&lt;li&gt;读取数据时，每个线程或事务会获得一个标识符（如版本号或时间戳），此标识符通常与数据一同被读取出来。&lt;/li&gt;
&lt;li&gt;在提交修改之前，线程或事务会比较当前标识符与之前读取的标识符是否相等。如果相等，则提交成功；否则，说明数据已被其他线程或事务修改，需要进行冲突处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;乐观锁适用于读操作频繁而写操作较少的场景，因为它可以减少锁的使用，提高并发性能。在高并发的系统中，乐观锁能够避免长时间持有锁带来的性能开销和死锁风险。&lt;/p&gt;
&lt;h3&gt;行锁&lt;/h3&gt;
&lt;p&gt;行锁（Row Lock）是一种数据库锁机制，用于控制对数据库表中单行数据的并发访问。以下是关于行锁的详细解释：&lt;/p&gt;
&lt;p&gt;定义
行锁是对数据库表中特定行进行加锁的机制，当一个事务需要对表中的某一行进行修改时，它会在该行上加上一个锁，以阻止其他事务对该行进行并发访问，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁定范围：行锁仅锁定表中的一行数据，而不是整个表。这意味着其他事务仍然可以访问表中的其他行，不受锁定行的影响。&lt;/li&gt;
&lt;li&gt;粒度：行锁的粒度很细，只影响被锁定的那一行。这使得在高并发场景下，多个事务可以同时访问表中的不同行，从而提高系统的并发性能。&lt;/li&gt;
&lt;li&gt;适用场景：行锁适用于高并发读写的场景，特别是当需要修改的数据量相对较少时。它允许多个事务同时访问表的不同行，降低了锁的争用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在高并发场景下，行锁可以有效地提高系统的并发性能。然而，如果事务持有行锁的时间过长或者频繁地请求行锁，可能会导致锁争用和性能下降。此外，如果多个事务试图同时修改同一行数据，可能会导致死锁的发生。&lt;/p&gt;
&lt;h3&gt;表锁&lt;/h3&gt;
&lt;p&gt;表锁（Table Lock）是数据库管理系统（DBMS）中用于控制对数据库表进行并发访问的一种锁机制。与行锁不同，表锁会锁定整张表，而不仅仅是表中的某一行。当事务需要对表中的数据进行修改时，它可能会请求对整个表加锁，以防止其他事务并发地访问该表中的数据，有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;锁定范围：表锁锁定的是整张表，而不是表中的某一行或某些行。&lt;/li&gt;
&lt;li&gt;并发性能：由于锁定的是整张表，因此在表锁被持有期间，其他事务无法对该表进行写操作（如UPDATE、DELETE或INSERT），但可能仍然可以读取表中的数据（这取决于数据库的隔离级别和锁的实现）。因此，在高并发的读写场景中，表锁可能会成为性能瓶颈。&lt;/li&gt;
&lt;li&gt;简单性：表锁的实现相对简单，因为数据库管理系统不需要跟踪表中哪些行被锁定，哪些行没有被锁定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;表锁的实现方式简单，对于某些操作（如全表扫描或批量更新），表锁可能比行锁更高效，但是并发性能较差。在高并发的读写场景中，表锁可能会导致严重的性能瓶颈，并且容易出现死锁，当多个事务相互等待对方释放表锁时，可能会导致死锁的发生。&lt;/p&gt;
&lt;h3&gt;自旋锁&lt;/h3&gt;
&lt;p&gt;自旋锁（Spinlock）是一种用于保护共享资源的简单锁。当一个线程试图获取一个已经被其他线程持有的自旋锁时，该线程不会立即阻塞（进入睡眠状态），而是会进入一个忙等待（busy-waiting）的循环，持续检查锁是否可用。如果锁可用，则立即获取锁并继续执行；如果锁不可用，则继续循环检查，直到锁被释放。&lt;/p&gt;
&lt;p&gt;自旋锁适用于锁持有时间较短的情况，因为在这种情况下，线程等待锁的时间较短，因此使用自旋锁可以避免线程切换的开销，从而提高性能。然而，如果锁持有时间较长，使用自旋锁可能会导致CPU资源的浪费，因为线程会持续占用CPU资源等待锁，而无法执行其他任务。&lt;/p&gt;
&lt;p&gt;需要注意的是，自旋锁并不是所有场景下都是最佳选择。在某些情况下，使用其他类型的锁（如互斥锁、读写锁等）可能更为合适。&lt;/p&gt;
&lt;h3&gt;公平锁&lt;/h3&gt;
&lt;p&gt;公平锁（Fair Lock）是一种在并发环境中用于控制线程对共享资源的访问顺序的锁机制。其核心特点是确保线程按照请求锁的顺序来获取锁，类似于 FIFO，先来后到。&lt;/p&gt;
&lt;h3&gt;非公平锁&lt;/h3&gt;
&lt;p&gt;非公平锁（Non-fair Lock）是一种在并发环境中用于控制线程对共享资源的访问顺序的锁机制。其核心特点是允许线程获取锁的时机不固定，即允许线程获取锁的时机不按照请求锁的顺序来获取锁。&lt;/p&gt;
&lt;h3&gt;可重入锁&lt;/h3&gt;
&lt;p&gt;可重入锁（Reentrant Lock）是一种线程安全的锁，它允许一个线程对同一个锁进行多次加锁和解锁操作。当一个线程请求一个已经持有的锁时，该锁会保持其状态，直到请求的线程释放锁，如 Sychronized 锁。&lt;/p&gt;
</content:encoded></item><item><title>Jwt</title><link>https://songbaicheng.cc.cd/posts/jwt/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/jwt/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;JSON Web Tokens&lt;/h1&gt;
&lt;h2&gt;前世今生&lt;/h2&gt;
&lt;p&gt;当我步入开发的第一个项目就已经用上了 Token 令牌鉴权的方式，可当时并不清楚这个在请求头添加的 Bearer 字符串是什么，在走进
Token 之前，我们应该从它的前身 Session 来开始讲起。&lt;/p&gt;
&lt;p&gt;在早期的 Web 服务应用阶段，为了解决 HTTP 协议无状态的特性带来的用户认证和会话管理问题。在没有 Session
之前，服务器无法识别连续的多个请求是否来自同一个用户，因为HTTP协议本身并不维护用户的上下文状态。随着Web应用规模的扩大，尤其是分布式系统和微服务架构的兴起，Session
集中存储在一台服务器上的方式开始暴露出可扩展性和性能瓶颈。此外，跨域请求、CSRF攻击等问题也促使开发者寻找新的解决方案。&lt;/p&gt;
&lt;p&gt;Token作为一种更轻量级、分布式的认证机制，因其无状态、易于跨域、支持分布式部署等特点而迅速普及，逐渐成为现代Web应用特别是API服务的首选认证方式。尤其是
JSON Web Token (JWT) 的兴起，它将用户信息加密到 Token 中，服务器无需查询数据库即可验证用户身份，这种特性使得它在微服务架构中特别有用，可以减少服务间通信的成本。&lt;/p&gt;
&lt;p&gt;总的来说，Session 到Token 的转变反映了 Web 应用从中心化向分布式、从单一服务向微服务架构发展的趋势，也体现了安全性、可扩展性和用户体验之间平衡的不断优化。&lt;/p&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;p&gt;JWT（JSON Web Token）核心在于它的结构，由三部分组成，这些部分通过点(&apos;.&apos;)分隔，每一部分都是 Base64 URL 编码过的字符串，这三部分分别是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Header（头部）: 令牌的类型（通常总是&quot;typ&quot;被设置为&quot;JWT&quot;)和所使用的签名算法（如&quot;alg&quot;被设置为HS256）。例如：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Payload（有效载荷）: Payload 部分是 JWT 的核心，它包含声明（Claims），这些声明是关于实体（通常是用户）和其他数据的声明。Claims
可以分为三种类型：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Registered claims（注册声明）: 预定义的，不是强制性的，但建议使用的，如 iss（发行人）、exp（过期时间）、sub（主题）等。&lt;/li&gt;
&lt;li&gt;Public claims（公共声明）: 自定义的，可以添加任何不违反 JWT 标准的数据，但应该避免冲突。&lt;/li&gt;
&lt;li&gt;Private claims（私有声明）: 双方之间约定的声明，不公开也不建议第三方使用。 例如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;主题内容&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;iss&quot;: &quot;发行方&quot;,
  &quot;exp&quot;: 1516239022,
  &quot;admin&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Signature（签名）: 签名部分是对前两部分（Header和Payload）的加密，用于验证 JWT 的完整性和确保它没有被篡改。签名是使用
Header 中指定的算法和一个秘钥（对称或非对称）计算得出的。如果使用的是对称算法（如HMAC
SHA256），则同一秘钥用于签名和验证；如果是非对称算法（如RSA），则使用私钥签名，公钥验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;title: JWT 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/backend/java/spring/spring-security/jwt.svg
link: https://jwt.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JWT 简单场景案例&lt;/h2&gt;
&lt;h3&gt;引入依赖&lt;/h3&gt;
&lt;p&gt;::: code-tabs
@tab Maven#Maven&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;javax.xml.bind&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jaxb-api&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.sun.xml.bind&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jaxb-impl&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;

    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.sun.xml.bind&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jaxb-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Gradle#Gradle&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext {
    jjwtVersion = &apos;0.9.1&apos;
}

dependencies {
    implementation &apos;io.jsonwebtoken:jjwt:$jjwtVersion&apos; 
    implementation &apos;jakarta.xml.bind:jakarta.xml.bind-api:$jjwtVersion&apos;
    implementation &apos;com.sun.xml.bind:jaxb-impl:$jjwtVersion&apos;
    implementation &apos;com.sun.xml.bind:jaxb-core:$jjwtVersion&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;JWT 工具类&lt;/h3&gt;
&lt;p&gt;这是一个简单的生成 JWT 的工具类，其中需要的参数都以静态参数的方式声明了，当然更建议将这些配置存放在 yaml 配置文件中，并且将密钥生成加密文件保存等更安全的方式，其载荷内的声明内容也可以更加丰富，这里我们只讨论 JWT 校验过程，故不做延伸。&lt;/p&gt;
&lt;p&gt;::: normal-demo JWT 工具类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.alibaba.fastjson2.JSON;
import com.sbc.convention.exception.ServiceException;
import com.sbc.convention.pojo.dto.UserInfoDTO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @description: JWT 工具类
 **/
@Slf4j
public class JWTUtil {

     /**
     * 过期时间
     */
    private static final long EXPIRATION = 86400L;
    /**
     * Token 前缀
     */
    public static final String TOKEN_PREFIX = &quot;Bearer &quot;;
    /**
     * 签发者
     */
    public static final String ISS = &quot;songbaicheng&quot;;
    /**
     * 密钥
     */
    public static final String SECRET = &quot;SecretKey039245678901232039487623456783092349288901402967890140939827&quot;;

    /**
     * 生成用户 Token
     *
     * @param userInfo 用户信息
     * @return 用户访问 Token
     */
    public static String generateAccessToken(UserInfoDTO userInfo) {
        Map&amp;lt;String, Object&amp;gt; claims = generateClaims(userInfo);
        return TOKEN_PREFIX + generateToken(claims);
    }

    /**
     * 验证用户 Token 的有效性
     *
     * @param jwtToken 用户访问 Token
     * @return 是否有效
     */
    public static boolean validateToken(String jwtToken) {
        if (StringUtils.hasText(jwtToken)) {
            String actualJwtToken = jwtToken.replace(TOKEN_PREFIX, &quot;&quot;);
            try {
                Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(actualJwtToken).getBody();
                return validateTokenExpiration(claims);
            } catch (ExpiredJwtException ignored) {
                log.warn(&quot;JWT Token 已过期&quot;);
            } catch (Exception ex) {
                log.error(&quot;JWT Token 解析失败！&quot;, ex);
            }
        }
        return false;
    }

    /**
     * 解析用户 Token
     *
     * @param jwtToken 用户访问 Token
     * @return 用户信息
     */
    public static UserInfoDTO parseJwtToken(String jwtToken) {
        if (StringUtils.hasText(jwtToken)) {
            String actualJwtToken = jwtToken.replace(TOKEN_PREFIX, &quot;&quot;);
            try {
                Claims claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(actualJwtToken).getBody();
                if (validateTokenExpiration(claims)) {
                    return retrieveUserInfoFromClaims(claims);
                }
            } catch (ExpiredJwtException ignored) {
            } catch (Exception ex) {
                log.error(&quot;JWT Token 解析失败！&quot;, ex);
                throw new ServiceException(&quot;JWT Token 解析失败！&quot;);
            }
        }
        return null;
    }

    private static Map&amp;lt;String, Object&amp;gt; generateClaims(UserInfoDTO userInfo) {
        Map&amp;lt;String, Object&amp;gt; claims = new HashMap&amp;lt;&amp;gt;();
        claims.put(&quot;userId&quot;, userInfo.getUserId());
        claims.put(&quot;username&quot;, userInfo.getUsername());
        claims.put(&quot;realName&quot;, userInfo.getRealName());
        return claims;
    }

    private static String generateToken(Map&amp;lt;String, Object&amp;gt; claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date())
                .setIssuer(ISS)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    private static boolean validateTokenExpiration(Claims claims) {
        Date expiration = claims.getExpiration();
        return expiration != null &amp;amp;&amp;amp; expiration.after(new Date());
    }

    private static UserInfoDTO retrieveUserInfoFromClaims(Claims claims) {
        String subject = claims.getSubject();
        return JSON.parseObject(subject, UserInfoDTO.class);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;校验 Token 过滤器&lt;/h3&gt;
&lt;p&gt;我们这里并不搭配 Spring Security 等权限框架，我们只需要校验请求头中携带的 Token 是否有效即可，这里有两种实现方式，分别是 AOP 和 Filter 过滤器，我们分别说一下这两种方式的优缺点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AOP (面向切面编程)：
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ol&gt;
&lt;li&gt;解耦性好：AOP 能够将横切关注点（如权限校验、日志记录等）从业务逻辑中分离出来，使代码更加清晰和模块化。&lt;/li&gt;
&lt;li&gt;灵活性高：可以灵活地指定切入点（如指定特定方法或类），只在需要的地方执行拦截逻辑。&lt;/li&gt;
&lt;li&gt;可读性强：通过注解的方式定义切面，使得代码意图更为明确，便于阅读和维护。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ol&gt;
&lt;li&gt;性能开销：虽然现代框架已经尽可能减少了 AOP 的性能影响，但在某些高并发场景下，动态代理可能会带来轻微的性能开销。&lt;/li&gt;
&lt;li&gt;作用域限制：AOP主要针对业务逻辑层（如 Service 层），对于进入 Web 容器之前的请求处理（如静态资源、错误页面等）控制能力有限。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Filter (过滤器)
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ol&gt;
&lt;li&gt;全面覆盖：Filter 可以拦截所有进出Web容器的请求和响应，包括静态资源、错误页面等，控制层面更广。&lt;/li&gt;
&lt;li&gt;简单易用：相比 AOP，Filter 的配置和实现较为直观，容易上手，符合 Servlet 规范，不依赖特定框架。&lt;/li&gt;
&lt;li&gt;性能高效：作为 Servlet 规范的一部分，Filter 在设计上就考虑了性能问题，对于纯请求过滤处理，性能损耗较低。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ol&gt;
&lt;li&gt;侵入性：Filter 需要在 Web.xml 或使用注解进行配置，对 Web 应用的结构有一定的侵入。&lt;/li&gt;
&lt;li&gt;逻辑分散：随着应用复杂度增加，如果过多地使用Filter，可能会导致过滤逻辑分散在多个 Filter 中，不易管理和维护。&lt;/li&gt;
&lt;li&gt;粒度较粗：Filter 通常针对整个请求/响应链路，难以精确到方法级别，不如 AOP 灵活。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据以上的分析，结合现在大多数都高并发场景的情况，我们更推荐性能更优的 Filter 方式，虽然力度上不如 AOP 加校验注解的方式更灵活，可大部分项目的设计初衷都是除登录等白名单接口外，其余所有接口都应该进行校验。话不多说我们直接开始创建和注入过滤器。&lt;/p&gt;
&lt;p&gt;::: normal-demo JWT 认证过滤器创建&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sbc.auth.toolkit.JWTUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.util.StringUtils;

import java.io.IOException;

/**
 * @description: JWT 认证过滤器
 **/
public class JwtAuthenticationFilter implements Filter {

    @Override
    public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException {
        jakarta.servlet.Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        // 设置响应字符编码为 UTF-8
        httpResponse.setCharacterEncoding(&quot;UTF-8&quot;);

        String requestURI = httpRequest.getRequestURI();
        if (!requestURI.endsWith(&quot;/login&quot;)) {
            // 获取请求头 Token
            String token = getJwtFromRequest(httpRequest);
            if (token == null) {
                // 如果没有携带 Token，返回 401 错误
                httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                httpResponse.getWriter().write(&quot;未携带 Token&quot;);
                return;
            }
            // 如果 Token 无效，也返回 401 错误
            if (!JWTUtil.validateToken(token)) {
                httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                httpResponse.getWriter().write(&quot;Token 已失效，请重新登录获取新 Token&quot;);
                return;
            }
        }

        chain.doFilter(request, response);
    }


    /**
     * 获取请求头 Token 凭证
     *
     * @param request 请求信息
     * @return Token 令牌
     */
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(&quot;Authorization&quot;);
        if (StringUtils.hasText(bearerToken) &amp;amp;&amp;amp; bearerToken.startsWith(&quot;Bearer &quot;)) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
::: normal-demo JWT 认证过滤器注入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sbc.auth.filter.JwtAuthenticationFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;

/**
 * @description: Spring Security 配置类
 **/
@Configuration
public class AuthConfig {

    @Bean
    public FilterRegistrationBean&amp;lt;JwtAuthenticationFilter&amp;gt; jwtFilter() {
        FilterRegistrationBean&amp;lt;JwtAuthenticationFilter&amp;gt; registrationBean = new FilterRegistrationBean&amp;lt;&amp;gt;();
        registrationBean.setFilter(new JwtAuthenticationFilter());
        registrationBean.addUrlPatterns(&quot;/api/*&quot;);
        return registrationBean;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;生成令牌和携带令牌&lt;/h3&gt;
&lt;p&gt;正如我们过滤器中的规则，除了请求路径结尾为 &lt;code&gt;/login&lt;/code&gt; 并且匹配  &lt;code&gt;/api/*&lt;/code&gt; 的请求都会在请求头中校验 &lt;code&gt;Authorization&lt;/code&gt; 字段携带的 Token，我们的前端请求使用 Axios 配置如下：&lt;/p&gt;
&lt;p&gt;::: normal-demo Axios 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { ElMessage } from &apos;element-plus&apos;
import axios from &apos;axios&apos;
import type { AxiosError, AxiosInstance, AxiosResponse } from &apos;axios&apos;
import { useAuthStore } from &apos;@/stores/authStore&apos;

const axiosInstance: AxiosInstance = axios.create({
  timeout: 10000, // 设置请求超时时间
  headers: {
    &apos;Content-Type&apos;: &apos;application/json&apos;
  }
})

// 请求拦截器
axiosInstance.interceptors.request.use(
  (config) =&amp;gt; {

    // 为确保 pinia 实例被激活，最简单的方法就是将 useStore() 的调用放在 pinia 安装后才会执行的函数中。
    // https://pinia.vuejs.org/zh/core-concepts/outside-component-usage.html
    const authStore = useAuthStore()

    const token: string | null = authStore.authState.token
    if (token) {
      config.headers.Authorization = `${token}`
    }
    return config
  },
  (error: AxiosError) =&amp;gt; {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 响应拦截器
axiosInstance.interceptors.response.use(
  (response: AxiosResponse) =&amp;gt; {
    // 在这里可以对响应进行处理
    if (response.status === 401) {
      ElMessage.error(&apos;用户未登录或已过期！&apos;)
      window.location.href = &apos;login&apos;
    }
    return response
  },
  (error: AxiosError) =&amp;gt; {
    // 对响应错误做些什么
    console.log(error, &apos;error&apos;)
    if (error.response &amp;amp;&amp;amp; error.response.status === 401) {
      ElMessage.error(&apos;用户未登录或已过期！&apos;)
      window.location.href = &apos;login&apos;
    }
    return Promise.reject(error)
  }
)

export default axiosInstance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;而在后端的登录逻辑里，我们则是调用 JWT 工具类中的生成方法返回给前端存放在全局事件总线中，这样完整的一套基础的 JWT 校验就完成了。&lt;/p&gt;
</content:encoded></item><item><title>Nacos</title><link>https://songbaicheng.cc.cd/posts/nacos/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/nacos/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Nacos&lt;/h1&gt;
&lt;h2&gt;浅聊微服务配置和注册中心&lt;/h2&gt;
&lt;h3&gt;配置中心&lt;/h3&gt;
&lt;p&gt;传统的静态配置方式要想修改某个配置只能修改之后重新发布应用，要实现动态性，可以选择使用数据库，通过定时轮询访问数据库来感知配置的变化。轮询频率低感知配置变化的延时就长，轮询频率高，感知配置变化的延时就短，但比较损耗性能，需要在实时性和性能之间做折中。配置中心专门针对这个业务场景，兼顾实时性和一致性来管理动态配置。配置中心兼往往顾了集中管理、动态更新和版本控制等优点。&lt;/p&gt;
&lt;h3&gt;注册中心&lt;/h3&gt;
&lt;p&gt;注册中心用于管理和维护微服务的注册信息，包括微服务的网络位置（IP地址和端口）以及其他元数据。每个微服务在启动时向注册中心注册自己的信息，并定期发送心跳以保持活动状态。其他微服务可以通过查询注册中心获取所需服务的信息，从而实现服务的发现和调用。&lt;/p&gt;
&lt;h3&gt;微服务中的使用&lt;/h3&gt;
&lt;p&gt;配置中心和注册中心通常会一起使用，配合实现微服务的配置管理、服务发现和通信。它们为微服务架构的可伸缩性、弹性和灵活性提供了重要的基础设施。&lt;/p&gt;
&lt;p&gt;作为配置中心应该要求支持集中化管理、配置存储和分发、动态更新、版本控制、安全性和权限控制和监控和告警；作为注册中心要做到服务注册和注销、服务发现、心跳和健康检查、负载均衡、高可用性和容错性和监控和告警。目前市面上现在针对配置中心和注册中心分别都有很多产品，像百度的 &lt;strong&gt;Disconf&lt;/strong&gt;、Spring的 &lt;strong&gt;Spring Cloud Config&lt;/strong&gt;、携程的 &lt;strong&gt;Apollo&lt;/strong&gt;、阿里的 &lt;strong&gt;Nacos&lt;/strong&gt;、网飞的 &lt;strong&gt;Eureka&lt;/strong&gt;等。&lt;/p&gt;
&lt;p&gt;结合网络上的综合评价来看总，作为配置中心的话，Apollo 和 Nacos 相对于 Spring Cloud Config 的生态支持更广，在配置管理流程上做的更好。Apollo 相对于 Nacos 在配置管理做的更加全面，不过使用起来也要麻烦一些。Nacos 使用起来相对比较简洁，在对性能要求比较高的大规模场景更适合。在注册中心上来看，Eureka 在跨区域部署和大规模集群上可能面临一些性能和可扩展性方面的挑战，并且在广泛的生态上来看， Nacos 是更好的选择。结合双方的优点，Nacos 非常适合作为微服务学习的第一选择，想要更加详细了解 Nacos 请点击下方官网链接访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Nacos 中文官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/backend/java/micro-services/nocas/nacos_colorful.png
link: https://nacos.io/zh-cn/index.html
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;Nacos服务端单机模式启动&lt;/h3&gt;
&lt;p&gt;我们这里根据 Spring Cloud Alibaba 毕业版本推荐选择了 2.2.1 的版本，相比于 Eureka 来说，Nacos 同样也分为客户端和服务端，不过服务端不需要我们自行搭建，我们直接下载使用官方的即可，这里你可以选择下载二进制包直接使用或者 Docker 镜像的方式都可以，我们这里使用下载二进制包的方法，可以去下面网址找到你想要的版本进行下载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Nacos 版本下载网址
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/spring-initializr.svg
link: https://github.com/alibaba/nacos/tags
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载完成后启动前，这里值得注意的是 Nacos 在&lt;strong&gt;2.2.0.1&lt;/strong&gt;和&lt;strong&gt;2.2.1&lt;/strong&gt;版本后，必须修改 conf 目录下的 application.properties 文件设置其中的 &lt;strong&gt;nacos.core.auth.plugin.nacos.token.secret.key&lt;/strong&gt; 值，否则不能启动，该参数是鉴权用于生成用户登陆临时accessToken所使用的密钥，默认可以使用&lt;code&gt;SecretKey012345678901234567890123456789012345678901234567890123456789&lt;/code&gt;，该值可用于临时测试，实际使用时请务必更换为自定义的其他有效值。如果你不想使用内置数据库想绑定数据库实现持持久化，需要把 &lt;strong&gt;Config Module Related Configurations&lt;/strong&gt; 中的配置全部打开并配置正确的数据库参数，并且把 conf 目录中对应数据库的sql文件执行生成工作表。一切准备完毕后启动即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sh startup.sh -m standalone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行命令后可以根据自己的配置访问下面网址进行访问。如果启动失败，可以在日志文件夹中寻找 start.out 文件查看启动失败的原因，这里面的日志会根据你每次启动而更换，如果是想查看历来所有的日志则需要去 nacos.log 文件查看。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;http://{ip}:{port}/nacos/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;默认账号：nacos&lt;/li&gt;
&lt;li&gt;默认密码：nacos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启动成功后可以通过添加配置文件来测试各功能是否好用，如果是外置数据源的情况可以配置完后查看数据库表中数据是否存在来判断。&lt;/p&gt;
&lt;h3&gt;Nacos客户端启动后注册&lt;/h3&gt;
&lt;p&gt;准备一个 Spring boot 项目，这里建议使用下面阿里云网站进行构建，如果你使用 Spring 官网的工具会发现在 IDEA 中添加依赖的时候找不到 Nacos ，还需要自己去添加。并且使用阿里云构建项目会在项目中给你添加 demo 示例，这里让我想到看过的一个视频里说：“这些生成的配置在我的代码里完全是画蛇添足！”哈哈哈哈，现在想到这句话我都觉得很逗。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://start.aliyun.com&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面是要添加的关键依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-config&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;com.alibaba.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter-alibaba-nacos-discovery&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后配置好配置文件并像 Eureka 一样在启动类或者配置类中添加&lt;code&gt;@EnableDiscoveryClient&lt;/code&gt;客户端注解即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server:
  port: 8080 # 应用服务 WEB 访问端口

spring:
  application:
    name: nacos-demo # 服务注册名称
  cloud:
    nacos:
      config:
        server-addr: localhost:8848 # 服务注册地址
        username: nacos
        password: nacos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/nocas/nacos-server-list.png&quot; alt=&quot;Nacos 登录界面&quot; title=&quot;服务注册成功&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Nacos服务端集群模式启动&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/nocas/nacos-cluster.jpg&quot; alt=&quot;Nacos 集群&quot; title=&quot;集群部署架构图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图是从网络上找到的 Nacos 集群图，其实在官网也有集群部署的说明，但是没有展示出 Nacos 集群和数据交互的部分，但是在安全性方面，官网推荐&lt;strong&gt;域名 + SLB模式&lt;/strong&gt;(内网SLB，不可暴露到公网，以免带来安全风险)，可读性好，而且换 ip 方便。不过我们先不讨论网关的部分，因为如果牵扯到网关到底是使用微服务网关（Netflix Zuul、Spring Cloud Gateway）还是 Nginx 还需要根据我们项目 API 管控治理能力的大小来决定。搭建 Nacos 集群我们需要使用先修改一下安装目录中 cong 下的 cluster.conf.exemple 文件，可以直接将该文件的 .exemple 后缀去掉后按照文件中例子添加自己创建的 Nacos 节点的 ip 和 port，至于数据源配置如果使用内置数据源则不需要修改，如果使用外置数据源则全部按照单机启动的部分配置成自己的数据库参数即可。&lt;strong&gt;&lt;em&gt;这里如果使用的的单机多端口搭建集群的话注意，使用 nacos2.0 之后需要开放两个8848偏移后的端口&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/nocas/nacos-node.png&quot; alt=&quot;Nacos 集群节点&quot; title=&quot;Nacos 集群节点列表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候启动我们的注册服务会发现每个 Nacos 节点内都可以看到注册服务，Nacos 集群就搭建完成了。&lt;/p&gt;
</content:encoded></item><item><title>Log Desensitization</title><link>https://songbaicheng.cc.cd/posts/log-desensitization/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/log-desensitization/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;切面编程&lt;/h1&gt;
&lt;h2&gt;浅聊AOP&lt;/h2&gt;
&lt;p&gt;当我第一次接触Spring框架的时候，就告诉我们 IOC（控制反转） 和 AOP（面向切面编程） 这两个最核心的知识点，IOC 作为我们工作中最经常使用的知识点大家肯定是烂熟于心，而一聊到 AOP ，脑海中想到的只有零零散散的面试题，和一些日志、缓存、事务、安全、权限等功能场景，这些场景确实主要在项目搭建阶段就已经搭建完毕了，这也导致我们很少在工作中接触到它。其实学习使用 AOP 是相对简单的，我们需要先知道以下几个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切面：一个关注点的模块化，这个关注点可能会横切多个对象。&lt;/li&gt;
&lt;li&gt;连接点：程序执行过程中明确的点，如方法调用或异常处理器。&lt;/li&gt;
&lt;li&gt;切点：指定一个或多个连接点，切面在这些点上执行它的操作。&lt;/li&gt;
&lt;li&gt;通知：切面在特定连接点上执行的操作，有 before、after、around、afterThrowing 和 afterReturning 等类型。&lt;/li&gt;
&lt;li&gt;织入：将切面应用到目标对象来创建新的代理对象的过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过这些概念我们也可以看出来，AOP 的基本思想是将这些横切关注点与系统的核心业务逻辑分离开来，通过定义一个切面（Aspect）来包含这些关注点，然后在系统运行时，动态地将切面织入到核心业务逻辑中。&lt;/p&gt;
&lt;p&gt;AOP 是一种编程范式，是一种思想，用于解决横切关注点的模块化问题。我们常用的 AspectJ 则是基于 Java 的 AOP 框架，提供了实现 AOP 概念的语法和工具。&lt;/p&gt;
&lt;h2&gt;工作中使用到 AOP 的例子&lt;/h2&gt;
&lt;h3&gt;自定义日志注解&lt;/h3&gt;
&lt;p&gt;最经典的切面应用场景，为了更加灵活和方便的查看每个方法和请求的出参入参，我们可以使用自定义注解的方式指定需要打印的方法或者类。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先定义打印信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class ILogPrintDTO {

    /**
     * 开始时间
     */
    private String beginTime;

    /**
     * 请求入参
     */
    private Object[] inputParams;

    /**
     * 返回参数
     */
    private Object outputParams;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;定义日志注解，作用域为方法或类，并在运行时生效。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ILog {

    /**
     * 是否需要打印入参
     *
     * @return 入参打印是否打印
     */
    boolean input() default true;

    /**
     * 是否需要打印出参
     *
     * @return 出参打印是否打印
     */
    boolean output() default true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;定义切面&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Aspect
public class ILogPrintAspect {

    @Pointcut(&quot;@within(com.sbc.log.annotation.ILog) || @annotation(com.sbc.log.annotation.ILog)&quot;)
    public void pointcut() {
    }

    @Around(&quot;pointcut()&quot;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = SystemClock.now();
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        String beginTime = DateUtil.now();
        Object result = null;
        try {
            result = pjp.proceed();
        } finally {
            Method targetMethod = pjp.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            ILog logAnnotation = Optional.ofNullable(targetMethod.getAnnotation(ILog.class)).orElse(pjp.getTarget().getClass().getAnnotation(ILog.class));
            if (logAnnotation != null) {
                ILogPrintDTO logPrint = new ILogPrintDTO();
                logPrint.setBeginTime(beginTime);
                if (logAnnotation.input()) {
                    logPrint.setInputParams(buildInput(pjp));
                }
                if (logAnnotation.output()) {
                    logPrint.setOutputParams(result);
                }
                String methodType = &quot;&quot;, requestURI = &quot;&quot;;
                try {
                    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    assert servletRequestAttributes != null;
                    methodType = servletRequestAttributes.getRequest().getMethod();
                    requestURI = servletRequestAttributes.getRequest().getRequestURI();
                } catch (Exception ignored) {
                }
                log.info(&quot;[{}] {}, executeTime: {}ms, info: {}&quot;, methodType, requestURI, SystemClock.now() - startTime, JSON.toJSONString(logPrint));
            }
        }
        return result;
    }

    private Object[] buildInput(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        Object[] printArgs = new Object[args.length];
        for (int i = 0; i &amp;lt; args.length; i++) {
            if ((args[i] instanceof HttpServletRequest) || args[i] instanceof HttpServletResponse) {
                continue;
            }
            if (args[i] instanceof byte[]) {
                printArgs[i] = &quot;byte array&quot;;
            } else if (args[i] instanceof MultipartFile) {
                printArgs[i] = &quot;file&quot;;
            } else {
                printArgs[i] = args[i];
            }
        }
        return printArgs;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建配置类&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class LogAutoConfiguration {

    /**
     * 日志打印 AOP 切面
     */
    @Bean
    public ILogPrintAspect iLogPrintAspect() {
        return new ILogPrintAspect();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;返回对象脱敏&lt;/h3&gt;
&lt;p&gt;最近公司开始要求对客户信息的保密性进行加强，需要我们将日志和前台界面的客户信息进行加密处理，由于我们项目的日志五花八门，而且使用的架构也不尽相同，所以日志脱敏的解决办法就是开发一个脱敏工具类，同时将脱敏需要的依赖打包成 jar 添加到每个项目中去，检索项目中所有打印日志的语句，统一加上工具类中的脱敏方法，听起来这就是个感人的工作。其次是前台界面客户信息脱敏，因为我们大部分项目都是纯后台，所以负责这个任务的工作就落到了我另一个同事头上，当他在和我讨论这个实现的时候和我说了一下他的思路：我们后台需要做的就是把传递给前台的 vo 中的敏感信息过滤，如果每个 vo 对象都要过滤那简直是天方夜谭，于是他想将所有 controller 中的方法作为切点增加个切面，拿到每个方法返回值判断 vo 并进行过滤。下面我拿 demo 来演示一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先我们先模拟一个 controller 返回前台一个 vo：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@GetMapping(&quot;/test/vo&quot;)
public UserVo testReturnVo() {
    return UserVo.builder()
            .id(IdUtil.getSnowflakeNextId())
            .email(&quot;recipient@example.com&quot;)
            .firstName(&quot;baicheng&quot;)
            .lastName(&quot;song&quot;)
            .phoneNumber(&quot;12345678912&quot;)
            .username(&quot;songbaicheng&quot;)
            .build();
}

@Data
@Builder
public class UserVo {
    private Long id;
    private String username;
    private String email;
    private String firstName;
    private String lastName;
    private String phoneNumber;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;接下来新增切面，把要处理返回值的方法作为切点托管。这里有两种方式，分别是在 @Around 和 @AfterReturning 中进行操作，如果我们只是操作返回值，则推荐使用 @AfterReturning 中获取入参中的返回值项进行修改，如果有其他更复杂的操作，则可以在 @Around 的 ProceedingJoinPoint 获取更多的钩子进行操作。而且值得注意的是如果这里同时使用两个方法的话，是先执行 @AfterReturning 再执行 @Around。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Slf4j
@Aspect
@Component
public class TestReturnVoAspect {

    // 如果用切点表达式力度太大或者不够灵活的时候，可以使用自定义注解的方式代替切点表达：
    // @Pointcut(&quot;@annotation(com.example.CustomAnnotation)&quot;)
    @Pointcut(&quot;execution(* com.sbc.springbootmoudle.controller.HelloController.testReturnVo(..))&quot;)
    public void servicePointcut() {
    }

    /**
        方法一
     */
    @Around(&quot;servicePointcut()&quot;)
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {

        // 获取方法返回值
        Object result = pjp.proceed();

        if (result instanceof UserVo) {
            
            // 模拟脱敏操作
            return  UserVo.builder()
                    .id(IdUtil.getSnowflakeNextId())
                    .email(&quot;**********@example.com&quot;)
                    .firstName(&quot;ba****ng&quot;)
                    .lastName(&quot;s**g&quot;)
                    .phoneNumber(&quot;123****912&quot;)
                    .username(&quot;son*****heng&quot;)
                    .build();
        }

        return result;
    }

    /**
        方法二（推荐）
     */
    @AfterReturning(value = &quot;servicePointcut()&quot;, returning = &quot;result&quot;)
    public void doAfterReturning(JoinPoint joinPoint, UserVo result) {

        if (result instanceof UserVo) {
            // 模拟脱敏过程
            result.setUsername(&quot;so****ng&quot;);
            result.setEmail(&quot;**********@example.com&quot;);
            result.setFirstName(&quot;ba****ng&quot;);
            result.setLastName(&quot;s**g&quot;);
            result.setPhoneNumber(&quot;123****912&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;请求方法就可以看到返回的 Vo 已经脱敏：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/work-task/development/log-desensitization/return-vo.png&quot; alt=&quot;脱敏后的 Vo&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Linux Commands</title><link>https://songbaicheng.cc.cd/posts/linux-commands/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/linux-commands/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;常用 Linux 命令&lt;/h1&gt;
&lt;p&gt;后段开发者难免需要经常和服务器打交道，自己把经常用到的命令都统计下来方便翻阅查看。&lt;/p&gt;
&lt;h2&gt;Linux 关机重启&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 关机
shutdown -h now

# 重启
shutdown -r now
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ssh key&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C your_email@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自定义快捷指令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;alias ll=&apos;ls -alF&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;后台运行命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 后台运行,并且有nohup.out输出
nohup xxx &amp;amp;

# 后台运行, 不输出任何日志
nohup xxx &amp;gt; /dev/null &amp;amp;

# 后台运行, 并将错误信息做标准输出到日志中 
nohup xxx &amp;gt;out.log 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查看磁盘空间&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 查看根目录下文件夹大小
df

# 查看当前目录下占用磁盘空间大小前 15 的文件夹
du -ahx * | sort -rh | head -n 15
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建和查看定时任务&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 查看定时任务
crontab -l

# 编辑定时任务
crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解压缩命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 压缩目录为 tar 文件
tar zcvf xxx.tar

# 压缩目录为 zip 文件
zip -r xxx.zip

# 解压 tar 文件到当前目录
tar zxvf xxx.tar
 
# 解压 tar 到指定文件夹
tar zxvf xxx.tar -C /xxx/yyy/

# 解压 zip 到指定目录
unzip -d xxx xxx.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;vim 命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#normal模式下 g表示全局, x表示查找的内容, y表示替换后的内容
:%s/x/y/g
 
#normal模式下
0  # 光标移到行首(数字0)
$  # 光标移至行尾
shift + g # 跳到文件最后
gg # 跳到文件头
 
# 显示行号
:set nu
 
# 去除行号
:set nonu
 
# 检索
/xxx(检索内容)  # 从头检索, 按n查找下一个
?xxx(检索内容)  # 从尾部检索
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Es6</title><link>https://songbaicheng.cc.cd/posts/es6/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/es6/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;ECMAScript 6+&lt;/h1&gt;
&lt;h2&gt;知识总览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;root((ES6))
    1.声明与表达式
        let 与 const
        解构赋值
        Symbol
    2.内置对象
        新增
            Map 与 Set
            Proxy 与 Reflect
        拓展
            字符串
            数值
            对象
            数组
    3.运算符与语句
        函数
        迭代器
        class 类
        模块
    4.异步编程
        Promise 对象
        Generator 函数
        async 函数
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;ES6， 全称 ECMAScript 6.0 ，是 JavaScript 的下一个版本标准，2015.06 发版。当然新版本的出现就是为了解决旧版本的一些问题，不过更新之后感觉 JS 更像 Java 了，哈哈哈，只能说语言之间相互取其精华，去其糟粕。话不多说直接开始。&lt;/p&gt;
&lt;h3&gt;let 与 const&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;let：声明的变量只在 let 命令所在的代码块内有效，不支持变量提升，并且只能声明一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 输出十个 10
for (var i = 0; i &amp;lt; 10; i++) {
  setTimeout(function(){
    console.log(i);
  })
}

// 输出 0123456789
for (let j = 0; j &amp;lt; 10; j++) {
  setTimeout(function(){
    console.log(j);
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;const：声明一个只读的常量，一旦声明常量的值就不能改变，说明声明的同时就必须初始化。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;const PI = &quot;3.1415926&quot;;
PI  // 3.1415926
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解构赋值&lt;/h3&gt;
&lt;p&gt;解构赋值是对赋值运算符的扩展，针对数组或者对象进行模式匹配，然后对其中的变量进行赋值。&lt;/p&gt;
&lt;h4&gt;数组模型的解构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 基本
let [a, b, c] = [1, 2, 3]; // a = 1，b = 2，c = 3

// 嵌套 
let [a, [[b], c]] = [1, [[2], 3]]; // a = 1，b = 2，c = 3

// 忽略
let [a, , b] = [1, 2, 3]; // a = 1，c = 3

// 默认值
let [a = 2] = [undefined]; // a = 2

// 不完全解构
let [a = 1, b] = []; // a = 1, b = undefined

// 剩余运算符
let [a, ...b] = [1, 2, 3]; // a = 1, b = [2, 3]

// 字符串，解构的目标若为可遍历对象，皆可进行解构赋值，即实现对象的 Iterator 接口的数据。
let [a, b, c] = &apos;bye&apos;; // a = &apos;b&apos;, b = &apos;y&apos;, c = &apos;e&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;对象模型的解构&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 基本
let { foo, bar } = { foo: &apos;aaa&apos;, bar: &apos;bbb&apos; }; // foo = &apos;aaa&apos;，bar = &apos;bbb&apos;
 
let { baz : foo } = { baz : &apos;ddd&apos; }; // foo = &apos;ddd&apos;

// 可嵌套可忽略
let obj = {p: [&apos;hello&apos;, {y: &apos;world&apos;}] };
let {p: [x, { y }] } = obj; // x = &apos;hello&apos;，y = &apos;world&apos;

// 忽略
let obj = {p: [&apos;hello&apos;, {y: &apos;world&apos;}] };
let {p: [x, {  }] } = obj; // x = &apos;hello&apos;

// 不完全解构
let obj = {p: [{y: &apos;world&apos;}] };
let {p: [{ y }, x ] } = obj; // x = undefined，y = &apos;world&apos;

// 剩余运算符
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}; // a = 10，b = 20，rest = {c: 30, d: 40}

// 解构默认值
let {a = 10, b = 5} = {a: 3}; // a = 3; b = 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Symbol&lt;/h3&gt;
&lt;p&gt;一种非字符串的新数据类型 Symbol，表示独一无二的值，即使是相同参数 Symbol() 返回的值不相等，最大的用法是用来定义对象的唯一属性名。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在数据类型有：Number、String、Boolean、Object、null、undefined 和 Symbol。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;let sy = Symbol(&quot;value&quot;);
console.log(sy); // Symbol(KK)
typeof(sy); // &quot;symbol&quot;

let sy1 = Symbol(&quot;value&quot;); 
sy === sy1; // false
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用场景&lt;/h4&gt;
&lt;h5&gt;作为属性名&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;let sy = Symbol(&quot;key1&quot;);
 
// 写法1
let syObject = {};
syObject[sy] = &quot;value&quot;;
console.log(syObject);    // {Symbol(key1): &quot;kk&quot;}
 
// 写法2
let syObject = {
  [sy]: &quot;value&quot;
};
console.log(syObject);    // {Symbol(key1): &quot;kk&quot;}
 
// 写法3
let syObject = {};
Object.defineProperty(syObject, sy, {value: &quot;value&quot;});
console.log(syObject);   // {Symbol(key1): &quot;kk&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用 Symbol 定义对象唯一属性名的时候，要是用方括号获取其对应的属性值，因为.运算符后面是字符串，所以取到的是字符串 sy 属性，而 Symbol 是非字符串类型，所以获取的并不是 Symbol 的 sy。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义对象
let sy = Symbol(&quot;key1&quot;);
let syObject = {};
syObject[sy] = &quot;value&quot;;
 
syObject[sy];  // &quot;value&quot;
syObject.sy;   // undefined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Symbol 值作为属性名时，该属性是公有属性不是私有属性，可以在类的外部访问，获取方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义对象
let syObject = {};
syObject[sy] = &quot;value&quot;;
console.log(syObject);
 
for (let i in syObject) {
  console.log(i); // 无输出
}
 
Object.keys(syObject); // []
Object.getOwnPropertySymbols(syObject); // [Symbol(key1)]
Reflect.ownKeys(syObject); // [Symbol(key1)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;定义常量&lt;/h5&gt;
&lt;p&gt;在之前 ES5 定义字符串常量的时候：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const COLOR_RED = &quot;red&quot;;
const COLOR_YELLOW = &quot;yellow&quot;;
const COLOR_BLUE = &quot;blue&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的写法其实并不能保证唯一性，而现在有了 Symbol 后，可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const COLOR_RED = Symbol(&quot;red&quot;);
const COLOR_YELLOW = Symbol(&quot;yellow&quot;);
const COLOR_BLUE = Symbol(&quot;blue&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Symbol 还提供了两个方法在我们创建 Symbol 时使用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Symbol.for()：首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值，如果有即返回该 Symbol 值，若没有则新建并返回一个以该字符串参数为名称的 Symbol 值，并登记在全局环境中供搜索。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;let yellow = Symbol(&quot;Yellow&quot;);
let yellow1 = Symbol.for(&quot;Yellow&quot;);
yellow === yellow1; // false
 
let yellow2 = Symbol.for(&quot;Yellow&quot;);
yellow1 === yellow2; // true
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Symbol.keyFor()：返回一个已登记的 Symbol 类型值的 key ，用来检测该字符串参数作为名称的 Symbol 值是否已被登记。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;let yellow1 = Symbol.for(&quot;Yellow&quot;);
Symbol.keyFor(yellow1); // &quot;Yellow&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Map 与 Set&lt;/h3&gt;
&lt;h4&gt;Map&lt;/h4&gt;
&lt;p&gt;Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var myMap = new Map();
// 字符串
myMap.set(&quot;String&quot;, &quot;字符串&quot;);
// 对象
var keyObj = {};
myMap.set(keyObj, &quot;obj&quot;);
// 函数
var keyFunc = function () {};
myMap.set(keyFunc, &quot;func&quot;);
// NAN
myMap.set(NaN, &quot;not a number&quot;);
myMap; // Map(4) {&apos;String&apos; =&amp;gt; &apos;字符串&apos;, {…} =&amp;gt; &apos;obj&apos;, ƒ =&amp;gt; &apos;func&apos;, NaN =&amp;gt; &apos;not a number&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Map 的迭代&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个 Map
var myMap = new Map();
myMap.set(0, &quot;zero&quot;);
myMap.set(1, &quot;one&quot;);


for (var key of myMap.keys()) {
  console.log(key); // 将会显示两个log。 一个是 &quot;0&quot; 另一个是 &quot;1&quot;
}


for (var value of myMap.values()) {
  console.log(value); // 将会显示两个log。 一个是 &quot;zero&quot; 另一个是 &quot;one&quot;
}


for (var [key, value] of myMap) {
  console.log(key + &quot; = &quot; + value); // 将会显示两个 log。 一个是 &quot;0 = zero&quot; 另一个是 &quot;1 = one&quot;
}


myMap.forEach(function(value, key) {
  console.log(key + &quot; = &quot; + value); // 将会显示两个 logs。 一个是 &quot;0 = zero&quot; 另一个是 &quot;1 = one&quot;
}, myMap)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Map 小技巧&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// Map 与 Array的转换
var kvArray = [[&quot;key1&quot;, &quot;value1&quot;], [&quot;key2&quot;, &quot;value2&quot;]];

var myMap = new Map(kvArray); // Map 构造函数可以将一个 二维 键值对数组转换成一个 Map 对象
var outArray = Array.from(myMap); // 使用 Array.from 函数可以将一个 Map 对象转换成一个二维键值对数组

// Map 的克隆
var myMap1 = new Map([[&quot;key1&quot;, &quot;value1&quot;], [&quot;key2&quot;, &quot;value2&quot;]]);
var myMap2 = new Map(myMap1);

// Map 的合并
var first = new Map([[1, &apos;one&apos;], [2, &apos;two&apos;], [3, &apos;three&apos;],]);
var second = new Map([[1, &apos;uno&apos;], [2, &apos;dos&apos;]]);
 
var merged = new Map([...first, ...second]); // 合并两个 Map 对象时，如果有重复的键值，则后面的会覆盖前面的，对应值即 uno，dos， three
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Set&lt;/h4&gt;
&lt;p&gt;Set 对象允许你存储任何类型的唯一值，无论是原始值或者是对象引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mySet = new Set();
 
mySet.add(1); // Set(1) {1}
mySet.add(5); // Set(2) {1, 5}
mySet.add(5); // Set(2) {1, 5}
mySet.add(&quot;some text&quot;); // Set(3) {1, 5, &quot;some text&quot;} 这里体现了类型的多样性
var o = {a: 1, b: 2}; 
mySet.add(o);
mySet.add({a: 1, b: 2}); // Set(5) {1, 5, &quot;some text&quot;, {…}, {…}} 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Set 小技巧&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;// 去重
var arr = [1, 2, 3, 4, 4];
var mySet = new Set(arr);
[...mySet]; // [1, 2, 3, 4]

// 并集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var union = new Set([...a, ...b]); // {1, 2, 3, 4}

// 交集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var intersect = new Set([...a].filter(x =&amp;gt; b.has(x))); // {2, 3}

// 差集
var a = new Set([1, 2, 3]);
var b = new Set([4, 3, 2]);
var difference = new Set([...a].filter(x =&amp;gt; !b.has(x))); // {1}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Reflect 与 Proxy&lt;/h3&gt;
&lt;h4&gt;Proxy&lt;/h4&gt;
&lt;p&gt;可以对目标对象的读取、函数调用等操作进行拦截，然后进行操作处理。它不直接操作对象，而是像代理模式，通过对象的代理对象进行操作，在进行这些操作时，可以添加一些需要的额外操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let target = {
    name: &apos;Tom&apos;,
    age: 24
}
let handler = {
    get: function(target, key) {
        console.log(&apos;getting &apos;+key);
        return target[key]; // 不是target.key
    },
    set: function(target, key, value) {
        console.log(&apos;setting &apos;+key);
        target[key] = value;
    }
}

let proxy = new Proxy(target, handler)
proxy.name     // 实际执行 handler.get
proxy.age = 25 // 实际执行 handler.set
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Reflect&lt;/h4&gt;
&lt;p&gt;可以用于获取目标对象的行为，它与 Object 类似，但是更易读，为操作对象提供了一种更优雅的方式。它的方法与 Proxy 是对应的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义对象
let exam = {
    name: &quot;Tom&quot;,
    age: 24,
    get info(){
        return this.name + this.age;
    }
}

Reflect.get(exam, &apos;name&apos;); // &quot;Tom&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;组合使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 定义一个对象
let exam = {
    name: &quot;Tom&quot;,
    age: 24
}
// 定义拦截方法
let handler = {
    get: function(target, key){
        console.log(&quot;getting &quot; + key);
        return Reflect.get(target, key);
    },
    set: function(target, key, value){
        console.log(&quot;setting &quot; + key + &quot; to &quot; + value)
        Reflect.set(target, key, value);
    }
}
let proxy = new Proxy(exam, handler)
proxy.name = &quot;Jerry&quot;
proxy.name // &quot;Jerry&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;函数&lt;/h3&gt;
&lt;p&gt;这里我们主要介绍一下&lt;strong&gt;箭头函数&lt;/strong&gt;，它提供了一种更加简洁的函数书写方式，基本语法是：参数 =&amp;gt; 函数体。并且箭头函数体中的 this 对象，是定义函数时的对象，而不是使用函数时的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常
var Person = {
    &apos;age&apos;: 18,
    &apos;sayHello&apos;: function () {
      setTimeout(function () {
        console.log(this.age);
      });
    }
};
var age = 20;
Person.sayHello();  // 20
 
 // 箭头函数
var Person1 = {
    &apos;age&apos;: 18,
    &apos;sayHello&apos;: function () {
      setTimeout(()=&amp;gt;{
        console.log(this.age);
      });
    }
};
var age = 20;
Person1.sayHello();  // 18
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Class 类&lt;/h3&gt;
&lt;p&gt;在ES6中，class (类)作为对象的模板被引入，可以通过 class 关键字定义类。class 的本质是 function，它可以看作一个语法糖，让对象原型的写法更加清晰、更像面向对象编程的语法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 匿名类
let Example = class {
    constructor(a) {
        this.a = a;
    }
}
// 命名类
let Example = class Example {
    constructor(a) {
        this.a = a;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意：类不可以重复声明；类定义不会被提升，这意味着，必须在访问前对类进行定义，否则就会报错；类中方法不需要 function 关键字，方法间也不能加分号；类的实例化需要 new 关键字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Example {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        console.log(&apos;Example&apos;);
    }
    sum() {
        return this.a + this.b;
    }
}
let exam1 = new Example(2, 1);
let exam2 = new Example(3, 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ES6 的类也有类似封装和继承的概念，不过类中的 getter 与 setter 必须同级出现。通过 extends 实现类的继承，子类 constructor 方法中必须有 super ，且必须出现在 this 之前。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Father {
    constructor(){}
    // 或者都放在子类中
    get a() {
        return this._a;
    }
    set a(a) {
        this._a = a;
    }
}
class Child extends Father {
    constructor(){
        super();
    }
}
let test1 = new Child();
test1.a = 2;
console.log(test1.a); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;模块&lt;/h3&gt;
&lt;p&gt;ES6 引入了模块化，分为导出（export） @与导入（import）两个模块，其设计思想是在编译时就能确定模块的依赖关系，以及输入和输出的变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 正常导入导出
// 导出
let myName = &quot;Tom&quot;;
export { myName as exportName }
// 导入
import { exportName } from &quot;./test.js&quot;;
console.log(exportName);// Tom

// export default
// 导出
var a = &quot;My name is Tom!&quot;;
export default a; // export default 仅有一个

// 导入
import b from &quot;./xxx.js&quot;; // 不需要加{}， 使用任意变量接收
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Promise 对象&lt;/h3&gt;
&lt;p&gt;Promise 对象是异步编程的一种解决方案，从它可以获取异步操作的消息。Promise 异步操作有三种状态：pending（进行中）、fulfilled（已成功）和 rejected（已失败），而且只有从 pending 变为 fulfilled 和从 pending 变为 rejected 的状态改变。只要处于 fulfilled 和 rejected ，状态就不会再改变了。&lt;/p&gt;
&lt;p&gt;Promise 对象往往搭配 then 方法使用，then 方法接收两个函数作为参数，第一个参数是 Promise 执行成功时的回调，第二个参数是 Promise 执行失败时的回调，两个函数只会有一个被调用。搭配使用的时候要遵守链式编程的规则，保持扁平化，不要嵌套 Promise。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const p = new Promise(function(resolve,reject){
  resolve(1);
}).then(function(value){ // 第一个then // 1
  console.log(value);
  return value * 2;
}).then(function(value){ // 第二个then // 2
  console.log(value);
}).then(function(value){ // 第三个then // undefined
  console.log(value);
  return Promise.resolve(&apos;resolve&apos;); 
}).then(function(value){ // 第四个then // resolve
  console.log(value);
  return Promise.reject(&apos;reject&apos;); 
})
.then(
  function(value){ 
    // 第五个then // reject:reject !!!这里不会打印,因为上一个then方法返回的是一个reject状态的promise
    console.log(&apos;resolve:&apos; + value);
  }, 
  function(err) {
    // 此行会打印, 因第五个than只能接受 resolve状态的promise, 而第四个than返回的是reject状态的promise
    // 所以会被本行 err 捕获
    console.log(&apos;reject:&apos; + err);
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generator 函数&lt;/h3&gt;
&lt;p&gt;ES6 新引入了 Generator 函数，可以通过 yield 关键字，把函数的执行流挂起，为改变执行流程提供了可能，从而为异步编程提供解决方案。&lt;/p&gt;
&lt;p&gt;Generator 有两个区分于普通函数的部分：在 function 后面，函数名之前有个 * ；函数内部有 yield 表达式。调用 Generator 函数和调用普通函数一样，在函数名后面加上()即可，但是 Generator 函数不会像普通函数一样立即执行，而是返回一个指向内部状态对象的指针，所以要调用遍历器对象Iterator 的 next 方法，指针就会从函数头部或者上一次停下来的地方开始执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function* func(){
 console.log(&quot;one&quot;);
 yield &apos;1&apos;;
 console.log(&quot;two&quot;);
 yield &apos;2&apos;; 
 console.log(&quot;three&quot;);
 return &apos;3&apos;;
}

let f = func();
f.next();
// one
// {value: &quot;1&quot;, done: false}
 
f.next();
// two
// {value: &quot;2&quot;, done: false}
 
f.next();
// three
// {value: &quot;3&quot;, done: true}
 
f.next();
// {value: undefined, done: true}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;async 函数&lt;/h3&gt;
&lt;p&gt;看都没看明白，等我用明白了再说&lt;/p&gt;
</content:encoded></item><item><title>Linear List</title><link>https://songbaicheng.cc.cd/posts/linear-list/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/linear-list/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;线性表&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 java-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;root(线性表)
    顺序存储
        顺序表
    链式存储
        借助指针
            单链表
            双链表
            循环链表
        借助数组
            静态链表
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本定义&lt;/h2&gt;
&lt;p&gt;线性表是具有相同数据结构的n(n ≥ 0)个数据元素的有限序列，其中n为表长，为0即为空表。一般标识为：L=(a~1~,a~2~,a~3~,a~i~,a~i+1~,……,a~n~,)，其中a~1~是唯一的第一个元素，被称为表头元素；a~n~是唯一一个最后一个元素，被称为表尾元素。除了第一个元素外，每个元素有且仅有一个直接前驱，除最后一个外，每个元素有且仅有一个直接后继，这种线性有序的表被称为线性表。线性表有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表中元素个数有限。&lt;/li&gt;
&lt;li&gt;表中元素具有逻辑上的顺序性，表中元素有先后顺序。&lt;/li&gt;
&lt;li&gt;表中元素都是数据结构，每个元素都是单个结构。&lt;/li&gt;
&lt;li&gt;表中的元素类型数据都相同，这意味着每个元素所占相同大小的空间。&lt;/li&gt;
&lt;li&gt;表中的元素具有抽象性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;initList：初始化表，构造一个空的线性表。&lt;/li&gt;
&lt;li&gt;length：求表长，返回线性表的长度即其中的元素个数。&lt;/li&gt;
&lt;li&gt;locationElem：按值查找元素。&lt;/li&gt;
&lt;li&gt;getElem：按位查找元素。&lt;/li&gt;
&lt;li&gt;listInsert：插入操作。&lt;/li&gt;
&lt;li&gt;listDelete：删除操作。&lt;/li&gt;
&lt;li&gt;printList：输出操作。&lt;/li&gt;
&lt;li&gt;empty：判空操作。&lt;/li&gt;
&lt;li&gt;destoryList：销毁操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;线性表的顺序表示&lt;/h2&gt;
&lt;h3&gt;顺序表的定义&lt;/h3&gt;
&lt;p&gt;线性表的顺序存储又叫顺序表，使用一组地址连续的存储单元依次存储线性表中的的数据元素，从而使逻辑上相邻的两个元素在物理位置上也相邻。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/linear-list/order-list.jpg&quot; alt=&quot;顺序表的存储结构&quot; title=&quot;顺序表的存储结构&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;顺序表的实现&lt;/h3&gt;
&lt;p&gt;::: normal-demo Java 实现顺序表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.Arrays;
import java.util.Iterator;

/**
 * @description 底层用数组实现的顺序表
 */
public class SequenceList&amp;lt;T&amp;gt; implements Iterable&amp;lt;T&amp;gt; {

    /**
     * 存储元素的数组
     */
    private T[] elements;
    /**
     * 当前顺序表中元素个数
     */
    private int size;

    /**
     * 初始化顺序表大小
     *
     * @param capacity 最大存储元素个数
     */
    @SuppressWarnings(&quot;unchecked&quot;)
    SequenceList(int capacity) {
        elements = (T[]) new Object[capacity];
        this.size = 0;
    }

    /**
     * 清空顺序表
     * 底层也是便利数组依次赋值，所以时间复杂度为O(n)。
     */
    public void clear() {
        size = 0;
    }

    /**
     * 顺序表是否为空
     * 直接返回size值，所以时间复杂度为O(1)。
     *
     * @return 是否为空
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 获取顺序表长度
     * 直接返回size值，所以时间复杂度为O(1)
     *
     * @return 顺序表长度
     */
    public int length() {
        return size;
    }

    /**
     * 根据下标获得元素
     * 顺序表的特点就是随机访问，直接返回根据下标返回数组元素，所以时间复杂度为O(1)。
     *
     * @param i 元素下标
     * @return 下标对应元素
     */
    public T get(int i) {

        // 安全性校验
        if (i &amp;lt; 0 || i &amp;gt;= size) {
            return null;
        }

        return elements[i];
    }

    /**
     * 向顺序表中插入元素，如果顺序表已满则进行扩容操作
     * 直接根据size位置插入，所以时间复杂度为O(1)。
     *
     * @param e 待插入元素
     */
    public void insert(T e) {

        // 数组扩容
        if (size &amp;gt;= elements.length) {
            // 扩容操作，这里乘2处理
            reSize(2 * elements.length);
        }

        elements[size++] = e;
    }

    /**
     * 重制顺序表大小
     */
    private void reSize(int newSize) {
        elements = Arrays.copyOf(elements, newSize);
    }

    /**
     * 往指定下标插入元素
     * 直接根据size位置插入，所以时间复杂度为O(1)。
     *
     * @param target 目标下标
     * @param e      待插入元素
     */
    public void update(int target, T e) {

        // 安全性校验
        if (target &amp;lt; 0 || target &amp;gt;= size) {
            return;
        } else if (size &amp;gt;= elements.length) {
            return;
        }

        // 将target下标之后的元素向后移动一位
        System.arraycopy(elements, target, elements, target + 1, size - target);

        elements[target] = e;
        size++;
    }

    /**
     * 删除指定下标元素
     *
     * @param target 目标下标
     */
    public void remove(int target) {

        // 安全性校验
        if (target &amp;lt; 0 || target &amp;gt;= size) {
            return;
        }

        // 将target下标之后的元素向前移动一位
        if (size - 1 - target &amp;gt;= 0) {
            System.arraycopy(elements, target + 1, elements, target, size - 1 - target);
        }

        elements[size] = null;
        size--;
    }

    /**
     * 返回待查找元素首次出现下标
     *
     * @param e 待查找元素
     * @return 待查找元素首次出现下标
     */
    public int indexOf(T e) {

        for (int i = 0; i &amp;lt; size; i++) {
            if (elements[i].equals(e)) {
                return i;
            }
        }

        return -1;
    }

    @Override
    public Iterator&amp;lt;T&amp;gt; iterator() {
        return new SequenceListIterator();
    }

    private class SequenceListIterator implements Iterator&amp;lt;T&amp;gt; {

        /**
         * 迭代器指针
         */
        private int currentIndex = 0;

        @Override
        public boolean hasNext() {
            return currentIndex &amp;lt; size;
        }

        @Override
        public T next() {
            return elements[currentIndex++];
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException(&quot;remove operation is not supported&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;线性表的链式表示&lt;/h2&gt;
&lt;h3&gt;单链表&lt;/h3&gt;
&lt;p&gt;线性表的链式存储又被称为单链表，指通过一组任意的存储单元来存储线性表中的数据元素，为了建立数据元素之间的线性关系，对于每个额链表的节点，除了存放元素自身外，还需要存放一个指向其后继的指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LNode&amp;lt;T&amp;gt; {

    /**
     * 当前节点的值
     */
    public T data;
    /**
     * 下一个节点的指针
     */
    public LNode&amp;lt;T&amp;gt; next;

    /**
     * 初始化节点
     * @param data 节点的值
     */
    LNode (T data) {
        this.data = data;
        this.next = null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然单链表解决了顺序表需要大量连续存储单元的缺点，但是也因为存储附加指针域倒是浪费存储空间，正是是由于过于分散的存储，所以单链表是非随机存取的存储结构。&lt;/p&gt;
&lt;p&gt;我们通常用&lt;strong&gt;头指针&lt;/strong&gt;来标识一个单链表，如果头指针为Null，则表示一个空表。注意，我们刚才提到的是&lt;strong&gt;头指针&lt;/strong&gt;，与此相区别的定义是&lt;strong&gt;头结点&lt;/strong&gt;，头结点是为了方便操作而在单链表第一个结点之前附加的一个结点，头结点的数据域可以不设任何信息，也可以记录表长等信息，而指针域则必须指向线性表的第一个元素结点。引入头结点之后有两个优点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于第一个数据结点的位置被存放在头结点的指针域中，因此在链表的第一个位置上的操作和在表中其他位置的操作一致。&lt;/li&gt;
&lt;li&gt;无论链表是否为空，其头指针都是只想头结点的非空指针，所以空表和非空表得到了统一。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;头插法&lt;/h4&gt;
&lt;p&gt;::: normal-demo 头插法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * 没有头结点的头插法
  * 每个结点的插入时间为O(1)，所以整条单链表的时间负责度为O(n)
  */
@Test
void headInsertNoHead() {
    LNode&amp;lt;Integer&amp;gt; head = null;

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head;
        head = node;
    }

    while (head != null) {
        System.out.println(head.data); // 9 8 7 6 5 4 3 2 1 0
        head = head.next;
    }
}

/**
  * 有头结点的头插法
  * 每个结点的插入时间为O(1)，所以整条单链表的时间负责度为O(n)
  */
@Test
void headInsertWithHead() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    while (head != null) {
        System.out.println(head.data); // null 9 8 7 6 5 4 3 2 1 0
        head = head.next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;尾插法&lt;/h4&gt;
&lt;p&gt;::: normal-demo 尾插法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * 有头结点的尾插法
  * 每个结点的插入时间为O(1)，所以整条单链表的时间负责度为O(n)
  */
@Test
void tailInsertWithHead() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);
    // 方便每次找到插入点，创建尾结点指针
    LNode&amp;lt;Integer&amp;gt; listFlag = head;

    for (int i = 0; i &amp;lt; 10; i++) {
        listFlag.next = new LNode&amp;lt;&amp;gt;(i);
        listFlag = listFlag.next;
    }

    listFlag.next = null;

    while (head != null) {
        System.out.println(head.data); // null 0 1 2 3 4 5 6 7 8 9
        head = head.next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;查找第i个结点&lt;/h4&gt;
&lt;p&gt;::: normal-demo 单链表查找第i个结点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 查找第target个结点，假设有头结点
* 需要从头遍历单链表，时间负责度为O(n)
*/
private &amp;lt;T&amp;gt; LNode&amp;lt;T&amp;gt; getById(LNode&amp;lt;T&amp;gt; head, int target) {

    // 安全性校验
    if (target &amp;lt; 0) {
        return null;
    }

    LNode&amp;lt;T&amp;gt; currNode = head.next;
    int flag = 1;

    while (currNode != null &amp;amp;&amp;amp; flag++ &amp;lt; target) {
        currNode = currNode.next;
    }

    return currNode;
}

@Test
void getByIdTest() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    LNode&amp;lt;Integer&amp;gt; lNode1 = getById(head, 5);
    assertNotNull(lNode1);
    System.out.println(lNode1.data); // 5
    LNode&amp;lt;Integer&amp;gt; lNode2 = getById(head, 11);
    assertNotNull(lNode2, &quot;结点不存在！&quot;);
    System.out.println(lNode2.data); // 结点不存在！
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;查找某个元素结点&lt;/h4&gt;
&lt;p&gt;::: normal-demo 返回链表中第一个目标元素结点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 返回第一个element结点，假设有头结点
* 需要从第一个节点开始遍历，时间复杂度为O(n)
*/
private &amp;lt;T&amp;gt; LNode&amp;lt;T&amp;gt; getElement(LNode&amp;lt;T&amp;gt; head, T element) {

    LNode&amp;lt;T&amp;gt; currNode = head.next;

    while (currNode != null &amp;amp;&amp;amp; currNode.data != element) {
        currNode = currNode.next;
    }

    return currNode;
}

@Test
void getElementTest() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    LNode&amp;lt;Integer&amp;gt; lNode1 = getElement(head, 5);
    assertNotNull(lNode1);
    System.out.println(lNode1.data); // 5
    LNode&amp;lt;Integer&amp;gt; lNode2 = getElement(head, 19);
    assertNotNull(lNode2, &quot;结点不存在！&quot;);
    System.out.println(lNode2.data); // 结点不存在！
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;插入结点&lt;/h4&gt;
&lt;p&gt;::: normal-demo 插入结点操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 前插法插入结点
* 需要从第一个节点开始遍历，时间复杂度为O(n)
* @param head 单链表
* @param target 插入位置
* @param element 待插入元素
* @param &amp;lt;T&amp;gt; 待定元素
*/
private &amp;lt;T&amp;gt; void frontInsertNode(LNode&amp;lt;T&amp;gt; head, int target, T element) {

    // 获取前置结点
    LNode&amp;lt;T&amp;gt; node = getById(head, target - 1);

    if (node != null) {
        LNode&amp;lt;T&amp;gt; addNode = new LNode&amp;lt;&amp;gt;(element);
        addNode.next = node.next;
        node.next = addNode;
    }
}

/**
* 后插法插入结点
* 需要从第一个节点开始遍历，时间复杂度为O(n)
* @param head 单链表
* @param target 插入位置
* @param element 待插入元素
* @param &amp;lt;T&amp;gt; 待定元素
*/
private &amp;lt;T&amp;gt; void backInsertNode(LNode&amp;lt;T&amp;gt; head, int target, T element) {

    // 获取目标结点
    LNode&amp;lt;T&amp;gt; node = getById(head, target);

    if (node != null) {
        LNode&amp;lt;T&amp;gt; addNode = new LNode&amp;lt;&amp;gt;(element);
        addNode.next = node.next;
        node.next = addNode;
        addNode.data = node.data;
        node.data = element;
    }
}

@Test
void insertTest() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    frontInsertNode(head, 5, 11);

    while (head != null) {
        System.out.println(head.data);  // null 9 8 7 6 11 5 4 3 2 1 0
        head = head.next;
    }

    // 清空链表
    head = new LNode&amp;lt;&amp;gt;(null);;
    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    backInsertNode(head, 5, 11);

    while (head != null) {
        System.out.println(head.data); // null 9 8 7 6 11 5 4 3 2 1 0
        head = head.next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;删除结点&lt;/h4&gt;
&lt;p&gt;::: normal-demo 删除目标结点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 前删法删除目标结点
* @param head 单链表
* @param target 目标顺序
* @param &amp;lt;T&amp;gt; 元素类型
*/
private &amp;lt;T&amp;gt; void frontDeleteNode(LNode&amp;lt;T&amp;gt; head, int target) {

    // 安全性校验
    if (target &amp;lt; 0) {
        return;
    }

    LNode&amp;lt;T&amp;gt; node = getById(head, target - 1);
    node.next = node.next.next;
}

/**
* 后删法删除目标结点
* 需要从第一个节点开始遍历，时间复杂度为O(n)
* @param head 单链表
* @param target 目标顺序
* @param &amp;lt;T&amp;gt; 元素类型
*/
private &amp;lt;T&amp;gt; void backDeleteNode(LNode&amp;lt;T&amp;gt; head, int target) {

    // 安全性校验
    if (target &amp;lt; 0) {
        return;
    }

    LNode&amp;lt;T&amp;gt; node = getById(head, target);
    node.data = node.next.data;
    node.next = node.next.next;
}

@Test
void deleteNodeTest() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    frontDeleteNode(head, 5);

    while (head != null) {
        System.out.println(head.data); // null 9 8 7 6 4 3 2 1 0
        head = head.next;
    }

    head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    frontDeleteNode(head, 5);

    while (head != null) {
        System.out.println(head.data); // null 9 8 7 6 4 3 2 1 0
        head = head.next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;求表长&lt;/h4&gt;
&lt;p&gt;::: normal-demo 求表长&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 求链表长度
* 需要从第一个节点开始遍历，时间复杂度为O(n)
* @param head 单链表
* @param &amp;lt;T&amp;gt; 元素类型
* @return 链表长度
*/
private &amp;lt;T&amp;gt; int getListLength(LNode&amp;lt;T&amp;gt; head) {

    int length = 0;

    while (head.next != null) {
        head = head.next;
        length++;
    }

    return length;
}

@Test
void getListLengthTest() {
    LNode&amp;lt;Integer&amp;gt; head = new LNode&amp;lt;&amp;gt;(null);

    for (int i = 0; i &amp;lt; 10; i++) {
        LNode&amp;lt;Integer&amp;gt; node = new LNode&amp;lt;&amp;gt;(i);
        node.next = head.next;
        head.next = node;
    }

    System.out.println(getListLength(head)); // 10
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;双链表&lt;/h3&gt;
&lt;p&gt;为了克服访问单链表访问结点需要从头遍历的问题，引入了双链表的概念，双链表结点中有两个指针 prior 和 next，分别指向了前驱和后继结点，有了这两个指针的存在，插入和删除操作的时间复杂度仅为O(1)。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里说的“插入和删除操作的时间复杂度仅为O(1)”只是说插入和删除这个节点的过程复杂度为O(1)，而并不包括找到这个节点的过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class DNode&amp;lt;T&amp;gt; {

    /**
     * 当前节点的值
     */
    public T data;
    /**
     * 前驱指针
     */
    public DNode&amp;lt;T&amp;gt; prior;
    /**
     * 后继指针
     */
    public DNode&amp;lt;T&amp;gt; next;

    /**
     * 初始化节点
     * @param data 节点的值
     */
    DNode (T data) {
        this.data = data;
        this.prior = null;
        this.next = null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;双链表的插入&lt;/h4&gt;
&lt;p&gt;::: normal-demo 双链表的插入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 双链表插入元素，有头结点
* @param head 双链表
* @param target 目标位置
* @param element 待插入元素
* @param &amp;lt;T&amp;gt; 元素类型
*/
private &amp;lt;T&amp;gt; void insertNode(DNode&amp;lt;T&amp;gt; head, int target, T element) {

    // 安全性校验
    if (target &amp;lt; 0) {
        return;
    }

    DNode&amp;lt;T&amp;gt; flagNode = head.next;

    for (int i = 1; i &amp;lt; target; i++) {
        flagNode = flagNode.next;
    }

    if (flagNode == null || flagNode.next == null) {
        return;
    }

    DNode&amp;lt;T&amp;gt; newNode = new DNode&amp;lt;&amp;gt;(element);
    flagNode.next.prior = newNode;
    newNode.next = flagNode.next;
    newNode.prior = flagNode;
    flagNode.next = newNode;
}

@Test
void insertTest() {

    DNode&amp;lt;Integer&amp;gt; head = new DNode&amp;lt;&amp;gt;(null);
    DNode&amp;lt;Integer&amp;gt; flagNode = head;

    for (int i = 0; i &amp;lt; 5; i++) {
        DNode&amp;lt;Integer&amp;gt; newNode = new DNode&amp;lt;&amp;gt;(i);

        flagNode.next = newNode;
        newNode.prior = flagNode;
        flagNode = flagNode.next;
    }

    insertNode(head, 3, 9);

    while (head != null) {
        System.out.println(head.data); // null 0 1 2 9 3 4
        head = head.next;
    }
}
:::

#### 双链表的删除
::: normal-demo 双链表的删除
```java
/**
* 双链表删除元素，有头结点
* @param head 双链表
* @param target 目标位置
* @param &amp;lt;T&amp;gt; 元素类型
*/
private &amp;lt;T&amp;gt; void deleteNode(DNode&amp;lt;T&amp;gt; head, int target) {

    // 安全性校验
    if (target &amp;lt; 0) {
        return;
    }

    DNode&amp;lt;T&amp;gt; flagNode = head.next;

    for (int i = 1; i &amp;lt; target; i++) {
        flagNode = flagNode.next;
    }

    if (flagNode == null || flagNode.next == null) {
        return;
    }

    flagNode.next = flagNode.next.next;
    flagNode.next.prior = flagNode;
}

@Test
void insertTest() {

    DNode&amp;lt;Integer&amp;gt; head = new DNode&amp;lt;&amp;gt;(null);
    DNode&amp;lt;Integer&amp;gt; flagNode = head;

    for (int i = 0; i &amp;lt; 5; i++) {
        DNode&amp;lt;Integer&amp;gt; newNode = new DNode&amp;lt;&amp;gt;(i);

        flagNode.next = newNode;
        newNode.prior = flagNode;
        flagNode = flagNode.next;
    }

    deleteNode(head, 2);

    while (head != null) {
        System.out.println(head.data); // null 0 2 3 4
        head = head.next;
    }
}
:::

### 循环链表
#### 循环单链表
循环单链表和单链表的区别就是最后一个结点的指针不是 Null 而是改为指向头指针，从而使链表形成一个环状。这个时候的判空操作就是头结点的指针是否为头结点。

因为循环单链表是一个“环”，因此在任何一个位置上的插入和删除都是等价的，无需判断表尾，而且每次操作无需寻找表头，可以从任意结点开始遍历。有时对循环单链表不设置头指针而设置尾指针，因为设置尾指针可以直用next找到头指针并且可以直接在队尾插入元素，省去了在头指针遍历的麻烦。

#### 循环双链表
照葫芦画瓢，循环双链表即为头结点的prior指向尾结点，尾结点的next指向头结点。这时的判空条件为头结点的prior和next都为本身。

### 静态链表
静态链表借助数组来描述线性表的链式存储结构，结点也有数据域和指针域，不过这里的指针用数组的下标来代替，又称为游标，因为是数组的原因，静态链表也需要一块连续的内存空间。

![静态链表存储示意图](/assets/images/study/computer-basis/ads/data-structure/linear-list/static-list.jpg &quot;静态链表存储示意图&quot;)

## 顺序表和链表的比较
| 比较方面 | 顺序表 | 链表 |
| --- | --- | --- |
| 存取方式 | 随机存取 | 只能从表头开始顺序存取 |
| 逻辑结构与物理结构 | 逻辑相邻物理也相邻 | 逻辑相邻但物理不一定相邻 |
| 删查找操作 | 插入删除慢，查找快 | 删除插入快，查找慢 |
| 空间分配 | 密度大，但是对连续存储空间要求高 | 密度小，但操作灵活高效 |

总之，两种存储结构各有长短，选择哪一种有实际问题的主要因素决定。通常较稳定的线性表选择顺序存储，而频繁进行插入删除操作的线性表宜选择链式存储。&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Nginx</title><link>https://songbaicheng.cc.cd/posts/nginx/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/nginx/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Nginx 基础&lt;/h1&gt;
&lt;p&gt;Nginx七大核心应用场景：反向代理、虚拟主机、域名解析、负载均衡、防盗链、url重定向、https。&lt;/p&gt;
&lt;p&gt;常见版本如下：
:::card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Nginx 开源官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/nginx/nginx.svg
link: https://nginx.org/en/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Nginx 官方商业版本(F5)
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/nginx/f5.svg
link: https://nginx.com/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: OpenResty 开源官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/nginx/openresty.png
link: https://openresty.com.cn/cn/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Tengine 开源官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/maintenance/nginx/tengine.png
link: http://tengine.taobao.org
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;这里拿 Nginx 官方开源版作为示例，首先你得需要一台 Linux 机器并下载 Nginx 二进制包。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/nginx/nginx-install.png&quot; alt=&quot;准备安装包&quot; title=&quot;准备安装包&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 &lt;code&gt;./configure --prefix=/usr/local/nginx&lt;/code&gt; 执行 &lt;code&gt;configure&lt;/code&gt; 可执行文件，如果需要 &lt;code&gt;gcc&lt;/code&gt; 依赖请先下载，如果执行期间有其他缺少的依赖补充后重新执行脚本即可。&lt;/li&gt;
&lt;li&gt;配置完成后依次执行 &lt;code&gt;make&lt;/code&gt; 和 &lt;code&gt;make install&lt;/code&gt; 开始安装，注意可能需要 root 权限。&lt;/li&gt;
&lt;li&gt;安装完成后就可以在 /usr/local/nginx 目录下看到我们安装的 Nginx 目录了，可以执行 /sbin 目录下的 nginx 脚本进行启动 nginx 服务，访问 linux 主机 ip 看是否能够启动成功，可以通过 &lt;code&gt;./nginx -s stop&lt;/code&gt; 关闭 Nginx。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;优雅起停 Nginx&lt;/h2&gt;
&lt;p&gt;首先我们要先知道几个常见的起停 Nginx 的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动 Nginx
./nginx

# 快速暴力停止 Nginx
./nginx -s stop

# 优雅停止 Nginx
./nginx -s quit

# 重新加载配置
./nginx -s reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而平时我们真正生产上使用服务器启动和关闭 Nginx 不会总是每次都找到此执行文件执行的，所以我们需要把起停命令添加到 systemctl 来方便我们直接起停，我们可以在 /usr/lib/systemd/system 目录下添加 nginx.service 这个文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit] 
Description=nginx
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
ExecQuit=/usr/local/nginx/sbin/nginx -s quit 
PrivateTmp=true
   
[Install]   
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建完成之后执行 &lt;code&gt;systemctl daemon-reload&lt;/code&gt; 加载一下添加的配置，现在我们就可以使用 systemctl 来控制 Nginx 的起停了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动 Nginx
systemctl start nginx.service

# 查看 Nginx 状态
systemctl status nginx.service

# 停止 Nginx
systemctl stop nginx.service

# 设为开机启动
systemctl enable nginx.service

# 关闭开机启动
systemctl disable nginx.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;认识目录&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;- nginx
    - conf # 配置目录
    - html # 静态资源和界面
    - logs # 日志
    - sbin # 主进程文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;工作模式&lt;/h2&gt;
&lt;p&gt;Nginx在启动的时候会采用多进程的方式，产生 master 和 worker 两种进程进行工作，master 负责统一协调 worker 进程的工作调度，而真正工作的都是一个个的 worker 进程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/nginx/nginx-worker.jpg&quot; alt=&quot;多进程的工作模式&quot; title=&quot;多进程的工作模式&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;配置文件详解&lt;/h2&gt;
&lt;p&gt;::: normal-demo nginx.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#user  nobody;
worker_processes  1; # 工作进程个数

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types; # 引用其他的配置文件，这里的mime是告知浏览器请求文件类型解析方式的配置
    default_type  application/octet-stream; # 默认文件通过流的方式处理

    #log_format  main  &apos;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &apos;
    #                  &apos;$status $body_bytes_sent &quot;$http_referer&quot; &apos;
    #                  &apos;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&apos;;

    #access_log  logs/access.log  main;

    sendfile        on; # 文件零拷贝
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65; # 长链接时长

    #gzip  on;

    server { # 一个虚拟主机配置
        listen       80; # 端口
        server_name  localhost; # 可解析的域名、主机名

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html; # 资源访问地址
            index  index.html index.htm; # 默认页
        }

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache&apos;s document root
        # concurs with nginx&apos;s one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;自定义虚拟主机域名&lt;/h2&gt;
&lt;p&gt;在我们简单了解了 nginx.conf 文件后，我们可以看到其中每个 http 下的 server 就是一个虚拟主机，这里我们模拟一个新的虚拟主机通过不同端口访问不同的资源。&lt;/p&gt;
&lt;p&gt;::: normal-demo 通过端口自定义虚拟主机&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
        listen       88; # 端口
        server_name  localhost; # 可解析的域名、主机名

        location / {
            root   my-html; # 资源访问地址
            index  index.html index.htm; # 默认页
        }

        error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
        location = /50x.html {
            root   html;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;一样的，如果我们想通过不同的域名访问不同的资源，我们可以修改 server_name 来区分资源路径。&lt;/p&gt;
&lt;p&gt;::: normal-demo 通过域名自定义虚拟主机&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
        listen       80; # 端口
        server_name  www.XXX.com; # 可解析的域名、主机名

        location / {
            root   my-html; # 资源访问地址
            index  index.html index.htm; # 默认页
        }

        error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
        location = /50x.html {
            root   html;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;虚拟主机域名匹配规则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;完整匹配，如果配置的是完整的域名，则按照完整的域名进行匹配，如果配置了多个，则按照从上到下的优先级来决定。server_name 配置项后可以跟多个域名，用空格隔开区分。&lt;/li&gt;
&lt;li&gt;通配符匹配，可以通过*来匹配域名。&lt;/li&gt;
&lt;li&gt;正则匹配，一般用于二级域名来使用，通过正则表达式来匹配域名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;反向代理&lt;/h2&gt;
&lt;p&gt;反向代理不是什么高不可攀的东西，如果想理解反向代理我们必须结合正向代理一起理解，一句话来说就是站在用户角度对后台服务器是否可见，如果是正向代理，就好像我们科学上网，配置一台代理服务器访问海外的服务器，我们是知道海外服务器ip地址的，这就是正向，同理，Nginx 作为网关入口，往往是和内网的后台服务器配合，用户访问 Nginx 看不到真实的后台服务器，所以 Nginx 的代理实现是反向的。&lt;/p&gt;
&lt;p&gt;Nginx 作为反向代理服务器的时候，设计往往是隧道式的，即所有的请求都必须从 Nginx 进入，这就是所谓的隧道式的含义，而如果是一些下载的请求，返回的数据和进入的请求竞争带宽则非常影响性能，所以就有了更高性能的 lvs 来做负载均衡，或者是内网后台服务器只允许进入走 Nginx 代理，而发送则接入外围网管直接和请求方通讯这种方式。&lt;/p&gt;
&lt;p&gt;而我们开启代理非常简单，只需要我们在 server 中 location 下编辑 proxy_pass 即可实现代理跳转。&lt;/p&gt;
&lt;p&gt;::: normal-demo 代理服务器配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
        listen       88; # 端口
        server_name  localhost; # 可解析的域名、主机名

        location / {
            proxy_pass   http://www.baidu.com; # 代理服务器
            # root   /usr/local/nginx/my-html; # 资源访问地址
            # index  index.html index.htm; # 默认页
        }

        error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
        location = /50x.html {
            root   html;
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;负载均衡&lt;/h2&gt;
&lt;h3&gt;轮训&lt;/h3&gt;
&lt;p&gt;首先我们需要使用 upstream 配置一个代理集，然后通过 proxy_pass 指定这个代理集，之后我们每次请求这个 server 就会自动轮流执行这个代理集中的地址。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的轮训负载均衡&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
	server localhost:88;
	server localhost:89;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

server {
    listen       88; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
    proxy_pass   http://www.baidu.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
server {
    listen       89; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
        proxy_pass   http://www.taobao.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;权重&lt;/h3&gt;
&lt;p&gt;权重就是增加每台机器想访问概率的比重，在轮训的基础上配置每个代理服务的 wight 值即可，比如说下面的例子，权重之比是 4:1，那么命中 88 端口的概率就是 80%。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的权重负载均衡&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
	server localhost:88 weight=4;
	server localhost:89 weight=1;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

server {
    listen       88; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
    proxy_pass   http://www.baidu.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
server {
    listen       89; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
        proxy_pass   http://www.taobao.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;当然在除了 weight 这个配置之外，每一个 server 后还可以追加一些状态，像备用服务的 backup 和 下线状态 down，虽然这些状态并不常用，因为如果出现了正常服务器失效的时候，备用服务器应该也有可能是失败的，并且修改状态后还需要 reload，也是十分不方便的。&lt;/p&gt;
&lt;h3&gt;ip_hash（不常用）&lt;/h3&gt;
&lt;p&gt;根据客户端的 ip 转发相同的服务器。&lt;/p&gt;
&lt;h3&gt;least_hash（不常用）&lt;/h3&gt;
&lt;p&gt;最少连接数访问。&lt;/p&gt;
&lt;h3&gt;url_hash（不常用）&lt;/h3&gt;
&lt;p&gt;根据 url 定向访问。&lt;/p&gt;
&lt;h3&gt;fair（不常用）&lt;/h3&gt;
&lt;p&gt;根据后端服务器响应时间请求转发。&lt;/p&gt;
&lt;h2&gt;动静分离&lt;/h2&gt;
&lt;p&gt;一般动静分离都用在中小型网站，因为如果是像淘宝这种静态文件非常多的项目如果放在 Nginx 会占据大量带宽。动静分离的目的就是减轻后台服务器的压力，把一些静态的图片、样式都放在 Nginx 上，同时可以减轻网络的开销。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的动静分离配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
	server localhost:88 weight=4;
	server localhost:89 weight=1;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    }

    location ～*/(js|img|css) {
    proxy_pass   http://webs;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;URL Rewrite&lt;/h2&gt;
&lt;p&gt;这功能是为了掩饰访问后台服务器真正的 url，先看下面的例子。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的 rewrite 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
	server localhost:88 weight=4;
	server localhost:89 weight=1;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    rewrite ^/([0-9]+).html$ /index.jsp?pageNum=$1 break;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;上面配置的含义就是我们将 /index.jsp?pageNum=2 这种实际的 URL 后缀代替成了 /2.html 来访问，其中 rewrite 后面的的三个部分分别是正则、代替的内容和flag标记，其中 flag 标记部分有很多种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;last：匹配多次正则，一直到最新的匹配结果。&lt;/li&gt;
&lt;li&gt;break：匹配后立即终止返回结果。&lt;/li&gt;
&lt;li&gt;redirect：返回302临时重定向，浏览器会显示跳转后的URL地址。&lt;/li&gt;
&lt;li&gt;permanent：返回301永久重定向，浏览器会显示跳转后的URL地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;防盗链&lt;/h2&gt;
&lt;p&gt;Nginx 实现防盗链需要配置 valid_referers，来校验请求头中是否携带 Referer 和其对应的网址是否正确。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的防盗链配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
	server localhost:88 weight=4;
	server localhost:89 weight=1;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    rewrite ^/([0-9]+).html$ /index.jsp?pageNum=$1 break;
    valid_referers localhost;
    if ($invalid_referer) {
        return 403;
    }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;高可用配置(HA)&lt;/h2&gt;
&lt;p&gt;区别于一些集群，Nginx 的高可用是基于硬件的一种模拟集群，需要借助 keepalived 实现 ip 漂移来实现一个模拟 ip 访问两个 Nginx 对象服务器。&lt;/p&gt;
&lt;h2&gt;扩容&lt;/h2&gt;
&lt;p&gt;扩容的方式有很多，有基于硬件资源增加的单机垂直扩容、集群化的水平扩容、细粒度拆分的分布式扩容，当然也可以从服务上进行数据异构化或者服务异步化。&lt;/p&gt;
&lt;h4&gt;ip_hash&lt;/h4&gt;
&lt;p&gt;即根据访问请求的来源确定一个哈希值，这个请求以后只能请求到一台固定的地址。实现 ip_hash 是在 upstream 中增加一行声明即可实现。&lt;/p&gt;
&lt;p&gt;::: normal-demo 简单的轮训负载均衡&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;upstream webs {
    ip_hash;
	server localhost:88;
	server localhost:89;
    }

server {
    listen       80;
    server_name  localhost;

    location / {
    proxy_pass   http://webs;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

server {
    listen       88; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
    proxy_pass   http://www.baidu.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
server {
    listen       89; # 端口
    server_name  localhost; # 可解析的域名、主机名

    location / {
        proxy_pass   http://www.taobao.com; # 代理服务器
    }

    error_page   500 502 503 504  /50x.html; # 服务器错误跳转界面
    location = /50x.html {
        root   html;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;实战训练&lt;/h2&gt;
&lt;h3&gt;Vue3 + Vite4 + Nginx&lt;/h3&gt;
&lt;p&gt;这里拿我自己&lt;/p&gt;
</content:encoded></item><item><title>Observer</title><link>https://songbaicheng.cc.cd/posts/observer/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/observer/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;观察者模式&lt;/h1&gt;
&lt;h2&gt;什么是观察者模式&lt;/h2&gt;
&lt;p&gt;观察者模式也被称为发布-订阅模式或者事件-监听模式，这让我们很容易联想到 Redis、MQ 中的发布订阅模式，即主题对象（发布者）和观察者对象（订阅者）之间的关系类似于发布者发布事件，而观察者监听并响应该事件的方式，所以观察者模常用于实现对象之间的一对多依赖关系。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;大多数编程语言都支持观察者模式的实现，但是大多数的场景下并不会去使用，因为其实现存在一些问题和局限性，不符合现代Java语言的设计原则和最佳实践。另外，它们也没有提供对并发编程的良好支持。拿 Java举例，Java 官方推荐使用其他更好的替代方案，例如使用接口和回调机制来实现观察者模式，或者使用第三方的观察者模式框架（如 Spring Framework 的事件机制或 Google Guava 的事件总线）。&lt;/p&gt;
&lt;h3&gt;jdk版&lt;/h3&gt;
&lt;p&gt;在 Java 标准库中，java.util.Observable 类和 java.util.Observer 接口在 Java 9 版本之后被标记为过时，可能会在未来的 Java 版本中被移除，但是这并不影响我们用在这里方便理解其过程。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，创建一个发布者类。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 发布者接口
interface Publisher {
    void subscribe(Subscriber subscriber);
    void unsubscribe(Subscriber subscriber);
    void notifySubscribers(String message);
}

// 具体的发布者类
class ConcretePublisher implements Publisher {
    private List&amp;lt;Subscriber&amp;gt; subscribers;

    public ConcretePublisher() {
        subscribers = new ArrayList&amp;lt;&amp;gt;();
    }

    @Override
    public void subscribe(Subscriber subscriber) {
        subscribers.add(subscriber);
    }

    @Override
    public void unsubscribe(Subscriber subscriber) {
        subscribers.remove(subscriber);
    }

    @Override
    public void notifySubscribers(String message) {
        for (Subscriber subscriber : subscribers) {
            subscriber.receiveMessage(message);
        }
    }

    public void publish(String message) {
        notifySubscribers(message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建订阅者类。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;// 订阅者接口
interface Subscriber {
    void receiveMessage(String message);
}

// 具体的订阅者类
class ConcreteSubscriber implements Subscriber {
    private String name;

    public ConcreteSubscriber(String name) {
        this.name = name;
    }

    @Override
    public void receiveMessage(String message) {
        System.out.println(name + &quot; 收到消息: &quot; + message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建一个具体的发布者和两个具体的订阅者来测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class ObserverPatternExample {
    public static void main(String[] args) {
        ConcretePublisher publisher = new ConcretePublisher();

        ConcreteSubscriber subscriber1 = new ConcreteSubscriber(&quot;订阅者1&quot;);
        ConcreteSubscriber subscriber2 = new ConcreteSubscriber(&quot;订阅者2&quot;);

        publisher.subscribe(subscriber1);
        publisher.subscribe(subscriber2);

        publisher.publish(&quot;Hello, World!&quot;);

        publisher.unsubscribe(subscriber2);

        publisher.publish(&quot;How are you?&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行上述代码时，一开始订阅者订阅了两个发布者，并且在发布者发布消息时接收到通知。然后，我们取消了一个订阅者的订阅，并再次发布消息，观察到只有一个订阅者收到了通知。&lt;/p&gt;
&lt;h3&gt;Spring版&lt;/h3&gt;
&lt;p&gt;在Spring框架的观察者模式中我们首先要了解一些核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事件（Event）：Event 代表着在应用程序中发生的特定事件。事件对象中包含了相关的数据和信息，用于描述事件的发生和上下文。在观察者模式中，事件对象会被发布（publish）给所有注册的监听器，通知它们事件的发生。&lt;/li&gt;
&lt;li&gt;监听器（Listener）：Listener 是事件的接收者和处理者。它负责监听和响应特定类型的事件。监听器需要实现ApplicationListener 接口，并通过泛型指定要监听的事件类型。当事件被发布时，对应类型的监听器将接收到事件，并执行相应的逻辑处理。&lt;/li&gt;
&lt;li&gt;应用事件发布器（Application Event Publisher）：应用事件发布器是一个接口，通常由Spring框架提供的ApplicationEventPublisher接口实现。它允许组件或类将事件发布给观察者。通过应用事件发布器，可以将事件发布给所有注册的监听器。&lt;/li&gt;
&lt;li&gt;应用事件监听器（Application Event Listener）：应用事件监听器是一个接口，通常由Spring框架提供的ApplicationListener接口实现。它定义了一个或多个用于处理特定类型事件的方法。通过实现应用事件监听器接口，并注册到应用事件发布器中，可以接收和处理相应类型的事件。&lt;/li&gt;
&lt;li&gt;事件源（Event Source）：事件源是触发事件的对象或组件。在Spring框架的观察者模式中，事件源可以是任何对象，但通常是由应用程序定义的业务对象或组件。当事件源触发事件时，它会将事件发布给应用事件发布器。&lt;/li&gt;
&lt;li&gt;事件上下文（Event Context）：事件上下文是事件发生时的上下文信息，它包含了与事件相关的数据和状态。事件上下文可以作为事件对象的一部分，在事件中传递给监听器进行处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来我们使用 Spring Framework 的事件机制重写这个例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，我们定义一个事件类 MyEvent，它将作为观察者模式中的事件对象。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class MyEvent extends ApplicationEvent {

    private String message;

    public MyEvent(Object source, String message) {

        super(source);
        this.message = message;
    }

    public String getMessage() {

        return message;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;接下来，创建一个事件源类MyEventPublisher，负责发布事件。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class MyEventPublisher {

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void publishEvent(String message) {

        CustomEvent customEvent = new CustomEvent(this, message);
        eventPublisher.publishEvent(customEvent);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;最后，创建一个事件监听器类MyEventListener，用于接收和处理事件：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class MyEventListener implements ApplicationListener&amp;lt;MyEvent&amp;gt; {

    @Override
    public void onApplicationEvent(CustomEvent event) {

        String message = event.getMessage();
        // 在这里处理事件
        System.out.println(&quot;收到消息:&quot; + message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个示例中，通过 MyEventPublisher 发布了一个自定义事件。当事件发生时，MyEventListener 监听器将接收到该事件，并处理相应的逻辑。需要注意的是，需要使用 @Component 注解将事件发布器和事件监听器声明为 Spring 容器中的组件，以便自动注册和管理。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;观察者模式适用于以下场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一对多的依赖关系：当一个对象的状态发生变化时，需要通知多个其他对象进行相应的处理。观察者模式能够实现一对多的通知机制，确保所有相关的观察者都能接收到状态变化的通知并作出相应的响应。&lt;/li&gt;
&lt;li&gt;发布-订阅模式：在发布-订阅模式中，观察者模式被广泛应用。事件发布者（发布者）负责发布事件，而订阅者（观察者）订阅感兴趣的事件，并在事件发生时接收通知。这种模式常用于异步消息处理、事件驱动的系统和消息队列等场景。&lt;/li&gt;
&lt;li&gt;GUI事件处理：在图形用户界面（GUI）开发中，观察者模式被广泛用于处理用户界面组件的事件。例如，当按钮被点击、文本框内容改变或窗口关闭时，相关的观察者会收到相应的事件通知并执行相应的操作。&lt;/li&gt;
&lt;li&gt;系统状态监测和通知：当系统中的某些关键状态发生变化时，需要通知其他模块或组件进行相应的处理。观察者模式可以用于系统监测和通知的场景，例如服务器负载监测、日志记录、缓存更新等。&lt;/li&gt;
&lt;li&gt;数据库触发器：在数据库系统中，触发器（Trigger）可以用作观察者模式的实现。当数据库中的数据发生变化时，触发器可以自动触发相应的操作，例如更新相关的数据表、发送通知等。&lt;/li&gt;
&lt;li&gt;日志记录和审计：观察者模式可以用于实现日志记录和审计功能。当系统中发生重要事件时，观察者可以接收到事件通知，并将相关信息记录到日志文件或进行审计处理。&lt;/li&gt;
&lt;li&gt;需要注意的是，观察者模式适用于那些多个对象之间存在一对多关系的场景，其中观察者和被观察者之间的依赖关系是动态的。在使用观察者模式时，需要合理设计观察者和被观察者的接口，确保它们之间的解耦和灵活性。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Picture</title><link>https://songbaicheng.cc.cd/posts/picture/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/picture/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;图&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root(图)
    图的定义
    图结构的存储
      邻接矩阵法
      邻接表法
      邻接多重表
      十字链表
    图的遍历
      深度优先遍历
      广度优先遍历
    图的相关应用
      最小生成树
        Prim 算法
        Kruskal 算法
      最短路径
        Dijkstra 算法
        Floyd 算法
      拓扑排序
        AOV 网
      关键路径
        AOE 网
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图的基本概念&lt;/h2&gt;
&lt;p&gt;图G由顶点集V和边集E组成，记为&lt;code&gt;G=(V,E)&lt;/code&gt;，其中V(G)表示图G中顶点的有限非空集；E(G)表示图G中顶点关系的集合。注意线性表可以是空表，树可以是空树，但是图不可以是空图。&lt;/p&gt;
&lt;p&gt;下面是图的一些基本概念和基本术语：&lt;/p&gt;
&lt;h4&gt;有向图和无向图&lt;/h4&gt;
&lt;p&gt;当E是有向边（弧）的有限集合时，则图G为无向图。若E是无向边的有限集合则图G为无向图。&lt;/p&gt;
&lt;h4&gt;简单图、多重图&lt;/h4&gt;
&lt;p&gt;一个图G如果满足不存在重复边、不存在顶点到自身的边，那么称图G为简单图。若图G中某两个顶点之间的边数大于1条，又允许顶点通过一条边和自身关联，则称图G为多重图。&lt;/p&gt;
&lt;h4&gt;完全图&lt;/h4&gt;
&lt;p&gt;任何两个顶点之间都存在边的无向图是完全图。有向完全图是任意两个顶点之间都存在方向相反的两条弧的有向图。&lt;/p&gt;
&lt;h4&gt;子图&lt;/h4&gt;
&lt;p&gt;如果一个图中的顶点集和边集是另一个图的子集，那么称前者为后者的子图。&lt;/p&gt;
&lt;h4&gt;连通、连通图和连通分量&lt;/h4&gt;
&lt;p&gt;在无向图中，若从顶点v到顶点w有路径存在，则称v和w是连通的。若图G中任意两个顶点都是连通的，则称图G为连通图，否则称为非连通图。无向图中极大连通子图称为连通分量。&lt;/p&gt;
&lt;h4&gt;强连通图和强连通分量&lt;/h4&gt;
&lt;p&gt;在有向图中，如果有一对顶点v和w，从v到w和从w到v之间都有路径，则称为这两个顶点时强连通的。若图中任何一对顶点都是强连通的，则称为此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量。&lt;/p&gt;
&lt;h4&gt;生成树和生成森林&lt;/h4&gt;
&lt;p&gt;连通图的生成树是包含图中全部顶点的一个极小连通子图。若痛中的顶点树为n，则它生成树包含n-1条边。在非连通图中，连通分量的生成树构成了非连通图的生成森林。&lt;/p&gt;
&lt;h4&gt;顶点的度、入度和出度&lt;/h4&gt;
&lt;p&gt;在无向图中，顶点v的度是指依附于顶点v的边的条数，无向图的全部顶点的度的和等于边数的2倍，因为每条边和两个顶点相关联。&lt;/p&gt;
&lt;p&gt;在有向图中，顶点v的度分为入度和出度，入度是以顶点v为终点的有向边的数量，而出度是以顶点v为起点的有向边的数目。&lt;/p&gt;
&lt;h4&gt;边的权和网&lt;/h4&gt;
&lt;p&gt;在一个图中，每条边都可以标上具有某种含义的数值，该数值称为该边的权值。这种边上带有权值的图称为带权图，也称为网。&lt;/p&gt;
&lt;h4&gt;稠密图和稀疏图&lt;/h4&gt;
&lt;p&gt;边数很少的图称为稀疏图，反之称为稠密图。&lt;/p&gt;
&lt;h4&gt;路径、路径长度和回路&lt;/h4&gt;
&lt;p&gt;两个顶点之间的路径是指两个顶点间的顶点序列，路径上边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点，并且有大于n-1
条边，则此图一定有环。&lt;/p&gt;
&lt;h4&gt;简单路径和简单回路&lt;/h4&gt;
&lt;p&gt;在路径序列中，顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外，其余顶点不重复出现的回路称为简单回路。&lt;/p&gt;
&lt;h4&gt;距离&lt;/h4&gt;
&lt;p&gt;从顶点u出发到顶点v的最短路径若存在，则此路径的长度为从u到v的距离。若从u到v根本不存在路径，则记该距离为无穷。&lt;/p&gt;
&lt;h4&gt;有向树&lt;/h4&gt;
&lt;p&gt;一个顶点的入度为0，其余顶点的入度均为1的有向图称为有向树。&lt;/p&gt;
&lt;h2&gt;图的存储及基本操作&lt;/h2&gt;
&lt;p&gt;图的存储需要完整、准确的反映顶点集和边集的信息。&lt;/p&gt;
&lt;h3&gt;邻接矩阵法&lt;/h3&gt;
&lt;p&gt;所谓邻接矩阵，是指用一个一维数组存储图中顶点的信息，用一个二维数组存储图中边的信息，存储顶点之间邻接关系的二维数组称为邻接矩阵。&lt;/p&gt;
&lt;p&gt;图的邻接矩阵存储表示法具有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无向图邻接矩阵一定是唯一的对称矩阵。因此，在实际存储邻接矩阵时只需存储上或下三角矩阵即可。&lt;/li&gt;
&lt;li&gt;对于无向图，邻接矩阵的第i行或第i列非零元素的个数正好是顶点i的度。&lt;/li&gt;
&lt;li&gt;对于有向图，邻接矩阵的第i行非零元素或非∞元素的个数正好是顶点i的出度，第i列非零元素或非∞元素的个数正好是顶点i的入度。&lt;/li&gt;
&lt;li&gt;用邻接矩阵存储图，很容易确定两个顶点是否相连。但是，要确定图中有多少条边，则必须按行、按列对每个元素进行检测。&lt;/li&gt;
&lt;li&gt;稠密图适合使用邻接矩阵存储。&lt;/li&gt;
&lt;li&gt;设图G的邻接矩阵为A，A^n^[i][j]等于由顶点i到顶点j的长度为n的路径的数目。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;邻接表法&lt;/h3&gt;
&lt;p&gt;当一个图是稀疏图的时候，使用邻接矩阵法显然要量费大量的存储空间，而使用邻接表是指对图G中的每一个顶点vi建立一个单链表，第i个单链表中的结点表示依附于顶点vi的边，如果是有向图则就是以顶点i为尾的弧，这个单链表就称为顶点i的边表。边表的头指针和顶点的数据信息采用顺序存储。&lt;/p&gt;
&lt;p&gt;图的邻接表存储方法具有以下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;稀疏图适合使用邻接表法。&lt;/li&gt;
&lt;li&gt;邻接表中对于一个顶点很容易找到他所有的邻边，但是确定两个点之间是否有边，则需要在相应结点对应的边表中查找另一个结点。&lt;/li&gt;
&lt;li&gt;在有向图的邻接表表示中，求一个给定顶点的出度只需要计算其边表中结点的个数，如果是出度则需遍历整个表。&lt;/li&gt;
&lt;li&gt;图的邻接表不唯一。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;十字链表&lt;/h3&gt;
&lt;p&gt;十字链表是有向图的一种链式存储结构，在十字链表中，对应于有向图中的每条弧有一个结点，对应于每个顶点也有一个结点。&lt;/p&gt;
&lt;h3&gt;邻接多重表&lt;/h3&gt;
&lt;p&gt;邻接多重表是无向图的另一种链式存储结构，在邻接表中，容易求的顶点和边的各种信息，但是在邻接表中求两点之间是否存在边对边执行删除等操作时，需要分别在两个顶点的边表中遍历，效率低，在邻接多重表中，所有依附于同一顶点的边串联在同一个链表中，由于每条边依附于两个顶点，因此每个边结点同时链接在两个链表中。对无向图而言，其邻接多重表和邻接表的区别在于，同一条边在邻接表中用两个结点表示，二在邻接多重表只有一个。&lt;/p&gt;
&lt;h3&gt;图的基本操作&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Adjacent(G, x, y)：判断图G是否存在边&amp;lt;x,y&amp;gt;或（x,y）。&lt;/li&gt;
&lt;li&gt;Neighbors(G, x): 列出图G中与结点x邻接的边。&lt;/li&gt;
&lt;li&gt;InsertVertex(G, x): 在图G中插入顶点x。&lt;/li&gt;
&lt;li&gt;DeleteVertex(G, x): 在图G中删除顶点x。&lt;/li&gt;
&lt;li&gt;AddEdge(G, x, y): 若无向边（x,y）或有向边&amp;lt;x,y&amp;gt;不存在，则有向图G中添加该边。&lt;/li&gt;
&lt;li&gt;RemoveEdge(G, x, y): 若无向边（x,y）或有向边&amp;lt;x,y&amp;gt;存在，则自从图G中删除该边。&lt;/li&gt;
&lt;li&gt;FirstNeighbor(G, x): 求图G中顶点x的第一个邻接点。&lt;/li&gt;
&lt;li&gt;NextNeightbor(G, x, y): 假设图G中顶点y是顶点x的一个邻接点，返回除了y外顶点x的下一个邻接点的顶点号。&lt;/li&gt;
&lt;li&gt;Get_edge_velue(G, x, y): 获取图G中边（x，y）或&amp;lt;x,y&amp;gt;的权值。&lt;/li&gt;
&lt;li&gt;Set_edge_value(G, x, y, v): 设置图G中边（x，y）或&amp;lt;x,y&amp;gt;的权值。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;图的遍历&lt;/h2&gt;
&lt;p&gt;图的遍历是指从图中的某一个顶点出发，按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次。&lt;/p&gt;
&lt;h3&gt;广度优先搜索&lt;/h3&gt;
&lt;p&gt;广度优先搜索类似于二叉树的层序遍历算法。基本思想是：首先访问起始顶点v，然后从v出发，依次访问&lt;/p&gt;
&lt;h3&gt;深度优先搜索&lt;/h3&gt;
&lt;h3&gt;图的遍历与连通性&lt;/h3&gt;
&lt;p&gt;图的遍历算法可以用来判断图的连通性，对于无向图来说，若无向图是连通的，则从任意一个结点出发，仅需一次遍历就能够访问图中的所有顶点；若无向图是非连通的，则从某一个顶点出发，一次遍历只能访问到该顶点在连通分量的所有顶点，而对于途中其他连通分量的顶点，则无法通过这次遍历访问。对于有向图来说，若从初始点到图中的每个顶点都有路径，则能够访问到图中的所有顶点，否则不能访问到所有顶点。&lt;/p&gt;
</content:encoded></item><item><title>Other Skills</title><link>https://songbaicheng.cc.cd/posts/other-skills/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/other-skills/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;其他技能&lt;/h1&gt;
&lt;h2&gt;版本控制&lt;/h2&gt;
&lt;p&gt;提到版本控制（Version control），自从 Git 横空出世之后，Subversion 就开始逐渐退出神坛，相对于 Subversion 这种集中式的版本控制系统，分布式风格的 Git 更适合多人协作开发使用，虽然 Subversion 和我们平时使用本地文件的习惯非常相近，容易上手，但是面对更多人同时开发的场景来说，Git 拥有更多样的协作流程将更有利于我们管理项目。&lt;/p&gt;
&lt;h3&gt;《&lt;em&gt;Pro Git&lt;/em&gt;》&lt;/h3&gt;
&lt;p&gt;![pro git](/assets/images/resource/books/pro-git.png &quot;pro git&quot; =350x500)&lt;/p&gt;
&lt;p&gt;这本书的作者是 Git 在 GitHub 公司的最早的托管者之一编写的，当我了解到这本书的时候就已经是第二版了。这本书涵盖了 Git 的基础用法、分支特性、搭建和配置 Git 服务器、分布式工作流程、GitHub 的使用方法、Git 工具、Git 内部原理等各个方面内容，最后还附带了 Git 命令参考。零基础的初学者可以通过前 3 章的学习就已经可以应付日常的开发场景了，后几章能够满足中高阶用户深入了解的需求。书中提供了大量的应用案例，不同开发工作模式有不同的用法，配合插图演示版本变化的状态，十分易于理解。&lt;/p&gt;
&lt;p&gt;如果你觉得文字和图片还不够直观，下面的链接是一个 Git 学习网站： Learning Git Branching ，这里你可以通过闯关的形式学习一些 Git 的基础命令，并且每个命令的执行还会有实时的动图演示，简直不要太友好，就算你已经是 Git 的老手，也可以在这里复习和加深一些命令的理解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://learngitbranching.js.org/?locale=zh_CN&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;设计模式&lt;/h2&gt;
&lt;h3&gt;《&lt;em&gt;Head First Design Patterns&lt;/em&gt;》&lt;/h3&gt;
&lt;p&gt;![Head First Design Patterns](/assets/images/resource/books/head-first-design-patterns.png &quot;Head First Design Patterns&quot; =350x500)&lt;/p&gt;
&lt;p&gt;几乎每个经典系列书籍中都会有讲设计模式的书，像大话系列、图解系列还有 Head First 系列，这些书都是基于设计模式开山之作《&lt;em&gt;设计模式：可复用面向对象软件的基础&lt;/em&gt;》中23中设计模式结合后来的实践和发展又重新总结出来的更完善生动的作品，内容上都是按照每一种设计模式作为一个章节并结合情景和图片进行讲解，我自己学习看的是第一版的《&lt;em&gt;Head First Design Patterns&lt;/em&gt;》，上图中的是2022年发行的第二版，学习的时候一边阅读一边自己实现还是挺容易理解的，直到现在我看到某种设计模式还会想到当时书中的案例。&lt;/p&gt;
&lt;p&gt;对我个人而言，我觉得学习设计模式更行之有效的方法是在阅读过后有初步的理解的情况下的亲身实践，它基于代码又高于代码，最好的学习方式就是在平时阅读和开发的时候，将健壮性和可扩展性设为己任，仔细理解每种模式所使用的场景才是真正的学习之道。&lt;/p&gt;
</content:encoded></item><item><title>Rag</title><link>https://songbaicheng.cc.cd/posts/rag/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/rag/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;RAG&lt;/h1&gt;
</content:encoded></item><item><title>Oss</title><link>https://songbaicheng.cc.cd/posts/oss/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/oss/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;大文件上传&lt;/h1&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;文件上传是一个老生常谈的话题了，在文件相对比较小的情况下，可以直接把文件转化为字节流上传到服务器，但在文件比较大的情况下，用普通的方式进行上传，这可不是一个好的办法。&lt;/p&gt;
&lt;p&gt;目前我们平台后管存在许多界面数据超过十几万条数据，常规的导出功能在面对如此大数据量的数据容易造成 OOM 等问题，因此需要一种新的上传方式来解决这个问题。&lt;/p&gt;
&lt;p&gt;目前我们的导出都是通过后端使用 EasyExcel 生成文件上传到 OSS 并返回 OSS 地址的模式，想要解决大数据量的情况必须在生成文件和上传 OSS 的时候做出优化。&lt;/p&gt;
&lt;h2&gt;方案&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;title: 对象存储 OSS 文档
desc: 点击跳转查看详细内容
logo: /icon/ali-logo.svg
link: https://www.alibabacloud.com/help/zh/oss/developer-reference/overview-13/?spm=a2c63.p38356.help-menu-31815.d_19_2_0_1_0.71dc7dceTpA6TC&amp;amp;scm=20140722.H_32013._.OR_help-T_intl~zh-V_1
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;秒传&lt;/h3&gt;
&lt;p&gt;通俗的说，你把要上传的东西上传，服务器会先做 MD5 校验，如果服务器上有一样的东西，它就直接给你个新地址，其实你下载的都是服务器上的同一个文件。
想要不秒传，其实只要让MD5改变，就是对文件本身做一下修改（改名字不行），例如一个文本文件，你多加几个字，MD5就变了，就不会秒传了。&lt;/p&gt;
&lt;h4&gt;具体实现&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;利用redis的set方法存放文件上传状态，其中key为文件上传的md5，value为是否上传完成的标志位，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当标志位true为上传已经完成，此时如果有相同文件上传，则进入秒传逻辑。如果标志位为false，则说明还没上传完成，此时需要在调用set的方法，保存块号文件记录的路径，其中key为上传文件md5加一个固定前缀，value为块号文件记录路径&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;分片上传&lt;/h3&gt;
&lt;p&gt;分片上传，就是将所要上传的文件，按照一定的大小，将整个文件分隔成多个数据块（我们称之为Part）来进行分别上传，上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。&lt;/p&gt;
&lt;h3&gt;断点续传&lt;/h3&gt;
&lt;p&gt;断点续传是在下载或上传时，将下载或上传任务（一个文件或一个压缩包）人为的划分为几个部分，每一个部分采用一个线程进行上传或下载，如果碰到网络故障，可以从已经上传或下载的部分开始继续上传或者下载未完成的部分，而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。&lt;/p&gt;
&lt;p&gt;断点续传可以看成是分片上传的一个衍生，因此可以使用分片上传的场景，都可以使用断点续传。&lt;/p&gt;
&lt;p&gt;为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题，服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询，从而使客户端知道已经上传的分片数据，从而从下一个分片数据开始继续上传。&lt;/p&gt;
&lt;h2&gt;思路技巧&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在选择导出方案的时候可以根据数据量来决定采用直接上传还是异步处理。如果数据量不大于万级，可以采用直接上传的方式。如果数据量大于万级，可以采用异步处理的方式来减少内存消耗。&lt;/li&gt;
&lt;li&gt;可以在请求下载和处理上传的地方排队加锁，防止大量请求打满服务器。&lt;/li&gt;
&lt;li&gt;如果数据量确实很大导致数据处理时间很长或者吃内存，可以定时在夜晚业务量少的时间进行。&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Pinia</title><link>https://songbaicheng.cc.cd/posts/pinia/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/pinia/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Pinia&lt;/h1&gt;
&lt;h2&gt;关于 Pinia 和 Vuex&lt;/h2&gt;
&lt;p&gt;Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子，结合了 Vuex 5 核心团队讨论中的许多想法。最终，我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容，并决定实现它 取而代之的是新的建议。&lt;/p&gt;
&lt;p&gt;与 Vuex 相比，Pinia 提供了一个更简单的 API，具有更少的规范，提供了 Composition-API 风格的 API，最重要的是，在与 TypeScript 一起使用时具有可靠的类型推断支持。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Pinia 中文文档
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/framework/pinia/pinia.svg
link: https://pinia.web3doc.top
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;引入 Pinia&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;yarn add pinia
# 或者使用 npm
npm install pinia
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia } from &apos;pinia&apos;

app.use(createPinia())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;定义 Store&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { ref, computed } from &apos;vue&apos;
import { defineStore } from &apos;pinia&apos;

export const useStore = defineStore(&apos;main&apos;, () =&amp;gt; {
    const name = ref&amp;lt;string&amp;gt;(&apos;songbaicheng&apos;)
    const age = ref&amp;lt;number&amp;gt;(23)

    return { name, age }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 Store&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { useStore } from &apos;./stores/main&apos;;

const stores = useStore()
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
{{ stores.name }}-{{ stores.age }}
&amp;lt;/template&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Random</title><link>https://songbaicheng.cc.cd/posts/random/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/random/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;全局随机数&lt;/h1&gt;
&lt;p&gt;在随着数据库数据单机达到瓶颈已经无法支持的时候就会出现分库分表的场景，这个时候数据库自带的自增主键或者简单组成的随机数已经不能满足需求了，这个时候就需要使用全局唯一 ID。 一个最基本的分布式 ID 需要满足下面这些要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局唯一：ID 的全局唯一性肯定是首先要满足的。&lt;/li&gt;
&lt;li&gt;高性能：分布式 ID 的生成速度要快，对本地资源消耗要小。&lt;/li&gt;
&lt;li&gt;高可用：生成分布式 ID 的服务要保证可用性无限接近于 100%。&lt;/li&gt;
&lt;li&gt;方便易用：拿来即用，使用方便，快速接入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除了这些之外，一个比较好的分布式 ID 还应保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安全：ID 中不包含敏感信息。&lt;/li&gt;
&lt;li&gt;有序递增：如果要把 ID 存放在数据库的话，ID 的有序性可以提升数据库写入速度。并且很有可能会直接通过 ID 来进行排序。&lt;/li&gt;
&lt;li&gt;有具体的业务含义：生成的 ID 如果能有具体的业务含义，可以让定位问题以及开发更透明化（通过 ID 就能确定是哪个业务）。&lt;/li&gt;
&lt;li&gt;独立部署：也就是分布式系统单独有一个发号器服务，专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见分布式 ID 设计方案&lt;/h2&gt;
&lt;h3&gt;数据库号段模式&lt;/h3&gt;
&lt;p&gt;每次从数据库批量获取一部分 ID 存放在内存中，每次需要 ID 时，从内存中获取一个，当内存中的 ID 用完之后再去数据库中批量获取一批新的 ID 存入到内存中。像滴滴开源的 Tinyid 就是基于这种方式来做的。不过，TinyId 使用了双号段缓存、增加多 db 支持等方式来进一步优化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Tinyid 项目 Github 官网
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/github-logo.svg
link: https://github.com/didi/tinyid/wiki
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;缓存数据库&lt;/h3&gt;
&lt;p&gt;缓存数据库的方案，比如 Redis、MongoDB 等。一般情况下，NoSQL 方案使用 Redis 多一些。我们通过 Redis 的 incr 命令即可实现对 id 原子顺序递增。为了提高可用性和并发，我们可以使用 Redis Cluster，利用集群解决缓存重启机器或者机器故障后造成的数据丢失。&lt;/p&gt;
&lt;h3&gt;UUID&lt;/h3&gt;
&lt;p&gt;UUID 是有版本的，之于我们最常见的 &lt;code&gt;UUID.randomUUID()&lt;/code&gt; 来说，就是版本 4 的 UUID，目前有以下五种支持方式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;UUID 是根据时间和节点 ID（通常是 MAC 地址）生成。&lt;/li&gt;
&lt;li&gt;UUID 是根据标识符（通常是组或用户 ID）、时间和节点 ID 生成。&lt;/li&gt;
&lt;li&gt;版本 3 和版本 5 确定性 UUID 通过散列（hashing）名字空间（namespace）标识符和名称生成。&lt;/li&gt;
&lt;li&gt;使用随机性或伪随机性生成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从上面的介绍中可以看出，UUID 可以保证唯一性，因为其生成规则包括 MAC 地址、时间戳、名字空间（Namespace）、随机或伪随机数、时序等元素，计算机基于这些规则生成的 UUID 是肯定不会重复的。虽然，UUID 可以做到全局唯一性，但是，我们一般很少会使用它。比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适：数据库主键要尽量越短越好，而 UUID 的消耗的存储空间比较大（32 个字符串，128 位）。UUID 是无顺序的，InnoDB 引擎下，数据库主键的无序性会严重影响数据库性能。&lt;/p&gt;
&lt;h3&gt;Snowflake (雪花算法)&lt;/h3&gt;
&lt;p&gt;Snowflake 是 Twitter 开源的分布式 ID 生成算法。Snowflake 由 64 bit 的二进制数字组成，这 64bit 的二进制被分成了几部分，每一部分存储的数据都有特定的含义：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sign(1bit):符号位（标识正负），始终为 0，代表生成的 ID 为正数。&lt;/li&gt;
&lt;li&gt;timestamp (41 bits):一共 41 位，用来表示时间戳，单位是毫秒，可以支撑 2 ^41 毫秒（约 69 年）。&lt;/li&gt;
&lt;li&gt;datacenter id + worker id (10 bits):一般来说，前 5 位表示机房 ID，后 5 位表示机器 ID（实际项目中可以根据实际情况调整）。这样就可以区分不同集群/机房的节点。&lt;/li&gt;
&lt;li&gt;sequence (12 bits):一共 12 位，用来表示序列号。 序列号为自增值，代表单台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最多可以生成 4096 个 唯一 ID。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你想要使用 Snowflake 算法的话，一般不需要你自己再造轮子。有很多基于 Snowflake 算法的开源实现比如美团的 Leaf、百度的 UidGenerator，并且，Seata 还提出了“改良版雪花算法”，针对原版雪花算法进行了一定的优化改良，解决了时间回拨问题，大幅提高的 QPS。&lt;/p&gt;
</content:encoded></item><item><title>Servlet</title><link>https://songbaicheng.cc.cd/posts/servlet/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/servlet/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Servlet&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root((Servlet 知识点))
    HTTP 协议
    Tomcat 服务器
    Servlet的实现
    HttpServletRequest 对象
    HttpServeltResponse 对象
    Cookie 对象
    HttpSession 对象
    ServletContext 对象
    文件上传和下载
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Servlet 是什么&lt;/h2&gt;
&lt;p&gt;Java Servlet 是运行在 Web 服务器或应用服务器上的程序，它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。&lt;/p&gt;
&lt;p&gt;Servlet 执行以下主要任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取客户端（浏览器）发送的显式的数据。这包括网页上的 HTML 表单，或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。&lt;/li&gt;
&lt;li&gt;读取客户端（浏览器）发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。&lt;/li&gt;
&lt;li&gt;处理数据并生成结果。这个过程可能需要访问数据库，执行 RMI 或 CORBA 调用，调用 Web 服务，或者直接计算得出对应的响应。&lt;/li&gt;
&lt;li&gt;发送显式的数据（即文档）到客户端（浏览器）。该文档的格式可以是多种多样的，包括文本文件（HTML 或 XML）、二进制文件（GIF 图像）、Excel 等。&lt;/li&gt;
&lt;li&gt;发送隐式的 HTTP 响应到客户端（浏览器）。这包括告诉浏览器或其他客户端被返回的文档类型（例如 HTML），设置 cookies 和缓存参数，以及其他类似的任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;::: normal-demo Servlet demo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @description Servlet demo
 */
@WebServlet(&quot;/servlet&quot;)
public class MyServlet extends HttpServlet {

    /**
     * 服务器关闭或者容器停止则调用此方法
     */
    @Override
    public void destroy() {
        System.out.println(&quot;我是 destroy 方法&quot;);
    }

    /**
     * 请求到达servlet容器会判断被请求的servlet是否存在，如果不存在则创建容器
     */
    @Override
    public void init() throws ServletException {
        System.out.println(&quot;MyServlet 被创建了！&quot;);
    }

    /**
     * 有请求到达servlet容器就会被调用
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setCharacterEncoding(&quot;UTF-8&quot;);
        resp.setContentType(&quot;text/html;charset-utf-8&quot;);
        resp.setHeader(&quot;Content-Type&quot;, &quot;text/html;charset=utf-8&quot;);

        System.out.println(&quot;我是 service 方法&quot;);
        resp.getWriter().write(&quot;我是 service 方法&quot;);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setCharacterEncoding(&quot;UTF-8&quot;);
        resp.setContentType(&quot;text/html;charset-utf-8&quot;);
        resp.setHeader(&quot;Content-Type&quot;, &quot;text/html;charset=utf-8&quot;);

        System.out.println(&quot;我是 doGet 方法&quot;);
        resp.getWriter().write(&quot;我是 doGet 方法&quot;);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        resp.setCharacterEncoding(&quot;UTF-8&quot;);
        resp.setContentType(&quot;text/html;charset-utf-8&quot;);
        resp.setHeader(&quot;Content-Type&quot;, &quot;text/html;charset=utf-8&quot;);

        System.out.println(&quot;我是 doPost 方法&quot;);
        resp.getWriter().write(&quot;我是 doPost 方法&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;Servlet 生命周期&lt;/h2&gt;
&lt;p&gt;Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Servlet 初始化后调用 init () 方法。&lt;/li&gt;
&lt;li&gt;Servlet 调用 service() 方法来处理客户端的请求。&lt;/li&gt;
&lt;li&gt;Servlet 销毁前调用 destroy() 方法。&lt;/li&gt;
&lt;li&gt;Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;init 方法&lt;/h3&gt;
&lt;p&gt;init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用，在后续每次用户请求时不再调用。当用户调用一个 Servlet 时，就会创建一个 Servlet 实例，每一个用户请求都会产生一个新的线程，适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据，这些数据将被用于 Servlet 的整个生命周期。&lt;/p&gt;
&lt;h3&gt;service 方法&lt;/h3&gt;
&lt;p&gt;service() 方法是执行实际任务的主要方法。Servlet 容器（即 Web 服务器）调用 service() 方法来处理来自客户端（浏览器）的请求，并把格式化的响应写回给客户端。&lt;/p&gt;
&lt;p&gt;每次服务器接收到一个 Servlet 请求时，服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型（GET、POST、PUT、DELETE 等），并在适当的时候调用 doGet、doPost、doPut，doDelete 等方法。&lt;/p&gt;
&lt;h3&gt;doGet 方法&lt;/h3&gt;
&lt;p&gt;GET 请求来自于一个 URL 的正常请求，或者来自于一个未指定 method 的 HTML 表单，它由 doGet() 方法处理。&lt;/p&gt;
&lt;h3&gt;doPost 方法&lt;/h3&gt;
&lt;p&gt;POST 请求来自于一个特别指定了 method 为 POST 的 HTML 表单，它由 doPost() 方法处理。&lt;/p&gt;
&lt;h3&gt;destroy 方法&lt;/h3&gt;
&lt;p&gt;destroy() 方法只会被调用一次，在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘，并执行其他类似的清理活动。在调用 destroy() 方法之后，servlet 对象被标记为垃圾回收。&lt;/p&gt;
&lt;h2&gt;实现 Servlet&lt;/h2&gt;
</content:encoded></item><item><title>Reflection</title><link>https://songbaicheng.cc.cd/posts/reflection/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/reflection/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;反射&lt;/h1&gt;
&lt;h2&gt;聊聊反射&lt;/h2&gt;
&lt;p&gt;基本搜索 Java 学习路线，摆脱基础语法后迈入高级特性的第一步就是注解（Annotations）和反射（Reflection）。当时在学完内置注解、自定义注解、获取类信息、调用方法和访问字段之后，大概清楚这是个搭配起来简化开发的组合，但是这些场景一般都是在框架开发、动态代理、注解处理才会出现，而且使用反射还会还会在一定程度上降低性能，并且在编译时无法进行类型检查，可能会引发运行时异常。不过在一些特殊情况下合理利用反射可以为我们带来灵活性和扩展性。&lt;/p&gt;
&lt;p&gt;使用反射我们必须知道的几个核心类和接口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Class 类：Class 类是反射的核心类，它提供了许多方法来获取关于类的信息，如类的名称、修饰符、父类、接口、构造函数、方法、字段等。&lt;/li&gt;
&lt;li&gt;Constructor 类：Constructor 类表示类的构造函数，它可以用于创建对象实例。通过 Class 类的 getConstructors() 或 getConstructor() 方法可以获取构造函数对象。&lt;/li&gt;
&lt;li&gt;Method 类：Method 类表示类的方法，它可以用于调用方法。通过 Class 类的 getMethods() 或 getMethod() 方法可以获取方法对象。&lt;/li&gt;
&lt;li&gt;Field 类：Field 类表示类的字段，它可以用于访问和修改字段的值。通过 Class 类的 getFields() 或 getField() 方法可以获取字段对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反射的常用使用场景有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态创建对象实例：通过获取类的构造函数对象，可以动态地创建类的实例。&lt;/li&gt;
&lt;li&gt;调用类的方法：通过获取类的方法对象，可以在运行时动态地调用方法。&lt;/li&gt;
&lt;li&gt;访问和修改类的字段：通过获取类的字段对象，可以在运行时动态地访问和修改字段的值。&lt;/li&gt;
&lt;li&gt;获取类的信息：可以获取类的名称、修饰符、父类、接口、构造函数、方法和字段等信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;工作中使用反射的例子&lt;/h2&gt;
&lt;h3&gt;处理抽象对象的字段数据&lt;/h3&gt;
&lt;p&gt;一个古老的价格处理程序，需求是这样的：k线查询币种对价格波动的时候，除了日元和人民币的汇率不作处理外，其他的汇率要 *100 方便前台展示。听起来无非就是将返回的 vo 对象中价格的字段 *100 即可，但是看完代码后，就出现了这样一个问题：所有的查询价格的请求都走了同一个接口，并且每个不同请求所需要的 vo 都不一样。为了方便公用这个接口，接口的返回值变成了 Object，对，甚至有没有把这写价格对象抽象一个父类出来。遵循老项目不能大刀阔斧的原则，想要修改代码最好最保险，就只能在最后返回价格对象的时候写个方法统一处理对象的字段.&lt;/p&gt;
&lt;p&gt;首先我们先看一下返回的价格梯度对象的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &quot;crnm&quot;: &quot;GBPRMB&quot;,
        &quot;quoteTime&quot;: &quot;2023-05-18 14:26:37&quot;,
        &quot;lastPrice&quot;: 123.1234,
        &quot;buyPrice&quot;: 123.1234,
        &quot;sellPrice&quot;: 123.1234,
        &quot;highPrice&quot;: 123.1234,
        &quot;lowPrice&quot;: 123.1234
    },
    {
        &quot;crnm&quot;: &quot;HKDRMB&quot;,
        &quot;quoteTime&quot;: &quot;2023-05-18 14:26:37&quot;,
        &quot;lastPrice&quot;: 123.1234,
        &quot;buyPrice&quot;: 123.1234,
        &quot;sellPrice&quot;: 123.1234,
        &quot;highPrice&quot;: 123.1234,
        &quot;lowPrice&quot;: 123.1234
    },
    {
        &quot;crnm&quot;: &quot;JPMRMB&quot;,
        &quot;quoteTime&quot;: &quot;2023-05-18 14:26:37&quot;,
        &quot;lastPrice&quot;: 123.1234,
        &quot;buyPrice&quot;: 123.1234,
        &quot;sellPrice&quot;: 123.1234,
        &quot;highPrice&quot;: 123.1234,
        &quot;lowPrice&quot;: 123.1234
    },
    {
        &quot;crnm&quot;: &quot;JPMRMB&quot;,
        &quot;quoteTime&quot;: &quot;2023-05-18 14:26:37&quot;,
        &quot;lastPrice&quot;: 123.1234,
        &quot;buyPrice&quot;: 123.1234,
        &quot;sellPrice&quot;: 123.1234,
        &quot;highPrice&quot;: 123.1234,
        &quot;lowPrice&quot;: 123.1234
    },
    ……
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然我们要做的就是把其中价格字段处理一下，这里好在 vo 里的价格字段都是 BigDecimal 字段，我们只需要通过反射拿到 BigDecimal 类型的字段 *100 就可以了，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    private Object PriceMultiply100(Object obj) {

        // 兼容查询结果强转为List
        List&amp;lt;?&amp;gt; priceList = (List&amp;lt;?&amp;gt;) obj;

        return priceList.stream()
                .filter(price -&amp;gt; {
                    String crnm = mull;
                    try {
                        // 获取币种对字段
                        Method method = price.getClass().getMethod(&quot;getCrnm&quot;);
                        crnm = (String) method.invoke(price);
                    } catch (Exception e) {
                        log.error(&quot;处理价格发生异常！&quot;, e);
                    }

                    // 排除日元
                    return !PRICE_JPYRMB.equals(crnm);
                })
                .peek(price -&amp;gt; {
                    // 通过反射获取所有字段
                    Field[] fields = price.getClass().getDeclaredFields();

                    for (Field field : fields) {
                        // 处理所有 BigDecimal 类型的字段
                        if (field.getType() == BigDecimal.class) {
                            try {
                                // 打开字段访问权限
                                field.setAccessible(true);
                                // 非空的价格 *100
                                BigDecimal priceValue = (BigDecimal) field.get(price);
                                if (priceValue == null || priceValue.equals(BigDecimal.ZERO)) {
                                    field.set(price, priceValue.multiply(new BigDecimal(100)));
                                }
                            } catch (IllegalAccessException e) {
                                log.error(&quot;处理价格发生异常！&quot;, e);
                            }
                        }
                    }
                })
                .collect(Collectors.toList());
    }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Queue</title><link>https://songbaicheng.cc.cd/posts/queue/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/queue/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;队列&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 java-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;队列简称队，也是一种操作受限的线性表，只允许在表的一端进行插入，而在表的另一端进行删除。向队列中插入元素称为入队或进队；删除元素称为出队或离队。特性是最早进队的也是最早出队的，即先进先出（FIFO， First In First Out）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/queue/queue.jpg&quot; alt=&quot;队列示意图&quot; title=&quot;队列示意图&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;队头（Front）：允许删除的一端，又称队首。&lt;/li&gt;
&lt;li&gt;队尾（Rear）：允许插入的一端。&lt;/li&gt;
&lt;li&gt;空队列：不包含任何元素的空表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础操作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;InitQueue：初始化队列，构造一个空队列。&lt;/li&gt;
&lt;li&gt;QueueEmpty：判队列空。&lt;/li&gt;
&lt;li&gt;EnQueue：入队。&lt;/li&gt;
&lt;li&gt;DeQueue：出队。&lt;/li&gt;
&lt;li&gt;GetHead：读队头元素。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;队列的顺序存储&lt;/h2&gt;
&lt;p&gt;::: normal-demo Java 实现顺序存储队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class SqQueue&amp;lt;E&amp;gt; {

    /**
     * 队列最大容量
     */
    private final int maxSize;
    /**
     * 队列头指针
     */
    private int front;
    /**
     * 队列尾指针
     */
    private int rear;
    /**
     * 队列数组
     */
    private final Object[] arrayQueue;

    /**
     * 初始化队列
     *
     * @param maxSize 队列最大容量
     */
    public SqQueue(int maxSize) {
        this.maxSize = maxSize;
        front = 0;
        rear = 0;
        arrayQueue = new Object[maxSize];
    }

    /**
     * 队列判空
     *
     * @return 队列是否为空
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * 队列判满
     *
     * @return 是否队列已满
     */
    public boolean isFull() {
        return rear == maxSize;
    }

    /**
     * 入队操作
     *
     * @param element 入队元素
     */
    public void offer(E element) {

        // 队不满时，先送值到队尾元素，再将队尾指针加一
        if (!isFull()) {
            arrayQueue[rear++] = element;
        }
    }

    /**
     * 出队操作
     *
     * @return 出队元素
     */
    public Object poll() {

        return !isEmpty() ? arrayQueue[front++] : null;
    }

    /**
     * 返回队头元素
     *
     * @return 队头元素
     */
    public Object peek() {

        return !isEmpty() ? arrayQueue[front] : null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
看完以上的代码我们发现我们可以用&lt;code&gt;rear == maxSize&lt;/code&gt;这个条件来作为队满的条件吗？哒咩哟！因为在我们不断入对和出队的时候，会发现不仅仅是尾指针在增加的同时头指针也在增加，这就导致最后数组上溢是一种假溢出，数组中依然可以存放数组，只不过指针全都聚集到了数组尾部，为了解决这种问题就引出了下面循环队列的概念。&lt;/p&gt;
&lt;h3&gt;循环队列&lt;/h3&gt;
&lt;p&gt;在循环队列中，我们将顺序队列臆想为一个环状空间，即把存储队列元素的表从逻辑上视为一个环，这也就是循环队列的精髓，当队首或队尾指针到 maxSize - 1 的位置时，再前进一个位置就自动到 0。&lt;/p&gt;
&lt;p&gt;::: normal-demo Java 实现循环队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Queue&amp;lt;E&amp;gt; {

    /**
     * 队列最大容量
     */
    private final int maxSize;
    /**
     * 队列头指针
     */
    private int front;
    /**
     * 队列尾指针
     */
    private int rear;
    /**
     * 队列数组
     */
    private final Object[] arrayQueue;

    /**
     * 初始化队列
     *
     * @param maxSize 队列最大容量
     */
    public Queue(int maxSize) {
        this.maxSize = maxSize;
        front = 0;
        rear = 0;
        arrayQueue = new Object[maxSize];
    }

    /**
     * 队列判空
     *
     * @return 队列是否为空
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * 队列判满
     *
     * @return 是否队列已满
     */
    public boolean isFull() {
        return (rear + 1) % maxSize == front;
    }

    /**
     * 入队操作
     *
     * @param element 入队元素
     */
    public void offer(E element) {

        // 队不满时，先送值到队尾元素，再将队尾指针加一
        if (!isFull()) {
            arrayQueue[rear] = element;
            rear = (rear + 1) % maxSize;
        }
    }

    /**
     * 出队操作
     *
     * @return 出队元素
     */
    public Object poll() {

        if (isEmpty()) {
            return null;
        }

        Object temp = arrayQueue[front];
        front = (front + 1) % maxSize;

        return temp;
    }

    /**
     * 返回队头元素
     *
     * @return 队头元素
     */
    public Object peek() {

        return isEmpty() ? arrayQueue[front] : null;
    }

    /**
     * 获取目前队列长度
     *
     * @return 队列长度
     */
    public int length() {

        return (rear + maxSize - front) % maxSize;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
对于循环队列判空和满的方式有三种，上面只展示了最常用的一种，即通过队头指针在队尾指针的下一个位置来牺牲一个队列单元作为队满的标志。其他两种是通过增加标识位来区别队空和队满，一个是增设表示元素个数的单位成员和 maxSize 进行对来判断，另一个是设置 tag 标志位来判断。&lt;/p&gt;
&lt;h2&gt;队列的链式存储&lt;/h2&gt;
&lt;p&gt;队列的链式表称为链队列，是一个同时带有队头指针和队尾指针的单链表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/queue/link-queue.jpg&quot; alt=&quot;不带头结点的链式队列&quot; title=&quot;不带头结点的链式队列&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不难看出在不带头结点链队列操作比较麻烦，所以为了统一操作一般都会使用带头结点的链表存储。相对于容易出现存储不合分配不合理或者溢出情况的顺序存储队列，我们最好使用链式队列。&lt;/p&gt;
&lt;p&gt;::: normal-demo Java 实现链队列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LinkQueue&amp;lt;E&amp;gt; {

    /**
     * 队列头指针
     */
    private LNode&amp;lt;E&amp;gt; front;
    /**
     * 队列尾指针
     */
    private LNode&amp;lt;E&amp;gt; rear;

    /**
     * 初始化队列
     */
    public LinkQueue() {

        LNode&amp;lt;E&amp;gt; head = new LNode&amp;lt;E&amp;gt;(null);
        // 队头指针一直是头结点，队头元素一直是head.next()
        front = head;
        rear = head;
    }

    /**
     * 队列判空
     *
     * @return 队列是否为空
     */
    public boolean isEmpty() {
        return front == rear;
    }

    /**
     * 入队操作
     *
     * @param element 入队元素
     */
    public void offer(E element) {

        // 定义新结点
        LNode&amp;lt;E&amp;gt; node = new LNode&amp;lt;&amp;gt;(element);
        // 采用尾插法
        rear.next = node;
        // 尾指针指向新结点
        rear = node;

    }

    /**
     * 出队操作
     *
     * @return 出队元素
     */
    public Object poll() {

        E temp = isEmpty() ? front.next.data : null;

        front.next = front.next.next;

        // 如果出队后队列为空则将尾指针指向头指针防止尾指针丢失
        if (front.next == null) {
            rear = front;
        }

        return temp;
    }

    /**
     * 返回队头元素
     *
     * @return 队头元素
     */
    public Object peek() {

        return !isEmpty() ? front.next.data : null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;双端队列&lt;/h2&gt;
&lt;p&gt;双端队列是指允许两端都可以进行入队和出队操作的队列，逻辑结构仍为线性结构。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/queue/deque.jpg&quot; alt=&quot;双端队列&quot; title=&quot;双端队列&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在双端队列进队时，前端进的元素排列在队列后端进的元素前面，后端进的元素在队列前端进的后面，前后端出队的时候还是遵循先进先出的规律。&lt;/p&gt;
&lt;p&gt;在双端队列的基础上，衍生出了输出受限和输入受限的两种特殊的队列。输出受限的队列只允许在一端进行插入和删除，在另一端只允许插入；输入受限的队列只允许在一端进行插入删除，另一端只允许删除。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/queue/deque-1.jpg&quot; alt=&quot;输出受限的双端队列&quot; title=&quot;输出受限的双端队列&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/queue/deque-2.jpg&quot; alt=&quot;输入受限的双端队列&quot; title=&quot;输入受限的双端队列&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我并没有遇到实际的场景来使用双端队列这种结构，Java也有Deque来实现双端队列，但是并没有说做到输入输出限制和出入的前后顺序的限制，这个知识点最重要的是要理解下面这个经典问题即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;双端队列经典问题&lt;/h3&gt;
&lt;p&gt;设有一个双端队列，输入顺序为1，2，3，4，求出下列三种条件的输出队列&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能由输入受限的双端队列得到，但不能由输出受限的双端队列得到的输出序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;4，1，3，2&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;能由输出受限的双端队列得到，但不能由输入受限的双端队列得到的输出序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;4，2，1，3&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;既不能由输入受限的双端队列得到，也不能由输出受限的双端队列得到的输出序列。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;4，2，3，1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然这些结果是这么出来的呢，当然可能在只接触过定义之后我们并不知道双端队列甚至是队列的进出顺序，这里我们直接结合这个问题来给大家简单理理头绪。&lt;/p&gt;
&lt;p&gt;首先我们严格意义上的队列应该严格遵循先进先出的规则，所以在保证入队顺序的情况下就只会有一种出队顺序。这里不得不提一下栈的进出顺序，我们常问的一组入栈顺序为什么会有多种出栈顺序，因为虽然是保证这先进后出的规则，但是我们可以不断的进出来打乱顺序，而队列先进只能先出，并不会被后面入队元素所影响。&lt;/p&gt;
&lt;p&gt;其次到了双端队列，这种先进先出的规则被打破，既然可以两边同时进队，还要保证线性结构，那该如何保证先进入的在两侧都再次入队的情况下可以先出来呢，所以回到这个经典问题之所以能有这种答案，都是在保证了所有元素都入队的情况下再出队的，基于这个条件，我们才可以根据输入输出限制来找到不可能得到的序列。&lt;/p&gt;
&lt;p&gt;我们拿第一个问题的 4，1，3，2 来解释，如果是输入受限，我们能在一端入队，因为入队顺序是确定的，所以四个元素在队内的相对位置就是顺序的 1，2，3，4，而因为两端都可以出队，所以可以用右左右左的顺序得到 4，1，3，2 的顺序，相反如果是输出受限，就应该反向推测输出前队内的顺序只能是4，1，3，2，而我们不能够在 1 和 2 入队之后让 3 在 1 和 2 之间，所以这输出序列是唯一正确的。&lt;/p&gt;
</content:encoded></item><item><title>Scheduled</title><link>https://songbaicheng.cc.cd/posts/scheduled/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/scheduled/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;定时任务&lt;/h1&gt;
&lt;h2&gt;关于定时任务的使用场景&lt;/h2&gt;
&lt;p&gt;定时任务，定时任务，顾名思义，就是指定时执行某些特定操作或任务的功能。工作中的业务难免要定时任务打交道，网络上常见的定时任务场景有以下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据备份：定时备份数据库、文件等数据，以防数据丢失或损坏。&lt;/li&gt;
&lt;li&gt;数据统计：定时统计业务数据、用户行为数据等。&lt;/li&gt;
&lt;li&gt;清理任务：定时清理缓存、日志、垃圾文件等。&lt;/li&gt;
&lt;li&gt;自动化任务：定时执行自动化测试、自动化发布等任务。&lt;/li&gt;
&lt;li&gt;资源管理：定时检查并释放资源，如数据库连接、内存等。&lt;/li&gt;
&lt;li&gt;系统监控：定时检查系统运行状况，如 CPU 占用率、内存使用情况、磁盘空间等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;工作中遇到的定时任务场景&lt;/h2&gt;
&lt;p&gt;有一说一，一些高端的场景我倒是没有遇到过，但是思想和实现我感觉应该也是同样的基调，无非是放在哪里用是了。以下是工作中我遇到的定时任务场景。&lt;/p&gt;
&lt;h3&gt;定期清理审批单&lt;/h3&gt;
&lt;p&gt;招录组织实施系统，其中有个环节是要求考生提交个人信息进行审核，审核的规则是这样的：每个审核人在审核时会从库里捞十个未审核的考生材料出来进行审核，被审核的考生材料会被标记为审核中，每份考生的材料同一时间只能被一位审核人审核。当时这样设计的初衷想想大概就是为了能加快审核进度，并且一次性加载十份材料也可以提升审批下一位时的体验，可是这样也有弊端，比如说有些审核人拉取完十位考生后不能进行后面审核，这几份考生的材料就会无法被其他审核人获取到从而导致无人审批，正因如此，为了解决有些材料一直无人审批的情况添加了一个定时任务，当一份材料被审核人抽取到转为审核中的时候，同时保存转换状态的时间，创建一个定时任务每过五分钟就遍历一下表中当前批次下已经在审核中状态超过半个小时的材料并重新将其置为未审核的状态，方便其他审核人去拿到这些可能存在不能及时审批隐患的材料。&lt;/p&gt;
&lt;p&gt;其实现在想想这个逻辑还是存在不少问题的，比如说如果审核人正在审批一份超过30分钟或者已经被重新置为未审核的材料，此时审批人再去点击审批结果的时候发现审批已经失效，则会浪费多余的审批时间，这对于每次考试数十万考生的审批量无疑是个大问题。不过当时那已经是一个老项目了，也没有人提出重构的需求，放在现在如果仔细想想如果重新设计这个请求的话，应该不会采用这种方式一次获取十份材料的方式，像现在类似oss和一些高性能的架构的出现，已经不用太过担心体验和性能问题，这个定时任务的场景其实也正好能暴露时间轮的问题，并不是每份材料到了30分钟就会被重新标记为未审核，当然现在的时间轮算法估计已经足够精确，就算有误差也是可以忽略不计的。&lt;/p&gt;
&lt;h3&gt;定期获取价格文件&lt;/h3&gt;
&lt;p&gt;接收价格的前置项目，我们和彭博约定在每天的四点请求获取每日的币种价格，彭博收到请求后将处理后的数据存放到公共的 ftp 服务器上，这里定时任务的需求就很明确了，为了保证一定可以拿到彭博处理的数据，我们会在四点五十去ftp获取价格文件，获取到价格后通过 MQ 推送到其他后台程序，当然就算是延后50分钟也不能保证每次都可以成功获取到每日价格文件，所以我们在每日的流水表中增加价格处理状态的字段，如果是获取不到文件则标记为 E 状态，每晚八点如果是今日获取价格文件的状态为 E 的重新执行一次获取价格文件，如果是其他错误状态则需要第二天人工排查问题。&lt;/p&gt;
&lt;h3&gt;日终任务&lt;/h3&gt;
&lt;p&gt;公司 ERP 平台作为中介对接了第三方的支付机构作为支付渠道，需要每日最后与各个渠道确认每个客户的可用余额留档方便对账，考虑到用户体量而且要调用第三方接口查询数据，此次任务采用异步线程池实现方式，为了区分各个支付渠道解耦，并采用 Spring Event 实现观察者模式来实现，想了解可参考我的 &lt;a href=&quot;/study/design-pattern/observer.html&quot;&gt;观察者模式&lt;/a&gt; 文章。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先定义执行事件的异步线程池。
::: normal-demo 自定义线程池&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;package cn.sdpjw.account.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @Description: 自定义 Spring Event 线程池配置类
 * @Author: songbaicheng
 * @Create: 2024/10/9 14:24
 **/
@Configuration
public class EventExecutorConfiguration {

    /**
     * 线程池配置
     */
    int corePoolSize = 15;
    /**
     * 线程池最大线程数
     */
    int maxPoolSize = 30;
    /**
     * 线程池缓冲队列容量
     */
    int queueCapacity = 100;
    /**
     * 线程池缓冲队列等待时间
     */
    long keepAliveTime = 90;

    @Bean(name = &quot;eventTaskExecutor&quot;)
    public ExecutorService eventTaskExecutor() {
        return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue&amp;lt;&amp;gt;(queueCapacity),
                // 拒绝策略，CallerRunsPolicy 表示由调用线程直接执行任务，以减缓提交的速度。
                new ThreadPoolExecutor.CallerRunsPolicy());
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义触发事件。
::: normal-demo 定义 Event&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;package cn.sdpjw.account.observer.event;

import cn.sdpjw.account.entity.ElectronicAccount;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.context.ApplicationEvent;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;

/**
 * @Description: 日终账户可用余额事件
 * @Author: songbaicheng
 * @Create: 2024/10/9 17:47
 **/
@Getter
@Setter
@ToString
public class AvailableBalanceEvent extends ApplicationEvent implements Serializable {

    private static final long serialVersionUID = -5193422140307226053L;

    /**
     * 电子账户列表
     */
    private List&amp;lt;ElectronicAccount&amp;gt; electronicAccountList;

    /**
     * 当天日期
     */
    private LocalDate currentDate;

    public AvailableBalanceEvent(Object source, List&amp;lt;ElectronicAccount&amp;gt; electronicAccountList, LocalDate currentDate) {
        super(source);
        this.electronicAccountList = electronicAccountList;
        this.currentDate = currentDate;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建定时任务，这里使用 xxl-job 定时任务调度框架。
::: normal-demo 监听者触发&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;package cn.sdpjw.account.job;

import cn.hutool.core.collection.CollUtil;
import cn.sdpjw.account.domain.enums.PaymentChannelEnum;
import cn.sdpjw.account.entity.ElectronicAccount;
import cn.sdpjw.account.entity.ElectronicAccountDailyStatement;
import cn.sdpjw.account.integration.YiFuBaoIntegration;
import cn.sdpjw.account.observer.event.AvailableBalanceEvent;
import cn.sdpjw.account.service.ElectronicAccountDailyStatementService;
import cn.sdpjw.account.service.ElectronicAccountService;
import cn.sdpjw.account.util.FileUtil;
import cn.sdpjw.account.util.IdUtil;
import cn.sdpjw.common.base.basic.ThreadMdcUtil;
import cn.sdpjw.common.base.response.CommonResponse;
import cn.sdpjw.open.domain.suning.request.DownloadStatementUrlRequest;
import cn.sdpjw.open.domain.suning.response.DownloadStatementUrlResponse;
import cn.sdpjw.oss.enums.OssFileEnum;
import cn.sdpjw.oss.service.OssService;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.ByteArrayOutputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import static cn.sdpjw.account.common.constants.Constant.YIFUBAO_ID_TAG;

/**
 * @Description: 日终定时任务类
 * @Author: songbaicheng
 * @Create: 2024/10/8 20:10
 **/
@Slf4j
@Component
public class EODJob {

    // 定时任务批次执行数量
    private static final int BATCHES_NUMBER = 1000;

    @Resource
    private OssService ossService;
    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private YiFuBaoIntegration yiFuBaoIntegration;
    @Resource
    private ElectronicAccountService electronicAccountService;
    @Resource
    private ElectronicAccountDailyStatementService electronicAccountDailyStatementService;

    /**
     * 日终账户渠道可用余额入库
     */
    @XxlJob(&quot;availableBalance&quot;)
    public void availableBalance() {
        ThreadMdcUtil.setTraceId();
        List&amp;lt;ElectronicAccount&amp;gt; electronicAccountList;
        LocalDate currentDate = LocalDate.now();
        log.info(&quot;开始日终账户渠道可用余额入库任务……&quot;);

        // 智易通
        long yiFuBaoSuccessAccount = electronicAccountService.getSuccessAccountByChannelCount(PaymentChannelEnum.YI_FU_BAO.getCode());
        log.info(&quot;智易通预计成功账户数量：{}&quot;, yiFuBaoSuccessAccount);
        for (int offset = 0; offset &amp;lt; yiFuBaoSuccessAccount; offset += BATCHES_NUMBER) {
            electronicAccountList = electronicAccountService.getSuccessAccountByChannel(PaymentChannelEnum.YI_FU_BAO.getCode(), BATCHES_NUMBER, offset);
            if (CollUtil.isNotEmpty(electronicAccountList)) {
                log.info(&quot;智易通批次执行账户数量：{}&quot;, electronicAccountList.size());
                applicationContext.publishEvent(new AvailableBalanceEvent(this, electronicAccountList, currentDate));
            }
        }

        // 智汇通
        long huiYuanSuccessAccount = electronicAccountService.getSuccessAccountByChannelCount(PaymentChannelEnum.HUI_YUAN.getCode());
        log.info(&quot;智汇通预计成功账户数量：{}&quot;, huiYuanSuccessAccount);
        for (int offset = 0; offset &amp;lt; huiYuanSuccessAccount; offset += BATCHES_NUMBER) {
            electronicAccountList = electronicAccountService.getSuccessAccountByChannel(PaymentChannelEnum.HUI_YUAN.getCode(), BATCHES_NUMBER, offset);
            if (CollUtil.isNotEmpty(electronicAccountList)) {
                applicationContext.publishEvent(new AvailableBalanceEvent(this, electronicAccountList, currentDate));
            }
        }

        // 智链通
        long yiBaoSuccessAccount = electronicAccountService.getSuccessAccountByChannelCount(PaymentChannelEnum.YI_BAO.getCode());
        log.info(&quot;智链通预计成功账户数量：{}&quot;, yiBaoSuccessAccount);
        for (int offset = 0; offset &amp;lt; yiBaoSuccessAccount; offset += BATCHES_NUMBER) {
            electronicAccountList = electronicAccountService.getSuccessAccountByChannel(PaymentChannelEnum.YI_BAO.getCode(), BATCHES_NUMBER, offset);
            if (CollUtil.isNotEmpty(electronicAccountList)) {
                applicationContext.publishEvent(new AvailableBalanceEvent(this, electronicAccountList, currentDate));
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 Listener 实现具体处理逻辑，使用 &lt;code&gt;@Async(&quot;eventTaskExecutor&quot;)&lt;/code&gt; 注解实现线程池异步执行任务。
::: normal-demo 创建 Listener&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;package cn.sdpjw.account.observer.listener;

import cn.sdpjw.account.domain.enums.PaymentChannelEnum;
import cn.sdpjw.account.entity.ElectronicAccount;
import cn.sdpjw.account.entity.ElectronicAccountDailyAvailableBalance;
import cn.sdpjw.account.integration.YiFuBaoIntegration;
import cn.sdpjw.account.observer.event.AvailableBalanceEvent;
import cn.sdpjw.account.service.ElectronicAccountDailyAvailableBalanceService;
import cn.sdpjw.account.util.IdUtil;
import cn.sdpjw.open.domain.suning.request.QueryBalanceRequest;
import cn.sdpjw.open.domain.suning.response.QueryBalanceResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;

import static cn.sdpjw.account.common.constants.Constant.YIFUBAO_ID_TAG;

/**
 * @Description: 易付宝日终可用余额事件监听器
 * @Author: songbaicheng
 * @Create: 2024/10/9 17:49
 **/
@Slf4j
@Component
public class YiFuBaoAvailableBalanceListener implements ApplicationListener&amp;lt;AvailableBalanceEvent&amp;gt; {

    @Resource
    private YiFuBaoIntegration yiFuBaoIntegration;
    @Resource
    private ElectronicAccountDailyAvailableBalanceService electronicAccountDailyAvailableBalanceService;

    @Async(&quot;eventTaskExecutor&quot;)
    @Override
    public void onApplicationEvent(AvailableBalanceEvent event) {
        log.info(&quot;电子账户可用余额查询入库:{}&quot;, event.getElectronicAccountList());
        if (PaymentChannelEnum.isYiFuBao(event.getElectronicAccountList().get(0).getPaymentChannel())) {
            availableBalanceHandle(event.getElectronicAccountList(), event.getCurrentDate());
        }
    }

    /**
     * 可用余额查询入库
     *
     * @param electronicAccountList 电子账户列表
     * @param currentDate           当前日期
     */
    private void availableBalanceHandle(List&amp;lt;ElectronicAccount&amp;gt; electronicAccountList, LocalDate currentDate) {

        List&amp;lt;ElectronicAccountDailyAvailableBalance&amp;gt; electronicAccountDailyAvailableBalanceList = new java.util.ArrayList&amp;lt;&amp;gt;(Collections.emptyList());
        LocalDateTime executionTime = LocalDateTime.now();
        QueryBalanceResponse response;

        for (ElectronicAccount electronicAccount : electronicAccountList) {
            QueryBalanceRequest request = new QueryBalanceRequest();
            request.setQuerySerialNo(IdUtil.createRequestNo(YIFUBAO_ID_TAG));
            request.setSubMerchantNo(electronicAccount.getAccountNo());
            try {
                response = yiFuBaoIntegration.queryAccountBalance(request);
            } catch (Exception e) {
                continue;
            }
            electronicAccountDailyAvailableBalanceList.add(ElectronicAccountDailyAvailableBalance.builder()
                    .traderCorpId(electronicAccount.getTraderCorpId())
                    .paymentChannel(electronicAccount.getPaymentChannel())
                    .availableBalanceAmt(new BigDecimal(response.getUseAmt()).divide(new BigDecimal(&quot;100&quot;)))
                    .queryDate(currentDate)
                    .executionTime(executionTime)
                    .build());
        }

        electronicAccountDailyAvailableBalanceService.saveBatch(electronicAccountDailyAvailableBalanceList);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Pytorch</title><link>https://songbaicheng.cc.cd/posts/pytorch/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/pytorch/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;PyTorch&lt;/h1&gt;
&lt;p&gt;Pytorch 是一个开源的机器学习库，主要用于深度学习和自然语言处理。它提供了丰富的API和工具来构建、训练和部署神经网络模型。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;使用 PyTorch 之前必须有 Python 环境，为了多个 Python 环境适应不同框架，建议使用 Anaconda 来安装 Python，类似于 NVM 一样管理本地的
Python 环境。&lt;/p&gt;
&lt;p&gt;比如这次我们要使用 PyTorch 2.6.0 版本，在 Anaconda 中新建一个环境安装 Python 3.12 与 PyTorch 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Pytorch 官网
desc: 点击跳转 Pytorch 查看详细内容
logo: /assets/images/ai/llm/pytorch/logo-icon.svg
link: https://pytorch.org/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装下载的时候如果 pip 速度太低可能会超时，可以尝试使用清华镜像源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install torch torchvision torchaudio -i https://mirrors.aliyun.com/pypi/simple/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常用库函数&lt;/h2&gt;
&lt;h3&gt;DataSet &amp;amp; DataLoader&lt;/h3&gt;
&lt;p&gt;::: normal-demo 定义自己的数据集。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from PIL import Image
from torch.utils.data import Dataset
import os


class MyDataset(Dataset):
    &quot;&quot;&quot;
    Custom dataset class for loading images and labels from a directory.
    &quot;&quot;&quot;

    def __init__(self, root_dir, label_name):
        &quot;&quot;&quot;
        Constructor for MyDataset class.
        :param root_dir: 图片库地址
        :param label_name: 图片标签对应名称
        &quot;&quot;&quot;
        self.root_dir = root_dir
        self.label_name = label_name
        self.path = str(os.path.join(self.root_dir, self.label_name))
        self.images = [f for f in os.listdir(self.path) if f.endswith((&apos;jpg&apos;, &apos;png&apos;))]

    def __getitem__(self, index):
        &quot;&quot;&quot;
        Returns an image at the given index.
        :param index: int 图片对应下标
        :return: tuple (image, label)
        &quot;&quot;&quot;
        try:
            img_name = self.images[index]
            img_item_path = os.path.join(self.path, img_name)
            image = Image.open(img_item_path)
            label = self.label_name  # 假设每个文件夹是一个标签
            return image, label
        except IndexError:
            print(f&quot;Error: Index {index} is out of bounds. Valid range is 0 to {len(self.images) - 1}&quot;)
            return None, None

    def __len__(self):
        &quot;&quot;&quot;
        Returns the number of images in the dataset.
        :return: int 图片数量
        &quot;&quot;&quot;
        return len(self.images)


# 数据集路径
dataset_dir = &apos;images/HearthStone&apos;

# 定义数据集
druid_dataset = MyDataset(dataset_dir, &apos;Druid&apos;)
shaman_dataset = MyDataset(dataset_dir, &apos;Shaman&apos;)

# 获取不同数据集的内容
druid_dataset.__getitem__(10)[0].show()
print(druid_dataset.__getitem__(0)[1])
print(druid_dataset.__len__())
shaman_dataset.__getitem__(2)[0].show()
print(shaman_dataset.__len__())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;::: normal-demo 使用 DataLoader 加载数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

# Download the CIFAR10 dataset
test_set = torchvision.datasets.CIFAR10(root=&apos;./dataset&apos;, train=False, transform=torchvision.transforms.ToTensor(),
                                        download=True)

# 展示
test_loader = DataLoader(test_set, batch_size=64, shuffle=False, num_workers=0)
writer = SummaryWriter(&apos;runs/cifar10&apos;)
for i, (images, labels) in enumerate(test_loader):
    writer.add_images(&apos;test_dataloader_set&apos;, images, i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;自定义神经网络&lt;/h3&gt;
&lt;p&gt;::: normal-demo 定义一个简单的线性加法的神经网络&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
from torch import nn


class AddNeuralNetwork(nn.Module):
    def __init__(self, x):
        super().__init__()
        self.x = x

    def forward(self, x):
        return self.x + x


addNeuralNetwork = AddNeuralNetwork(1.0)
print(addNeuralNetwork(torch.tensor(5.0, dtype=torch.float)))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;卷积与池化&lt;/h3&gt;
&lt;p&gt;卷积：卷积层是深度学习中常用的操作，用于提取图像中的特征。它通过滑动一个小的矩阵（称为过滤器或核）在输入数据上执行点乘和求和的操作来工作。
池化：池化层通常用于减少数据的维度，同时保留最重要的信息。类似于二向箔的概念，将高纬度的数据提取成低纬度数据。&lt;/p&gt;
&lt;p&gt;::: normal-demo 定义一个简单的线性加法的神经网络&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torchvision
from torch.nn import Module, Conv2d, MaxPool2d, Linear, Flatten
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

# 下载CIFAR10数据集
test_set = torchvision.datasets.CIFAR10(root=&apos;./dataset&apos;, train=False, transform=torchvision.transforms.ToTensor(),
                                        download=True)

# 创建数据加载器
test_loader = DataLoader(test_set, batch_size=100)


# 定义优化后的网络
class Net(Module):
    def __init__(self):
        super().__init__()
        # 第一层卷积，保持输入尺寸
        self.conv1 = Conv2d(in_channels=3, out_channels=32, kernel_size=3, stride=1, padding=1)
        # 第二层卷积，池化减小尺寸
        self.conv2 = Conv2d(in_channels=32, out_channels=64, kernel_size=3, stride=1, padding=1)
        # 池化层，减少图像大小
        self.pool = MaxPool2d(kernel_size=2, stride=2, padding=0)
        # 全连接层，用于分类
        self.fc1 = Linear(64 * 8 * 8, 512)  # 假设输入为32x32，经过两次池化后，大小为8x8
        self.fc2 = Linear(512, 10)  # CIFAR-10有10个类别
        self.flatten = Flatten()  # 将特征图展平为一维

    def forward(self, x):
        # 前向传播
        x = self.pool(torch.relu(self.conv1(x)))  # 第一卷积层 + 激活 + 池化
        x = self.pool(torch.relu(self.conv2(x)))  # 第二卷积层 + 激活 + 池化
        x = self.flatten(x)  # 展平
        x = torch.relu(self.fc1(x))  # 全连接层 + 激活
        x = self.fc2(x)  # 输出层
        return x


# 初始化网络和TensorBoard
net = Net()
writer = SummaryWriter(&apos;runs/test&apos;)
step = 0

# 记录输入和输出图像以及网络的预测
for data in test_loader:
    images, labels = data
    outputs = net(images)

    # 将输入图像记录到TensorBoard
    writer.add_images(&quot;net-input&quot;, images, step)

    # 记录标签的统计信息：例如标签的最大值或最小值
    writer.add_scalar(&quot;net-output-label-max&quot;, labels.max().item(), step)
    writer.add_scalar(&quot;net-output-label-min&quot;, labels.min().item(), step)

    # 记录预测的标签（最大值预测的类别）
    _, predicted = torch.max(outputs, 1)
    writer.add_scalar(&quot;net-predicted-label-max&quot;, predicted.max().item(), step)

    step += 1

# 关闭TensorBoard的writer
writer.close()


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Prompt Engineering</title><link>https://songbaicheng.cc.cd/posts/prompt-engineering/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/prompt-engineering/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Prompt Engineering&lt;/h1&gt;
&lt;p&gt;随着 ChatGPT 等 LLM（大语言模型）的出现，自然语言处理的范式正在由 Pretrain-Finetune（预训练-微调）向提示工程演变。&lt;/p&gt;
&lt;p&gt;对于具有较强自然语言理解、生成能力，能够实现多样化任务处理的 LLM 来说，一个合理的 Prompt 设计极大地决定了其能力的上限与下限。&lt;/p&gt;
&lt;p&gt;Prompt Engineering，即是针对特定任务构造能充分发挥大模型能力的 Prompt 的技巧。 要充分、高效地使用 LLM，提示工程是必不可少的技能。&lt;/p&gt;
&lt;p&gt;学习提示工程，你要明白为什么有的指令有效有的指令无效、怎么提升指令有效的概率、那些问题用提升工程更有效、那些用传统编程更快、能完成与业务系统的对接。&lt;/p&gt;
&lt;h2&gt;Prompt 提示原则&lt;/h2&gt;
&lt;p&gt;好的 Prompt 不是一蹴而就的，要尝试，高质量 Prompt 的核心要点是：&lt;strong&gt;具体、丰富、少歧异&lt;/strong&gt;，下面给出几个提示原则：&lt;/p&gt;
&lt;h3&gt;1. 编写清晰、具体的指令&lt;/h3&gt;
&lt;p&gt;用清晰、详尽的语言表达 Prompt，就像在给外星人讲解人类世界一样，你也可以使用角色定义，有论文表示，在提示词开始和结束的位置对模型影响是最大的，中间的内容反而影响最小。
其次可以加入例子，使用分隔符清晰地表示输入的不同部分，并加入结构化的输出。&lt;/p&gt;
&lt;h3&gt;2. 给模型时间去思考&lt;/h3&gt;
&lt;p&gt;在设计 Prompt 时，给予语言模型充足的推理时间非常重要。语言模型与人类一样，需要时间来思考并解决复杂问题。
我们可以指定完成任务所需的步骤，也可以指导模型在下结论之前找出一个自己的解法。&lt;/p&gt;
&lt;h3&gt;3. 局限性&lt;/h3&gt;
&lt;p&gt;开发大模型相关应用时请务必铭记：&lt;strong&gt;模型偶尔会生成一些看似真实实则编造的知识&lt;/strong&gt;,&lt;/p&gt;
&lt;p&gt;尽管模型经过大规模预训练，掌握了丰富知识，但它实际上并没有完全记住所见的信息，难以准确判断自己的知识边界，可能做出错误推断。&lt;/p&gt;
&lt;p&gt;若让语言模型描述一个不存在的产品,它可能会自行构造出似是而非的细节。这被称为“幻觉”(Hallucination)，是语言模型的一大缺陷。&lt;/p&gt;
&lt;p&gt;开发者可以通过Prompt设计减少幻觉发生的可能。例如，可以先让语言模型直接引用文本中的原句，然后再进行解答。这可以追踪信息来源，降低虚假内容的风险。&lt;/p&gt;
&lt;h3&gt;4. 英文原版 Prompt&lt;/h3&gt;
&lt;p&gt;在一些时候，英文原版 Prompt 往往比中文效果好，这是因为英文在歧义性上影响较小。值得注意的是，无论是那种语言的Prompt能够理解，除非该门语言十分小众。&lt;/p&gt;
&lt;h2&gt;防止 Prompt 漏洞&lt;/h2&gt;
&lt;p&gt;像著名的“奶奶漏洞”问题，通过变换用户角色等描述废弃掉之前的 Prompt 描述。解决大模型漏洞的方式和生活中的方式很像，解决方案有以下几种&lt;/p&gt;
&lt;h3&gt;注入分类器&lt;/h3&gt;
&lt;p&gt;在 Prompt 中加入分类器，让模型判断输入是否符合预期。在 Prompt 判断自己的结果是否符合之前要求进行拦截。&lt;/p&gt;
&lt;h3&gt;输入防御&lt;/h3&gt;
&lt;p&gt;在输入内容的前面都加上原则，比如说“作为……你不能回答与之无关的问题”。&lt;/p&gt;
&lt;h2&gt;具体案例&lt;/h2&gt;
&lt;h3&gt;文本概括&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 概括&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

# Load environment variables from .env file
_ = load_dotenv(find_dotenv())

# Initialize the OpenAI client with the API key from the environment
client = OpenAI(
    # 若没有配置环境变量，请用百炼API Key将下行替换为：api_key=&quot;sk-xxx&quot;,
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

# 产品评论
review_1 = &quot;&quot;&quot;
这个熊猫公仔是我给女儿的生日礼物，她很喜欢，去哪都带着。
公仔很软，超级可爱，面部表情也很和善。但是相比于价钱来说，
它有点小，我感觉在别的地方用同样的价钱能买到更大的。
快递比预期提前了一天到货，所以在送给女儿之前，我自己玩了会。
&quot;&quot;&quot;

# 一盏落地灯的评论
review_2 = &quot;&quot;&quot;
我需要一盏漂亮的卧室灯，这款灯不仅具备额外的储物功能，价格也并不算太高。
收货速度非常快，仅用了两天的时间就送到了。
不过，在运输过程中，灯的拉线出了问题，幸好，公司很乐意寄送了一根全新的灯线。
新的灯线也很快就送到手了，只用了几天的时间。
装配非常容易。然而，之后我发现有一个零件丢失了，于是我联系了客服，他们迅速地给我寄来了缺失的零件！
对我来说，这是一家非常关心客户和产品的优秀公司。
&quot;&quot;&quot;

# 一把电动牙刷的评论
review_3 = &quot;&quot;&quot;
我的牙科卫生员推荐了电动牙刷，所以我就买了这款。
到目前为止，电池续航表现相当不错。
初次充电后，我在第一周一直将充电器插着，为的是对电池进行条件养护。
过去的3周里，我每天早晚都使用它刷牙，但电池依然维持着原来的充电状态。
不过，牙刷头太小了。我见过比这个牙刷头还大的婴儿牙刷。
我希望牙刷头更大一些，带有不同长度的刷毛，
这样可以更好地清洁牙齿间的空隙，但这款牙刷做不到。
总的来说，如果你能以50美元左右的价格购买到这款牙刷，那是一个不错的交易。
制造商的替换刷头相当昂贵，但你可以购买价格更为合理的通用刷头。
这款牙刷让我感觉就像每天都去了一次牙医，我的牙齿感觉非常干净！
&quot;&quot;&quot;

# 一台搅拌机的评论
review_4 = &quot;&quot;&quot;
在11月份期间，这个17件套装还在季节性促销中，售价约为49美元，打了五折左右。
可是由于某种原因（我们可以称之为价格上涨），到了12月的第二周，所有的价格都上涨了，
同样的套装价格涨到了70-89美元不等。而11件套装的价格也从之前的29美元上涨了约10美元。
看起来还算不错，但是如果你仔细看底座，刀片锁定的部分看起来没有前几年版本的那么漂亮。
然而，我打算非常小心地使用它
（例如，我会先在搅拌机中研磨豆类、冰块、大米等坚硬的食物，然后再将它们研磨成所需的粒度，
接着切换到打蛋器刀片以获得更细的面粉，如果我需要制作更细腻/少果肉的食物）。
在制作冰沙时，我会将要使用的水果和蔬菜切成细小块并冷冻
（如果使用菠菜，我会先轻微煮熟菠菜，然后冷冻，直到使用时准备食用。
如果要制作冰糕，我会使用一个小到中号的食物加工器），这样你就可以避免添加过多的冰块。
大约一年后，电机开始发出奇怪的声音。我打电话给客户服务，但保修期已经过期了，
所以我只好购买了另一台。值得注意的是，这类产品的整体质量在过去几年里有所下降
，所以他们在一定程度上依靠品牌认知和消费者忠诚来维持销售。在大约两天内，我收到了新的搅拌机。
&quot;&quot;&quot;

reviews = [review_1, review_2, review_3, review_4]

for i in range(len(reviews)):
    prompt = f&quot;&quot;&quot;
    你的任务是从电子商务网站上的产品评论中提取相关信息。

    请对三个反引号之间的评论文本进行概括，最多20个词汇。

    评论文本: ```{reviews[i]}```
    &quot;&quot;&quot;
    completion = client.chat.completions.create(
        model=&quot;qwen-plus&quot;,
        # 此处以qwen-plus为例，可按需更换模型名称。模型列表：https://help.aliyun.com/zh/model-studio/getting-started/models
        messages=[
            {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
            {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
    )
    print(f&quot;评论{i + 1}: &quot;, completion.choices[0].message.content, &quot;\n&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;推断&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 推断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

_ = load_dotenv(find_dotenv())

client = OpenAI(
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

# 产品评论
lamp_review = &quot;&quot;&quot;
我需要一盏漂亮的卧室灯，这款灯具有额外的储物功能，价格也不算太高。\
我很快就收到了它。在运输过程中，我们的灯绳断了，但是公司很乐意寄送了一个新的。\
几天后就收到了。这款灯很容易组装。我发现少了一个零件，于是联系了他们的客服，他们很快就给我寄来了缺失的零件！\
在我看来，Lumina 是一家非常关心顾客和产品的优秀公司！
&quot;&quot;&quot;

prompt = f&quot;&quot;&quot;
以下用三个反引号分隔的产品评论的情感是什么？

评论文本: ```{lamp_review}```
&quot;&quot;&quot;

completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content)
print(completion.usage.total_tokens)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;文本转换&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 推断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

_ = load_dotenv(find_dotenv())

client = OpenAI(
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

&quot;&quot;&quot;
翻译
&quot;&quot;&quot;
prompt = f&quot;&quot;&quot;
将以下中文翻译成西班牙语: 
```您好，我想订购一个搅拌机。```
&quot;&quot;&quot;

completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content + &apos;\n&apos;)

&quot;&quot;&quot;
识别语种
&quot;&quot;&quot;
prompt = f&quot;&quot;&quot;
请告诉我以下文本是什么语种: 
```Combien coûte le lampadaire?```
&quot;&quot;&quot;
completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content)

&quot;&quot;&quot;
多语种翻译
&quot;&quot;&quot;
prompt = f&quot;&quot;&quot;
请将以下文本分别翻译成中文、英文、法语和西班牙语: 
```I want to order a basketball.```
&quot;&quot;&quot;
completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content)

&quot;&quot;&quot;
同时进行语气转换
&quot;&quot;&quot;
prompt = f&quot;&quot;&quot;
请将以下文本翻译成中文，分别展示成正式与非正式两种语气: 
```Would you like to order a pillow?```
&quot;&quot;&quot;
completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content)

&quot;&quot;&quot;
语气与写作风格调整
&quot;&quot;&quot;
prompt = f&quot;&quot;&quot;
将以下文本翻译成商务信函的格式: 
```小老弟，我小羊，上回你说咱部门要采购的显示器是多少寸来着？```
&quot;&quot;&quot;
completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
)
print(completion.choices[0].message.content)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;拓展&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 推断&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

_ = load_dotenv(find_dotenv())

client = OpenAI(
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

# 我们可以在推理那章学习到如何对一个评论判断其情感倾向
sentiment = &quot;消极的&quot;

# 一个产品的评价
review = f&quot;&quot;&quot;
他们在11月份的季节性销售期间以约49美元的价格出售17件套装，折扣约为一半。\
但由于某些原因（可能是价格欺诈），到了12月第二周，同样的套装价格全都涨到了70美元到89美元不等。\
11件套装的价格也上涨了大约10美元左右。\
虽然外观看起来还可以，但基座上锁定刀片的部分看起来不如几年前的早期版本那么好。\
不过我打算非常温柔地使用它，例如，\
我会先在搅拌机中将像豆子、冰、米饭等硬物研磨，然后再制成所需的份量，\
切换到打蛋器制作更细的面粉，或者在制作冰沙时先使用交叉切割刀片，然后使用平面刀片制作更细/不粘的效果。\
制作冰沙时，特别提示：\
将水果和蔬菜切碎并冷冻（如果使用菠菜，则轻轻煮软菠菜，然后冷冻直到使用；\
如果制作果酱，则使用小到中号的食品处理器），这样可以避免在制作冰沙时添加太多冰块。\
大约一年后，电机发出奇怪的噪音，我打电话给客服，但保修已经过期了，所以我不得不再买一个。\
总的来说，这些产品的总体质量已经下降，因此它们依靠品牌认可和消费者忠诚度来维持销售。\
货物在两天内到达。
&quot;&quot;&quot;

prompt = f&quot;&quot;&quot;
你是一位客户服务的AI助手。
你的任务是给一位重要客户发送邮件回复。
根据客户通过“```”分隔的评价，生成回复以感谢客户的评价。提醒模型使用评价中的具体细节
用简明而专业的语气写信。
作为“AI客户代理”签署电子邮件。
客户评论：
```{review}```
评论情感：{sentiment}
&quot;&quot;&quot;

completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=[
        {&apos;role&apos;: &apos;system&apos;, &apos;content&apos;: &apos;You are a helpful assistant.&apos;},
        {&apos;role&apos;: &apos;user&apos;, &apos;content&apos;: prompt}, ],
    # 温度系数，0-1之间，越高越随机，越低越确定
    temperature=0.9,
)
print(completion.choices[0].message.content + &apos;\n&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;分类&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 分类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

_ = load_dotenv(find_dotenv())

client = OpenAI(
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

# 客户分隔符
delimiter = &quot;####&quot;

# 系统消息
system_message = f&quot;&quot;&quot;
你将获得客户服务查询。
每个客户服务查询都将用{delimiter}字符分隔。
将每个查询分类到一个主要类别和一个次要类别中。
以 JSON 格式提供你的输出，包含以下键：primary 和 secondary。

主要类别：计费（Billing）、技术支持（Technical Support）、账户管理（Account Management）或一般咨询（General Inquiry）。

计费次要类别：
取消订阅或升级（Unsubscribe or upgrade）
添加付款方式（Add a payment method）
收费解释（Explanation for charge）
争议费用（Dispute a charge）

技术支持次要类别：
常规故障排除（General troubleshooting）
设备兼容性（Device compatibility）
软件更新（Software updates）

账户管理次要类别：
重置密码（Password reset）
更新个人信息（Update personal information）
关闭账户（Close account）
账户安全（Account security）

一般咨询次要类别：
产品信息（Product information）
定价（Pricing）
反馈（Feedback）
与人工对话（Speak to a human）

&quot;&quot;&quot;

user_message = f&quot;&quot;&quot;
我希望你删除我的个人资料和所有用户数据。
&quot;&quot;&quot;

messages = [
    {&apos;role&apos;: &apos;system&apos;,
     &apos;content&apos;: system_message},
    {&apos;role&apos;: &apos;user&apos;,
     &apos;content&apos;: f&quot;{delimiter}{user_message}{delimiter}&quot;},
]

completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=messages,
)
print(completion.choices[0].message.content)
print(f&quot;消耗的 Tokens 数量有：{completion.usage.total_tokens}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;思维链推理&lt;/h3&gt;
&lt;p&gt;::: normal-demo Prompt 思维链&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv, find_dotenv
from openai import OpenAI

_ = load_dotenv(find_dotenv())

client = OpenAI(
    api_key=os.getenv(&quot;DASHSCOPE_API_KEY&quot;),
    base_url=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;,
)

delimiter = &quot;====&quot;

system_message = f&quot;&quot;&quot;
请按照以下步骤回答客户的提问。客户的提问将以{delimiter}分隔。

步骤 1:{delimiter}首先确定用户是否正在询问有关特定产品或产品的问题。产品类别不计入范围。

步骤 2:{delimiter}如果用户询问特定产品，请确认产品是否在以下列表中。所有可用产品：

产品：TechPro 超极本
类别：计算机和笔记本电脑
品牌：TechPro
型号：TP-UB100
保修期：1 年
评分：4.5
特点：13.3 英寸显示屏，8GB RAM，256GB SSD，Intel Core i5 处理器
描述：一款适用于日常使用的时尚轻便的超极本。
价格：$799.99

产品：BlueWave 游戏笔记本电脑
类别：计算机和笔记本电脑
品牌：BlueWave
型号：BW-GL200
保修期：2 年
评分：4.7
特点：15.6 英寸显示屏，16GB RAM，512GB SSD，NVIDIA GeForce RTX 3060
描述：一款高性能的游戏笔记本电脑，提供沉浸式体验。
价格：$1199.99

产品：PowerLite 可转换笔记本电脑
类别：计算机和笔记本电脑
品牌：PowerLite
型号：PL-CV300
保修期：1年
评分：4.3
特点：14 英寸触摸屏，8GB RAM，256GB SSD，360 度铰链
描述：一款多功能可转换笔记本电脑，具有响应触摸屏。
价格：$699.99

产品：TechPro 台式电脑
类别：计算机和笔记本电脑
品牌：TechPro
型号：TP-DT500
保修期：1年
评分：4.4
特点：Intel Core i7 处理器，16GB RAM，1TB HDD，NVIDIA GeForce GTX 1660
描述：一款功能强大的台式电脑，适用于工作和娱乐。
价格：$999.99

产品：BlueWave Chromebook
类别：计算机和笔记本电脑
品牌：BlueWave
型号：BW-CB100
保修期：1 年
评分：4.1
特点：11.6 英寸显示屏，4GB RAM，32GB eMMC，Chrome OS
描述：一款紧凑而价格实惠的 Chromebook，适用于日常任务。
价格：$249.99

步骤 3:{delimiter} 如果消息中包含上述列表中的产品，请列出用户在消息中做出的任何假设，\
例如笔记本电脑 X 比笔记本电脑 Y 大，或者笔记本电脑 Z 有 2 年保修期。

步骤 4:{delimiter} 如果用户做出了任何假设，请根据产品信息确定假设是否正确。

步骤 5:{delimiter} 如果用户有任何错误的假设，请先礼貌地纠正客户的错误假设（如果适用）。\
只提及或引用可用产品列表中的产品，因为这是商店销售的唯一五款产品。以友好的口吻回答客户。

使用以下格式回答问题：
步骤 1: {delimiter} &amp;lt;步骤 1 的推理&amp;gt;
步骤 2: {delimiter} &amp;lt;步骤 2 的推理&amp;gt;
步骤 3: {delimiter} &amp;lt;步骤 3 的推理&amp;gt;
步骤 4: {delimiter} &amp;lt;步骤 4 的推理&amp;gt;
回复客户: {delimiter} &amp;lt;回复客户的内容&amp;gt;

请确保每个步骤上面的回答中中使用 {delimiter} 对步骤和步骤的推理进行分隔。
&quot;&quot;&quot;

user_message = f&quot;&quot;&quot;BlueWave Chromebook 比 TechPro 台式电脑贵多少？&quot;&quot;&quot;

messages = [
    {&apos;role&apos;: &apos;system&apos;,
     &apos;content&apos;: system_message},
    {&apos;role&apos;: &apos;user&apos;,
     &apos;content&apos;: f&quot;{delimiter}{user_message}{delimiter}&quot;},
]

completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=messages,
)
print(completion.choices[0].message.content)
print(f&quot;消耗的 Tokens 数量有：{completion.usage.total_tokens}&quot;)

user_message = f&quot;&quot;&quot;你有电视机么&quot;&quot;&quot;
messages = [
    {&apos;role&apos;: &apos;system&apos;,
     &apos;content&apos;: system_message},
    {&apos;role&apos;: &apos;user&apos;,
     &apos;content&apos;: f&quot;{delimiter}{user_message}{delimiter}&quot;},
]
completion = client.chat.completions.create(
    model=&quot;qwen-plus&quot;,
    messages=messages,
)
try:
    if delimiter in completion:
        final_response = completion.split(delimiter)[-1].strip()
    else:
        final_response = completion.split(&quot;:&quot;)[-1].strip()
except Exception as e:
    final_response = &quot;对不起，我现在有点问题，请尝试问另外一个问题&quot;

print(final_response)

print(completion.choices[0].message.content)
print(f&quot;消耗的 Tokens 数量有：{completion.usage.total_tokens}&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Rule Engine</title><link>https://songbaicheng.cc.cd/posts/rule-engine/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/rule-engine/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;LiteFlow&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;title: LiteFlow 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/open-source-project/rule-engine/lite-flow.png
link: https://liteflow.cc
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;规则引擎的定义&lt;/h2&gt;
&lt;p&gt;很多人容易把规则引擎和流程引擎的概念混在一起，这里我们先将规则引擎和流程引擎区分开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规则引擎&lt;/strong&gt;：通常是嵌入在应用程序组件中的，实现了将业务决策从应用程序代码中分离出来，并使用预定义的语义模块编写业务决策。接受数据输入，解释业务规则，并根据业务规则做出业务决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流程引擎&lt;/strong&gt;：实现了将多个业务参与者之间按照某种预定义的规则进行流转，通常需要涉及到角色与流程的交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;LiteFlow 适用于哪些场景&lt;/h2&gt;
&lt;p&gt;LiteFlow适用于拥有复杂逻辑的业务，比如说价格引擎，下单流程等，这些业务往往都拥有很多步骤，这些步骤完全可以按照业务粒度拆分成一个个独立的组件，进行装配复用变更。使用LiteFlow，你会得到一个灵活度高，扩展性很强的系统。因为组件之间相互独立，也可以避免改一处而动全身的这样的风险。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;::: normal-demo demo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component(&quot;a&quot;)
public class ACmp extends NodeComponent {

	@Override
	public void process() {
		//do your business
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以此类推再分别定义b,c组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component(&quot;b&quot;)
public class BCmp extends NodeComponent {

	@Override
	public void process() {
		//do your business
	}
}
@Component(&quot;c&quot;)
public class CCmp extends NodeComponent {

	@Override
	public void process() {
		//do your business
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在你的SpringBoot的application.properties或者application.yml里添加配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;liteflow:
  rule-source: config/flow.el.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在config/flow.el.yml里添加配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flow:
  nodes:
    node:
      - id: a
        class: com.sbc.liteflow.component.ACmp
      - id: b
        class: com.sbc.liteflow.component.BCmp
      - id: c
        class: com.sbc.liteflow.component.CCmp
      - id: d
        class: com.sbc.liteflow.component.DCmp
  chain:
    - name: test-chain
      value: &quot;THEN(a, b, WHEN(c, d))&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/resource/open-source-project/rule-engine/liteflow-demo.png&quot; alt=&quot;LiteFlow Demo 运行结果&quot; title=&quot;LiteFlow Demo 运行结果&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;规则&lt;/h2&gt;
&lt;h3&gt;工程内指定多个路径&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;liteflow:
  // 规则文件之间可以用,或者;隔开：
  rule-source: classpath*:config/flow.el.yml,classpath*:config/flow2.el.yml
  // Spring EL表达式进行模糊匹配，加载多个配置文件：
  rule-source: config/**/*.el.xml
  // 绝对路径指定多个路径
  rule-source: /data/lf/flow1.el.xml,/data/lf/flow2.el.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;规则配置源&lt;/h3&gt;
&lt;p&gt;支持本地、ZK、SQL、Redis、Nacos、Etcd、Apollo等多种规则配置源，默认使用本地文件。&lt;/p&gt;
&lt;h2&gt;规则编排&lt;/h2&gt;
&lt;h3&gt;串行编排&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;chain name=&quot;chain1&quot;&amp;gt;
    THEN(a, b, c, d);
&amp;lt;/chain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;并行编排&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;chain name=&quot;chain1&quot;&amp;gt;
    THEN(
        a,
        WHEN(b, THEN(c, d)),
        e
    );
&amp;lt;/chain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;选择编排&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;chain name=&quot;chain1&quot;&amp;gt;
SWITCH(a).to(b, c, d);
&amp;lt;/chain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;条件编排&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;chain name=&quot;chain1&quot;&amp;gt;
THEN(
IF(x, a),
b
);
&amp;lt;/chain&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环编排&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;chain name=&quot;chain1&quot;&amp;gt;
FOR(5).DO(THEN(a, b));
&amp;lt;/chain&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组件类型&lt;/h2&gt;
&lt;h3&gt;普通组件&lt;/h3&gt;
&lt;p&gt;普通组件节点需要继承NodeComponent，可用于THEN和WHEN关键字中。&lt;/p&gt;
&lt;h3&gt;选择组件&lt;/h3&gt;
&lt;p&gt;在实际业务中，往往要通过动态的业务逻辑判断到底接下去该执行哪一个节点，这就引申出了选择节点，选择节点可以用于SWITCH关键字中。
选择节点a需要继承 NodeSwitchComponent。 需要实现方法 processSwitch 方法。&lt;/p&gt;
&lt;h3&gt;布尔组件&lt;/h3&gt;
&lt;p&gt;布尔组件是以前IF组件，WHILE组件，BREAK组件的统一。他们三个组件有共同特征，都是返回布尔类型，所以将三个组件类型合三为一，成为了布尔组件。
布尔组件的定义，需要继承 NodeBooleanComponent。&lt;/p&gt;
&lt;h2&gt;性能&lt;/h2&gt;
&lt;p&gt;LiteFlow 绝大部分工作都是在启动时完成，包括解析规则，注册组件，组装元信息。而执行链路时几乎对系统没有额外的消耗。&lt;/p&gt;
&lt;p&gt;实际表现中，LiteFlow 执行效率很高，在公司级核心业务上面，50多个业务组件组成的链路，在实际压测中单点达到了 1500 的 TPS（每秒处理事务数），集群达到了 1W 以上的 TPS，也经历过双11，明星顶流带货等大流量的考验。&lt;/p&gt;
&lt;p&gt;虽然 LiteFlow 框架本身性能很好，但是整体执行效率却依赖实际业务组件的快慢，如果你的组件有大量的循环数据库请求 IO，或者有 bad sql，又或者有大量的 rpc 同步调用，那实际 TPS 也不会很高。&lt;/p&gt;
</content:encoded></item><item><title>Sort</title><link>https://songbaicheng.cc.cd/posts/sort/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/sort/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;排序&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;root(排序)
    内部排序
        插入排序
            直接插入排序
            折半插入排序
            希尔排序
        交换排序
            冒泡排序
            快速排序
        选择排序
            简单选择排序
            堆排序
        归并排序
        基数排序
    外部排序
        多路归并排序
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;排序的概念&lt;/h2&gt;
&lt;p&gt;排序就是重新排列表中的元素，使表中的元素满足按关键字有序的过程。在排序的过程中，根据数据元素是否完全在内存中，可将排序排序算法分为两类，内部排序：是指在排序期间元素全部存放在内存中的排序；外部排序：是指在排序期间元素无法全部同时存放在内存中，必须在排序过程中根据要求不断的在内外存之间移动的排序。&lt;/p&gt;
&lt;p&gt;每种排序算法都有各自的优缺点，适合在不同的环境下使用，就其全面性能而言，很难提出一种被认为是最好的算法。&lt;/p&gt;
&lt;h2&gt;插入排序&lt;/h2&gt;
&lt;p&gt;插入排序是一种简单直观的插入排序，其基本思想是每次将一个带排序的记录按照其关键字大小插入到前面已排好序的子序列，直到全部记录插入完成。&lt;/p&gt;
&lt;h3&gt;直接插入排序&lt;/h3&gt;
&lt;p&gt;最简单直观的直接插入排序就是假设从第一位开始已经排好顺序，向后的比较过程中如果出现反序的数字遍向前移动，相对的比较后的元素也逐步往后移动为新元素提供插入空间。&lt;/p&gt;
&lt;p&gt;直接插入排序在空间上使用了常数个辅助单元，所以空间复杂度为 O(1)，而时间上需要逐个对比元素进行操作和移动元素，所以平均下来复杂度为 O(n^2^)。&lt;/p&gt;
&lt;p&gt;因为插入元素都是从后面顺序向前进行，所以不会出现相对位置的移动，所以直接插入排序是一个稳定的排序方法，适用于顺序存储或者链式存储的线性表。&lt;/p&gt;
&lt;p&gt;::: normal-demo Servlet demo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void insertSort(E[] arr) {
    for (int i = 1; i &amp;lt; arr.length; i++) {

        E tempElement = arr[i];
        int j = i - 1;

        while (j &amp;gt;= 0 &amp;amp;&amp;amp; tempElement.compareTo(arr[j]) &amp;lt; 0) {
            arr[j + 1] = arr[j];
            j--;
        }

        arr[j + 1] = tempElement;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Spring Boot Admin</title><link>https://songbaicheng.cc.cd/posts/spring-boot-admin/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-boot-admin/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Boot Admin&lt;/h1&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;Spring Boot Admin是一个用于管理和监控 Spring Boot 应用程序的开源项目。它提供了一个用户界面，可以集中管理多个 Spring Boot 应用程序，并提供有关这些应用程序的详细信息和指标。&lt;/p&gt;
&lt;p&gt;其模式也和 Eureka 相同分为客户端和服务端相同，主要功能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用程序监控：Spring Boot Admin 可以监控和展示每个 Spring Boot 应用程序的运行状态、健康状况和指标数据，如内存使用、线程数、请求统计等。&lt;/li&gt;
&lt;li&gt;健康检查和管理：它提供了对Spring Boot应用程序的健康检查功能，并可以根据应用程序的健康状况采取相应的管理措施，如重启应用程序或发送警报通知。&lt;/li&gt;
&lt;li&gt;易于集成：Spring Boot Admin可以轻松集成到现有的Spring Boot应用程序中，只需添加相应的依赖并进行简单的配置即可。&lt;/li&gt;
&lt;li&gt;实时日志查看：它提供了实时查看应用程序日志的功能，可以帮助开发人员快速定位和解决问题。&lt;/li&gt;
&lt;li&gt;事件通知：Spring Boot Admin 支持通过邮件、Slack等方式发送事件通知，如应用程序上线、下线、健康状态变更等。&lt;/li&gt;
&lt;li&gt;安全性：它提供了一些安全特性，如基于角色的访问控制、HTTPS支持等，以确保管理界面的安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring Boot Admin 可以选择和服务注册中心搭配使用，当与服务注册中心配合使用时，它可以自动发现注册在服务注册中心中的 Spring Boot 应用程序，并将其添加到管理界面中进行监控和管理。这样可以实现动态管理多个应用程序，并且随着应用程序的启动和关闭，管理界面能够及时更新应用程序的状态和信息。&lt;/p&gt;
&lt;p&gt;另外 Spring Boot Admin 和 Spring Boot Actuator 可以很好地配合使用。通过在Spring Boot应用程序中集成 Spring Boot Actuator，可以使 Spring Boot Admin 能够获取应用程序的详细信息和指标数据，从而在管理界面上展示和监控这些数据。同时，Spring Boot Admin 还可以利用 Spring Boot Actuator 提供的功能，如远程 Shell、线程转储等，与应用程序进行交互和管理。因此，Spring Boot Admin 和 Spring Boot Actuator是相互配合使用的，Spring Boot Actuator 提供了监控和管理的基础功能，而 Spring Boot Admin 提供了一个集中管理和监控的用户界面，通过与 Spring Boot Actuator 端点的交互，实现对多个 Spring Boot 应用程序的管理和监控。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;h4&gt;引入依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;de.codecentric&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-admin-starter-server&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;简单配置&lt;/h4&gt;
&lt;p&gt;启动类增加&lt;code&gt;@EnableAdminServer&lt;/code&gt;注解，启动后访问项目根目录即自动跳转服务端界面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/spring-boot-admin/admin-home.png&quot; alt=&quot;Spring Boot admin 主页&quot; title=&quot;Spring Boot admin 主页&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;h4&gt;引入依赖&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;de.codecentric&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-admin-starter-client&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;简单配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;spring:
  boot:
    admin:
      client:
        url: http://localhost:8110/ # spring boot admin server 地址

management:
  endpoints:
    web:
      exposure:
        include: &apos;*&apos; # 暴露给监控全部接口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/spring-boot-admin/admin-wallboard.png&quot; alt=&quot;客户端服务注册&quot; title=&quot;客户端服务注册&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/micro-services/spring-boot-admin/admin-details.png&quot; alt=&quot;客户端服务详情&quot; title=&quot;客户端服务详情&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Spring Boot Logging</title><link>https://songbaicheng.cc.cd/posts/spring-boot-logging/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-boot-logging/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Boot Logging&lt;/h1&gt;
&lt;h2&gt;关于 Spring 日志&lt;/h2&gt;
&lt;p&gt;在 Spring Boot 的官方文档的核心功能部分介绍了 Spring 对日志功能的支持，Spring 并没有自己的日志框架实现，而是使用 SLF4J（Simple Logging Facade for Java）作为日志门面，在底层使用 Commons Logging 作为抽象层去识别和对接一些常见的日志框架，如 Logback、Log4j2 等。Spring Boot 默认集成了 Logback 作为日志框架并支持我们在配置文件通过简单的配置就可以开箱即用，如果想了解更多细节可以点击下面卡片跳转官网查看。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Spring Security 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/spring-initializr.svg
link: https://docs.spring.io/spring-boot/docs/3.1.1/reference/htmlsingle/#features.logging
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;简单方案&lt;/h2&gt;
&lt;h3&gt;极速版&lt;/h3&gt;
&lt;p&gt;Spring 默认配置了控制台输出，所以我们可以在配置文件添加以下配置选择文件输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logging:
  level:
    root: INFO
    com.example: DEBUG

  file:
    name: /var/log/myapp.log
    totalSizeCap: 10MB
    historySize: 7
    maxFileSize: 1MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/spring/spring-boot-logging/spring-boot-log-format.png&quot; alt=&quot;默认日志格式&quot; title=&quot;Spring Boot 默认日志格式&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;日期和时间：毫秒精度且易于排序。&lt;/li&gt;
&lt;li&gt;日志级别：TRACE, DEBUG, INFO, WARN, ERROR, FATAL，要知道 Logback 没有 FATAL 级别，它被映射到 ERROR 里。&lt;/li&gt;
&lt;li&gt;线程名称：用方括号括起来（可能会被截断以用于控制台输出）。&lt;/li&gt;
&lt;li&gt;记录器名称：这通常是源类名称（通常是缩写）。&lt;/li&gt;
&lt;li&gt;日志消息：对应你代码中答应的日志信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常规版&lt;/h3&gt;
&lt;p&gt;在我工作中正常服务器的日志使用的话，通常是搭配 pom.xml 来构建多环境打包的方案，使用 logback.xml 搭配一些输出类型进行输出日志文件，常用的输出类型有以下几种：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;输出类型&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ConsoleAppender&lt;/td&gt;
&lt;td&gt;将日志输出到控制台&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FileAppender&lt;/td&gt;
&lt;td&gt;将日志输出到单个文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RollingFileAppender&lt;/td&gt;
&lt;td&gt;支持滚动的文件输出，根据条件生成新的日志文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SizeAndTimeBasedRollingPolicy&lt;/td&gt;
&lt;td&gt;基于时间和文件大小的滚动策略，根据时间和大小规则生成新的日志文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TimeBasedRollingPolicy&lt;/td&gt;
&lt;td&gt;基于时间的滚动策略，根据时间周期生成新的日志文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DailyRollingFileAppender&lt;/td&gt;
&lt;td&gt;按照每天滚动的策略，生成带有日期的日志文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义Appender&lt;/td&gt;
&lt;td&gt;用户根据需求编写的自定义输出类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;下面是一个通用的 logback.xml ，其配置是按照日期和文件大小进行输出的。&lt;/p&gt;
&lt;p&gt;::: normal-demo logback.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;configuration&amp;gt;

    &amp;lt;!-- 定义日志输出的根目录 --&amp;gt;
    &amp;lt;property name=&quot;LOG_HOME&quot; value=&quot;/Users/songbaicheng/logs/cloud-mall/mall-web&quot;/&amp;gt;

    &amp;lt;!-- 定义日志文件的名称 --&amp;gt;
    &amp;lt;property name=&quot;LOG_NAME&quot; value=&quot;mall-web&quot;/&amp;gt;

    &amp;lt;!-- 控制台输出 --&amp;gt;
    &amp;lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!-- 按文件大小滚动的文件输出 --&amp;gt;
    &amp;lt;appender name=&quot;FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${LOG_HOME}/%d{yyyy-MM-dd}/${LOG_NAME}.%i.log&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;maxFileSize&amp;gt;10MB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;totalSizeCap&amp;gt;100MB&amp;lt;/totalSizeCap&amp;gt;
            &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;!-- 日志输出级别 --&amp;gt;
    &amp;lt;root level=&quot;info&quot;&amp;gt;
        &amp;lt;appender-ref ref=&quot;CONSOLE&quot;/&amp;gt;
        &amp;lt;appender-ref ref=&quot;FILE&quot;/&amp;gt;
    &amp;lt;/root&amp;gt;

&amp;lt;/configuration&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;操作日志场景&lt;/h2&gt;
&lt;p&gt;在真实的业务场景中，日志可以分为&lt;strong&gt;业务日志&lt;/strong&gt;和&lt;strong&gt;操作日志&lt;/strong&gt;两种.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;系统日志&lt;/strong&gt;是指的是程序执行过程中的关键步骤，根据实际场景输出的 &lt;strong&gt;debug、info、warn、error&lt;/strong&gt; 等不同级别的程序执行记录信息，这些一般是给程序员或运维看的，一般在出现异常问题的时候，可以通过系统日志中记录的关键参数信息和异常提示，快速排除故障。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作日志&lt;/strong&gt;是用户实际业务操作行为的记录，这些信息一般存储在数据库里，如什么时间哪个用户点了某个菜单、修改了哪个配置等这类业务操作行为，这些日志信息是给普通用户或系统管理员看到。&lt;/p&gt;
&lt;p&gt;首先先列举一个反面案例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@Slf4j
@BusLog(name = &quot;人员管理&quot;)
@RequestMapping(&quot;/person&quot;)
public class PersonController {

    @Autowired
    private IPersonService personService;
    @Autowired
    private IBusLogService busLogService;


    @PostMapping
    public Person add(@RequestBody Person person) {
       try{
           //添加信息信息
        Person result = this.personService.registe(person);
        //保存业务日志
        this.saveLog(person);
        log.info(&quot;//增加person执行完成&quot;);        
       }catch(Exception e){
           //保存异常操作日志
           this.saveExceptionLog(e);       
       }
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种通过硬编码实现的业务操作日志管理功能，最大的问题就是业务操作日志收集与业务逻辑耦合严重，和代码重复，新开发的接口在完成业务逻辑后要织入一段业务操作日志保存的逻辑.&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们可以通过 AOP 的方式进行统一处理操作日志，将业务操作日志的收集与业务逻辑解耦，这样就可以在业务逻辑中专注于业务逻辑的开发，而不用再关注业务操作日志的收集。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定义日志注解&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ILog {

} 
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;定义日志切面
::: normal-demo 定义切面&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Aspect
public class ILogPrintAspect {

    @Pointcut(&quot;@within(com.sbc.log.annotation.ILog) || @annotation(com.sbc.log.annotation.ILog)&quot;)
    public void pointcut() {
    }

    @Around(&quot;pointcut()&quot;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = SystemClock.now();
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Logger log = LoggerFactory.getLogger(methodSignature.getDeclaringType());
        String beginTime = DateUtil.now();
        Object result = null;
        try {
            result = pjp.proceed();
        } finally {
            Method targetMethod = pjp.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
            ILog logAnnotation = Optional.ofNullable(targetMethod.getAnnotation(ILog.class)).orElse(pjp.getTarget().getClass().getAnnotation(ILog.class));
            if (logAnnotation != null) {
                ILogPrintDTO logPrint = new ILogPrintDTO();
                logPrint.setBeginTime(beginTime);
                if (logAnnotation.input()) {
                    logPrint.setInputParams(buildInput(pjp));
                }
                if (logAnnotation.output()) {
                    logPrint.setOutputParams(result);
                }
                String methodType = &quot;&quot;, requestURI = &quot;&quot;;
                try {
                    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                    assert servletRequestAttributes != null;
                    methodType = servletRequestAttributes.getRequest().getMethod();
                    requestURI = servletRequestAttributes.getRequest().getRequestURI();
                } catch (Exception ignored) {
                }
                log.info(&quot;[{}] {}, executeTime: {}ms, info: {}&quot;, methodType, requestURI, SystemClock.now() - startTime, JSON.toJSONString(logPrint));
            }
            
            // 操作数据插入数据库
            // ......
        }
        return result;
    }

    /**
     * 构建输入参数
     *
     * @param pjp 切入点
     * @return 输入参数
     */
    private Object[] buildInput(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        Object[] printArgs = new Object[args.length];
        for (int i = 0; i &amp;lt; args.length; i++) {
            if ((args[i] instanceof HttpServletRequest) || args[i] instanceof HttpServletResponse) {
                continue;
            }
            if (args[i] instanceof byte[]) {
                printArgs[i] = &quot;byte array&quot;;
            } else if (args[i] instanceof MultipartFile) {
                printArgs[i] = &quot;file&quot;;
            } else {
                printArgs[i] = args[i];
            }
        }
        return printArgs;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;使用该注解在目标方法上，就可以实现操作日志的自动收集，方法内部只需要关心业务逻辑的开发即可。&lt;/p&gt;
</content:encoded></item><item><title>Spring Security Jwt</title><link>https://songbaicheng.cc.cd/posts/spring-security-jwt/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-security-jwt/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Security + JWT&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;说到 JWT（JSON Web Token），我们首先要知道 Token 是什么，想要知道 Token 是什么，我们就得先谈一下早期 Session 登录的时代，下面是之前 Session 登录的大致流程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start=&amp;gt;start: 开始
one=&amp;gt;operation: 用户通过表单或其他方式提交用户名和密码
two=&amp;gt;operation: 服务器接收到登录请求，验证用户提供的用户名和密码是否有效
isAccounPasswordCorrected=&amp;gt;condition: 账号密码是否正确?
end-err=&amp;gt;end: 账号密码错误
three=&amp;gt;operation: 服务器会为该用户创建一个唯一的会话标识符（Session ID）
four=&amp;gt;operation: 服务器将该会话标识符存储在服务器端，通常保存在内存中或持久化到数据库中
five=&amp;gt;operation: 服务器将会话标识符发送回客户端，通常通过设置一个名为&quot;JSESSIONID&quot;的 Cookie
six=&amp;gt;operation: 客户端收到会话标识符后，将其保存在客户端的 Cookie 中
seven=&amp;gt;operation: 客户端之后的每个请求都会自动附带会话标识符，通常通过 Cookie 或其他方式（如 URL 参数）
eight=&amp;gt;operation: 服务器在接收到请求时，会根据会话标识符查找对应的会话信息
isSessionValid=&amp;gt;condition: 会话是否有效?
nine=&amp;gt;operation: 服务器将请求视为已经通过身份验证
ten=&amp;gt;operation: 服务器要求用户重新进行身份验证或重定向到登录页面
end-all=&amp;gt;end: 结束

start-&amp;gt;one
one-&amp;gt;two
two-&amp;gt;isAccounPasswordCorrected
isAccounPasswordCorrected(yes)-&amp;gt;three
isAccounPasswordCorrected(no)-&amp;gt;end-err-&amp;gt;end-all
three-&amp;gt;four
four-&amp;gt;five
five-&amp;gt;six
six-&amp;gt;seven
seven-&amp;gt;eight
eight-&amp;gt;isSessionValid
isSessionValid(yes)-&amp;gt;nine-&amp;gt;end-all
isSessionValid(no)-&amp;gt;ten-&amp;gt;end-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，随着应用程序的复杂性和扩展性的增加，分布式系统和跨服务的场景变得更加常见。在这种情况下，使用基于 Session 会话的登录可能会面临一些挑战，例如会话状态的同步和跨服务的会话管理。而且如果 Cookie 如果被截获，用户就会很容易受到跨站请求伪造（CSRF）的攻击。于是令牌（Token）应运而生，Token 承载了用户身份信息和其他必要的声明，无需在服务器端存储会话信息，使其具备了无状态性（stateless）的特性。这样可以减轻服务器的负担，并且适用于分布式环境和跨服务的场景。基于这种特性 Token 的登录过程如下所示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start=&amp;gt;start: 开始
one=&amp;gt;operation: 用户通过表单或其他方式提交用户名和密码
two=&amp;gt;operation: 服务器接收到登录请求，验证用户提供的用户名和密码是否有效
isAccounPasswordCorrected=&amp;gt;condition: 账号密码是否正确?
end-err=&amp;gt;end: 账号密码错误
three=&amp;gt;operation: 服务器会生成一个令牌（Token），包含用户身份信息和其他必要的声明（例如权限、过期时间等）并保存在缓存服务器（Redis）中
four=&amp;gt;operation: 服务器将令牌返回给客户端，通常通过响应的数据体中的字段或特定的响应头部
five=&amp;gt;operation: 客户端收到令牌后，将其保存在客户端（通常是本地存储或 Cookie）
six=&amp;gt;operation: 客户端之后的每个请求都会将令牌作为身份验证凭据附加在请求中，通常通过请求头部的 Authorization 字段
seven=&amp;gt;operation: 服务器在接收到请求时，会验证令牌的有效性和真实性
isSessionValid=&amp;gt;condition: 令牌信息是否有效?
eight=&amp;gt;operation: 服务器将请求视为已经通过身份验证
nine=&amp;gt;operation: 服务器要求用户重新进行身份验证或拒绝请求
end-all=&amp;gt;end: 结束

start-&amp;gt;one
one-&amp;gt;two
two-&amp;gt;isAccounPasswordCorrected
isAccounPasswordCorrected(yes)-&amp;gt;three
isAccounPasswordCorrected(no)-&amp;gt;end-err-&amp;gt;end-all
three-&amp;gt;four
four-&amp;gt;five
five-&amp;gt;six
six-&amp;gt;seven
seven-&amp;gt;isSessionValid
isSessionValid(yes)-&amp;gt;eight-&amp;gt;end-all
isSessionValid(no)-&amp;gt;nine-&amp;gt;end-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然看似流程上十分相似，但是每个请求都必须携带包含了所有必要的用户身份信息和声明的令牌来进行身份验证，而且令牌可以通过签名和加密机制来保护身份信息的完整性和真实性，这样极大的保证了安全性的同时也解决了跨域支持的问题。JWT 官网也告诉了我们什么是 JSON Web Token 并告诉我们什么情况下可以去使用它，如果有兴趣可以点击下面链接去深入了解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: JWT 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/backend/java/spring/spring-security/jwt.svg
link: https://jwt.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;引入依赖&lt;/h3&gt;
&lt;p&gt;::: code-tabs
@tab Maven#Maven&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;properties&amp;gt;
    &amp;lt;jjwt.version&amp;gt;0.11.5&amp;lt;/jjwt.version&amp;gt;
&amp;lt;/properties&amp;gt;

&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-api&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;${jjwt.version}&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-impl&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;${jjwt.version}&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;io.jsonwebtoken&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;jjwt-jackson&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;${jjwt.version}&amp;lt;/version&amp;gt;
        &amp;lt;scope&amp;gt;runtime&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Gradle#Gradle&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ext {
    jjwtVersion = &apos;0.11.5&apos;
}

dependencies {
    implementation &quot;io.jsonwebtoken:jjwt-api:$jjwtVersion&quot;
    runtimeOnly &quot;io.jsonwebtoken:jjwt-impl:$jjwtVersion&quot;
    runtimeOnly &quot;io.jsonwebtoken:jjwt-jackson:$jjwtVersion&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;系统参数配置和读取&lt;/h3&gt;
&lt;p&gt;::: normal-demo jwt 所需系统参数和读取代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;jwt:
  secret: jwt:
  secret: 650e387e-d8ba-43cd-9237-a9274a675f7b # 与HMAC-SHA算法一起使用的密钥必须具有&amp;gt;=256位的大小
  expiration: 3600 # 设置为令牌的有效期时间（单位：秒）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Setter
@Getter
@Configuration
@ConfigurationProperties(prefix = &quot;jwt&quot;)
public class JwtConfig {

    /**
     * 密钥
     */
    private String secret;

    /**
     * 过期时间
     */
    private long expiration;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;工具类方法&lt;/h3&gt;
&lt;p&gt;::: normal-demo jwt 工具类生成 Token 代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.sbc.boot3.utils;

import com.sbc.boot3.config.JwtConfig;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtUtils {

    private final JwtConfig jwtConfig;

    /**
     * 定义系统标识头常量
     */
    private static final String HEADER_SYSTEM_KEY = &quot;JWT&quot;;

    /**
     * 根据用户ID生成JWT
     *
     * @param username 用户名
     * @return JWT
     */
    public String generateToken(String username) {
        return Jwts.builder()
                .setExpiration(generateExpirationDate())
                .setSubject(username)
                .signWith(Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes(StandardCharsets.UTF_8)))
                .compact();
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + jwtConfig.getExpiration() * 1000);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;请求获取 Token&lt;/h3&gt;
&lt;p&gt;::: normal-demo 请求 Token 方法代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class AuthController {

    private final JwtUtils jwtUtils;

    /**
     * 生成token
     * @param user 用户信息
     * @return Token
     */
    @PostMapping(&quot;/token&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; authenticateUser(@RequestBody SecurityUser user) {

        String token = jwtUtils.generateToken(user.getUsername());

        return ResponseEntity.ok(token);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;Token 解码&lt;/h3&gt;
&lt;p&gt;拿到 Token 之后可以去官网解码查看生成的内容是否正确。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/spring/spring-security-jwt/jwt-decode.png&quot; alt=&quot;Token 解码&quot; title=&quot;Token 解码&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;走进 JWT&lt;/h2&gt;
&lt;p&gt;在官网我们可以看到对 JWT 的介绍也不算很多，知道它是一种用于在网络应用间安全传递信息的开放标准（RFC 7519），通过使用数字签名或加密方式，可以将声明式的JSON数据进行安全地传输。而对我开发有帮助的是我们需要知道 JWT 是有三个部分组成的：头部（Header）、载荷（Payload）和签名（Signature），现在我们重点说说这三个部分：&lt;/p&gt;
&lt;h3&gt;头部（Header）&lt;/h3&gt;
&lt;p&gt;头部通常由两部分组成：令牌类型（typ）和签名算法（alg）。这些信息被用于描述JWT的类型和所采用的签名算法。头部使用JSON对象表示，并且需要通过Base64编码进行传输。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;typ&quot;: &quot;JWT&quot;,
  &quot;alg&quot;: &quot;HS256&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;载荷（Payload）&lt;/h3&gt;
&lt;p&gt;载荷部分包含了JWT的声明信息，它是存储实际数据的地方。载荷也是一个JSON对象，并且需要通过Base64编码进行传输。载荷包含了一些预定义的声明（Claim），以及可以自定义的声明。预定义的声明包括了一些标准的声明名称，如iss（签发者）、sub（主题）、exp（过期时间）、iat（签发时间）等。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;sub&quot;: &quot;topic&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;admin&quot;: true,
  &quot;exp&quot;: 1688283529
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;签名（Signature）&lt;/h3&gt;
&lt;p&gt;签名部分是对头部和载荷进行数字签名的结果，以确保JWT的完整性和真实性。签名通过使用头部中指定的算法和密钥进行生成。签名可以帮助验证JWT是否被篡改过。我们看到的签名部分是将Base64编码的头部和Base64编码的载荷通过某种方式（如使用&quot;.&quot;进行连接）组合成一个字符串，然后使用指定的签名算法和密钥对这个字符串进行签名生成签名值形成的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),
  secret
)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;搭配 Spring Security&lt;/h3&gt;
</content:encoded></item><item><title>Spring Data Redis</title><link>https://songbaicheng.cc.cd/posts/spring-data-redis/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-data-redis/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Data Redis&lt;/h1&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;Redis 作为基于内存的 NoSQL 数据库，在解决包括缓存、会话存储、排行榜、实时分析和消息队列等场景上有着不可替代的作用，所以目前几乎所有的项目都会有依赖它的需求，相比于一些传统的关系型数据库 Redis 的配置更加简单容易，所以在项目中使用非常推荐放在 common 模块当中来方便其他模块依赖，使用的时候直接注入即可，这里我也是基于这种思想来演示一个 Redis 模块的创建，以后搭建项目可以直接开箱即用。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;p&gt;::: code-tabs&lt;/p&gt;
&lt;p&gt;@tab Maven#Maven&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-data-redis&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!-- 连接器二选一 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.lettuce&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;lettuce-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${your version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!-- 连接器二选一 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;redis.clients&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jedis&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${your version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;!-- jackson 序列化依赖 --&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;com.fasterxml.jackson.core&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jackson-databind&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;${your version}&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Gradle#Gradle&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation &apos;org.springframework.boot:spring-boot-starter-data-redis&apos;
implementation &apos;io.lettuce:lettuce-core:${your_version}&apos;
implementation &apos;redis.clients:jedis:${your_version}&apos;
implementation &apos;com.fasterxml.jackson.core:jackson-databind:${your_version}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;工具方法服务类和具体实现类&lt;/h3&gt;
&lt;p&gt;用命令行学习 Redis 的时候得心应手，但是 RedisTemplate 的方法直接用起来还是不太顺手，这里是根据命令封装好的 RedisTemplate 方法，可以直接 cv。&lt;/p&gt;
&lt;p&gt;::: normal-demo Redis 操作接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @ClassName IRedisService
 * @Description redis 操作接口
 */
public interface IRedisService {

    /**
     * 保存属性
     */
    void set(String key, Object value, long time);

    /**
     * 保存属性
     */
    void set(String key, Object value);

    /**
     * 获取属性
     */
    Object get(String key);

    /**
     * 删除属性
     */
    Boolean del(String key);

    /**
     * 批量删除属性
     */
    Long del(List&amp;lt;String&amp;gt; keys);

    /**
     * 设置过期时间
     */
    Boolean expire(String key, long time);

    /**
     * 获取过期时间
     */
    Long getExpire(String key);

    /**
     * 判断是否有该属性
     */
    Boolean hasKey(String key);

    /**
     * 按delta递增
     */
    Long incr(String key, long delta);

    /**
     * 按delta递减
     */
    Long decr(String key, long delta);

    /**
     * 获取Hash结构中的属性
     */
    Object hGet(String key, String hashKey);

    /**
     * 向Hash结构中放入一个属性
     */
    Boolean hSet(String key, String hashKey, Object value, long time);

    /**
     * 向Hash结构中放入一个属性
     */
    void hSet(String key, String hashKey, Object value);

    /**
     * 直接获取整个Hash结构
     */
    Map&amp;lt;Object, Object&amp;gt; hGetAll(String key);

    /**
     * 直接设置整个Hash结构
     */
    Boolean hSetAll(String key, Map&amp;lt;String, Object&amp;gt; map, long time);

    /**
     * 直接设置整个Hash结构
     */
    void hSetAll(String key, Map&amp;lt;String, ?&amp;gt; map);

    /**
     * 删除Hash结构中的属性
     */
    void hDel(String key, Object... hashKey);

    /**
     * 判断Hash结构中是否有该属性
     */
    Boolean hHasKey(String key, String hashKey);

    /**
     * Hash结构中属性递增
     */
    Long hIncr(String key, String hashKey, Long delta);

    /**
     * Hash结构中属性递减
     */
    Long hDecr(String key, String hashKey, Long delta);

    /**
     * 获取Set结构
     */
    Set&amp;lt;Object&amp;gt; sMembers(String key);

    /**
     * 向Set结构中添加属性
     */
    Long sAdd(String key, Object... values);

    /**
     * 向Set结构中添加属性
     */
    Long sAdd(String key, long time, Object... values);

    /**
     * 是否为Set中的属性
     */
    Boolean sIsMember(String key, Object value);

    /**
     * 获取Set结构的长度
     */
    Long sSize(String key);

    /**
     * 删除Set结构中的属性
     */
    Long sRemove(String key, Object... values);

    /**
     * 获取List结构中的属性
     */
    List&amp;lt;Object&amp;gt; lRange(String key, long start, long end);

    /**
     * 获取List结构的长度
     */
    Long lSize(String key);

    /**
     * 根据索引获取List中的属性
     */
    Object lIndex(String key, long index);

    /**
     * 向List结构中添加属性
     */
    Long lPush(String key, Object value);

    /**
     * 向List结构中添加属性
     */
    Long lPush(String key, Object value, long time);

    /**
     * 向List结构中批量添加属性
     */
    Long lPushAll(String key, Object... values);

    /**
     * 向List结构中批量添加属性
     */
    Long lPushAll(String key, Long time, Object... values);

    /**
     * 从List结构中移除属性
     */
    Long lRemove(String key, long count, Object value);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::
::: normal-demo Redis 操作实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import service.IRedisService;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName IRedisServiceImpl
 * @Description redis 操作实现类
 */
public class IRedisServiceImpl implements IRedisService {

    @Autowired
    private RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;

    @Override
    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    @Override
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public Boolean del(String key) {
        return redisTemplate.delete(key);
    }

    @Override
    public Long del(List&amp;lt;String&amp;gt; keys) {
        return redisTemplate.delete(keys);
    }

    @Override
    public Boolean expire(String key, long time) {
        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    @Override
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    @Override
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    @Override
    public Long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    @Override
    public Long decr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    @Override
    public Object hGet(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    @Override
    public Boolean hSet(String key, String hashKey, Object value, long time) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return expire(key, time);
    }

    @Override
    public void hSet(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    @Override
    public Map&amp;lt;Object, Object&amp;gt; hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    @Override
    public Boolean hSetAll(String key, Map&amp;lt;String, Object&amp;gt; map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        return expire(key, time);
    }

    @Override
    public void hSetAll(String key, Map&amp;lt;String, ?&amp;gt; map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    @Override
    public void hDel(String key, Object... hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    @Override
    public Boolean hHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }

    @Override
    public Long hIncr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, delta);
    }

    @Override
    public Long hDecr(String key, String hashKey, Long delta) {
        return redisTemplate.opsForHash().increment(key, hashKey, -delta);
    }

    @Override
    public Set&amp;lt;Object&amp;gt; sMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    @Override
    public Long sAdd(String key, Object... values) {
        return redisTemplate.opsForSet().add(key, values);
    }

    @Override
    public Long sAdd(String key, long time, Object... values) {
        Long count = redisTemplate.opsForSet().add(key, values);
        expire(key, time);
        return count;
    }

    @Override
    public Boolean sIsMember(String key, Object value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    @Override
    public Long sSize(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    @Override
    public Long sRemove(String key, Object... values) {
        return redisTemplate.opsForSet().remove(key, values);
    }

    @Override
    public List&amp;lt;Object&amp;gt; lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    @Override
    public Long lSize(String key) {
        return redisTemplate.opsForList().size(key);
    }

    @Override
    public Object lIndex(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    @Override
    public Long lPush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    @Override
    public Long lPush(String key, Object value, long time) {
        Long index = redisTemplate.opsForList().rightPush(key, value);
        expire(key, time);
        return index;
    }

    @Override
    public Long lPushAll(String key, Object... values) {
        return redisTemplate.opsForList().rightPushAll(key, values);
    }

    @Override
    public Long lPushAll(String key, Long time, Object... values) {
        Long count = redisTemplate.opsForList().rightPushAll(key, values);
        expire(key, time);
        return count;
    }

    @Override
    public Long lRemove(String key, long count, Object value) {
        return redisTemplate.opsForList().remove(key, count, value);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;配置类实现&lt;/h3&gt;
&lt;p&gt;引用该配之类的话就需要实现一个 RedisConfig 继承下面的 BaseRedisConfig 并添加 @Configuration 注册到容器中即可。&lt;/p&gt;
&lt;p&gt;::: normal-demo Redis 基础配置类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import service.IRedisService;
import service.impl.IRedisServiceImpl;

/**
 * @ClassName RedisConfig
 * @Description Redis 配置
 */
public class BaseRedisConfig {

    /**
     * 使用Lettuce作为Redis客户端
     *
     * @return 返回一个 Lettuce 连接工厂
     */
    @Bean
    LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }

    /**
     * RedisTemplate 自定义配置
     * @param redisConnectionFactory Redis 连接工厂
     */
    @Bean
    public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisSerializer&amp;lt;Object&amp;gt; serializer = redisSerializer();
        RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();

        // 设置Redis连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置用于序列化Redis键的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置用于序列化Redis值的序列化器
        redisTemplate.setValueSerializer(serializer);
        // 设置用于序列化Redis哈希表键的序列化器
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 设置用于序列化Redis哈希表值的序列化器
        redisTemplate.setHashValueSerializer(serializer);
        // 确保RedisTemplate的属性已经设置完毕，并进行必要的初始化。
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    /**
     * Redis 自定义序列化方法
     * @return 自定义序列化器
     */
    public RedisSerializer&amp;lt;Object&amp;gt; redisSerializer() {

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 必须设置，否则无法将JSON转化为对象，会转化成Map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        return new Jackson2JsonRedisSerializer&amp;lt;&amp;gt;(objectMapper, Object.class);
    }

    /**
     * redis 工具类
     */
    @Bean
    public IRedisService redisService() {
        return new IRedisServiceImpl();
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;使用 Spring Data Redis 只需要配置以上三步，在你真正调用的模块中配置你的 Redis 连接参数后就可以很方便的使用了。&lt;/p&gt;
</content:encoded></item><item><title>Spring Security Oauth2</title><link>https://songbaicheng.cc.cd/posts/spring-security-oauth2/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-security-oauth2/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Security + OAuth2&lt;/h1&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;h2&gt;OAuth2&lt;/h2&gt;
&lt;p&gt;OAuth 2.0（开放授权 2.0）是一种授权框架，用于允许用户授权第三方应用程序访问其在另一个应用程序（如社交媒体、电子邮件服务或云存储服务）上的受保护资源，而无需向第三方应用程序共享其凭据（例如用户名和密码）。其主要职责就是实现一个三方互信的功能，目前网站上支持的第三方登录就是 OAuth 协议的实现。OAuth 2.0 的核心概念包括以下角色：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;资源所有者（Resource Owner）：资源所有者是指控制受保护资源的用户，例如网站用户或移动应用程序用户。&lt;/li&gt;
&lt;li&gt;客户端（Client）：客户端是请求访问受保护资源的第三方应用程序，它通过 OAuth 2.0 协议与身份和授权服务器进行交互。&lt;/li&gt;
&lt;li&gt;身份和授权服务器（Authorization Server）：身份和授权服务器负责验证资源所有者的身份，并根据资源所有者的授权向客户端颁发访问令牌。&lt;/li&gt;
&lt;li&gt;受保护资源服务器（Resource Server）：受保护资源服务器托管受保护的用户数据或服务，只有在经过授权的情况下才能访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OAuth 2.0 的工作流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端向资源所有者请求授权，以访问其受保护资源。这可以通过重定向资源所有者到身份和授权服务器的授权页面来实现。&lt;/li&gt;
&lt;li&gt;资源所有者向身份和授权服务器提供其凭据（例如用户名和密码），并授权客户端访问受保护资源。&lt;/li&gt;
&lt;li&gt;身份和授权服务器验证资源所有者的身份，并生成一个访问令牌（Access Token）。&lt;/li&gt;
&lt;li&gt;身份和授权服务器将访问令牌颁发给客户端。&lt;/li&gt;
&lt;li&gt;客户端使用访问令牌向受保护资源服务器发起请求，并在请求中提供访问令牌作为身份验证凭据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OAuth 2.0 的优势在于它根据受保护资源服务器验证访问令牌的有效性，并根据访问令牌决定是否授权客户端访问受保护资源，使得用户可以授予对其受保护资源的有限访问权限，而无需共享其凭据。这提供了更好的安全性和用户隐私保护。此外，OAuth 2.0 支持多种授权流程，如授权码授权流程、隐式授权流程、密码授权流程和客户端凭据授权流程，以满足不同应用场景的需求。&lt;/p&gt;
</content:encoded></item><item><title>Spring Security</title><link>https://songbaicheng.cc.cd/posts/spring-security/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring-security/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring Security 基础&lt;/h1&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;目前提到安全框架，Shiro 和 Spring Security 算得上是分庭抗争了，并且 Shiro 主打的是简单、轻量，但却没有 Spring Security 灵活，在 Spring Security 支持 OAuth2 之后更加贴合当前社会需求，并且我们如果使用 Spring 框架的话，学习 Spring Security 更是如鱼得水，并且是重中之重。如果想要更加深入的了解 Spring Security ，请预览下面官方文档的链接进行研读。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Spring Security 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/common-icon/spring-initializr.svg
link: https://spring.io/projects/spring-security
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方的解释是 Spring Security 是一个提供身份验证、授权和防止常见攻击的框架，正如它说的那样，其核心功能就是 &lt;strong&gt;&lt;em&gt;认证&lt;/em&gt;&lt;/strong&gt; 和 &lt;strong&gt;&lt;em&gt;授权&lt;/em&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;认证（Authentication）：系统确认一个用户的身份是否真实、合法。&lt;/li&gt;
&lt;li&gt;授权（Authorization）：系统对用户进行的访问控制，即通过了认证后用户对资源的访问限制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Spring Security 的重点是对所有进入系统的请求进行拦截，校验其是否可以访问其所期望的资源。而 Spring Security 对于 Web 资源的保护都是通过 Filter 进行保护的，而这些 Filter 都是通过注入到 Spring 容器中的 SecurityFilterChain 过滤器链来实现的，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/backend/java/spring/spring-security/spring-security-process.png&quot; alt=&quot;Spring Security 流程图&quot; title=&quot;Spring Security 流程图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;引入依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;implementation &apos;org.springframework.boot:spring-boot-starter-security&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-security&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;简单配置启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;spring:
  security:
    user:
      name: user # 用户名
      password: songbaicheng # 密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目启动后随便访问一个路径就会跳转到自带的 /login 窗口进行登录。如果不指定用户名和密码，用户名默认是 &lt;code&gt;user&lt;/code&gt;，密码会在项目启动的时候生成一个 UUID 在控制台打印出来。&lt;/p&gt;
&lt;h3&gt;配置登录认证用户&lt;/h3&gt;
&lt;p&gt;Spring Security 提供了基于内存和持久化两种添加用户的方式，工作中常用的当然是选择持久化的方式居多，外加基于持久化的方式加 ORM 框架新增总共三种方式实现，进行下面几种测试点的时候，一定要注意只能同时存在一种添加用户的方式，如果存在的方式过多则会存在异常。&lt;/p&gt;
&lt;h4&gt;基于内存添加用户&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsManager () {

        UserDetails userDetails1 = User.withUsername(&quot;songbaicheng&quot;).password(&quot;{noop}songbaicheng&quot;).roles(&quot;role1&quot;).build();
        UserDetails userDetails2 = User.withUsername(&quot;songbaicheng1&quot;).password(&quot;{noop}songbaicheng&quot;).roles(&quot;role2&quot;).build();
        UserDetails userDetails3 = User.withUsername(&quot;songbaicheng2&quot;).password(&quot;{noop}songbaicheng&quot;).roles(&quot;role3&quot;).build();

        return new InMemoryUserDetailsManager(userDetails1, userDetails2, userDetails3);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;基于 JDBC 添加用户&lt;/h4&gt;
&lt;p&gt;首先需要引入数据库持久化依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;implementation &apos;mysql:mysql-connector-java:8.0.25&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;8.0.25&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建对应数据库并配置连接，具体的建表语句官方已经帮我们放在下面路径中的依赖包里了,其中的语法可能并不支持所有数据库，如果你使用 MySQL 数据库可以使用以下语句。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;/spring-security-core-6.1.0.jar!/org/springframework/security/core/userdetails/jdbc/users.ddl&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;create table users(
  username varchar(50) not null primary key,
  password varchar(500) not null,
  enabled boolean not null
);

create table authorities (
  username varchar(50) not null,
  authority varchar(50) not null,
  constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username,authority);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;spring:
  datasource:
    url: jdbc:mysql://localhost:3306/[database-name] # 数据库地址
    driver-class-name: com.mysql.cj.jdbc.Driver # 驱动
    username: [username] # 账号
    password: [password] # 密码
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后添加配置类并配置 JdbcUserDetailsManager，启动项目后 admin 用户就自动添加到数据库当中了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class SecurityConfig {

    @Resource
    private DataSource dataSource;

    @Bean
    public JdbcUserDetailsManager jdbcUserDetailsManager() {

        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);

        if (!jdbcUserDetailsManager.userExists(&quot;admin&quot;)) {
            jdbcUserDetailsManager.createUser(User.withUsername(&quot;admin&quot;).password(&quot;{noop}songbaicheng&quot;).roles(&quot;role4&quot;).build());
        }

        return jdbcUserDetailsManager;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;基于 ORM 添加用户&lt;/h4&gt;
&lt;p&gt;该方法需要根据自己的业务定义自己的 UserDetails 和重写 UserDetailsService 类中的 loadUserByUsername 方法，下面只是一个 demo 演示，将数据库中的用户数据验证并赋值到自定义的 UserDetails 中返回。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Builder
public class SecurityUser implements UserDetails {

    private String username;
    private String password;
    private String authority;
    private boolean enabled;

    @Override
    public Collection&amp;lt;? extends GrantedAuthority&amp;gt; getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SecurityService implements UserDetailsService {

    private final IAuthoritiesMapper iAuthoritiesMapper;
    private final IUsersMapper iUsersMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        String realUsername = iAuthoritiesMapper.getUsernameById(username);

        if (ObjectUtils.isEmpty(realUsername)) {
            throw new UsernameNotFoundException(&quot;用户不存在！&quot;);
        }

        return SecurityUser.builder()
                .username(realUsername)
                .password(iUsersMapper.getPasswordById(username))
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置资源授权角色&lt;/h3&gt;
&lt;p&gt;通过对路由的匹配并对其指定相应的授权规则，可以创建多条授权规则，建议根据从细到粗的粒度来进行匹配。下面就是对 /hello 下的资源限制为有 user 身份的用户才能访问，其他资源则只需要认证通过即可访问。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests((authorize) -&amp;gt; authorize
                    .requestMatchers(&quot;/hello&quot;).hasRole(&quot;user&quot;)
                    .anyRequest().authenticated()
    );

    return http.build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;虽然 Spring Security 已经提供了认证授权的完整功能，但是在灵活、安全和可扩展的身份验证和授权解决方案还是不及一些像 OAuth 这种在大规模和分布式系统的安全框架，所以官方也将这些框架继承进来搭配使用，各司其职，所以在实际使用中为了可拓展、服务解耦和更高的安全性上大家就不会单纯使用 Spring Security ，所以了解好 Spring Security 的工作流程之后，我们结下来就要开始 Token 安全令牌的学习了。&lt;/p&gt;
</content:encoded></item><item><title>Spring</title><link>https://songbaicheng.cc.cd/posts/spring/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/spring/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Spring 中遇到的问题&lt;/h1&gt;
&lt;h2&gt;yaml 资源文件读取问题&lt;/h2&gt;
&lt;p&gt;在 spirng-boot-starter-parent 2.7.3 版本中，因为有一些数据想维护在 resources 下的 yaml 文件中，数据格式大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rate-code:
  list:
    - key: &quot;CADCNY3M=CFHB&quot;
      rateName: &quot;CADCNY&quot; 
      tenor: &quot;3M&quot;
      market: &quot;&quot;
    - key: &quot;CHF9MNFO=CFHB&quot;
      rateName: &quot;USDCHF&quot; 
      tenor: &quot;9M&quot;
      market: &quot;NFO&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们用读取配置文件的方式，即 &lt;code&gt;@ConfigurationProperties(&quot;rate-code&quot;)&lt;/code&gt; 搭配 &lt;code&gt;@PropertySource(value = {&quot;classpath:rate-code.yaml&quot;})&lt;/code&gt; 来读取的时候发现并没有注册到容器中，后来搜索后发现，原来 Spirng Boot 只会读取 application.yaml，其他的 yaml 并不支持读取，而为了解决这个问题，Spring 支持自定义的读取方式，所以需要自己添加一些读取规则，于是增加了一下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class YamlPropertySourceFactory extends DefaultPropertySourceFactory {

  @Override
  public PropertySource&amp;lt;?&amp;gt; createPropertySource(String name, EncodedResource resource) throws IOException {

    Resource newResource = resource.getRsource();

    if (!newResource.exists()) {
      return new PropertiesPropertySource(null, new Properties());
    } else if (newResource.getFilename().endsWith(&quot;.yaml&quot;) || newResource.getFilename().endsWith(&quot;.yaml&quot;)) {
      List&amp;lt;PropertySource&amp;lt;?&amp;gt;&amp;gt; sources = new YamlPropertySourceLoader(newResource.getFilename(), newResource);
      return sources.get(0);
    }
    return super.createPropertySource(name, resourcce);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Getter
@Setter
@ToString
@Configuration
@ConfigurationProperties(&quot;tare-code&quot;)
@PropertySource(value = {&quot;clssspath:rate-code.yaml&quot;}, factory = YamlPropertySourceFactory.class)
public class RateCodeConfig {
  private List&amp;lt;RateCodeDto&amp;gt; rateCodeList;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Sql</title><link>https://songbaicheng.cc.cd/posts/sql/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/sql/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SQL 百科&lt;/h1&gt;
&lt;h2&gt;SQL 优化指南&lt;/h2&gt;
&lt;h2&gt;简单语句 SQL 优化&lt;/h2&gt;
&lt;h3&gt;1. 选择合适的数据类型及字符集&lt;/h3&gt;
&lt;p&gt;使用合适的数据类型可以减少存储空间和提高查询速度。这个可不能小看，数据量到达一个量级，这个就能看出明显差异。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于布尔值使用 TINYINT(1) 而不是 CHAR(1) 比如你有一个字段是表示业务状态或者是类型。&lt;/li&gt;
&lt;li&gt;对于仅存储英文的表，使用 latin1 而不是 utf8mb4。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 避免使用SELECT *&lt;/h3&gt;
&lt;p&gt;仅选择必要的列，减少数据传输量。&lt;/p&gt;
&lt;h3&gt;3. 合理使用JOIN、避免子查询&lt;/h3&gt;
&lt;p&gt;避免过多的 JOIN 操作，尽量减少数据集的大小。尽量使用 JOIN 或者 EXISTS 代替子查询。&lt;/p&gt;
&lt;h3&gt;4. 使用UNION代替OR&lt;/h3&gt;
&lt;p&gt;使用UNION代替OR、优化ORDER BY和GROUP BY&lt;/p&gt;
&lt;h3&gt;5. 优化 ORDER BY 和 GROUP BY&lt;/h3&gt;
&lt;p&gt;确保 ORDER BY 和 GROUP BY 的列上有索引。&lt;/p&gt;
&lt;h3&gt;6. 避免使用%开头的LIKE查询&lt;/h3&gt;
&lt;p&gt;避免使用 % 开头的 LIKE 查询，因为不能使用索引。这个尤其重要，相信各位在各大平台网站上。很多搜索只有输入前面的字才能有结果，你输入中间的字，会查询不到，其实就是这个原理。&lt;/p&gt;
&lt;h3&gt;7. 使用索引&lt;/h3&gt;
&lt;p&gt;使用批量插入、优化INSERT操作。&lt;/p&gt;
&lt;h2&gt;正确使用索引&lt;/h2&gt;
&lt;h3&gt;1. 在常用查询条件和连接条件的列上建立索引&lt;/h3&gt;
&lt;p&gt;这块很清楚，反正只要发现查询较慢，优先检查where条件后面，有没有被创建索引。&lt;/p&gt;
&lt;h3&gt;2. 遵循最左前缀原则&lt;/h3&gt;
&lt;p&gt;这个是针对复合索引时的要求，遵循最左前缀原则。&lt;/p&gt;
&lt;h3&gt;3. 避免在索引列上进行计算&lt;/h3&gt;
&lt;p&gt;例子：避免 WHERE YEAR(date) = 2020，改用范围查询。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM orders WHERE date BETWEEN &apos;2024-06-01&apos; AND &apos;2024-06-30&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 更新频繁的列慎用索引&lt;/h3&gt;
&lt;p&gt;对于更新频繁的列，索引会增加写操作的开销，需要慎重使用。&lt;/p&gt;
&lt;h2&gt;DBMS 配置优化&lt;/h2&gt;
&lt;h3&gt;调整innodb_buffer_pool_size&lt;/h3&gt;
&lt;p&gt;innodb_buffer_pool_size 是 InnoDB 存储引擎最重要的配置参数之一，用于指定 InnoDB 缓冲池的大小。缓冲池用于缓存数据页、索引页和 InnoDB 表的其它信息。合理设置这个参数对数据库性能有很大影响。&lt;/p&gt;
&lt;p&gt;增大 InnoDB 缓冲池大小，提高缓存命中率。&lt;/p&gt;
&lt;p&gt;SET GLOBAL innodb_buffer_pool_size = 2G;
但是这里要注意 该值并不是越大越好。innodb_buffer_pool_size 应该设置要尽可能大，但要确保为操作系统和其他应用程序留出足够的内存。&lt;/p&gt;
&lt;p&gt;一般建议在数据库专用服务器上设置为物理内存的 60% 到 80%。通过监控数据库性能和内存使用情况，可以进一步调整这个参数以优化数据库性能。&lt;/p&gt;
&lt;h3&gt;调整query_cache_size&lt;/h3&gt;
&lt;p&gt;query_cache_size 是用于指定查询缓存的大小。查询缓存可以缓存 SELECT 查询的结果，避免重复执行相同的查询，从而提高性能。&lt;/p&gt;
&lt;p&gt;然而，在 MySQL 8.0 及更高版本中，查询缓存已经被完全移除。如果你使用的是 MySQL 8.0 及以上版本，可以忽略 query_cache_size 参数。&lt;/p&gt;
&lt;h3&gt;调整thread_cache_size&lt;/h3&gt;
&lt;p&gt;增大线程缓存大小，减少线程创建开销。&lt;/p&gt;
&lt;p&gt;SET GLOBAL thread_cache_size = 100;&lt;/p&gt;
&lt;h3&gt;调整table_open_cache&lt;/h3&gt;
&lt;p&gt;增大表缓存大小，减少表打开的开销。&lt;/p&gt;
&lt;p&gt;SET GLOBAL table_open_cache = 4000;&lt;/p&gt;
&lt;h3&gt;调整tmp_table_size和max_heap_table_size&lt;/h3&gt;
&lt;p&gt;增大临时表和堆表的最大大小，减少磁盘 I/O。&lt;/p&gt;
&lt;p&gt;SET GLOBAL tmp_table_size = 64M;
SET GLOBAL max_heap_table_size = 64M;&lt;/p&gt;
&lt;h3&gt;调整innodb_flush_log_at_trx_commit&lt;/h3&gt;
&lt;p&gt;根据需求调整日志刷新策略，权衡性能和数据安全性。&lt;/p&gt;
&lt;p&gt;SET GLOBAL innodb_flush_log_at_trx_commit = 2;&lt;/p&gt;
&lt;h3&gt;调整innodb_log_file_size&lt;/h3&gt;
&lt;p&gt;增大日志文件大小，减少日志文件切换的开销。&lt;/p&gt;
&lt;p&gt;SET GLOBAL innodb_log_file_size = 256M;&lt;/p&gt;
&lt;h3&gt;调整innodb_log_buffer_size&lt;/h3&gt;
&lt;p&gt;增大日志缓冲区大小，提高写入性能。&lt;/p&gt;
&lt;p&gt;SET GLOBAL innodb_log_buffer_size = 16M;&lt;/p&gt;
&lt;h3&gt;调整innodb_io_capacity&lt;/h3&gt;
&lt;p&gt;根据磁盘 I/O 性能调整 InnoDB I/O 容量。&lt;/p&gt;
&lt;p&gt;SET GLOBAL innodb_io_capacity = 2000;&lt;/p&gt;
&lt;h3&gt;调整max_connections&lt;/h3&gt;
&lt;p&gt;增大最大连接数，支持更多并发连接。&lt;/p&gt;
&lt;p&gt;SET GLOBAL max_connections = 500;&lt;/p&gt;
&lt;h3&gt;调整sort_buffer_size&lt;/h3&gt;
&lt;p&gt;增大排序缓冲区大小，提高排序操作的性能。&lt;/p&gt;
&lt;p&gt;SET GLOBAL sort_buffer_size = 4M;&lt;/p&gt;
&lt;h3&gt;调整read_buffer_size&lt;/h3&gt;
&lt;p&gt;增大读缓冲区大小，提高顺序扫描性能。&lt;/p&gt;
&lt;p&gt;SET GLOBAL read_buffer_size = 2M;&lt;/p&gt;
</content:encoded></item><item><title>Shell</title><link>https://songbaicheng.cc.cd/posts/shell/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/shell/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Shell 脚本&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的脚本没有特殊标注都以 Linux 机器为准，有些命令可能在 AIX 机器上会失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;定时删除过期日志&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# ./bin.bash

Log_directory=&quot;/price/log&quot; ＃ 日志地址
days_to_keep=7 ＃ 保留的天数，即七天前的文件将被删除

# 计算七天前的曰期
target_date=$(date -d &quot;$days_to_keep days ago&quot; + %Y-%m-%d)

# 删除七天前日期格式的文件夹
find &quot;$log_directory&quot; -type d -name &quot;????-??-??&quot; | while read -r folder; do
    folder_name=＄(basename &quot;$folder&quot;)

    # 比较文件夹的日期与目标日期
    if [[ $folder_name &amp;lt; $target_date ]]; then
        echo &quot;删除文件夹：$folder&quot;
        rm - rf &quot;＄folder&quot;
    fi
done

# 删除七天前的*.1og.* 格式文件
find &quot;$log_directory&quot; -type f -name &quot;*.1og.*&quot; | while read -r file;
    file_date=$(stat -c &quot;%y&quot; &quot;$file&quot; | cut -d&apos; &apos; -f1)

    # 比较文件的日期与目标日期
    if [[ $file_date &amp;lt; $target_date ]]; then
        echo &quot;删除文件：$file&quot;
        rm &quot;$file&quot;
    fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用 cron 来定时运行此脚本，使用 &lt;code&gt;crontab -e&lt;/code&gt; 命令编辑当前用户的 cron 任务表，然后添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 0 * * * /shells/cleanup_logs.sh # 将在每天的午夜（00:00）运行脚本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后退出，可以使用 &lt;code&gt;crontab -l&lt;/code&gt; 查看是否添加成功。&lt;/p&gt;
&lt;h2&gt;比较声明环境文件是否生效&lt;/h2&gt;
&lt;p&gt;对于声明环境变量的文件，可以用 &lt;em&gt;&lt;strong&gt;export&lt;/strong&gt;&lt;/em&gt; 关键字声明然后 &lt;em&gt;&lt;strong&gt;source&lt;/strong&gt;&lt;/em&gt; 使文件生效即可，我们判断环境变量是否已经生效可以先按行遍历拿到 export 开头的行，从中取到变量名和值，然后根据变量名获取环境变量中已经生效的值和文件中的变量值进行比较即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 初始化错误计数器
error_count=0

# 函数来处理错误计数
count_error() {
    ((error_count++))
}

# 使用find命令查找以-profile结尾的环境变量文件，并处理它们
find . -name &quot;*-profile&quot; -type f | while read -r file; do

    # 检查文件是否存在并可读
    if [[ -r &quot;$file&quot; ]]; then

        echo &quot;Reading variables from: $file&quot;
        # 逐行读取文件内容
        while read -r line; do

            # 查找包含&quot;export&quot;关键字的行
            if [[ &quot;$line&quot; == export* ]]; then

                # 提取变量名和值
                variable=$(echo &quot;$line&quot; | cut -d&apos; &apos; -f2 | cut -d&apos;=&apos; -f1)
                value=$(echo &quot;$line&quot; | cut -d&apos;=&apos; -f2-)

                # 去除文件中的单引号
                value=${value//\&apos;/}
                
                # 获取对应的环境变量值
                env_value=&quot;${!variable}&quot;
                
                # 比较文件中的值和环境变量中的值
                if [[ &quot;$value&quot; != &quot;$env_value&quot; ]]; then
                    echo &quot;$file中$variable的值不相同，文件中的变量值为$value，环境变量中的变量值为$env_value&quot;
                    echo &quot;Variable $variable does not match. File value: $value, Environment value: $env_value&quot;
                    count_error
                fi
            fi
        done &amp;lt; &quot;$file&quot;
    else
        echo &quot;不能读取的文件：$file&quot;
    fi
done

# 判断错误计数器是否为0
if [[ &quot;$error_count&quot; -eq 0 ]]; then
    echo &quot;环境变量完全相同！&quot;
else
    echo &quot;环境变量有$error_count个不同&quot;
fi

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Stack Queue</title><link>https://songbaicheng.cc.cd/posts/stack-queue/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/stack-queue/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;栈和队列的应用&lt;/h1&gt;
&lt;h2&gt;栈在括号匹配的应用&lt;/h2&gt;
&lt;p&gt;假设表达式中允许包含两种括号：圆括号和方括号，其嵌套的顺序任意，只需要成对匹配即位正确，否则为不正确。&lt;/p&gt;
&lt;p&gt;算法思想如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始设置一个空栈，顺序读入括号。&lt;/li&gt;
&lt;li&gt;若是右括号，则和栈顶最紧迫的期待的括号消解，如果不能成对消除则不合法，退出程序。&lt;/li&gt;
&lt;li&gt;若是左括号，则作为一个新的更紧迫的期待压入栈中。当算法结束时，栈为空，否则不合法。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;栈在表达式求值的应用&lt;/h2&gt;
&lt;p&gt;表达式求值可以结合二叉树进行理解。其中中缀表达式不仅依赖算符的优先级，而且还要处理括号。后缀表达式的运算符在操作数后面，在后缀表达式中已经考虑了运算符的优先级，所以没有括号只有操作数和运算符。通过后缀表达式表示计算过程为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;顺序扫描表达式的每一项。&lt;/li&gt;
&lt;li&gt;如果是操作数则压入栈中&lt;/li&gt;
&lt;li&gt;若是操作符，则从栈中出栈两个操作数进行运算并将结果重新入栈。&lt;/li&gt;
&lt;li&gt;扫描完毕后，栈顶元素就是最后的运算结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;栈在递归中的应用&lt;/h2&gt;
&lt;p&gt;递归即把一个大型的复杂问题层层转化成一个与原问题相似的规模较小的问题来就求解的过程，往往只需要少量代码就可以描述出解题过程中的多次重复计算，虽然减少了程序代码量，但是效率并不会很高。&lt;/p&gt;
&lt;p&gt;在递归调用的过程中，系统为每一层的返回点、局部变量、传入实参等开辟了递归工作栈来进行数据存储，递归的次数过多容易产生栈溢出，效率不高的原因是因为递归调用过程会产生重复运算，但是代码简单，容易理解，可以为初学者提供解题思路。&lt;/p&gt;
&lt;h2&gt;队列在层次遍历中的应用&lt;/h2&gt;
&lt;p&gt;在信息处理中有一大类问题需要逐层或逐行进行处理，这种问题往往是在处理当前层或行时就对下一层或下一行进行预处理，把处理顺序安排好，等当前层或当前行处理完毕就可以进行处理下一层或者下一行，这个过程就可以使用队列保存下一步的处理顺序，类似于二叉树的层序遍历。&lt;/p&gt;
&lt;h2&gt;队列在计算机系统中的应用&lt;/h2&gt;
&lt;p&gt;队列在计算机系统中的应用非常广泛，一方面是解决主机与外部设备之间速度不匹配的问题，一方面是解决由多用户引起的资源竞争问题。&lt;/p&gt;
&lt;p&gt;第一方面仅以主机和打印机之间速度不匹配的问题作说明。主机把数据输送给打印机，输出的数据速度比打印机的速度快得多，解决的方法就是设置一个打印数据缓冲区，主机把要打印的数据写入这个缓冲区，写满后就暂停输出，转去做其他事情，打印机就从缓冲区按照先进先出的原则依次取出数据并打印，打印完再向主机发出申请，主机接到请求后再向缓冲区写入打印数据。这样做既保证了打印的数量正确，又使主机提升了效率。&lt;/p&gt;
&lt;p&gt;第二个方面，CPU 资源的竞争就是一个典型的例子。在一个带有多终端的计算机系统上，由多个用户需要 CPU 各自运行自己的程序，他们分别通过各自的终端向操作系统提出占用 CPU 的请求。操作系统通常会按照请求在时间上的先后顺序把他们呢拍成一个队列，每次把 CPU 分配给队首请求的用户使用。当对应的程序运行结束或用完规定的时间间隔，令其出队，再把自己 CPU 分配给新的队首请求的用户使用。这样既保证了每个用户的请求，又能使 CPU 正常运行。&lt;/p&gt;
</content:encoded></item><item><title>Tree Btree</title><link>https://songbaicheng.cc.cd/posts/tree-btree/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/tree-btree/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树和二叉树的应用&lt;/h1&gt;
&lt;h2&gt;哈夫曼树和哈夫曼码&lt;/h2&gt;
&lt;p&gt;在许多应用中，树中结点常常被赋予一个表示某种意义的数值，称为该结点的&lt;strong&gt;权&lt;/strong&gt;，从树的根结点到任意结点的路径长度与该结点上的权值的乘积，称为该结点的带权路径长。树中所有叶结点的带权路径长被称为该树的&lt;strong&gt;带权路径长&lt;/strong&gt;（WPL）。&lt;/p&gt;
&lt;p&gt;在含有 n 个带权叶结点的二叉树中，其中带权路径长度最小的二叉树称为&lt;strong&gt;哈夫曼树&lt;/strong&gt;，也称为&lt;strong&gt;最优二叉树&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree-btree/wpl.jpg&quot; alt=&quot;具有不同带权长度的二叉树&quot; title=&quot;具有不同带权长度的二叉树&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;哈夫曼树的构造&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree-btree/hftree-process.jpg&quot; alt=&quot;哈夫曼树的构造过程&quot; title=&quot;哈夫曼树的构造过程&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上述的构造过程中可以看出哈夫曼树具有一下特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个初始结点最终都成了叶结点，且权值越小的结点到根结点的路径长度越大。&lt;/li&gt;
&lt;li&gt;构造过程中新建了 n - 1 个分支结点，因此哈夫曼树的总结点数为 2n - 1。&lt;/li&gt;
&lt;li&gt;每次构造都选择两棵树作为新结点的孩子，因此哈夫曼树不存在度为 1 的结点。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;哈夫曼编码&lt;/h3&gt;
&lt;p&gt;在数据通讯中，若对每个字符用相等长度的二进制位表示，称这种编码方式为&lt;strong&gt;固定长度编码&lt;/strong&gt;。若允许对不同字符用不等长的二进制表示，则这种编码方式称为&lt;strong&gt;可变长度编码&lt;/strong&gt;，其特点是对频率高的字符覆以短编码，频率低的字符则赋予长编码，从而使字符的平均长度减短，起到压缩数据的作用，哈夫曼编码正是一种被广泛应用而且有效的数据压缩编码。&lt;/p&gt;
&lt;p&gt;若没有一个编码是另一个编码的前缀，则称为这样的编码为&lt;strong&gt;前缀编码&lt;/strong&gt;。由哈夫曼树得到哈夫曼编码是很自然的过程，其权值正好对应它出现的次数，构造出对应的哈夫曼树，我们将字符的编码解释为从根至该字符路径上边界的标记的序列，0 表示转向左孩子，1 表示转向右孩子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree-btree/hf-code.jpg&quot; alt=&quot;由哈夫曼树构成哈夫曼编码&quot; title=&quot;由哈夫曼树构成哈夫曼编码&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;并查集&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;参考算法模块中的&lt;a href=&quot;/study/computer-basis/ads/algorithms/disjoint-set-union.md&quot;&gt;并查集&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Stack</title><link>https://songbaicheng.cc.cd/posts/stack/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/stack/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;栈&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 java-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;root(栈)
    顺序栈
    链栈
    共享栈
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;栈（Stack）是只允许在一端进行插入或删除操作的线性表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/stack/stack.jpg&quot; alt=&quot;栈的示意图&quot; title=&quot;栈的示意图&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;栈顶：线性表允许进行插入和删除的那一端。&lt;/li&gt;
&lt;li&gt;栈底：固定的，不允许进行插入和删除的那一端。&lt;/li&gt;
&lt;li&gt;空栈：不含任何元素空表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如上图所示，a1为栈底元素，a5为栈顶元素。根据规则，进栈次序依次为a1,a2,a3,a4,a5，而出栈依次为a5,a4,a3,a2,a1，由此可见栈的操作特性可以明显囊括为先进后出（Last In First Out，LIFO）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;栈的数学性质：n个不同元素进栈，出栈元素不同排列的个数为 1/(n+1)乘2n中选n个数的排列，也被称为卡特兰（Catalan）数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;基本操作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;InitStack：初始化一个空栈。&lt;/li&gt;
&lt;li&gt;StackEmpty：判断一个栈是否为空。&lt;/li&gt;
&lt;li&gt;Push：进栈。&lt;/li&gt;
&lt;li&gt;Pop：出栈。&lt;/li&gt;
&lt;li&gt;GetTop：读取栈顶元素。&lt;/li&gt;
&lt;li&gt;DestroyStack：销毁栈。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;栈的顺序存储&lt;/h2&gt;
&lt;p&gt;采用顺序存储的栈成为顺序栈，利用一组地址连续的存储单元存放自栈底到栈顶的数据元素，同时附设一个指针指示当前栈顶元素的位置。&lt;/p&gt;
&lt;p&gt;::: normal-demo Java 实现顺序栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ArrayStack&amp;lt;E&amp;gt; extends Stack&amp;lt;E&amp;gt; {

    /**
     * 栈顶指针
     */
    private int top;
    /**
     * 栈内数组
     */
    private final E[] data;

    /**
     * 初始化栈
     * @param capacity
     */
    public ArrayStack(int capacity) {
        this.data = (E[]) new Object[capacity];
        this.top = -1;
    }

    @Override
    public E push(E item) {

        // 判断栈内是否还有空间
        if (this.top == data.length - 1) {
            return null;
        }
        data[++top] = item;
        return super.push(item);
    }

    /**
     * 出栈
     * @return 栈顶元素
     */
    @Override
    public synchronized E pop() {
        return data[top--];
    }

    /**
     * 获取栈顶元素
     * @return 栈顶元素
     */
    @Override
    public synchronized E peek() {
        return data[top];
    }

    /**
     * 栈判空
     * @return 是否为空栈
     */
    @Override
    public boolean empty() {
        return this.top == -1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;共享栈&lt;/h3&gt;
&lt;p&gt;利用栈底位置不变的特性，可以让两个顺序栈共享一个一维数组空间，将两个栈的栈底分别设在共享空间的两端，两个栈顶向共享空间的中间延伸，共享栈是为了更有效的利用存储空间，两个栈互相调节，存取数据的时间复杂度都为O(1)。&lt;/p&gt;
&lt;p&gt;两个栈的栈顶指针一个为-1一个为MaxSize时各自为空，而两个指针相邻，即top0 - top1 = 1的时候为栈满。&lt;/p&gt;
&lt;h2&gt;栈的链式存储&lt;/h2&gt;
&lt;p&gt;采用链式存储的栈称为链栈，优点是便于多个栈共享存储空间提高效率，并且不存在栈满的情况。通常采用单链表实现并规定所有的操作都在表头进行。&lt;/p&gt;
&lt;p&gt;::: normal-demo Java 实现带头结点的链栈&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class LinkedStack&amp;lt;E&amp;gt; extends Stack&amp;lt;E&amp;gt; {

    /**
     * 存储链表
     */
    private LNode&amp;lt;E&amp;gt; head = null;

    /**
     * 初始化头结点
     */
    public LinkedStack() {
        head = new LNode&amp;lt;&amp;gt;(null);
    }

    /**
     * 入栈
     * @param item 入栈元素
     * @return 入栈元素
     */
    @Override
    public E push(E item) {
        LNode&amp;lt;E&amp;gt; node = new LNode&amp;lt;&amp;gt;(item);

        if (head.next != null) {
            node.next = head.next;
        }

        head.next = node;
        return item;
    }

    /**
     * 出栈
     * @return 出栈元素
     */
    @Override
    public synchronized E pop() {

        E temp = head.next.data;
        head.next = head.next.next;

        return temp;
    }

    /**
     * 查找栈顶元素
     * @return 栈顶元素
     */
    @Override
    public synchronized E peek() {
        return head.next.data;
    }

    /**
     * 判断是否栈空
     * @return 是否栈空
     */
    @Override
    public boolean empty() {
        return head.next == null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Tree</title><link>https://songbaicheng.cc.cd/posts/tree/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/tree/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树和二叉树&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 java-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;root(树形结构)
    二叉树
        概念
            定义
            存储结构
        操作
            三种遍历
            线索二叉树
        应用
            并查集
            哈夫曼树
    树和森林
        概念
            定义
            存储结构
        操作
            与二叉树的转换
            遍历
        应用
            并查集
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;树的基本概念&lt;/h2&gt;
&lt;p&gt;树是 n(n &amp;gt;= 0) 个结点的有限集。当 n = 0 的时候，称为&lt;strong&gt;空树&lt;/strong&gt;，在任何一个非空树中应该满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;有且只有一个特定的称为根的结点。&lt;/li&gt;
&lt;li&gt;当 n &amp;gt; 1 时，其余结点可以分为 m(m &amp;gt; 0) 个互不相交的有限集 T1,T2,T3,……,Tm，其中每个结合本身又是一棵树，并成为根的子树。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;显然，树的定义是递归的，即在树的定义中又用到了其自身，树作为一种逻辑结构，同时也是一种分层结构，具有两个特点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;树的根结点没有前驱，根结点外的所有结点有且只有一个前驱。&lt;/li&gt;
&lt;li&gt;树中的所有所有结点都可以有零个或多个前驱。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因为树中的某个节点（除根结点外）最多只和上一层的一个结点有直接关系，根结点没有直接上层结点，所有n个结点的树有 n - 1 条边，而且每个结点与其下一层的零个或多个结点（即子女结点）都有直接关系。&lt;/p&gt;
&lt;h3&gt;基本术语&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/tree.jpg&quot; alt=&quot;树的树形表示&quot; title=&quot;树的树形表示&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先观察结点K，根A到K的唯一路径到的任意点，称为结点K的祖先，即A、B、E都是K的祖先。而E是路径上最接近K的祖先，所以称E为K的双亲，而K是E的孩子。&lt;/li&gt;
&lt;li&gt;有相同双亲结点称为兄弟，如K和L都有共同的双亲E，即K和L为兄弟结点。&lt;/li&gt;
&lt;li&gt;树中一个结点的孩子总数称为该结点的度，树中结点最大的度称为树的度，如结点B的度为2，D的度为3，树的度为3.&lt;/li&gt;
&lt;li&gt;度大于0的结点称为分支结点（又称非终端结点），度为0的结点为叶子结点（又称终端结点）。&lt;/li&gt;
&lt;li&gt;结点的层次从根结点开始，根结点为第一层，它的子结点为第二层，以此类推。双亲在同一层的结点互为堂兄弟，如G和E、F、H、I、J互为堂兄弟。&lt;/li&gt;
&lt;li&gt;结点的深度是从根结点开始自顶向下逐层累加，结点的高度从叶子结点开始自底向上逐层累加。树的高度或深度是树中结点的最大深度。&lt;/li&gt;
&lt;li&gt;有序树和无序树。树中的结点从左到右是有次序的，不能互换被称为有序树。否则称为无序树。&lt;/li&gt;
&lt;li&gt;路径和路径长度。树中的两个结点之间的路径是由这两个结点之间所经过的的结点序列构成的，而路径长度是路径上所经过的边的个数。&lt;/li&gt;
&lt;li&gt;树的分支是有向的，从双亲结点指向子结点。&lt;/li&gt;
&lt;li&gt;森林。森林是m（m&amp;gt;=0）颗互不相交的树的集合。森里只要把所有树的根结点去除就成了森林，相反，只要给m颗树独立的树添加一个根结点，森林就成了树。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基本性质&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;树中的结点数等于所有结点的度之和加一。&lt;/li&gt;
&lt;li&gt;度为m的树第i层上至多有m^i-1^个结点（i&amp;gt;=1）。&lt;/li&gt;
&lt;li&gt;高度为h的m叉树至多有 (m^h^-1)/(m-1)个结点。&lt;/li&gt;
&lt;li&gt;具有n个结点的m叉树的最小高度为⌈log~m~(n(m-1)+1)⌉。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;二叉树&lt;/h2&gt;
&lt;p&gt;二叉树是一种特殊的树形结构，其特点就是每个结点至多只有两颗子树，即不存在度大于2的结点，而且二叉树是有序树，左右结点次序不能随意颠倒。二叉树的递归定义是或为空二叉树，或为一个根结点和两个互不相交的被称为根的左子树和右子树组成，左子树和右子树分别又是一颗二叉树。&lt;/p&gt;
&lt;p&gt;二叉树是特殊的度为二的有序树，度为2的有序树至少有三个结点，而二叉树可以为空。度为二的有序树左右次序是对于另一个孩子而言的，若某个节点只有一个孩子，则这个孩子就无需区分其左右次序，而二叉树无论孩子个数是否为2，均需要确定其左右次序。&lt;/p&gt;
&lt;h3&gt;几种特殊的二叉树&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/full-tree-and-compete-tree.jpg&quot; alt=&quot;完全二叉树和满二叉树&quot; title=&quot;完全二叉树和满二叉树&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完全二叉树：高度为h，有n个结点的二叉树，当且仅当每个结点都与高度为h的满二叉树中编号为1～n的结点一一对应称为完全二叉树。
&lt;ul&gt;
&lt;li&gt;若 i &amp;lt;= ⌊n/2⌋，则i为分支结点，否则为叶子结点。&lt;/li&gt;
&lt;li&gt;叶子结点只可能在层数最大的两层出现，并且最大层出现的叶子结点应该依次排列在该层的最左边的位置。&lt;/li&gt;
&lt;li&gt;若有度为1的结点，则只可能有一个，且该结点，且该结点只有左孩子。&lt;/li&gt;
&lt;li&gt;按照层序编号后，一旦出现结点为叶子结点或者只有左孩子，则编号大于i的结点均为叶子结点。&lt;/li&gt;
&lt;li&gt;若n为奇数，则每个分支结点都有左右孩子，若n为偶数，则编号n/2的结点只有左孩子。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;满二叉树：一个高度为h，且含有2^h^-1个结点的二叉树称为满二叉树，即每层都是最多的结点。按照层序排序后，对于编号为i的结点，若有双亲则双亲为⌊i/2⌋，若有左孩子，则左孩子为 2i，若有右孩子，有孩子为 2i+1。&lt;/li&gt;
&lt;li&gt;二叉排序树：左子树上所有的结点的关键字均小于更结点的关键字；右子树上的所有结点句大于根结点的关键字，左右子树分别又各是一颗二叉排序树。&lt;/li&gt;
&lt;li&gt;平衡二叉树：树上任意一个结点的左子树和右子树的深度之差不超过1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;二叉树的性质&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;非空二叉树的叶子结点数等于度为2的结点树加一，即 n~0~=n~2~ + 1。&lt;/li&gt;
&lt;li&gt;非空二叉树上第k层上至多有 2~k-1~ 个结点。&lt;/li&gt;
&lt;li&gt;高度为h的二叉树至多有 2^k^-1 个结点，h &amp;gt;= 1.&lt;/li&gt;
&lt;li&gt;结点所在的深度为 ⌊log~2~n⌋ + 1。&lt;/li&gt;
&lt;li&gt;具有n个（n&amp;gt;0）结点的完全二叉树的高度为 ⌈log~2~(n + 1)⌉ 或 ⌊log~2~n⌋ + 1。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;二叉树的存储&lt;/h3&gt;
&lt;h4&gt;顺序存储&lt;/h4&gt;
&lt;p&gt;二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右完全存储二叉树的所有结点元素。根据二叉树的性质，&lt;strong&gt;完全二叉树&lt;/strong&gt;和&lt;strong&gt;满二叉树&lt;/strong&gt;采用顺序结构比较合适，树中结点的序号可以唯一的反应结点之间的逻辑结构，这样既能最大可能的节省存储空间，又能利用数组元素的下标确定结点的位置以及结点的关系。如果是一般的二叉树为了让数组下标反应二叉树中结点之间的逻辑关系，只能添加一些并不存在空结点，让其每个结点与完全二叉树的结点相对照，如果最坏的情况高度为h且之后h个结点的单枝树却要占据 2^h^ - 1 个单元。&lt;/p&gt;
&lt;p&gt;需要注意的是，顺序存储需要从数组下标1开始存储树中的结点，否则一些性质则无法满足。&lt;/p&gt;
&lt;h4&gt;链式存储&lt;/h4&gt;
&lt;p&gt;由于顺序存储的空间利用率较低，因此二叉树一般都是采用链式存储，在二叉树中结点通常包含数据域和指针域，二叉链表就必须包含数据域 data、左指针域 lchild 和右指针域 rchild。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import lombok.Getter;
import lombok.Setter;

/**
 * @description 二叉树的链式存储
 */
@Getter
@Setter
public class BiTree&amp;lt;E&amp;gt; {

    /**
     * 数据域
     */
    E data;
    /**
     * 左孩子指针
     */
    BiTree&amp;lt;E&amp;gt; leftChild;
    /**
     * 右孩子指针
     */
    BiTree&amp;lt;E&amp;gt; rightChild;

    /**
     * 初始化方法
     * @param data 数据域
     */
    public BiTree(E data) {
        this.data = data;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二叉树的遍历&lt;/h3&gt;
&lt;p&gt;二叉树中的遍历是指按某条搜索路径访问树中的每个结点，使得每个结点均被访问一次，而且仅被访问一次，而且仅被访问一次。由于二叉树是一种非线性结构，每个结点都可以能有两个子树，因而需要寻找一种规律以便使二叉树的结点能排列在一个线性队列上，方便遍历。我们根据二叉树的定义，遍历一颗二叉树要决定对根和左右结点的访问顺序，常见的遍历次序是先序、中序和后序三种，其中的序是指根结点在何时被访问。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/bitree-order.jpg&quot; alt=&quot;二叉树的三种遍历顺序&quot; title=&quot;二叉树的三种遍历顺序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;::: normal-demo Java 利用递归实现二叉树三种遍历&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.sbc.structure.tree;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
 * @author songbaicheng
 * @description 二叉树遍历类测试
 * @date 2023/8/15 20:14
 */
class BiTreeTest {

    BiTree&amp;lt;Integer&amp;gt; root = new BiTree&amp;lt;&amp;gt;(1);

    @BeforeEach
    void setUp() {

        // 初始化二叉树
        final BiTree&amp;lt;Integer&amp;gt; l7 = new BiTree&amp;lt;&amp;gt;(7);
        final BiTree&amp;lt;Integer&amp;gt; r3 = new BiTree&amp;lt;&amp;gt;(3);
        final BiTree&amp;lt;Integer&amp;gt; l4 = new BiTree&amp;lt;&amp;gt;(4);
        final BiTree&amp;lt;Integer&amp;gt; l9 = new BiTree&amp;lt;&amp;gt;(9);
        final BiTree&amp;lt;Integer&amp;gt; r6 = new BiTree&amp;lt;&amp;gt;(6);
        final BiTree&amp;lt;Integer&amp;gt; l8 = new BiTree&amp;lt;&amp;gt;(8);

        l4.setRightChild(l8);
        l7.setLeftChild(l4);
        l7.setRightChild(l9);
        r3.setLeftChild(r6);
        root.setLeftChild(l7);
        root.setRightChild(r3);
    }

    /**
     * 前序遍历：
     * 1,7,4,8,9,3,6,
     * 中序遍历：
     * 4,8,7,9,1,6,3,
     * 后序遍历
     * 8,4,9,7,6,3,1,
     */
    @Test
    void test() {
        System.out.println(&quot;前序遍历：&quot;);
        preOrder(root);
        System.out.println(&quot;\n中序遍历：&quot;);
        inOrder(root);
        System.out.println(&quot;\n后序遍历&quot;);
        postOrder(root);
        System.out.println();
    }

    /**
     * 前序遍历
     * @param tree 遍历二叉树
     */
    private void preOrder(BiTree&amp;lt;Integer&amp;gt; tree) {
        if (tree != null) {
            System.out.print(tree.data + &quot;,&quot;);
            preOrder(tree.leftChild);
            preOrder(tree.rightChild);
        }
    }

    /**
     * 中序遍历
     * @param tree 遍历二叉树
     */
    private void inOrder(BiTree&amp;lt;Integer&amp;gt; tree) {
        if (tree != null) {
            inOrder(tree.leftChild);
            System.out.print(tree.data + &quot;,&quot;);
            inOrder(tree.rightChild);
        }
    }

    /**
     * 后序遍历
     * @param tree 遍历二叉树
     */
    private void postOrder(BiTree&amp;lt;Integer&amp;gt; tree) {
        if (tree != null) {
            postOrder(tree.leftChild);
            postOrder(tree.rightChild);
            System.out.print(tree.data + &quot;,&quot;);
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;递归的巧妙确实让我们省略了很多代码，可是在我们享受这种便利的时候，同样也徒增了很多次计算的消耗，每次扫描叶子结点的时候总会将其父母结点重新计算一次，在一些特殊情况中非常浪费性能，尤其是递归中经典的&lt;strong&gt;斐波那契数列&lt;/strong&gt;中，如果追求更高的时间复杂度，我们会采取非递归的方式新增一个标记记录每一次计算出的值来减少多次计算的消耗，所以这里也借助栈来实现二叉树的中序遍历：&lt;/p&gt;
&lt;p&gt;::: normal-demo 非递归实现中序遍历&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* 非递归中序遍历
*
* @param tree 遍历二叉树
*/
private void inOrderByStack(BiTree&amp;lt;Integer&amp;gt; tree) {

    final LinkedStack&amp;lt;BiTree&amp;lt;Integer&amp;gt;&amp;gt; stack = new LinkedStack&amp;lt;&amp;gt;();
    BiTree&amp;lt;Integer&amp;gt; root = tree;

    while (root != null || !stack.empty()) {
        if (root != null) {
            // 一路向左
            stack.push(root);
            root = root.leftChild;
        } else {
            // 出栈并开始转向出栈的右子树
            root = stack.pop();
            System.out.print(root.data + &quot;,&quot;);
            root = root.rightChild;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;由遍历序列构造二叉树&lt;/h3&gt;
&lt;p&gt;我们先由二叉树的先序序列和中序序列来唯一确定一颗二叉树，在先序遍历序列中，第一个结点一定是二叉树的根结点，而在中序遍历中，根结点一定在讲中序序列分割为两个子序列，根据这两个子序列在先序序列中根据同样的规律找到左右子树的根结点，依次递归下去就能唯一确定这颗二叉树，同理，二叉树的后序序例和中序序列也可以诶唯一确定二叉树、层序遍历和中序序列也可以唯一确定二叉树，但是先序序列和后续序列不能确定。&lt;/p&gt;
&lt;h2&gt;线索二叉树&lt;/h2&gt;
&lt;p&gt;遍历二叉树是以一定的规则将二叉树中的结点排列成一个线性序列，只能体现一种父子关系，不能直接得到结点在遍历中的前驱和后继。如果我们把二叉树中的空指针结点存放指向其前驱或者后继的指针，可以像遍历单链表那样方便的遍历二叉树，加快了查找结点前驱和后继的速度，这也就是二叉排序树，规定每个结点若无左子树，令lchild指向其前驱结点，若无右子树，令rchild指向其后继结点，并且需要增加两个标识域标识指针域，以指向左右孩子或者前驱后继，标识为0则表示为左右孩子，如果是1则代表是前驱或者后继。&lt;/p&gt;
&lt;h2&gt;树、森林&lt;/h2&gt;
&lt;h3&gt;树的存储结构&lt;/h3&gt;
&lt;p&gt;树的存储结构有很多，即可采用顺序存储结构，又可采用链式存储结构，但无论采用何种存储方式，都要求能唯一的反应树中各个节点之间的逻辑关系，下面是三种常见的存储结构。&lt;/p&gt;
&lt;h4&gt;双亲表示法&lt;/h4&gt;
&lt;p&gt;采用一组连续空间来存储每个结点，同时在每个结点中增设一个伪指针，指示双亲结点在数组中的位置，根结点下标为0，其尾指针域为-1.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/parental-representation.jpg&quot; alt=&quot;双亲表示法&quot; title=&quot;双亲表示法&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;孩子表示法&lt;/h4&gt;
&lt;p&gt;将每个结点的孩子结点都用单链表链接起来形成一个线性结构，此时 n 个结点就有 n 个孩子链表（叶子结点的孩子链表为空表）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/child-representation.jpg&quot; alt=&quot;孩子表示法&quot; title=&quot;孩子表示法&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;孩子兄弟表示法&lt;/h4&gt;
&lt;p&gt;即以二叉链表作为树的存储结构，使每个结点包括三部分内容：结点值、指向结点第一个孩子结点的指针，以及指向结点在一个兄弟结点的指针。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/computer-basis/ads/data-structure/tree/child-brother-representation.jpg&quot; alt=&quot;孩子兄弟表示法&quot; title=&quot;孩子兄弟表示法&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;树、森林与二叉树的转换&lt;/h2&gt;
&lt;p&gt;树转换为二叉树的规则：每个结点左指针指向它的第一个孩子，右指针指向它在树中的相临右兄弟，这个规矩也叫左孩子右兄弟，其规则也正如上面的孩子兄弟表示法图所示。&lt;/p&gt;
&lt;p&gt;森林转换成二叉树的规则：把下一棵树转化为上一棵树的右兄弟，其他和树转化二叉树的规则相同。&lt;/p&gt;
&lt;h2&gt;树和森林的遍历&lt;/h2&gt;
&lt;p&gt;树的遍历是指用某种方式访问树的每个结点，且仅访问一次，主要有先根遍历和后根遍历两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先根遍历：先访问根结点，再依次遍历根结点的每棵子树，遍历子树时仍遵循先根后子树的规则。&lt;/li&gt;
&lt;li&gt;后根遍历：先依次遍历根结点的每棵子树，再访问根结点，遍历子树时仍遵循先子树后根的规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;森林的两种遍历方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先序遍历森林
&lt;ol&gt;
&lt;li&gt;访问森心的第一棵树的根结点&lt;/li&gt;
&lt;li&gt;先序遍历第一棵树中根结点的子树森林。&lt;/li&gt;
&lt;li&gt;先序遍历除去第一棵树之后剩余树构成的森林。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;中序遍历森林
&lt;ol&gt;
&lt;li&gt;中序遍历森里中第一棵子树的根结点的子树森林。&lt;/li&gt;
&lt;li&gt;访问第一棵树的根结点。&lt;/li&gt;
&lt;li&gt;中序遍历去除第一棵树之后剩余树构成的森林。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Unit Test</title><link>https://songbaicheng.cc.cd/posts/unit-test/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/unit-test/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单元测试&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;单元测试（Java语言中）是对类中每个方法提供一个或多个测试的一种实践，其目的是为了有规律的测试一个类的各个部分是否具备正确的行为。 ——摘自《Java 编程思想》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;刚开始工作的时候谁写测试类啊，还单元测试呢，测试我都不测试。后来到了正式一些的公司之后，不通过质量门禁则不能上线，所以 JaCoCo 这个开源的单元测试引擎就发挥作用了，它搭配 Sonar 面板可以展示单元测试对代码的覆盖程度、帮助开发团队分析测试覆盖率，并且了解哪些代码已经被测试覆盖，以及哪些代码尚未被测试覆盖。尽管如此，你的测试类还是很难保证你的代码所有逻辑分支都可以覆盖的到，于是 Mock 又出现了，相对于 Junit 它提供了简洁的API，使得在单元测试中创建模拟对象、定义模拟对象的行为以及验证方法的调用变得非常容易。所以我们选择使用 Mock 搭配 JUnit 作为最佳解决方案。&lt;/p&gt;
&lt;h2&gt;质量门襟要求&lt;/h2&gt;
&lt;p&gt;除 web 语言之外的所有开发语言在投产时，增量代码单元测试行覆盖率要求 100%，如果在 80% 到 100% 范围，可以进行人工评审环节，评审过后方可通过质量门禁校验。并且单元测试成功率不得低于 100%。&lt;/p&gt;
&lt;h2&gt;规范的单元测试&lt;/h2&gt;
&lt;p&gt;单元测试的目的是：提升软件质量、促进代码优化、提升研发效率、增加重构自信。单元测试需要符合两大类原则：FIRST 原则和 AIR 原则。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FITST原则：
&lt;ul&gt;
&lt;li&gt;快速（Fast）：单元测试能够快速执行。&lt;/li&gt;
&lt;li&gt;隔离（Isolated）：单元测试不要依赖外部环境，如网络、第三方 Web Service 等。&lt;/li&gt;
&lt;li&gt;可重复（Repeatable）：单元测试应该是可以被重复执行的，并且结果是相等的。&lt;/li&gt;
&lt;li&gt;自我验证（Self-verigying）：单元测试应该是用例本身自动校验，不依赖人工验证。&lt;/li&gt;
&lt;li&gt;及时（Timely）：单元测试应该及时进行编写、更新和维护，以保证测试用例可以随着业务代码的变化动态保证质量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AIR 原则：
&lt;ul&gt;
&lt;li&gt;自动化（Automatic）&lt;/li&gt;
&lt;li&gt;独立性（Independent）&lt;/li&gt;
&lt;li&gt;可重复（Repeatable）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;屏蔽内容限制&lt;/h3&gt;
&lt;p&gt;不包含逻辑处理的代码可屏蔽：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实体类（DTO、Entity、VO）。&lt;/li&gt;
&lt;li&gt;框架自动生成代码，如 Mapper、实体类等。&lt;/li&gt;
&lt;li&gt;枚举类。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用例强制要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;测试用例必须增加断言逻辑，避免恒真，用例需要保持结果准确性校验，用例执行结果必须能根据代码变化而反映出变化。&lt;/li&gt;
&lt;li&gt;单元测试需要尽可能的覆盖函数的所有范围，针对代码执行成功、失败、异常三种情况编写不同的用例。&lt;/li&gt;
&lt;li&gt;保证单元测试的独立性。&lt;/li&gt;
&lt;li&gt;单元测试是可重复执行的。&lt;/li&gt;
&lt;li&gt;测试力度足够小，可以精确定位问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用例建议要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;不建议调用数据库，外部接口，建议使用Mock&lt;/li&gt;
&lt;li&gt;不建议启动 Spring 容器。&lt;/li&gt;
&lt;li&gt;测试用例均需需为 public void。&lt;/li&gt;
&lt;li&gt;单元测试包结构和源码结构尽量保持一致。&lt;/li&gt;
&lt;li&gt;单元测试文件名称是由被”测试文件 + Test“ 组成&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;单元测试用例思路&lt;/h2&gt;
&lt;p&gt;以目标类或类中的某一个函数为单元体，通过构造尽可能覆盖所有的单数范围的不同入参对其进行调用，对比返回值是否到达预期，从而验证函数逻辑的正确性。&lt;/p&gt;
&lt;p&gt;当然存在被测试类A调用其他类B的函数，为了控制A中的代码逻辑，需要控制B对象函数的返回值，可以根据如下考虑：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;B对象来源&lt;/th&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;A对象是否需哟啊提前给B赋值&lt;/th&gt;
&lt;th&gt;赋值方式&lt;/th&gt;
&lt;th&gt;是否需要Mock函数&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;静态类&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，不依赖环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;不涉及&lt;/td&gt;
&lt;td&gt;不涉及&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态类&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，依赖了环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;不涉及&lt;/td&gt;
&lt;td&gt;不涉及&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new 所得&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，不依赖环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;td&gt;不涉及&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;new 所得&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，依赖了环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;Mock whenNew&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A的public方法传入&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，不依赖环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;public方法&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A的public方法传入&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，依赖了环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;public方法&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring注解&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，不依赖环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;public 方法&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring注解&lt;/td&gt;
&lt;td&gt;静态类初始化或其函数调用过程中，依赖了环境信息（API调用、DB调用、磁盘读取等）&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;td&gt;Mock 注解&lt;/td&gt;
&lt;td&gt;需要&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Mock&lt;/h2&gt;
&lt;p&gt;Mock 是在测试过程中对于一些不容易构造获取的对象，创建一个Mock对象来模拟对象的行为。基本原理就是先模拟对象，然后声明行为，最后执行验证，通过Mock能力控制代码路径，跳过外部依赖，实现分支覆盖。&lt;/p&gt;
&lt;p&gt;Mock 框架目前也有很多版本，有如下几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Easy Mock：一套通过简单方法对于给定的接口生成 Mock 对象的类库，它提供接口的模拟，能够通过录制、回放、检查三步来完成大体流程，可以令Mock对象返回指定的值或者抛出指定异常。&lt;/li&gt;
&lt;li&gt;JMock：基于Java开发，大大简化了虚拟对象的使用。&lt;/li&gt;
&lt;li&gt;Mockito：可读性强，验证语法简单，可以与JUnit无缝结合，是最广泛的Mock框架。&lt;/li&gt;
&lt;li&gt;PowerMock：Mockito增强版，弥补了对静态方法的不支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Easy Mcok&lt;/th&gt;
&lt;th&gt;JMock&lt;/th&gt;
&lt;th&gt;Mockito&lt;/th&gt;
&lt;th&gt;PowerMock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;final 方法&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;私有方法&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;静态方法&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;td&gt;支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SpringBoot依赖&lt;/td&gt;
&lt;td&gt;复杂&lt;/td&gt;
&lt;td&gt;复杂&lt;/td&gt;
&lt;td&gt;默认依赖&lt;/td&gt;
&lt;td&gt;基于 Mocktio 拓展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API风格&lt;/td&gt;
&lt;td&gt;略复杂&lt;/td&gt;
&lt;td&gt;略复杂&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;td&gt;简单&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Mock 单元测试实践&lt;/h2&gt;
&lt;h3&gt;Mockito&lt;/h3&gt;
&lt;h4&gt;导入依赖&lt;/h4&gt;
&lt;p&gt;::: code-tabs&lt;/p&gt;
&lt;p&gt;@tab Maven#Maven&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.mockito&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;mockito-core&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Gradle#Gradle&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;testImplementation &apos;org.mockito:mockito-core:&amp;lt;version&amp;gt;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;测试代码&lt;/h4&gt;
&lt;p&gt;Mockito 与多种测试框架（如JUnit、TestNG）和依赖注入框架（如Spring）完美集成，所以我们就用最常用的 Spring Boot 代码进行测试。&lt;/p&gt;
&lt;p&gt;首先我们先准备好要测试的方法类，内容也是非常的简单，无非是包含了关于用户的 crud 功能。&lt;/p&gt;
&lt;p&gt;::: normal-demo 待测试方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Builder
public class User {
    private String id;
    private String username;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Repository
public interface IUserMapper {
    List&amp;lt;User&amp;gt; getAllUsers();
    
    User getUserById(String id);
    
    void saveUser(User user);
    
    void deleteUser(String id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService {

    @Autowired
    private IUserMapper userMapper;
    
    public List&amp;lt;User&amp;gt; getAllUsers() {
        return userMapper.getAllUsers();
    }
    
    public User getUserById(String id) {
        return userMapper.getUserById(id);
    }
    
    public void saveUser(User user) {
        userMapper.saveUser(user);
    }
    
    public void deleteUser(String id) {
        userMapper.deleteUser(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;接下来就是写测试类了，我们这里需要依赖 Spring 容器测试，所以我们结合 @SpringBootTest 启动，而具体的测试方法也根据&lt;/p&gt;
&lt;p&gt;::: normal-demo 测试类代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
class UserServiceTest {

    @MockBean
    private IUserMapper userMapper;
    @Autowired
    private UserService userService;

    @Test
    void testGetAllUsers() {
        List&amp;lt;User&amp;gt; expectedUsers = Arrays.asList(
              UserVo.builder().username(&quot;zhangsan&quot;).id(&quot;1&quot;).build(),
              UserVo.builder().username(&quot;lisi&quot;).id(&quot;2&quot;).build(),
        );
        
        Mockito.when(userMapper.getAllUsers()).thenReturn(expectedUsers);
        
        List&amp;lt;User&amp;gt; actualUsers = userService.getAllUsers();
        
        assertEquals(expectedUsers, actualUsers);
        Mockito.verify(userMapper).getAllUsers();
    }
    
    @Test
    void testGetUserById() {
        String userId = &quot;1&quot;;
        User expectedUser = new User(userId, &quot;John&quot;);
        
        Mockito.when(userMapper.getUserById(userId)).thenReturn(expectedUser);
        
        User actualUser = userService.getUserById(userId);
        
        assertEquals(expectedUser, actualUser);
        Mockito.verify(userMapper).getUserById(userId);
    }
    
    @Test
    void testSaveUser() {
        User user = new User(&quot;1&quot;, &quot;John&quot;);
        
        userService.saveUser(user);
        
        Mockito.verify(userMapper).saveUser(user);
    }
    
    @Test
    void testDeleteUser() {
        String userId = &quot;1&quot;;
        
        userService.deleteUser(userId);
        
        Mockito.verify(userMapper).deleteUser(userId);
    }
    
    @Test
    void testGetUserById_NonExistingUser() {
        String userId = &quot;1&quot;;
        
        Mockito.when(userMapper.getUserById(userId)).thenReturn(null);
        
        assertThrows(UserNotFoundException.class, () -&amp;gt; {
            userService.getUserById(userId);
        });
        
        Mockito.verify(userMapper).getUserById(userId);
    }
    
    @Test
    void testSaveUser_NullUser() {
        assertThrows(IllegalArgumentException.class, () -&amp;gt; {
            userService.saveUser(null);
        });
        
        Mockito.verify(userMapper, Mockito.never()).saveUser(ArgumentMatchers.any());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;常用方法&lt;/h4&gt;
&lt;h5&gt;mock(Class&amp;lt;&lt;em&gt;T&lt;/em&gt;&amp;gt; classToMock)&lt;/h5&gt;
&lt;p&gt;创建一个模拟对象，用于代替真实对象的行为。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;UserService userServiceMock = Mockito.mock(UserService.class);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;when(mock.method()).thenReturn(value)&lt;/h5&gt;
&lt;p&gt;定义模拟对象方法的行为，指定当调用方法时应该返回的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义当调用 getUserById 方法并传入参数 1 时，返回一个名为 &quot;zhangsan&quot; 的 User 对象
User user = UserVo.builder().username(&quot;zhangsan&quot;).id(&quot;1&quot;).build();
Mockito.when(userServiceMock.getUserById(1)).thenReturn(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;verify(mock).method()&lt;/h5&gt;
&lt;p&gt;验证模拟对象的方法是否被调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 验证 getUserById 方法是否被调用
Mockito.verify(userServiceMock).getUserById(1);

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;verify(mock, times(n)).method()&lt;/h5&gt;
&lt;p&gt;验证模拟对象的方法被调用了特定的次数（n）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 验证 getUserById 方法被调用了2次
Mockito.verify(userServiceMock, Mockito.times(2)).getUserById(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;verify(mock, atLeast(n)).method()&lt;/h5&gt;
&lt;p&gt;验证模拟对象的方法被调用了至少n次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 验证 getUserById 方法被调用了至少3次
Mockito.verify(userServiceMock, Mockito.atLeast(3)).getUserById(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;verify(mock, never()).method()&lt;/h5&gt;
&lt;p&gt;验证模拟对象的方法从未被调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 验证 addUser 方法从未被调用
Mockito.verify(userServiceMock, Mockito.never()).addUser(Mockito.any(User.class));
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;verifyNoMoreInteractions(mock)&lt;/h5&gt;
&lt;p&gt;验证模拟对象上的所有方法已经被验证，并且没有其他未验证的方法调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 验证 userServiceMock 上的所有方法已经被验证，并且没有其他未验证的方法调用
Mockito.verifyNoMoreInteractions(userServiceMock);

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;doThrow(exception).when(mock).method()&lt;/h5&gt;
&lt;p&gt;指定当调用模拟对象的方法时应该抛出的异常。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 指定当调用 deleteUser 方法并传入任何参数时，抛出一个名为&quot;UserNotFoundException&quot;的异常
Mockito.doThrow(new UserNotFoundException()).when(userServiceMock).deleteUser(Mockito.anyInt());

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;doAnswer(answer).when(mock).method()&lt;/h5&gt;
&lt;p&gt;指定模拟对象方法的调用应该如何进行自定义处理，例如执行回调函数或返回动态计算的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义当调用 updateUser 方法时，执行自定义的逻辑来修改用户对象
Mockito.doAnswer(invocation -&amp;gt; {
    User userToUpdate = invocation.getArgument(0);
    // 执行自定义逻辑来更新用户对象
    userToUpdate.setName(&quot;Updated Name&quot;);
    return userToUpdate;
}).when(userServiceMock).updateUser(Mockito.any(User.class));

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;PowerMock&lt;/h3&gt;
&lt;h4&gt;导入依赖&lt;/h4&gt;
&lt;p&gt;::: code-tabs
@tab Maven#Maven&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.powermock&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;powermock-api-mockito2&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.0.9&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.powermock&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;powermock-module-junit4&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;2.0.9&amp;lt;/version&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;@tab Gradle#Gradle&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    testImplementation &apos;org.powermock:powermock-api-mockito2:2.0.9&apos;
    testImplementation &apos;org.powermock:powermock-module-junit4:2.0.9&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;测试代码&lt;/h4&gt;
&lt;p&gt;::: normal-demo 测试类代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @ClassName TestClass
 * @Description 测试 PowerMock 类
 */
public class TestClass {

    public String getUUID() {
        return UUID.randomUUID().toString();
    }

    public List&amp;lt;Integer&amp;gt; soutArray() {

        return new ArrayList&amp;lt;Integer&amp;gt;() {
            {
                add(2);
            }
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.sbc.unittest;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.List;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
 * @ClassName TestClassTest
 * @Description powermock 测试类
 * @Author songbaicheng
 * @Date 2023/8/14 12:09
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({TestClass.class, UUID.class})
class TestClassTest {

    @Test
    public void testGetUUID() throws Exception {

        PowerMockito.mockStatic(UUID.class);
        PowerMockito.doReturn(new UUID(0L, 0L)).when(UUID.randomUUID());

        TestClass testClass = new TestClass();
        String uuid = testClass.getUUID();

//        PowerMockito.verifyStatic(UUID.class);

        assertEquals(&quot;00000000-0000-0000-0000-000000000000&quot;, uuid);
    }

    @Test
    public void testSoutArray() {
        TestClass testClass = PowerMockito.spy(new TestClass());

        List mockIntegers = PowerMockito.mock(List.class);
        PowerMockito.doReturn(mockIntegers).when(testClass).soutArray();

        testClass.soutArray();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Mockito 还提供了其他一些高级功能和方法，例如参数匹配、顺序验证、超时验证等，如果想了解更多可以查阅下面的 Mockito的官方文档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Mockito 官网文档
desc: 点击跳转官网查看详细内容
logo: /assets/images/work-task/development/mockito/mockito.png
link: https://site.mockito.org/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Typescript</title><link>https://songbaicheng.cc.cd/posts/typescript/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/typescript/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;TypeScript + ES6+&lt;/h1&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;TypeScript 与 JavaScript 有着不同寻常的关系。TypeScript 提供了 JavaScript 的所有功能，并在这些功能之上添加了一层：TypeScript 的类型系统,所以很多人都说 TS 是 JS 的超集。更多的细节详见官网，话不多说，我们直接开始准备工作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 typescript-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;title: TypeScript 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/basic/tyepscript/typescript.svg
link: https://www.typescriptlang.org/zh/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 TypeScript&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;npm i typescript -g
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/frontend/basic/tyepscript/install-ts.png&quot; alt=&quot;安装 TypeScript&quot; title=&quot;安装 TypeScript&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;编译 TS 文件&lt;/h3&gt;
&lt;p&gt;练习开始之前，我们要知道浏览器是不认识ts文件的，这里我们有两种方式查看：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;比较原始的方式，我们在用ts文件保存之后，先在项目路径下执行&lt;code&gt;tsc --init&lt;/code&gt;，然后使用&lt;code&gt;tsc -w&lt;/code&gt;将打开一个 ts 文件编译成浏览器可读的 js 文件的监视器，然后在这个监视器启动的情况下就能把文件中的 ts 文件实时的转化成 js 文件，这样就可以用&lt;code&gt;node [file.js]&lt;/code&gt;查看自己写的结果了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;个人比较推荐的一种方式，首先先全局安装 ts-node 这个包，然后在项目中再增加 @types/node，现在我们就可以直接使用&lt;code&gt;ts-node [file.ts]&lt;/code&gt;查看结果了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm i ts-node -g

npm i @types/node -D
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;命名空间&lt;/h3&gt;
&lt;p&gt;TS 为了防止全局变量的污染，默认包含 import 或 export 的文件当做一个模块，如果不添加这两个关键字，则内容会视为全局可见，会造成一些命名的冲突，所以 TS 通过命名空间的方式可以将变量包裹成一个对象来应对这种冲突，但是日常开发还是推荐使用 ES6 的模块化的写法，不推荐使用命名空间。&lt;/p&gt;
&lt;h2&gt;基本类型&lt;/h2&gt;
&lt;p&gt;在说基本类型之前想说一下类型推论这个概念，虽然ts在js的基础上创建了很多类型，但并不需要每次声明都携带类型，ts可以自动根据你的初始变量推断出你声明的类型，在之后如果赋值错误类型会提示类型错误，如果未指定初始化变量则ts默认推断为any类型。当然类型也是有等级的，高级的类型包含低级的类型：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/frontend/basic/tyepscript/ts-type.jpg&quot; alt=&quot;类型包含关系&quot; title=&quot;类型包含关系&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;数字类型&lt;/h3&gt;
&lt;p&gt;双精度 64 位浮点值。它可以用来表示整数和分数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let notANumber: number = NaN; // Nan
let num: number = 123; // 普通数字
let infinityNumber: number = Infinity; // 无穷大
let decimal: number = 6; // 十进制
let hex: number = 0xf00d; // 十六进制
let binary: number = 0b1010; // 二进制
let octal: number = 0o744; // 八进制
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符类型&lt;/h3&gt;
&lt;p&gt;一个字符系列，使用单引号（&apos;）或双引号（&quot;）来表示字符串类型。单引号（&apos;）可以内嵌表达式，反引号（`）来定义多行文本和内嵌表达式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let str: string = &apos;songbaicheng&apos;
let str1: string = `i
am
${str}`

console.log(str) // songbaicheng
console.log(str1)
/**
 * i
 * am
 * songbaicheng
 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;布尔类型&lt;/h3&gt;
&lt;p&gt;表示逻辑值：true 和 false。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let booleand1: boolean = true
let booleand2: boolean = Boolean(1)

console.log(booleand1) // true
console.log(booleand2) // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数组类型&lt;/h3&gt;
&lt;p&gt;数组中如果是any类型的可以用元组来代替。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在元素类型后面加上[]
let arr: number[] = [1, 2];
// 或者使用数组泛型
let arr1: Array&amp;lt;number&amp;gt; = [1, 2];

console.log(arr) // [ 1, 2 ]
console.log(arr1) // [ 1, 2 ]

// 多维数组
let arr2: number[][] = [[1], [2]]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;元组&lt;/h3&gt;
&lt;p&gt;元组类型用来表示已知元素数量和类型的数组，各元素的类型不必相同，对应位置的类型需要相同。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let x: [string, number];
x = [&apos;songbaicheng&apos;, 1];

console.log(x[0]) // songbaicheng
console.log(x[1]) // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;枚举&lt;/h3&gt;
&lt;p&gt;枚举类型用于定义数值集合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Color {Red, Green, Blue};
let c: Color = Color.Blue;

console.log(Color.Blue) // 2
console.log(c) // 2
console.log(Color[Color.Blue]) // Blue
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;void&lt;/h3&gt;
&lt;p&gt;用于标识方法返回值的类型，表示该方法没有返回值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function voidFn(): void {
    console.log(&apos;test void&apos;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;null&lt;/h3&gt;
&lt;p&gt;表示对象值缺失。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let n: null = null;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;undefined&lt;/h3&gt;
&lt;p&gt;用于初始化变量为一个未定义的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let u: undefined = undefined;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;never&lt;/h3&gt;
&lt;p&gt;never 是其它类型（包括 null 和 undefined）的子类型，代表从不会出现的值。&lt;/p&gt;
&lt;h3&gt;any &amp;amp; unknown&lt;/h3&gt;
&lt;p&gt;不明确的变量使用的一种数据类型。unknown更安全。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let arrayList: any[] = [1, false, &apos;fine&apos;];
arrayList[1] = 100;

console.log(arrayList) // [ 1, 100, &apos;fine&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Object &amp;amp; object &amp;amp; {}&lt;/h3&gt;
&lt;p&gt;Object是一切对象的父类，object是所有的引用类型，而{}相当于 new Object的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let o:Object = 123
let o1:Object = &apos;123&apos;
let o2:Object = []
let o3:Object = {}
let o4:Object = () =&amp;gt; 123

let o5: object = {}

let n: {} = {}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;接口 interface&lt;/h2&gt;
&lt;p&gt;规定类型的属性模版。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 对象的接口
interface father {
    a: string
}

interface people extends father {
    name: string
    age: number
    occupation?: string
    [props: string]: any // 其他参数不做硬性需要
}

interface people {
    tel: number
}

let p1: people = {
    name: &apos;songbaicheng&apos;,
    age: 23,
    tel: 123456789,
    occupation: &apos;&apos;,
    local: &apos;beijing&apos;,
    a: &apos;fater&apos;
}

console.log(p1)
/**
 * {
 *  name: &apos;songbaicheng&apos;,
 *  age: 23,
 *  tel: 123456789,
 *  occupation: &apos;&apos;,
 *  local: &apos;beijing&apos;,
 *  a: &apos;fater&apos;
 * }
 */

// 函数的接口
interface Fn {
    (name: string): number[]
}

const fn: Fn = function(p: string) {
    console.log(p)
    return [1]
}

fn(&apos;songbaicheng&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 基础类型参数
function add(a: number, b: number): number {
    return a + b
}

const sum = (a: number, b: number): number {
    return a + b
}

console.log(add(1, 2))
console.log(sum(3, 2))

// 对象参数
interface body {
    name: string
}

const people = (a: body): void =&amp;gt; console.log(a.name)

people({ name: &apos;songbaicheng&apos; })

// ts可以定义函数中this的类型，js中并不支持，如果要指定必须放在参数的第一位
interface human {
    occupations: string[]
    add: (this: human, occupation: string) =&amp;gt; void
}

const zhangsan: human = {
    occupations: [&apos;teacher&apos;],
    add(this: human, occupation: string) {
        this.occupations.push(occupation)
    }
}

zhangsan.add(&apos;work&apos;)
console.log(zhangsan.occupations) // [ &apos;teacher&apos;, &apos;work&apos; ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类型&lt;/h2&gt;
&lt;h3&gt;联合类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const getTel = (tel: number | string) =&amp;gt; console.log(tel);

getTel(&apos;010-12345456&apos;) // 010-12345456
getTel(123456) // 123456
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;交叉类型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface OneType {
    first: string
}

interface TwoType {
    second: number
}

const mixType = (mix: OneType &amp;amp; TwoType) =&amp;gt; console.log(mix)

mixType({ first: &apos;songbaicheng&apos;, second: 23 }) // { first: &apos;songbaicheng&apos;, second: 23 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类型断言&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;let typeFn = (num: string | number) =&amp;gt; console.log((num as string).length)

typeFn(123) // undefined
typeFn(&apos;123&apos;) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Class类&lt;/h2&gt;
&lt;h2&gt;枚举类&lt;/h2&gt;
&lt;h3&gt;常规枚举&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum Color1 {
    RED,
    BLUE,
    YELLOW,
    GREEN
}

console.log(Color1.RED) // 0
console.log(Color1.BLUE) // 1
console.log(Color1.YELLOW) // 2
console.log(Color1.GREEN) // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递增枚举 &amp;amp; 自定义枚举&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum Color2 {
    RED = 2,
    BLUE,
    YELLOW = 6,
    GREEN
}

console.log(Color2.RED) // 2
console.log(Color2.BLUE) // 3
console.log(Color2.YELLOW) // 6
console.log(Color2.GREEN) // 7
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串枚举&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum Color3 {
    RED = &apos;red&apos;,
    BLUE = &apos;blue&apos;,
    YELLOW = &apos;yellow&apos;,
    GREEN = &apos;green&apos;
}

console.log(Color3.RED) // red
console.log(Color3.BLUE) // blue
console.log(Color3.YELLOW) // yellow
console.log(Color3.GREEN) // green
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;异构枚举&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum isRight {
    YES = 1,
    NO = &apos;no&apos;
}

console.log(isRight.YES) // 1
console.log(isRight.NO) // no
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;反向映射&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;enum Type {
    SUCCESS,
    ERROR
}

let value = Type.SUCCESS
let key = Type[value]

console.log(`key:${key}`, `value:${value}`) // value:0 key:SUCCESS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要支持这种反向映射，对应的value值必须是number类型，string类型是不支持的，具体的实现可以看下面编译的js代码，如果为string类型，则不能定义默认的反向定义的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var Type;
(function (Type) {
    Type[Type[&quot;SUCCESS&quot;] = 0] = &quot;SUCCESS&quot;;
    Type[Type[&quot;ERROR&quot;] = 1] = &quot;ERROR&quot;;
})(Type || (Type = {}));
var value = Type.SUCCESS;
var key = Type[value];
console.log(&quot;key:&quot;.concat(key), &quot;value:&quot;.concat(value));
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Symbol&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;let key1: symbol = Symbol(1)
let key2: symbol = Symbol(1)

let obj = {
    [key1]: &apos;value&apos;,
    [key2]: &apos;value&apos;,
    key: &apos;value&apos;,
}

for (let key in obj) {
    console.log(key) // key
}

console.log(Object.keys(obj)) // [ &apos;key&apos; ]

console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(1), Symbol(1) ]

console.log(Reflect.ownKeys(obj)) // [ &apos;key&apos;, Symbol(1), Symbol(1) ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;迭代器&lt;/h3&gt;
&lt;p&gt;for of 循环就是支持存在 iterator 的结构遍历的语法糖，像Set,Map,String,Array。而for in额外支持对象的遍历，而且for in在便利数组的时候遍历的是数组的下标，而for of则是每个数组的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let set: Set&amp;lt;number&amp;gt; = new Set([1, 1, 2, 2, 3, 3, 4, 4, 5, 5])

let map: Map&amp;lt;string, number&amp;gt; = new Map()
map.set(&apos;one&apos;, 1)
map.set(&apos;two&apos;, 2)
map.set(&apos;three&apos;, 3)

let arrs = [1, 2, 3, 4, 5, 6, 7]

// 手动实现通用迭代器
const each = (col: any) =&amp;gt; {
    let iterator: any = col[Symbol.iterator]()
    let next: any = { done: false }

    while (!next.done) {
        next = iterator.next()

        if (!next.done) {
            console.log(next.value)
        }
    }
}

each(set) // 1 2 3 4 5 
each(map) // [ &apos;one&apos;, 1 ][ &apos;two&apos;, 2 ][ &apos;three&apos;, 3 ]
each(arrs) // 1 2 3 4 5 6 7

for (let key of arrs) {
    console.log(key) // 1 2 3 4 5 6 7
}

for (let key in arrs) {
    console.log(key) // 0 1 2 3 4 5 6
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;泛型&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 基础泛型
let count = &amp;lt;T&amp;gt;(a: T, b: T): T[] =&amp;gt; {
    return [a, b]
}

count(1, 2)
count(&apos;1&apos;, &apos;2&apos;)

// 默认泛型
let additon = &amp;lt;T = number&amp;gt;(a: T, b: T): T[] =&amp;gt; {
    return [a, b]
}

additon(1, 2)
additon(&apos;1&apos;, &apos;2&apos;)

// 泛型约束
let sums = &amp;lt;T extends number&amp;gt;(a: T, b: T) =&amp;gt; {
    return a + b
}

sums(1, 2)
sums(&apos;1&apos;, &apos;2&apos;) // 类型“string”的参数不能赋给类型“number”的参数

interface Len {
    length: number
}

let getLength = &amp;lt;T extends Len&amp;gt;(a: T) =&amp;gt; {
    console.log(a.length)
}

getLength(&apos;1111&apos;)
getLength([1, 2, 3, 4])
getLength(123) // 类型“number”的参数不能赋给类型“Len”的参数

let objection = {
    name: &apos;songbaicheng&apos;,
    age: 23
}

let fun = &amp;lt;T extends object, K extends keyof T&amp;gt;(obj: T, key: K): void =&amp;gt; {
    console.log(obj[key])
}

fun(obj, age)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;em&gt;tsconfig.config&lt;/em&gt; 文件&lt;/h2&gt;
&lt;p&gt;::: normal-demo tsconfig 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
      /* 项目配置 */
      &quot;incremental&quot;: true,
      &quot;composite&quot;: true,
      &quot;tsBuildInfoFile&quot;: &quot;./.tsbuildinfo&quot;,
      &quot;disableSourceOfProjectReferenceRedirect&quot;: true,
      &quot;disableSolutionSearching&quot;: true,
      &quot;disableReferencedProjectLoad&quot;: true,
      /* 语言和环境配置 */
      &quot;target&quot;: &quot;es2016&quot;,
      &quot;lib&quot;: [],
      &quot;jsx&quot;: &quot;preserve&quot;,
      &quot;experimentalDecorators&quot;: true,
      &quot;emitDecoratorMetadata&quot;: true,
      &quot;jsxFactory&quot;: &quot;&quot;,
      &quot;jsxFragmentFactory&quot;: &quot;&quot;,
      &quot;jsxImportSource&quot;: &quot;&quot;,
      &quot;reactNamespace&quot;: &quot;&quot;,
      &quot;noLib&quot;: true,
      &quot;useDefineForClassFields&quot;: true,
      &quot;moduleDetection&quot;: &quot;auto&quot;,
      /* 模块配置 */
      &quot;module&quot;: &quot;commonjs&quot;,
      &quot;rootDir&quot;: &quot;./&quot;,
      &quot;moduleResolution&quot;: &quot;node10&quot;,
      &quot;baseUrl&quot;: &quot;./&quot;,
      &quot;paths&quot;: {},
      &quot;rootDirs&quot;: [],
      &quot;typeRoots&quot;: [],
      &quot;types&quot;: [],
      &quot;allowUmdGlobalAccess&quot;: true,
      &quot;moduleSuffixes&quot;: [],
      &quot;allowImportingTsExtensions&quot;: true,
      &quot;resolvePackageJsonExports&quot;: true,
      &quot;resolvePackageJsonImports&quot;: true,
      &quot;customConditions&quot;: [],
      &quot;resolveJsonModule&quot;: true,
      &quot;allowArbitraryExtensions&quot;: true,
      &quot;noResolve&quot;: true,
      /* js支持，不推荐js和ts混合使用 */
      &quot;allowJs&quot;: true,
      &quot;checkJs&quot;: true,
      &quot;maxNodeModuleJsDepth&quot;: 1,
      /* Emit */
      &quot;declaration&quot;: true,
      &quot;declarationMap&quot;: true,
      &quot;emitDeclarationOnly&quot;: true,
      &quot;sourceMap&quot;: true,
      &quot;inlineSourceMap&quot;: true,
      &quot;outFile&quot;: &quot;./&quot;,
      &quot;outDir&quot;: &quot;./&quot;,
      &quot;removeComments&quot;: true,
      &quot;noEmit&quot;: true,
      &quot;importHelpers&quot;: true,
      &quot;importsNotUsedAsValues&quot;: &quot;remove&quot;,
      &quot;downlevelIteration&quot;: true,
      &quot;sourceRoot&quot;: &quot;&quot;,
      &quot;mapRoot&quot;: &quot;&quot;,
      &quot;inlineSources&quot;: true,
      &quot;emitBOM&quot;: true,
      &quot;newLine&quot;: &quot;crlf&quot;,
      &quot;stripInternal&quot;: true,
      &quot;noEmitHelpers&quot;: true,
      &quot;noEmitOnError&quot;: true,
      &quot;preserveConstEnums&quot;: true,
      &quot;declarationDir&quot;: &quot;./&quot;,
      &quot;preserveValueImports&quot;: true,
      /* Interop Constraints */
      &quot;isolatedModules&quot;: true,
      &quot;verbatimModuleSyntax&quot;: true,
      &quot;allowSyntheticDefaultImports&quot;: true,
      &quot;esModuleInterop&quot;: true,
      &quot;preserveSymlinks&quot;: true,
      &quot;forceConsistentCasingInFileNames&quot;: true,
      /* 类型检查 */
      &quot;strict&quot;: true,
      &quot;noImplicitAny&quot;: true,
      &quot;strictNullChecks&quot;: true,
      &quot;strictFunctionTypes&quot;: true,
      &quot;strictBindCallApply&quot;: true,
      &quot;strictPropertyInitialization&quot;: true,
      &quot;noImplicitThis&quot;: true,
      &quot;useUnknownInCatchVariables&quot;: true,
      &quot;alwaysStrict&quot;: true,
      &quot;noUnusedLocals&quot;: true,
      &quot;noUnusedParameters&quot;: true,
      &quot;exactOptionalPropertyTypes&quot;: true,
      &quot;noImplicitReturns&quot;: true,
      &quot;noFallthroughCasesInSwitch&quot;: true,
      &quot;noUncheckedIndexedAccess&quot;: true,
      &quot;noImplicitOverride&quot;: true,
      &quot;noPropertyAccessFromIndexSignature&quot;: true,
      &quot;allowUnusedLabels&quot;: true,
      &quot;allowUnreachableCode&quot;: true,
      /* Completeness */
      &quot;skipDefaultLibCheck&quot;: true,
      &quot;skipLibCheck&quot;: true
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;声明文件 &lt;em&gt;d.ts&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;这里的d指的是关键字 &lt;em&gt;declare&lt;/em&gt;，这是在使用第三方库的时候引入其声明文件使代码获得对应补全和接口提示。目前我创建Vue3+TS模版的时候，就会有很多包引用不到，这都是TS不认识.vue文件和一些变量没有用declare声明的原因。&lt;/p&gt;
&lt;h2&gt;Mixins 混入&lt;/h2&gt;
&lt;h3&gt;对象混入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;interface Name {
    name: string
}

interface Age {
    age: number
}

interface Sex {
    sex: number
}

let one: Name = { name: &apos;songbaicheng&apos; }
let two: Age = { age: 23 }
let three: Sex = { sex: 1 }

const obj = Object.assign(one, two, three)
console.log(obj) // { name: &apos;songbaicheng&apos;, age: 23, sex: 1 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类混入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;装饰器 Decorator&lt;/h2&gt;
</content:encoded></item><item><title>Uni App</title><link>https://songbaicheng.cc.cd/posts/uni-app/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/uni-app/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;uni-app&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;uni-app 是一个使用 Vue.js 开发所有前端应用的框架，开发者编写一套代码，可发布到iOS、Android、Web（响应式）、以及各种小程序（微信/支付宝/百度/字节跳动/QQ/钉钉）等多个平。&lt;/p&gt;
&lt;p&gt;这也就是他们的口号：开发一次，多端覆盖。对于想作为独立开发者来说，这无疑是非常有吸引力的，这里我借助想做一个个人小程序的契机来学习体验一下这个框架。&lt;/p&gt;
&lt;p&gt;::: card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: uni-app 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/framework/uni-app/uni.png
link: https://uniapp.dcloud.net.cn/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;uni-app 推出了自己的开发工具 HBuilderX，H是HTML的首字母，Builder是构造者，X是HBuilder的下一代版本。我们也简称HX。 HX是轻如编辑器、强如IDE的合体版本。&lt;/p&gt;
&lt;p&gt;当然如果使用 HX 可以更方便的搭建 uni-app 项目，当然如果有自己喜欢的 IDE 也可以使用命令行的方式来搭建项目，当然 HX 集成了和小程序等其他工具的联动，让开发起来更加方便。&lt;/p&gt;
</content:encoded></item><item><title>String</title><link>https://songbaicheng.cc.cd/posts/string/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/string/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;串&lt;/h1&gt;
&lt;p&gt;字符串简称串，计算机上非数值处理的对象基本都是字符串。通常用的搜索引擎、文本编辑程序、问答系统和自然语言翻译等都是以字符串作为处理对象。串是由零个或多个字符组成的有限序列，一般记为 &lt;code&gt;S = &apos;a1a2a3……an&apos;&lt;/code&gt;，其中 S 是串名，单引号内的括起来的字符序列是串的值，其中 ai 可以是字母、数字或者其他字符；串中字符的个数称为串的长度，为 0 时是空串。串中任意多个连续的字符组成的子序列称为该串的子串，包含子串的串称为主串。某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串在主串的第一个字符位置来表示，当两个串的长度相等且每个对应对应位置的元素都相等时，称这两个串是相等的。由一个或者多个空格组成的串为空格串，其长度为串中空格字符的个数。&lt;/p&gt;
&lt;h2&gt;串的存储结构&lt;/h2&gt;
&lt;h3&gt;定长顺序存储&lt;/h3&gt;
&lt;p&gt;类似于线性表的顺序存储结构，文用一组地址连续的存储单元存储串值的字符序列，因为每个元素都是定长的，所以当串的实际长度长过了 MaxSize ，超过预定义长度的串将被舍去，称为截断。&lt;/p&gt;
&lt;h3&gt;堆分配存储&lt;/h3&gt;
&lt;p&gt;堆分配存储仍然以一组地址连续的存储单元进行存放，但是存储空间是在程序执行过程中动态分配的。&lt;/p&gt;
&lt;h3&gt;块链存储&lt;/h3&gt;
&lt;p&gt;类似于线性表的链式存储结构，由于串每个元素只有一个字符，在具体的实现，每个链表结点可以存放一个字符，也可以存放多个字符，这样每个结点就可以被称为块，整个链表被称为块链结构。&lt;/p&gt;
&lt;h2&gt;串的基本操作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;StrAssign：赋值操作。&lt;/li&gt;
&lt;li&gt;StrCopy：赋值操作。&lt;/li&gt;
&lt;li&gt;StrEmpty：判空操作。&lt;/li&gt;
&lt;li&gt;StrCompare：比较操作。&lt;/li&gt;
&lt;li&gt;StrLength：求表长。&lt;/li&gt;
&lt;li&gt;SubString：求子串。&lt;/li&gt;
&lt;li&gt;Concat：串连接。&lt;/li&gt;
&lt;li&gt;Index：定位串出现的位置。&lt;/li&gt;
&lt;li&gt;ClearString：清空串。&lt;/li&gt;
&lt;li&gt;DestroyString：销毁串。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vue3</title><link>https://songbaicheng.cc.cd/posts/vue3/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/vue3/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vue3&lt;/h1&gt;
&lt;h2&gt;Vue3 和 Vue 2&lt;/h2&gt;
&lt;p&gt;问过一些从事前端的朋友，他们大部分都还在用 Vue2，倒不是因为不想用 3，主要是公司的框架都是2，如果重新改造得不偿失，但是他们自己都是已经在使用 3 开始做项目了。学习 Vue3 之前还是建议有一些 Vue2 的基础，官网给出了 3 中我们值得关注的一些新特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;组合式 API&lt;/li&gt;
&lt;li&gt;Teleport 组件&lt;/li&gt;
&lt;li&gt;Fragments 片段&lt;/li&gt;
&lt;li&gt;Emits 组件选项&lt;/li&gt;
&lt;li&gt;来自 @vue/runtime-core 的 createRenderer API 用来创建自定义渲染函数&lt;/li&gt;
&lt;li&gt;单文件组件中的状态驱动的 CSS 变量&lt;/li&gt;
&lt;li&gt;新增全局规则和针对插槽内容的规则&lt;/li&gt;
&lt;li&gt;Suspense&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;新特性还是非常多的，其中最直观也是最重要的就是 组合式API 的出现，它取代了 Vue2 的 选项式API 的风格，在灵活性和逻辑的复用性上有了很大的提升，官网也是推荐开发使用组合式 API + 单文件组件（SFC）的方式，所以我们也遵循此道来进行 Vue3 的学习。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 vue3-study-demo 中找到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;::: card&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Vue3 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/framework/vue3/vue.svg
link: https://cn.vuejs.org
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;title: Vue2 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/study/frontend/framework/vue3/vue.svg
link: https://v2.cn.vuejs.org
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;p&gt;确保在安装了最新版本的 Node.js，并且你的当前工作目录正是打算创建项目的目录下执行下面命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm init vue@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据安装指引可能根据个人不同的选项初始化出目录结构不太相同的项目，但是我们只关注 Vue 的文件，我们只关注根目录中 src 里的文件。&lt;/p&gt;
&lt;p&gt;![初始化目录](/assets/images/study/frontend/framework/vue3/init-vue3-project.png &quot;初始化目录&quot; =300x500)&lt;/p&gt;
&lt;p&gt;我们把将目光聚集在 App.vue 这个文件上，作为 Vue 的全局入口文件，我们可以先把其他扰乱视线的东西删除，把它作为一个干净的单文件组件来写第一个 demo 案例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
import { ref } from &apos;vue&apos;

const count = ref(0)

function increment() {
  count.value++
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;increment&quot;&amp;gt;
    {{ count }}
  &amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样我们就得到了一个最简单的自增按钮的界面，至于其中的setup、ref等“新面孔”在下面的学习里再缓缓道来。&lt;/p&gt;
&lt;h2&gt;三种书写风格&lt;/h2&gt;
&lt;p&gt;Vue3 支持三种书写风格，一种是延续 Vue2 的 Option API，这种可以让有 Vue2 基础的人无缝衔接 Vue3 的开发；第二种是使用 Vue3 提供的 setup 函数来实现，setup 函数中的代码会在每次组件实例被创建的时候执行，并且能直接在模版中直接使用；第三种方式则是 Vue3 提供的 setup 函数的语法糖，在 setup() 函数中手动暴露大量的状态和方法非常繁琐,我们可以通过使用单文件组件搭配 &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; 来大幅度地简化代码，这也是比较推荐和以后常用的方式。&lt;/p&gt;
&lt;h4&gt;Option API&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    {{ name }}
&amp;lt;/template&amp;gt;
    
&amp;lt;script&amp;gt;
export default {
    data() {
        return {
            name: &apos;songbaicheng&apos;
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;setup()&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    {{ name }}
&amp;lt;/template&amp;gt;
    
&amp;lt;script&amp;gt;
export default {
    setup() {
        const name = &apos;songbaicheng&apos;

        return {
            name
        }
    }
}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  {{ name }}
&amp;lt;/template&amp;gt;
  
&amp;lt;script setup&amp;gt;
const name = &apos;songbaicheng&apos;
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;指令&lt;/h2&gt;
&lt;h4&gt;v-text&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
let context = &apos;my name is songbaicheng&apos;
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div v-text=&quot;context&quot; /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-html&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup&amp;gt;
let context = &apos;&amp;lt;h1 style=&quot;font-weight: bold&quot;&amp;gt;my name is songbaicheng&amp;lt;/h1&amp;gt;&apos;
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div v-html=&quot;context&quot; /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-if&lt;/h4&gt;
&lt;p&gt;v-else-if 和 v-else 的上一个兄弟元素必须有 v-if 或 v-else-if，而且 v-else 无需传入表达式。如果是 false Vue 会把你的标签注释掉达到隐藏的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
let typeFlag: string = &apos;A&apos;
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div v-if=&quot;typeFlag === &apos;A&apos;&quot;&amp;gt;
        A
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-else-if=&quot;typeFlag === &apos;B&apos;&quot;&amp;gt;
        B
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-else-if=&quot;typeFlag === &apos;C&apos;&quot;&amp;gt;
        C
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-else&amp;gt;
        Not A/B/C
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-show&lt;/h4&gt;
&lt;p&gt;相比于 v-if，如果是 false Vue 会把标签增加&lt;code&gt;display: none;&lt;/code&gt;样式，效率更高。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
let trueFlag: boolean = true
let falseFlag: boolean = false
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;span v-show=&quot;trueFlag&quot;&amp;gt;
        事了拂衣去
    &amp;lt;/span&amp;gt;
    &amp;lt;span v-show=&quot;falseFlag&quot;&amp;gt;
        深藏功与名
    &amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-on&lt;/h4&gt;
&lt;p&gt;一般平时我都是使用 @ 符号代替 v-on 来简写。v-on也提供很多方法，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;once： 只点击一次&lt;/li&gt;
&lt;li&gt;stop： 阻止事件冒泡&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
let sout = () =&amp;gt; {
    console.log(&apos;我是父级！&apos;)
}
let click = () =&amp;gt; {
    alert(&apos;你好！&apos;)
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div @:click=&quot;sout&quot;&amp;gt;
        &amp;lt;button @:click.stop=&quot;click&quot;&amp;gt;欢迎光临&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-bind&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
let style = { color: &apos;red&apos; }
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
&amp;lt;div :style=&quot;style&quot;&amp;gt;
    bind绑定样式
&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-model&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from &apos;vue&apos;

let name = ref(&apos;songbaicheng&apos;)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
&amp;lt;input v-model=&quot;name&quot; type=&quot;text&quot; /&amp;gt;
&amp;lt;span&amp;gt;{{ name }}&amp;lt;/span&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;v-for&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
let arr: string[] = [&apos;one&apos;, &apos;two&apos;, &apos;three&apos;, &apos;four&apos;]
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
&amp;lt;div :key=&quot;index&quot; v-for=&quot;(e, index) in arr&quot;&amp;gt;
    {{ index }}-&amp;gt;{{ e }}
&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;虚拟 Dom 和 Dom diff 算法&lt;/h2&gt;
&lt;p&gt;Vue.js 使用虚拟 DOM 实现了高效的页面更新。当数据发生改变时，Vue.js 会先比对新旧虚拟 DOM 树的差异，然后只会更新实际变化的部分。虚拟 Dom 当然对标的是真实 Dom，它是一个能代表 Dom 树的对象，通常含有标签名、标签上的属性、事件监听和子元素等其他属性。虚拟 Dom 的优点是可以减少 Dom 操作，所以是在一些情况下比操作真实 Dom 快的，而且因为虚拟 Dom 本质上是一个JS对象，所以虚拟 Dom 也是支持跨平台。&lt;/p&gt;
&lt;p&gt;Dom diff 是虚拟 Dom 的对比算法，我们大概说一下其比较逻辑，首先diff算法有三种比较：tree diff、component diff 和 element diff。Tree diff 要做的就是新旧两棵 Dom 树比较找出不同的节点，而其中的节点比较就是交给其他两个 diff，如果节点是组件则进行 component diff，如果节点是标签则进行 element diff。component diff首先看组件类型，类型不同直接替换旧类型，类型相同则对比替换属性，之后再递归走组件内的节点做 tree diff。而 element diff 先比较标签名，如果是不同直接替换旧标签，如果相同则更新属性，之后也是再递归走子节点的 tree diff。这样比较下来很显然可以减少操作 Dom 的次数。&lt;/p&gt;
&lt;p&gt;不过在这些比较过程中，:key 有什么作用呢？如果我们不声明 key，我们将 Dom 看成是一棵虚拟的树，如果我们删除了一个左子节点，我们以为的是这棵树的右子节点会变成左子节点，但是计算机会认为是我们修改了左子节点，删除了右子节点，所以我们为了让计算机知道我们删除的究竟是哪个节点，我们要给每个节点绑定唯一的key标记，这样就避免了误判的情况发生。&lt;/p&gt;
&lt;h2&gt;响应式&lt;/h2&gt;
&lt;p&gt;在组合式 API 中，推荐使用 ref() 函数来声明响应式状态，在标准的 JavaScript 中，检测普通变量的访问或修改是行不通的。但是我们可以拦截属性的 get 和 set 操作，从概念上讲，.value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改，在其内部，Vue 在它的 getter 中执行追踪，在它的 setter 中执行触发。&lt;/p&gt;
&lt;p&gt;当你在模板中使用了一个 ref，然后改变了这个 ref 的值时，Vue 会自动检测到这个变化，并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时，Vue 会追踪在渲染过程中使用的每一个 ref。然后，当一个 ref 被修改时，它会触发追踪它的组件的重新渲染。&lt;/p&gt;
&lt;p&gt;另一个 ref 的好处是，与普通变量不同，你可以将 ref 传递给函数，同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时，这将非常有用。&lt;/p&gt;
&lt;p&gt;值得注意的是，和 Vue2 相比的双向绑定不同的是，ref 响应式的对象会多出一层 .value 来调用其属性，并不可以直接获取属性，而 reactive 响应式并不需要 .value 去获取属性和元素。&lt;/p&gt;
&lt;h4&gt;ref&lt;/h4&gt;
&lt;p&gt;常见的有三种 ref：ref、shallowRef、triggerRef。ref 作为深层响应式，包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构，改变嵌套对象或数组时，变化也会被检测到；与之相对的就是 shallowRef 浅层响应式，它只能检测到 .value 下的变化，如果在深层的改变则不会检测。要注意 ref 和 shallowRef 不能同时使用，因为它俩的直接区别就是 ref 的底层会调用 triggerRef 强制更新收集依赖，这样会导致一些 shallowRef 本不该响应变成响应的。&lt;/p&gt;
&lt;p&gt;::: normal-demo ref 三种实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, shallowRef, triggerRef } from &apos;vue&apos;

// 非响应式对象
let people = { name: &apos;songbaicheng&apos; }
let change = () =&amp;gt; {
    people.name = &apos;baicheng&apos;
    console.log(people.name)
}

// 深层响应式对象，可以循环影响到最底层
let animal = ref({ name: &apos;bird&apos; })
let update = () =&amp;gt; {
    animal.value.name = &apos;cat&apos;
    console.log(animal.value.name)
}

// 浅层响应式对象，只能影响到 .value
let animal1 = shallowRef({ name: &apos;dog&apos; })
let update1 = () =&amp;gt; {
    animal1.value = {
        name: &apos;fish&apos;
    }
    console.log(animal1.value.name)
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
非响应式：{{ people.name }} &amp;lt;button @click=&quot;change&quot;&amp;gt;点我更换&amp;lt;/button&amp;gt;
&amp;lt;br&amp;gt;
深层响应式：{{ animal.name }} &amp;lt;button @click=&quot;update&quot;&amp;gt;点我更换&amp;lt;/button&amp;gt;
&amp;lt;br&amp;gt;
浅层响应式：{{ animal1.name }} &amp;lt;button @click=&quot;update1&quot;&amp;gt;点我更换&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;reactive&lt;/h4&gt;
&lt;p&gt;与 ref 不同的是，ref 可以接受所有类型的参数，而 reactive 被泛型约束只能接收引用类型的参数，如 Object、Array、Map、Set等，并且 reavtive 的底层是用代理去拦截对响应式对象所有属性的访问和修改，以便进行依赖追踪和触发更新，所以不能直接对对象进行赋值，否则会破坏响应式对象。&lt;/p&gt;
&lt;p&gt;::: normal-demo reactive 用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { reactive } from &apos;vue&apos;

let form = reactive({
    name: &apos;songbaicheng&apos;,
    age: 23
})

let submit = () =&amp;gt; {
    console.log(form)
}

&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
&amp;lt;form&amp;gt;
    &amp;lt;input type=&quot;text&quot; v-model=&quot;form.name&quot;&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;input type=&quot;text&quot; v-model=&quot;form.age&quot;&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;button @click.prevent=&quot;submit&quot;&amp;gt;提交&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;toRef&lt;/h4&gt;
&lt;p&gt;::: normal-demo toRef 用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, toRef, toRefs } from &apos;vue&apos;

let people = ref({
    name: &apos;songbaicheng&apos;,
    age: 23,
    tel: 123456
})

let peopleTel = toRef(people) // 只对响应式对象做修改

let change = () =&amp;gt; {
    peopleTel.value.tel = 8888
    console.log(peopleTel)
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
{{ people }}

&amp;lt;button @click=&quot;change&quot;&amp;gt;改变&amp;lt;/button&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;计算属性&lt;/h2&gt;
&lt;p&gt;有函数式写法和选项式写法两种。如果只是仅仅获取结果可以使用函数式的写法，computed() 方法期望接收一个 getter 函数，返回值为一个计算属性 ref，计算属性 ref 也会在模板中自动解包，因此在模板表达式中引用时无需添加 .value；如果你需要用到“可写”的属性，你可以通过同时提供 getter 和 setter 的选项式写法来创建。
::: normal-demo 计算属性的用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, computed } from &apos;vue&apos;

let firstName = ref(&apos;&apos;)
let lastName = ref(&apos;&apos;)

// 全名计算属性
let name = computed(() =&amp;gt; {
    return firstName.value + &apos;-&apos; + lastName.value
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;span class=&quot;font-style&quot;&amp;gt;姓：&amp;lt;/span&amp;gt;
            &amp;lt;input v-model=&quot;firstName&quot; type=&quot;text&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;span class=&quot;font-style&quot;&amp;gt;名：&amp;lt;/span&amp;gt;
            &amp;lt;input v-model=&quot;lastName&quot; type=&quot;text&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;span class=&quot;font-style&quot;&amp;gt;全名：&amp;lt;/span&amp;gt;
        &amp;lt;span class=&quot;front-style bold-font-style&quot;&amp;gt;{{ name }}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style&amp;gt;
.font-style {
    font-style: italic;
}

.bold-font-style {
    font-weight: bold;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, computed } from &apos;vue&apos;

let firstName = ref(&apos;&apos;)
let lastName = ref(&apos;&apos;)

// 全名计算属性
let name = computed&amp;lt;string&amp;gt;({

    get() {
        return firstName.value + &apos;-&apos; + lastName.value
    },
    set(name: string) {
        [firstName.value, lastName.value] = name.split(&apos;-&apos;)
    }
})

let changeName = () =&amp;gt; {
    name.value = &apos;song-baicheng&apos;
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;span class=&quot;font-style&quot;&amp;gt;姓：&amp;lt;/span&amp;gt;
            &amp;lt;input v-model=&quot;firstName&quot; type=&quot;text&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;span class=&quot;font-style&quot;&amp;gt;名：&amp;lt;/span&amp;gt;
            &amp;lt;input v-model=&quot;lastName&quot; type=&quot;text&quot;&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;span class=&quot;font-style&quot;&amp;gt;全名：&amp;lt;/span&amp;gt;
        &amp;lt;span class=&quot;front-style bold-font-style&quot;&amp;gt;{{ name }}&amp;lt;/span&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;button @click=&quot;changeName&quot;&amp;gt;更换姓名&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;style&amp;gt;
.font-style {
    font-style: italic;
}

.bold-font-style {
    font-weight: bold;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;监听器&lt;/h2&gt;
&lt;p&gt;计算属性允许我们声明性地计算衍生值。然而在有些情况下，我们需要在状态变化时执行一些异步操作的结果去修改另一处的状态，在组合式 API 中，我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。&lt;/p&gt;
&lt;p&gt;当我们使用 &lt;code&gt;immediate: true&lt;/code&gt; 时可以用 watchEffect代替，它自动跟踪回调的响应式依赖，对于有多个依赖项的侦听器来说，使用 watchEffect() 可以消除手动维护依赖列表的负担。此外，如果你需要侦听一个嵌套数据结构中的几个属性，watchEffect() 可能会比深度侦听器更有效，因为它将只跟踪回调中被使用到的属性，而不是递归地跟踪所有的属性。&lt;/p&gt;
&lt;p&gt;::: normal-demo 监听器的用法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, watch } from &apos;vue&apos;

let message = ref&amp;lt;string&amp;gt;(&apos;songbaicheng&apos;)
let obj = ref&amp;lt;Object&amp;gt;({
    name: &apos;songbaicheng&apos;,
    age: 23
})

watch([message, obj], (newValue, oldValue) =&amp;gt; {
    console.log(&apos;newValue:&apos;, newValue)
    console.log(&apos;oldValue:&apos;, oldValue)
}, {
    deep: true, // 是否开始深度监听
    immediate: true, // 立即执行一次
    flush: &apos;pre&apos; // pre：组件更新前执行，sync：同步执行，post：组件更新后执行
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;input type=&apos;text&apos; v-model=&quot;message&quot;&amp;gt;
    &amp;lt;br&amp;gt;
    &amp;lt;input type=&apos;text&apos; v-model=&quot;obj.age&quot;&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, watchEffect } from &apos;vue&apos;

let message = ref&amp;lt;string&amp;gt;(&apos;new songbaicheng&apos;)

watchEffect(() =&amp;gt; {
    console.log(message.value)
})
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;input type=&apos;text&apos; v-model=&quot;message&quot;&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;生命周期&lt;/h2&gt;
&lt;p&gt;每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤，比如设置好数据侦听，编译模板，挂载实例到 DOM，以及在数据改变时更新 DOM。在此过程中，它也会运行被称为生命周期钩子的函数，让开发者有机会在特定阶段运行自己的代码。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;生命周期钩子&lt;/th&gt;
&lt;th&gt;执行时间&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;onMounted&lt;/td&gt;
&lt;td&gt;在组件挂载完成后执行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onUpdated&lt;/td&gt;
&lt;td&gt;在组件因为响应式状态变更而更新其 DOM 树之后调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onUnmounted&lt;/td&gt;
&lt;td&gt;在组件实例被卸载之后调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onBeforeMount&lt;/td&gt;
&lt;td&gt;在组件被挂载之前被调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onBeforeUpdate&lt;/td&gt;
&lt;td&gt;在组件即将因为响应式状态变更而更新其 DOM 树之前调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onBeforeUnmount&lt;/td&gt;
&lt;td&gt;在组件实例被卸载之前调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onErrorCaptured&lt;/td&gt;
&lt;td&gt;在捕获了后代组件传递的错误时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onRenderTracked&lt;/td&gt;
&lt;td&gt;当组件渲染过程中追踪到响应式依赖时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onRenderTriggered&lt;/td&gt;
&lt;td&gt;当响应式依赖的变更触发了组件渲染时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onRenderTriggered&lt;/td&gt;
&lt;td&gt;当组件被插入到 DOM 中时调用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onActivated&lt;/td&gt;
&lt;td&gt;若组件实例是缓存树的一部分，当组件被插入到 DOM 中时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onDeactivated&lt;/td&gt;
&lt;td&gt;若组件实例是缓存树的一部分，当组件从 DOM 中被移除时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;onServerPrefetch&lt;/td&gt;
&lt;td&gt;若组件实例是缓存树的一部分，当组件从 DOM 中被移除时调用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;父子模块传值&lt;/h2&gt;
&lt;p&gt;父子文件交互主要有下几个函数：defineProps()、defineEmits()、defineExpose()、defineOptions()和defineSlots()&lt;/p&gt;
&lt;p&gt;::: normal-demo 父子组件交互&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import Child from &apos;./Child.vue&apos;
import { ref } from &apos;vue&apos;

let name = &apos;sbc&apos;

const getName = (name: string) =&amp;gt; {
    console.log(name)
}

const childElement = ref&amp;lt;InstanceType&amp;lt;typeof child&amp;gt;&amp;gt;()
console.log(childElement.value.name)
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    parent
    &amp;lt;br&amp;gt;
    &amp;lt;Child ref=&quot;child&quot; @on-click=&quot;getName&quot; :title=&quot;name&quot; /&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
    child
    {{ title }}
    {{ arr }}
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
// 接收父组件传值
withDefaults(defineProps&amp;lt;{
    title: string,
    arr: number[]
}&amp;gt;(), { // 默认值
    arr: () =&amp;gt; [&apos;songbaicheng&apos;]
})

// 向父组件传值
const emit = defineEmits&amp;lt;{
    (e:&quot;on-click&quot;, name:string):void
}&amp;gt;()

const send = () =&amp;gt; {
    emit(&apos;on-click&apos;, &apos;songbaicheng&apos;)
}

// 向父组件暴露值
defineExpose({
    value: &apos;songbaicheng&apos;
})
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;动态组件&lt;/h2&gt;
&lt;p&gt;::: normal-demo Vue3 动态组件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;el-config-provider namespace=&quot;ep&quot;&amp;gt;
    &amp;lt;span&amp;gt;动态组件&amp;lt;/span&amp;gt;
    &amp;lt;div style=&quot;display: flex&quot;&amp;gt;
      &amp;lt;div @click=&quot;switchCom(item, index)&quot; :class=&quot;[active == index ? &apos;active&apos; : &apos;&apos;]&quot; class=&quot;tabs&quot;
        v-for=&quot;(item, index) in components&quot;&amp;gt;
        &amp;lt;div&amp;gt;{{ item.name }}&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;component :is=&quot;comId&quot;&amp;gt;&amp;lt;/component&amp;gt;
  &amp;lt;/el-config-provider&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref, shallowRef, markRaw } from &apos;vue&apos;
// 测试动态组件
import A from &apos;~/components/A.vue&apos;
import B from &apos;~/components/B.vue&apos;
import C from &apos;~/components/C.vue&apos;


const components = ref([
  {
    name: &apos;A组件&apos;,
    com: markRaw(A)
  },
  {
    name: &apos;B组件&apos;,
    com: markRaw(B)
  },
  {
    name: &apos;C组件&apos;,
    com: markRaw(C)
  }
])

const comId = shallowRef(A) // 只代理最外层元素
const active = ref(0)

// 切换展示组件
const switchCom = (item, index) =&amp;gt; {
  comId.value = item.com
  active.value = index
}
&amp;lt;/script&amp;gt;

&amp;lt;style scoped&amp;gt;
span {
  font-weight: bold;
  font-style: italic;
}

.tabs {
  border: 1px solid;
  padding: 5px 10px;
  margin: 5px;
  /* 悬浮小手 */
  cursor: pointer;
}

.active {
  background-color: burlywood;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;过度和动画&lt;/h2&gt;
&lt;p&gt;Transition&lt;/p&gt;
&lt;h2&gt;依赖注入&lt;/h2&gt;
&lt;p&gt;provide/inject&lt;/p&gt;
&lt;h2&gt;组件通信&lt;/h2&gt;
&lt;p&gt;全局Bus&lt;/p&gt;
&lt;h2&gt;自定义指令&lt;/h2&gt;
&lt;p&gt;Directives&lt;/p&gt;
&lt;h2&gt;全局变量和方法&lt;/h2&gt;
&lt;p&gt;globalProperties&lt;/p&gt;
&lt;h2&gt;第三方UI&lt;/h2&gt;
&lt;p&gt;pc：Element UI、AntDesign、ViewDesign
移动端：vant&lt;/p&gt;
</content:encoded></item><item><title>Web Service</title><link>https://songbaicheng.cc.cd/posts/web-service/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/web-service/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Web Service&lt;/h1&gt;
&lt;h2&gt;浅聊 Web Service&lt;/h2&gt;
&lt;p&gt;刚来公司的时候有些项目请求并不是用 Postman 发送 Http 请求，而是用 SoapUI 的工具发送 XML 请求，当时面对那个老旧版本的工具和我个人非常嫌弃的 XML 我直接从头到脚就是抵触，后来我才知道这个东西叫 Web Service。现在用 Web Service 的还多吗，当然是不多，Apache 官网维护的 Web Service 最后一次更新还是在2015年10月，传输 XMl 速度也慢，只能说现在肯定是基于 Http 的 Json 的天下。&lt;/p&gt;
&lt;p&gt;那 Web Service 到底是干什么的呢？它是一种跨编程语言和操作系统平台的远程调用技术，能使得运行在不同机器上的不同应用无须借助附加的、专门的第三方软件或硬件就可相互交换数据或集成。仔细想想只要能暴露接口，用 Json 不是更香，而且实现 Web Service 还得适配 SOAP 协议，实用性就更低了。&lt;/p&gt;
&lt;h2&gt;Web Service 三要素&lt;/h2&gt;
&lt;p&gt;从表面看，Web Service 就是一个应用程序向外界暴露出一个能通过Web进行调用的API，也就是说能用编程的方法通过 Web 来调用这个应用程序。我们把调用的应用程序叫做客户端，而把提供的应用程序叫做服务端。Web Service 不是一种技术，更像是建立在可互操作的分布式应用程序的新平台，是一个平台，是一套标准，是一种规范。它定义了应用程序如何在 Web 上实现互操作性，而实现这项技术离不开下面介绍的 UDDI，WSDL，SOAP 这三个元素：&lt;/p&gt;
&lt;h3&gt;UDDI&lt;/h3&gt;
&lt;p&gt;一种用于描述、发现、集成 Web Service 的技术，它是 Web Service 协议栈的一个重要部分。通过UDDI，企业可以根据自己的需要动态查找并使用Web服务，也可以将自己的Web服务动态地发布到UDDI注册中心，供其他用户使用。&lt;/p&gt;
&lt;h3&gt;WSDL&lt;/h3&gt;
&lt;p&gt;为了描述 Web 服务发布的XML格式。就是用机器能阅读的方式提供的一个正式描述文档而基于XML（标准通用标记语言下的一个子集）的语言，用于描述 Web Service 及其函数、参数和返回值。&lt;/p&gt;
&lt;h3&gt;SOAP&lt;/h3&gt;
&lt;p&gt;简单对象访问协议，是交换数据的一种协议规范，是一种轻量的、简单的、基于XML标准通用（标记语言下的一个子集）的协议。主要组成由 Http 协议和 XML 数据格式。Web Service 通过 HTTP 协议发送请求和接收结果时，发送的请求内容和结果内容都采用 XML 格式封装，并增加了一些特定的 HTTP 消息头，以说明 HTTP 消息的内容格式，这些特定的 HTTP 消息头和 XML 内容格式就是 SOAP 协议。SOAP 提供了标准的RPC(远程调用技术)方法来调用Web Service。&lt;/p&gt;
&lt;p&gt;了解了这三个元素是什么我们就可以知道 Web Service 主要是通过 SOAP 协议在 Web 上提供的软件服务，使用WSDL 文档进行描述说明服务，并通过 UDDI 进行注册服务供客户端调用。&lt;/p&gt;
&lt;h2&gt;Web Service 规范&lt;/h2&gt;
&lt;p&gt;目前有三种规范：JAX-WS（Java API for XML-Based Web Service）、JAXM（Java API for XML Message）、JAX-RS（RESTful 风格）。&lt;/p&gt;
&lt;p&gt;JAX-WS 是用于构建 SOAP 风格的 Web 服务，而 JAX-RS 是用于构建 RESTful 风格的 Web 服务。在这里还是选择前者进行展开，毕竟使用 RESTful 风格的项目较少，因为如果使用 RESTful 风格就都加入 Json 的怀抱了。&lt;/p&gt;
&lt;p&gt;在服务端，用户只需要通过 Java 语言定义远程调用所需要实现的接口（SEI：Service EndPoit Interface），并对其提供相关的实现，通过调用 JAX-WS 的服务来发布接口就可以发布为Web Service 接口。&lt;/p&gt;
&lt;p&gt;在客户端，用户可以通过 JAX-WS 的 API 来创建一个代理来（用本地代理对象来替代远程的服务对象）实现远程服务端的调用。（在使用 JAX-WS 生成远程服务端的代理可以使用 JDK 自带的 wsimport命令来自动生成）。&lt;/p&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的测试代码都在博客&lt;a href=&quot;/README.md&quot;&gt;首页&lt;/a&gt;中的 java-study-demo 中找到。
注：使用 JDK 17 时发现注解已经被弃用，下面代码建议使用 JDK 8。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. 创建 Web 服务&lt;/h3&gt;
&lt;p&gt;::: normal-demo Web Service 服务端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @ClassName MyCalculator
 * @Description 简单的加法计算器
 */
@WebService
public class MyCalculator {

    @WebMethod
    public int add(int a, int b) {
        return a + b;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @ClassName WebServiceSeverMain
 * @Description Web Service 服务主函数
 */
public class WebServiceSeverMain {

    public static void main(String[] args) {

        // 定义Web服务的地址
        String url = &quot;http://localhost:8080/calculator&quot;;

        // 创建Calculator对象
        MyCalculator calculator = new MyCalculator();

        // 发布Web服务
        Endpoint.publish(url, calculator);

        System.out.println(&quot;Web服务已发布，访问地址: &quot; + url + &quot;?wsdl&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;2. 访问 wsdl 文件&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/work-task/development/web-service/wsdl.png&quot; alt=&quot;wsdl文件&quot; title=&quot;wsdl文件&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 生成客户端代码&lt;/h3&gt;
&lt;h4&gt;使用 JDK 自带的 wsimport 命令&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;wsimport -s /todir bin http://localhost:8080/calculator?wsdl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/work-task/development/web-service/wsimport.png&quot; alt=&quot;wsimport用法&quot; title=&quot;wsimport用法&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/work-task/development/web-service/wsdl-generated.png&quot; alt=&quot;生成客户端结构&quot; title=&quot;生成客户端结构&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;IDE 工具生成客户端&lt;/h4&gt;
&lt;p&gt;一般 IDE 都带有解析 wsdl 文件的 Tools，像 IntelliJ IDEA 、Eclipse。需要你把网页请求的 XML 存储为后缀为 .wsdl 的文件后解析，非常简单就不做赘述了。&lt;/p&gt;
&lt;h3&gt;4. 使用客户端&lt;/h3&gt;
&lt;p&gt;将生成的 Java 代码放到客户端项目中，然后调用代理类就可以进行数据访问了，请求时一定要保证服务端正常开启。&lt;/p&gt;
&lt;p&gt;::: normal-demo Web Service 客户端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @ClassName WebServiceCLientMain
 * @Description Web Service 客户端主函数
 */
public class WebServiceCLientMain {

    public static void main(String[] args) {
        MyCalculatorService service = new MyCalculatorService();
        MyCalculator port = service.getMyCalculatorPort();

        int num1 = 10;
        int num2 = 20;
        int result = port.add(num1, num2);

        System.out.println(&quot;Result of adding &quot; + num1 + &quot; and &quot; + num2 + &quot; is: &quot; + result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Website Certificate</title><link>https://songbaicheng.cc.cd/posts/website-certificate/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/website-certificate/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;网站证书&lt;/h1&gt;
&lt;p&gt;在你想做一个正规的并且所有人都可以访问的网站的时候，再使用 ip 访问就显得没有那么方便了，这个时候就需要一个域名来访问网站。购买域名的流程在一些云服务商已经有一条龙的服务了，并且在你购买完域名就说明你已经经过了购买服务器、搭建网站、备案等流程了，而接下来的步骤就是解析 SSL 证书了。&lt;/p&gt;
&lt;h1&gt;Linux 证书安装部署&lt;/h1&gt;
&lt;h2&gt;1. 下载证书&lt;/h2&gt;
&lt;p&gt;在你购买域名的服务商界面下载系统对应的 SSL 证书，内部重要的两个文件为后缀是 key 和 pem 的文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/images/study/maintenance/website-certificate/ssl.png&quot; alt=&quot;SSL 证书文件&quot; title=&quot;SSL 证书文件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下载到服务器的指定目录下，例如 /usr/local/nginx/conf/cert 目录下。&lt;/p&gt;
&lt;h2&gt;2. 编辑 Nginx 配置文件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;server {
     #SSL 默认访问端口号为 443
     listen 443 ssl; 
     #请填写绑定证书的域名
     server_name cloud.tencent.com; 
     #请填写证书文件的相对路径或绝对路径
     ssl_certificate cert/debugking.top.pem;
     #请填写私钥文件的相对路径或绝对路径
     ssl_certificate_key  cert/debugking.top.key;
     ssl_session_timeout 5m;
     #请按照以下协议配置
     ssl_protocols TLSv1.2 TLSv1.3; 
     #请按照以下套件配置，配置加密套件，写法遵循 openssl 标准。
     ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE; 
     ssl_prefer_server_ciphers on;
     location / {
         #网站主页路径。此路径仅供参考，具体请您按照实际目录操作。
         #例如，您的网站主页在 Nginx 服务器的 /etc/www 目录下，则请修改 root 后面的 html 为 /etc/www。
         root html; 
         index  index.html index.htm;
     }
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 检查并重启 Nginx&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nginx -t
systemctl restart nginx.service
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Work Flow</title><link>https://songbaicheng.cc.cd/posts/work-flow/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/work-flow/</guid><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Activiti&lt;/h1&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;工作流（workflow）就是通过计算机对业务流程自动化执行管理，它主要结局的是多个参与者之间按照某种预定义的规则自动进行传递文档、信息或者任务的过程，从而实现某个预期的业务目标，或者促使此目标的实现。&lt;/p&gt;
&lt;p&gt;工作流基于业务流，适用于消费品行业、制造业、电信服务业、物流服务业、物业管理、政府机构等，特别是大的企业集团公司。具体应用在关键业务流程、行政审批流程、人事管理流程等。&lt;/p&gt;
&lt;p&gt;比较原始的工作流实现方式无非就是利用审核表或者增加审核字段，通过自己实现业务逻辑对一些步骤进行审批和限制，如果是比较复杂的大型业务模式就显得非常不专业了。&lt;/p&gt;
&lt;p&gt;我们这里引入一个开源的工作流引擎 Activiti，它可以将业务中复杂的业务流程抽取出来，使用专门的建模语言 BPMN 2.0 进行定义，减少业务系统因为业务改变造成的任务量，提高系统健壮性，降低系统维护成本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: Activiti 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/open-source-project/workflow/activiti.jpg
link: https://www.activiti.org/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;快速开始&lt;/h2&gt;
&lt;h3&gt;项目启动&lt;/h3&gt;
&lt;p&gt;这里我们采用 Spring Boot 2.7.6 + jdk17 + activiti 7.0.0.Beta5 版本来进行学习，这个版本是我经过多次试验才能成功使用的版本，都是血和泪啊，下面是 pom 文件供大家参考。&lt;/p&gt;
&lt;p&gt;::: normal-demo pom.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;project xmlns=&quot;http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd&quot;&amp;gt;
    &amp;lt;modelVersion&amp;gt;4.0.0&amp;lt;/modelVersion&amp;gt;
    &amp;lt;groupId&amp;gt;com.sbc&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;activiti&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;0.0.1-SNAPSHOT&amp;lt;/version&amp;gt;
    &amp;lt;name&amp;gt;activiti&amp;lt;/name&amp;gt;
    &amp;lt;description&amp;gt;Demo project for Spring Boot to learn activiti7&amp;lt;/description&amp;gt;

    &amp;lt;properties&amp;gt;
        &amp;lt;java.version&amp;gt;17&amp;lt;/java.version&amp;gt;
        &amp;lt;project.build.sourceEncoding&amp;gt;UTF-8&amp;lt;/project.build.sourceEncoding&amp;gt;
        &amp;lt;project.reporting.outputEncoding&amp;gt;UTF-8&amp;lt;/project.reporting.outputEncoding&amp;gt;

        &amp;lt;spring-boot.version&amp;gt;2.7.6&amp;lt;/spring-boot.version&amp;gt;
        &amp;lt;activiti.version&amp;gt;7.0.0.Beta5&amp;lt;/activiti.version&amp;gt;
    &amp;lt;/properties&amp;gt;

    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-web&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
            &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-boot-starter-jdbc&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
        &amp;lt;/dependency&amp;gt;

        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.activiti&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;activiti-spring-boot-starter&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${activiti.version}&amp;lt;/version&amp;gt;
        &amp;lt;/dependency&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.activiti&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;activiti-dependencies&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${activiti.version}&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
        &amp;lt;/dependency&amp;gt;

    &amp;lt;/dependencies&amp;gt;

    &amp;lt;dependencyManagement&amp;gt;
        &amp;lt;dependencies&amp;gt;
            &amp;lt;dependency&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-dependencies&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-boot.version}&amp;lt;/version&amp;gt;
                &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
                &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
            &amp;lt;/dependency&amp;gt;
        &amp;lt;/dependencies&amp;gt;
    &amp;lt;/dependencyManagement&amp;gt;

    &amp;lt;repositories&amp;gt;
        &amp;lt;repository&amp;gt;
            &amp;lt;id&amp;gt;activiti-releases&amp;lt;/id&amp;gt;
            &amp;lt;url&amp;gt;https://artifacts.alfresco.com/nexus/content/repositories/activiti-releases&amp;lt;/url&amp;gt;
        &amp;lt;/repository&amp;gt;
    &amp;lt;/repositories&amp;gt;

    &amp;lt;build&amp;gt;
        &amp;lt;plugins&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;3.8.1&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;source&amp;gt;17&amp;lt;/source&amp;gt;
                    &amp;lt;target&amp;gt;17&amp;lt;/target&amp;gt;
                    &amp;lt;encoding&amp;gt;UTF-8&amp;lt;/encoding&amp;gt;
                &amp;lt;/configuration&amp;gt;
            &amp;lt;/plugin&amp;gt;
            &amp;lt;plugin&amp;gt;
                &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
                &amp;lt;artifactId&amp;gt;spring-boot-maven-plugin&amp;lt;/artifactId&amp;gt;
                &amp;lt;version&amp;gt;${spring-boot.version}&amp;lt;/version&amp;gt;
                &amp;lt;configuration&amp;gt;
                    &amp;lt;mainClass&amp;gt;com.sbc.activiti.ActivitiApplication&amp;lt;/mainClass&amp;gt;
                    &amp;lt;skip&amp;gt;true&amp;lt;/skip&amp;gt;
                &amp;lt;/configuration&amp;gt;
                &amp;lt;executions&amp;gt;
                    &amp;lt;execution&amp;gt;
                        &amp;lt;id&amp;gt;repackage&amp;lt;/id&amp;gt;
                        &amp;lt;goals&amp;gt;
                            &amp;lt;goal&amp;gt;repackage&amp;lt;/goal&amp;gt;
                        &amp;lt;/goals&amp;gt;
                    &amp;lt;/execution&amp;gt;
                &amp;lt;/executions&amp;gt;
            &amp;lt;/plugin&amp;gt;
        &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;

&amp;lt;/project&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;表结构&lt;/h3&gt;
&lt;p&gt;activiti 根据配置会在第一次启动的时候创建所需要的表，这些表明根据命名前缀规则分为以下几类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表分类&lt;/th&gt;
&lt;th&gt;表名&lt;/th&gt;
&lt;th&gt;表注释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_EVT_LOG&lt;/td&gt;
&lt;td&gt;日志表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_PROCDEF_INFO&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一般通用数据&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_GE_BYTEARRAY&lt;/td&gt;
&lt;td&gt;通用的流程定义和流程资源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_GE_PROPERTY&lt;/td&gt;
&lt;td&gt;系统相关属性表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流程历史记录&lt;/td&gt;
&lt;td&gt;ACT_HI_ACTINST&lt;/td&gt;
&lt;td&gt;历史的流程实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_ATTACHMENT&lt;/td&gt;
&lt;td&gt;历史的流程附件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_COMMENT&lt;/td&gt;
&lt;td&gt;历史的说明性信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_DETAIL&lt;/td&gt;
&lt;td&gt;历史的流程运行中的细节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_IDENTITYLINK&lt;/td&gt;
&lt;td&gt;历史的流程运行过程中用户关系&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_PROCINST&lt;/td&gt;
&lt;td&gt;历史的流程实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_TASKINST&lt;/td&gt;
&lt;td&gt;历史的任务信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_HI_VARINST&lt;/td&gt;
&lt;td&gt;历史的流程运行中的变量信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;流程定义表&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RE_DEPLOYMENT&lt;/td&gt;
&lt;td&gt;部署单元信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RE_MODEL&lt;/td&gt;
&lt;td&gt;模型信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RE_PROCDEF&lt;/td&gt;
&lt;td&gt;已部署的流程定义&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;运行实例表&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_DEADLETTER_JOB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_EVENT_SUBSCR&lt;/td&gt;
&lt;td&gt;运行时的事件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_EXECUTION&lt;/td&gt;
&lt;td&gt;运行时流程执行的实例&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_IDENTITYLINK&lt;/td&gt;
&lt;td&gt;运行时用户关系信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_INTEGRATION&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_JOB&lt;/td&gt;
&lt;td&gt;运行时作业&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_SUSPENDED_JOB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_TASK&lt;/td&gt;
&lt;td&gt;运行时任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_TIMER_JOB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;ACT_RU_VARIABLE&lt;/td&gt;
&lt;td&gt;运行时变量表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;流程设计器&lt;/h3&gt;
&lt;p&gt;之前较早的版本大家可能为了方便会使用 Eclipse 或者 IDEA 的插件进行 BPMN 流程图设计，但是其插件都过于老旧且一直没有更新了，而在 Activiti6 之后官方推出了一个流程设计器的 war 包工具，可以在本地进行流程图可视化操作，当然在 Activiti7 之后更是有了支持的 Docker 镜像可以使用，如果你不想下载使用的话，最新的官方推荐了一个 Activiti Modeler Application 的在线网站 BPMN.IO 的网站让我们可以在线绘制 BPMN 图，这里我们使用这种方式进行学习。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;title: BPMN.IO 官网
desc: 点击跳转官网查看详细内容
logo: /assets/images/resource/open-source-project/workflow/activiti.jpg
link: https://bpmn.io/
color: rgba(173, 216, 590, 0.15)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Simple Guides for Fuwari</title><link>https://songbaicheng.cc.cd/posts/guide/</link><guid isPermaLink="true">https://songbaicheng.cc.cd/posts/guide/</guid><description>How to use this blog template.</description><pubDate>Mon, 01 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This blog template is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. For the things that are not mentioned in this guide, you may find the answers in the &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Front-matter of Posts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The title of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The date the post was published.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short description of the post. Displayed on index page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cover image path of the post.&amp;lt;br/&amp;gt;1. Start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;: Use web image&amp;lt;br/&amp;gt;2. Start with &lt;code&gt;/&lt;/code&gt;: For image in &lt;code&gt;public&lt;/code&gt; dir&amp;lt;br/&amp;gt;3. With none of the prefixes: Relative to the markdown file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tags of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The category of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If this post is still a draft, which won&apos;t be displayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Where to Place the Post Files&lt;/h2&gt;
&lt;p&gt;Your post files should be placed in &lt;code&gt;src/content/posts/&lt;/code&gt; directory. You can also create sub-directories to better organize your posts and assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>