diff --git a/.gitignore b/.gitignore index af0556b..0e9b3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,13 @@ .DS_Store +*.log node_modules/ -/dist/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln +*.bat +*.sh +.idea/ *.py +public/ +docs/.vuepress/.temp/ +docs/.vuepress/.cache/ +ByteDanceVerify.html +google053c90e5a7354c40.html +sogousiteverification.txt \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..59cab98 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +topjavaer.cn diff --git a/Java/JVM.md b/Java/JVM.md deleted file mode 100644 index b19e730..0000000 --- a/Java/JVM.md +++ /dev/null @@ -1,831 +0,0 @@ - - -> 本文已经收录到github仓库,此仓库用于分享Java相关知识总结,包括Java基础、MySQL、Springboot、mybatis、Redis、rabbitMQ等等,欢迎大家提pr和star! -> -> github地址:https://github.com/Tyson0314/Java-learning -> -> gitee地址:https://gitee.com/tysondai/Java-learning - - - - - -- [内存结构](#%E5%86%85%E5%AD%98%E7%BB%93%E6%9E%84) - - [程序计数器](#%E7%A8%8B%E5%BA%8F%E8%AE%A1%E6%95%B0%E5%99%A8) - - [虚拟机栈](#%E8%99%9A%E6%8B%9F%E6%9C%BA%E6%A0%88) - - [本地方法栈](#%E6%9C%AC%E5%9C%B0%E6%96%B9%E6%B3%95%E6%A0%88) - - [堆](#%E5%A0%86) - - [方法区](#%E6%96%B9%E6%B3%95%E5%8C%BA) - - [永久代](#%E6%B0%B8%E4%B9%85%E4%BB%A3) - - [元空间](#%E5%85%83%E7%A9%BA%E9%97%B4) - - [运行时常量池](#%E8%BF%90%E8%A1%8C%E6%97%B6%E5%B8%B8%E9%87%8F%E6%B1%A0) - - [直接内存](#%E7%9B%B4%E6%8E%A5%E5%86%85%E5%AD%98) - - [对象的访问定位](#%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%AE%BF%E9%97%AE%E5%AE%9A%E4%BD%8D) -- [类文件结构](#%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84) -- [类的生命周期](#%E7%B1%BB%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) -- [类加载的过程](#%E7%B1%BB%E5%8A%A0%E8%BD%BD%E7%9A%84%E8%BF%87%E7%A8%8B) - - [加载](#%E5%8A%A0%E8%BD%BD) - - [验证](#%E9%AA%8C%E8%AF%81) - - [准备](#%E5%87%86%E5%A4%87) - - [解析](#%E8%A7%A3%E6%9E%90) - - [初始化](#%E5%88%9D%E5%A7%8B%E5%8C%96) -- [双亲委派模型](#%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B) - - [实现](#%E5%AE%9E%E7%8E%B0) -- [对象死亡](#%E5%AF%B9%E8%B1%A1%E6%AD%BB%E4%BA%A1) - - [引用计数法](#%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E6%B3%95) - - [可达性分析](#%E5%8F%AF%E8%BE%BE%E6%80%A7%E5%88%86%E6%9E%90) - - [可作为GC Roots的对象](#%E5%8F%AF%E4%BD%9C%E4%B8%BAgc-roots%E7%9A%84%E5%AF%B9%E8%B1%A1) - - [引用](#%E5%BC%95%E7%94%A8) - - [强引用](#%E5%BC%BA%E5%BC%95%E7%94%A8) - - [软引用](#%E8%BD%AF%E5%BC%95%E7%94%A8) - - [弱引用](#%E5%BC%B1%E5%BC%95%E7%94%A8) - - [虚引用](#%E8%99%9A%E5%BC%95%E7%94%A8) - - [常量回收](#%E5%B8%B8%E9%87%8F%E5%9B%9E%E6%94%B6) - - [类的卸载](#%E7%B1%BB%E7%9A%84%E5%8D%B8%E8%BD%BD) -- [内存分配与回收策略](#%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E4%B8%8E%E5%9B%9E%E6%94%B6%E7%AD%96%E7%95%A5) - - [Minor GC 和 Full GC](#minor-gc-%E5%92%8C-full-gc) - - [内存分配策略](#%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5) - - [对象优先在 Eden 分配](#%E5%AF%B9%E8%B1%A1%E4%BC%98%E5%85%88%E5%9C%A8-eden-%E5%88%86%E9%85%8D) - - [大对象直接进入老年代](#%E5%A4%A7%E5%AF%B9%E8%B1%A1%E7%9B%B4%E6%8E%A5%E8%BF%9B%E5%85%A5%E8%80%81%E5%B9%B4%E4%BB%A3) - - [长期存活的对象进入老年代](#%E9%95%BF%E6%9C%9F%E5%AD%98%E6%B4%BB%E7%9A%84%E5%AF%B9%E8%B1%A1%E8%BF%9B%E5%85%A5%E8%80%81%E5%B9%B4%E4%BB%A3) - - [动态对象年龄判定](#%E5%8A%A8%E6%80%81%E5%AF%B9%E8%B1%A1%E5%B9%B4%E9%BE%84%E5%88%A4%E5%AE%9A) - - [空间分配担保](#%E7%A9%BA%E9%97%B4%E5%88%86%E9%85%8D%E6%8B%85%E4%BF%9D) - - [Full GC 的触发条件](#full-gc-%E7%9A%84%E8%A7%A6%E5%8F%91%E6%9D%A1%E4%BB%B6) - - [调用 System.gc()](#%E8%B0%83%E7%94%A8-systemgc) - - [老年代空间不足](#%E8%80%81%E5%B9%B4%E4%BB%A3%E7%A9%BA%E9%97%B4%E4%B8%8D%E8%B6%B3) - - [空间分配担保失败](#%E7%A9%BA%E9%97%B4%E5%88%86%E9%85%8D%E6%8B%85%E4%BF%9D%E5%A4%B1%E8%B4%A5) -- [垃圾回收算法](#%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95) - - [标记清除算法](#%E6%A0%87%E8%AE%B0%E6%B8%85%E9%99%A4%E7%AE%97%E6%B3%95) - - [复制清除算法](#%E5%A4%8D%E5%88%B6%E6%B8%85%E9%99%A4%E7%AE%97%E6%B3%95) - - [标记整理算法](#%E6%A0%87%E8%AE%B0%E6%95%B4%E7%90%86%E7%AE%97%E6%B3%95) - - [分类收集算法](#%E5%88%86%E7%B1%BB%E6%94%B6%E9%9B%86%E7%AE%97%E6%B3%95) - - [记忆集](#%E8%AE%B0%E5%BF%86%E9%9B%86) -- [垃圾收集器](#%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8) - - [Serial 收集器](#serial-%E6%94%B6%E9%9B%86%E5%99%A8) - - [ParNew 收集器](#parnew-%E6%94%B6%E9%9B%86%E5%99%A8) - - [Parallel Scavenge 收集器](#parallel-scavenge-%E6%94%B6%E9%9B%86%E5%99%A8) - - [Serial Old 收集器](#serial-old-%E6%94%B6%E9%9B%86%E5%99%A8) - - [Parallel Old 收集器](#parallel-old-%E6%94%B6%E9%9B%86%E5%99%A8) - - [CMS 收集器](#cms-%E6%94%B6%E9%9B%86%E5%99%A8) - - [回收过程](#%E5%9B%9E%E6%94%B6%E8%BF%87%E7%A8%8B) - - [CMS垃圾回收特点](#cms%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%89%B9%E7%82%B9) - - [G1收集器](#g1%E6%94%B6%E9%9B%86%E5%99%A8) - - [回收过程](#%E5%9B%9E%E6%94%B6%E8%BF%87%E7%A8%8B-1) -- [JVM调优工具](#jvm%E8%B0%83%E4%BC%98%E5%B7%A5%E5%85%B7) - - [jps](#jps) - - [jstack](#jstack) - - [jstat](#jstat) - - [jmap](#jmap) -- [补充](#%E8%A1%A5%E5%85%85) - - [对象头](#%E5%AF%B9%E8%B1%A1%E5%A4%B4) - - [main方法执行过程](#main%E6%96%B9%E6%B3%95%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) - - [对象创建过程](#%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B) -- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - - -## 内存结构 - -Java 内存模型(JMM)是基于共享内存的多线程通信机制。 - -JVM内存结构 = 类加载器 + 执行引擎 + 运行时数据区域 。 - -![image-20210905150636105](http://img.dabin-coder.cn/image/image-20210905150636105.png) - -> 图片来源:深入理解Java虚拟机-周志明 - -### 程序计数器 - -程序计数器主要有两个作用: - -1. 当前线程所执行的字节码的行号指示器,通过改变它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 - -程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 - -### 虚拟机栈 - -Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。 - -局部变量表是用于存放方法参数和方法内的局部变量。 - -每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。 - -- 部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接 -- 部分符号引用在运行期间转化为直接引用,这种转化就是动态链接 - -Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。 - -可以通过 -Xss 参数来指定每个线程的 Java 虚拟机栈内存大小,在 JDK 1.4 中默认为 256K,而在 JDK 1.5+ 默认为 1M: - -```java -java -Xss2M -``` - -### 本地方法栈 - -虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。Native 方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。 - -本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 - -### 堆 - -此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆。 - -Java 堆可以细分为:新生代(Eden 空间、From Survivor、To Survivor 空间)和老年代。进一步划分的目的是更好地回收内存,或者更快地分配内存。 - -通过 -Xms设定程序启动时占用内存大小,通过 -Xmx 设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。 - -```java -java -Xms1M -Xmx2M -``` - -通过 -Xss 设定每个线程的堆栈大小。设置这个参数,需要评估一个线程大约需要占用多少内存,可能会有多少线程同时运行等。 - -> 在这里也给大家分享一个github仓库,上面放了**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -> -> github地址:https://github.com/Tyson0314/java-books -> -> 如果github访问不了,可以访问gitee仓库。 -> -> gitee地址:https://gitee.com/tysondai/java-books - -### 方法区 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区逻辑上属于堆的一部分。 - -对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。 - -#### 永久代 - -方法区是 JVM 的规范,而永久代(PermGen)是方法区的一种实现方式,并且只有 HotSpot 有永久代。而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。 - -#### 元空间 - -JDK 1.8 的时候,HotSpot 的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。 - -为什么要将永久代替换为元空间呢? - -永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。 - -### 运行时常量池 - -运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。 - -![](http://img.dabin-coder.cn/image/string-new.png) - -![](http://img.dabin-coder.cn/image/string-intern.png) - -![](http://img.dabin-coder.cn/image/string-equal.png) - -> 图片来源:https://blog.csdn.net/soonfly - -### 直接内存 - -直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。 - -NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。 - -直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。 - -### 对象的访问定位 - -Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种: - -- 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。 - -![](http://img.dabin-coder.cn/image/object-handle.png) - -- 直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。 - -![](http://img.dabin-coder.cn/image/direct-pointer.png) - - - -## 类文件结构 - -Class 文件结构: - -```java -ClassFile { - u4 magic; //Class 文件的标志 - u2 minor_version;//Class 的小版本号 - u2 major_version;//Class 的大版本号 - u2 constant_pool_count;//常量池的数量 - cp_info constant_pool[constant_pool_count-1];//常量池 - u2 access_flags;//Class 的访问标记 - u2 this_class;//当前类 - u2 super_class;//父类 - u2 interfaces_count;//接口 - u2 interfaces[interfaces_count];//一个类可以实现多个接口 - u2 fields_count;//Class 文件的字段属性 - field_info fields[fields_count];//一个类会可以有个字段 - u2 methods_count;//Class 文件的方法数量 - method_info methods[methods_count];//一个类可以有个多个方法 - u2 attributes_count;//此类的属性表中的属性数 - attribute_info attributes[attributes_count];//属性表集合 -} -``` - -魔数:class 文件标志。 - -文件版本:高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。 - -常量池:存放字面量和符号引用。字面量类似于Java的常量,如字符串,声明为final的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。 - -访问标志:识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。 - -当前类索引this_class:类索引用于确定这个类的全限定名。 - -属性表集合:在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。 - - - -## 类的生命周期 - -加载、验证、准备、解析、初始化、使用和卸载。 - -![](http://img.dabin-coder.cn/image/image-20210905002423703.png) - -## 类加载的过程 - -类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个对象,这个对象封装了类在方法区内的数据结构,并且提供了访问方法区内的类信息的接口。 - -### 加载 - -类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。 - -1. 通过全类名获取定义此类的二进制字节流 -2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 -3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口 - -### 验证 - -确保Class文件的字节流中包含的信息符合虚拟机规范,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。 - -### 准备 - -为类变量分配内存并设置类变量初始值的阶段。此阶段进行内存分配的仅包括类变量,不包括实例变量和final修饰的static变量(因为final在编译的时候就会分配了),实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 - -### 解析 - -虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。 - -### 初始化 - -初始化阶段就是执行类构造器\()方法的过程。\()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成的,由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。 - - - -## 双亲委派模型 - -一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求委派给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。 - -![](http://img.dabin-coder.cn/image/image-20210905002827546.png) - -双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证。 - -### 实现 - -双亲委派模型的具体实现代码在抽象类 java.lang.ClassLoader 中,此类的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。 - -```java -public abstract class ClassLoader { - // The parent class loader for delegation - private final ClassLoader parent; - - public Class loadClass(String name) throws ClassNotFoundException { - return loadClass(name, false); - } - - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // First, check if the class has already been loaded - Class c = findLoadedClass(name); - if (c == null) { - try { - if (parent != null) { - c = parent.loadClass(name, false); - } else { - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } - - if (c == null) { - // If still not found, then invoke findClass in order - // to find the class. - c = findClass(name); - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - - protected Class findClass(String name) throws ClassNotFoundException { - throw new ClassNotFoundException(name); - } -} -``` - - - -## 对象死亡 - -堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。 - -![](http://img.dabin-coder.cn/image/object-dead.png) - -### 引用计数法 - -给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。 - -这种方法很难解决对象之间相互循环引用的问题。 - -```java -public class ReferenceCountingGc { - Object instance = null; - public static void main(String[] args) { - ReferenceCountingGc objA = new ReferenceCountingGc(); - ReferenceCountingGc objB = new ReferenceCountingGc(); - objA.instance = objB; - objB.instance = objA; - objA = null; - objB = null; - } -} -``` - -### 可达性分析 - -通过GC Root对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到GC Root没有任何的引用链相连时,说明这个对象是不可用的。 - -![](http://img.dabin-coder.cn/image/gc-root-refer.png) - -#### 可作为GC Roots的对象 - -1. 虚拟机栈(栈帧中的本地变量表)中引用的对象 -2. 本地方法栈中JNI(Native方法)引用的对象 -3. 方法区中类静态属性引用的对象 -4. 方法区中常量引用的对象 -5. 所有被同步锁(synchronized关键字)持有的对象。 - -### 引用 - -引用分为强引用、软引用、弱引用、虚引用四种。 - -在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。 - -#### 强引用 - -垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 - -#### 软引用 - -如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 - -#### 弱引用 - -在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 - -#### 虚引用 - -虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。**虚引用主要用来跟踪对象被垃圾回收的活动**。 - -### 常量回收 - -运行时常量池主要回收的是废弃的常量。假如在常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。 - -### 类的卸载 - -需要同时满足下面 3 个条件才能算是 “无用的类” : - -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 -- 加载该类的 ClassLoader 已经被回收。 -- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 - -虚拟机可以对满足上述 3 个条件的无用类进行回收,但不一定被回收。 - - - -## 内存分配与回收策略 - -### Minor GC 和 Full GC - -- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 - -- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。 - -### 内存分配策略 - -#### 对象优先在 Eden 分配 - -大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。 - -#### 大对象直接进入老年代 - -大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 - -可以设置JVM参数 -XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。 - -#### 长期存活的对象进入老年代 - -通过参数 `-XX:MaxTenuringThreshold` 可以设置对象进入老年代的年龄阈值。对象在 Survivor 中每经过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。 - -#### 动态对象年龄判定 - -虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 - -#### 空间分配担保 - -在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。 - -### Full GC 的触发条件 - -对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下条件: - -#### 调用 System.gc() - -只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 - -#### 老年代空间不足 - -老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 - -#### 空间分配担保失败 - -使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 - - - -## 垃圾回收算法 - -### 标记清除算法 - -标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收所有被标记的对象。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。 - -![](http://img.dabin-coder.cn/image/image-20210905003458130.png) - -### 复制清除算法 - -半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 - -![](http://img.dabin-coder.cn/image/image-20210905003714551.png) - -特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。 - -### 标记整理算法 - -根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 - -### 分类收集算法 - -根据各个年代的特点采用最适当的收集算法。 - -一般将堆分为新生代和老年代。 - -- 新生代使用:复制算法 -- 老年代使用:标记清除算法或者标记整理算法 - -在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。 - -由于对象之间会存在跨代引用,如果要进行一次新生代垃圾收集,除了需要遍历新生代对象,还要额外遍历整个老年代的所有对象,这会给内存回收带来很大的性能负担。 - -跨代引用相对于同代引用来说仅占极少数。存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。 - -所以没必要为了少量的跨代引用去扫描整个老年代,只需在新生代建立一个全局的数据结构 Remembered Set,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。 - -#### 记忆集 - -记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。Card Table 是最常用的一种记忆集实现形式。字节数组CARD_TABLE的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。 - -一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。 - - - -## 垃圾收集器 - -经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器CMS,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化。 - -JDK1.7和1.8中默认使用的是Parallel Scavenge和Parallel Old收集器组合。jdk1.9 默认垃圾收集器是G1。 - -```java -java -XX:+PrintCommandLineFlags -version -``` - -7个垃圾收集器的特点: - -| 收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 | -| :-------------------: | :--------------: | :-----------: | :----------------: | :----------: | :---------------------------------------: | -| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 | -| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 | -| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 | -| **Parallel Old** | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 | -| **G1** | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS | - -### Serial 收集器 - -单线程收集器,使用一条垃圾收集线程去完成垃圾收集工作,在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。 - -![](http://img.dabin-coder.cn/image/serial-collector.png) - -特点:简单高效;内存消耗最小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。 - -### ParNew 收集器 - -Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。 - -![](http://img.dabin-coder.cn/image/parnew-collector.png) - -除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。 - -### Parallel Scavenge 收集器 - -新生代收集器,基于复制清除算法实现的收集器。吞吐量优先收集器,也是能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间))。Parallel Scavenge 收集器关注点是吞吐量,高效率的利用 CPU 资源。CMS 垃圾收集器关注点更多的是用户线程的停顿时间。 - -Parallel Scavenge收集器提供了两个参数用于**精确控制吞吐量**,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 - --XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。 - --XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。 - -相比ParNew收集器的优点: - -1. 精确控制吞吐量; -2. 垃圾收集的自适应的调节策略。通过参数-XX:+UseAdaptiveSizePolicy 打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大的吞吐量。调整的参数包括新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等。 - -### Serial Old 收集器 - -Serial 收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 - -### Parallel Old 收集器 - -Parallel Scavenge 收集器的老年代版本。多线程垃圾收集,使用标记-整理算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 - -### CMS 收集器 - -Concurrent Mark Sweep 并发标记清除,目的是获取最短应用停顿时间。第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程基本上同时工作。在并发标记和并发清除阶段,虽然用户线程没有被暂停,但是由于垃圾收集器线程占用了一部分系统资源,应用程序的吞吐量会降低。 - -#### 回收过程 - -基于标记清除算法实现,垃圾收集整个过程分为四个步骤: - -- 初始标记: stw暂停所有的其他线程,记录直接与 gc root 直接相连的对象,速度很快 。 -- 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。 -- 重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会stw,停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。 -- 并发清除:清除死亡对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。 - -![](http://img.dabin-coder.cn/image/cms-collector.png) - -由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 - -优点:并发收集,低停顿。 - -缺点: - -- 标记清除算法导致收集结束有大量空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 -- 会产生浮动垃圾,由于CMS并发清理阶段用户线程还在运行着,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好等到下一次GC去处理; -- 对处理器资源非常敏感。在并发阶段,收集器占用了一部分线程资源,导致应用程序变慢,降低总吞吐量。 - -#### CMS垃圾回收特点 - -1. cms只会回收老年代和永久代(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻代; -2. cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%,不能等到old内存用尽时回收,否则会导致并发回收失败。因为需要预留空间给用户线程运行。 - -### G1收集器 - -G1垃圾收集器的目标是用在多核、大内存的机器上,在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。 - -在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。 - -G1将整个堆分成相同大小的分区(Region),有四种不同类型的分区:Eden、Survivor、Old和Humongous(大对象)。分区的大小取值范围为1M到32M,都是2的幂次方。Region大小可以通过`-XX:G1HeapRegionSize`参数指定。Humongous区域用于存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。 - -![](http://img.dabin-coder.cn/image/g1-region.jpg) - -G1 收集器对各个Region回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大的回收停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值最大的 Region。 - -Java堆分成多个独立Region,Region里面会存在跨Region引用对象,在垃圾回收寻找GC Roots需要扫描整个堆。G1采用了Rset(Remembered Set)来避免扫描整个堆。每个Region会有一个RSet,记录了哪些Region引用本Region中对象,即谁引用了我的对象,这样的话,在做可达性分析的时候就可以避免全堆扫描。 - -特点:可以由用户指定期望的垃圾收集停顿时间。 - -#### 回收过程 - -G1 收集器的运作大致分为以下几个步骤: - -- 初始标记:。stw暂停所有的其他线程,记录直接与 gc root 直接相连的对象,速度很快 。 -- 并发标记。从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 -- 最终标记。对用户线程做另一个短暂的暂停,用于处理并发阶段对象引用出现变动的区域。 -- 筛选回收。对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。 - - - -## JVM调优工具 - -### jps - -列出本机所有java进程的pid。 - -选项 - -- -q 仅输出VM标识符,不包括class name,jar name,arguments in main method -- -m 输出main method的参数 -- -l 输出完全的包名,应用主类名,jar的完全路径名 -- -v 输出jvm参数 -- -V 输出通过flag文件传递到JVM中的参数(.hotspotrc文件或-XX:Flags=所指定的文件 -- -Joption 传递参数到vm,例如:-J-Xms48m - -```bash -jps -lvm -//output -//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8 -``` - -### jstack - -查看某个Java进程内的线程堆栈信息。-l,long listings,打印额外的锁信息,发生死锁时可以使用`jstack -l pid`观察锁持有情况。 - -```java -jstack -l 4124 | more -``` - -output: - -```java -"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000] - java.lang.Thread.State: WAITING (parking) - at sun.misc.Unsafe.park(Native Method) - - parking to wait for <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) - at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) - at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) - at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) - at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) - at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) - at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) - at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) - at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) - at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) - at java.lang.Thread.run(Thread.java:748) - - Locked ownable synchronizers: - - None -``` - -### jstat - -虚拟机各种运行状态信息(类装载、内存、垃圾收集、jit编译等运行数据)。gcuitl 查看新生代、老年代及持久代GC的情况。 - -```java -jstat -gcutil 4124 - S0 S1 E O M CCS YGC YGCT FGC FGCT GCT - 0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275 -``` - -### jmap - -查看堆内存快照。查看进程中新生代、老年代、永久代的使用情况。 - -查询进程4124的堆内存快照: - -```java ->jmap -heap 4124 -Attaching to process ID 4124, please wait... -Debugger attached successfully. -Server compiler detected. -JVM version is 25.221-b11 - -using thread-local object allocation. -Parallel GC with 6 thread(s) - -Heap Configuration: - MinHeapFreeRatio = 0 - MaxHeapFreeRatio = 100 - MaxHeapSize = 4238344192 (4042.0MB) - NewSize = 88604672 (84.5MB) - MaxNewSize = 1412431872 (1347.0MB) - OldSize = 177733632 (169.5MB) - NewRatio = 2 - SurvivorRatio = 8 - MetaspaceSize = 21807104 (20.796875MB) - CompressedClassSpaceSize = 1073741824 (1024.0MB) - MaxMetaspaceSize = 17592186044415 MB - G1HeapRegionSize = 0 (0.0MB) - -Heap Usage: -PS Young Generation -Eden Space: - capacity = 327155712 (312.0MB) - used = 223702392 (213.33922576904297MB) - free = 103453320 (98.66077423095703MB) - 68.37795697725736% used -From Space: - capacity = 21495808 (20.5MB) - used = 0 (0.0MB) - free = 21495808 (20.5MB) - 0.0% used -To Space: - capacity = 23068672 (22.0MB) - used = 0 (0.0MB) - free = 23068672 (22.0MB) - 0.0% used -PS Old Generation - capacity = 217579520 (207.5MB) - used = 41781472 (39.845916748046875MB) - free = 175798048 (167.65408325195312MB) - 19.20285144484187% used - -27776 interned Strings occupying 3262336 bytes. -``` - -查询进程pid = 41843 存活的对象占用内存前100排序: `jmap -histo:live 41843 | head -n 100` - - - -## 补充 - -### 对象头 - -Java对象保存在内存中时,由以下三部分组成:对象头、实例数据和对齐填充字节。 - -java的对象头由以下三部分组成:mark word、指向类信息的指针和数组长度(数组对象才有)。 - -mark word包含:对象的hashcode、分代年龄和锁标志位。 - -对象的实例数据就是在java代码中对象的属性和值。 - -对齐填充字节:因为JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数。 - -**内存对齐的主要作用是:** - -1. 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 -2. 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。 - -### main方法执行过程 - -以下是示例代码: - -```java -public class App { - public static void main(String[] args) { - Student s = new Student("dabin"); - s.getName(); - } -} - -class Student { - public String name; - - public Student(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } -} -``` - -执行main方法的步骤如下: - -1. 编译好 App.java 后得到 App.class 后,执行 App.class,系统会启动一个 JVM 进程,从 classpath 路径中找到一个名为 App.class 的二进制文件,将 App 的类信息加载到运行时数据区的方法区内,这个过程叫做 App 类的加载 -2. JVM 找到 App 的主程序入口,执行main方法 -3. 这个main中的第一条语句为 `Student student = new Student("dabin") `,就是让 JVM 创建一个Student对象,但是这个时候方法区中是没有 Student 类的信息的,所以 JVM 马上加载 Student 类,把 Student 类的信息放到方法区中 -4. 加载完 Student 类后,JVM 在堆中为一个新的 Student 实例分配内存,然后调用构造函数初始化 Student 实例,这个 Student 实例持有 **指向方法区中的 Student 类的类型信息** 的引用 -5. 执行student.getName()时,JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 getName() 的字节码地址。 -6. 执行getName() - - - -### 对象创建过程 - -1. 类加载检查 - - 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 - -2. 分配内存 - - 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。 - -3. 初始化零值 - - 分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 - -4. 设置对象头 - - Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 - -5. 执行init方法 - - 按照Java代码进行初始化。 - - - -## 参考资料 - -- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社 diff --git "a/Java/JVM\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" "b/Java/JVM\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 3f2af40..0000000 --- "a/Java/JVM\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,706 +0,0 @@ -## 讲一下JVM内存结构? - -JVM内存结构分为5大区域,**程序计数器**、**虚拟机栈**、**本地方法栈**、**堆**、**方法区**。 - -![](http://img.dabin-coder.cn/image/jvm内存结构0.png) - -### 程序计数器 - -线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址。程序计数器主要有两个作用: - -1. 当前线程所执行的字节码的行号指示器,通过它实现**代码的流程控制**,如:顺序执行、选择、循环、异常处理。 -2. 在多线程的情况下,程序计数器用于**记录当前线程执行的位置**,当线程被切换回来的时候能够知道它上次执行的位置。 - -程序计数器是唯一一个不会出现 `OutOfMemoryError` 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 - -### 虚拟机栈 - -Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:**局部变量表**、**操作数栈**、**动态链接**、**方法出口信息**。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。 - -局部变量表是用于存放方法参数和方法内的局部变量。 - -每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。 - -- 部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接 -- 部分符号引用在运行期间转化为直接引用,这种转化就是动态链接 - -Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。Java 虚拟机栈会出现两种错误:`StackOverFlowError` 和 `OutOfMemoryError`。 - -可以通过` -Xss `参数来指定每个线程的虚拟机栈内存大小: - -```java -java -Xss2M -``` - -### 本地方法栈 - -虚拟机栈为虚拟机执行 `Java` 方法服务,而本地方法栈则为虚拟机使用到的 `Native` 方法服务。`Native` 方法一般是用其它语言(C、C++等)编写的。 - -本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。 - -### 堆 - -堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作`GC`堆。堆可以细分为:新生代(`Eden`空间、`From Survivor`、`To Survivor`空间)和老年代。 - -通过 `-Xms`设定程序启动时占用内存大小,通过`-Xmx`设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出`OutOfMemory`异常。 - -```java -java -Xms1M -Xmx2M -``` - -### 方法区 - -方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 - -对方法区进行垃圾回收的主要目标是**对常量池的回收和对类的卸载**。 - -**永久代** - -方法区是 JVM 的规范,而永久代`PermGen`是方法区的一种实现方式,并且只有 `HotSpot` 有永久代。对于其他类型的虚拟机,如`JRockit`没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。 - -**元空间** - -JDK 1.8 的时候,`HotSpot`的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。 - -为什么要将永久代替换为元空间呢? - -永久代内存受限于 JVM 可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。 - -### 运行时常量池 - -运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String 类的 intern()方法,也会被放入运行时常量池。 - -### 直接内存 - -直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 `OutOfMemoryError` 错误出现。 - -NIO的Buffer提供了`DirectBuffer`,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。`DirectBuffer`直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。 - -直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。 - -## Java对象的定位方式 - -Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种: - -- 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。 -- 直接指针。reference 中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销,虚拟机hotspot主要是使用直接指针来访问对象。 - -## 说一下堆栈的区别? - -1. 堆的**物理地址分配**是不连续的,性能较慢;栈的物理地址分配是连续的,性能相对较快。 -2. 堆存放的是**对象的实例和数组**;栈存放的是**局部变量,操作数栈,返回结果**等。 - -3. 堆是**线程共享**的;栈是**线程私有**的。 - -## 什么情况下会发生栈溢出? - -- 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出`StackOverFlowError`异常。这种情况通常是因为方法递归没终止条件。 -- 新建线程的时候没有足够的内存去创建对应的虚拟机栈,虚拟机会抛出`OutOfMemoryError`异常。比如线程启动过多就会出现这种情况。 - -## 类文件结构 - -Class 文件结构如下: - -```java -ClassFile { - u4 magic; //类文件的标志 - u2 minor_version;//小版本号 - u2 major_version;//大版本号 - u2 constant_pool_count;//常量池的数量 - cp_info constant_pool[constant_pool_count-1];//常量池 - u2 access_flags;//类的访问标记 - u2 this_class;//当前类的索引 - u2 super_class;//父类 - u2 interfaces_count;//接口 - u2 interfaces[interfaces_count];//一个类可以实现多个接口 - u2 fields_count;//字段属性 - field_info fields[fields_count];//一个类会可以有个字段 - u2 methods_count;//方法数量 - method_info methods[methods_count];//一个类可以有个多个方法 - u2 attributes_count;//此类的属性表中的属性数 - attribute_info attributes[attributes_count];//属性表集合 -} -``` - -主要参数如下: - -**魔数**:`class`文件标志。 - -**文件版本**:高版本的 Java 虚拟机可以执行低版本编译器生成的类文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的类文件。 - -**常量池**:存放字面量和符号引用。字面量类似于 Java 的常量,如字符串,声明为`final`的常量值等。符号引用包含三类:类和接口的全限定名,方法的名称和描述符,字段的名称和描述符。 - -**访问标志**:识别类或者接口的访问信息,比如这个`Class`是类还是接口,是否为 `public` 或者 `abstract` 类型等等。 - -**当前类的索引**:类索引用于确定这个类的全限定名。 - -## 什么是类加载?类加载的过程? - -类的加载指的是将类的`class`文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个此类的对象,通过这个对象可以访问到方法区对应的类信息。 - -![](http://img.dabin-coder.cn/image/类加载.png) - -**加载** - -1. 通过类的全限定名获取定义此类的二进制字节流 -2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构 -3. 在内存中生成一个代表该类的`Class`对象,作为方法区类信息的访问入口 - -**验证** - -确保Class文件的字节流中包含的信息符合虚拟机规范,保证在运行后不会危害虚拟机自身的安全。主要包括四种验证:**文件格式验证,元数据验证,字节码验证,符号引用验证**。 - -**准备** - -为类变量分配内存并设置类变量初始值的阶段。 - -**解析** - -虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用用于描述目标,直接引用直接指向目标的地址。 - -**初始化** - -开始执行类中定义的`Java`代码,初始化阶段是调用类构造器的过程。 - -## 什么是双亲委派模型? - -一个类加载器收到一个类的加载请求时,它首先不会自己尝试去加载它,而是把这个请求**委派**给父类加载器去完成,这样层层委派,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。 - -![](http://img.dabin-coder.cn/image/双亲委派.png) - -双亲委派模型的具体实现代码在 `java.lang.ClassLoader`中,此类的 `loadClass()` 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 `ClassNotFoundException`,此时尝试自己去加载。源码如下: - -```java -public abstract class ClassLoader { - // The parent class loader for delegation - private final ClassLoader parent; - - public Class loadClass(String name) throws ClassNotFoundException { - return loadClass(name, false); - } - - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - // First, check if the class has already been loaded - Class c = findLoadedClass(name); - if (c == null) { - try { - if (parent != null) { - c = parent.loadClass(name, false); - } else { - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { - // ClassNotFoundException thrown if class not found - // from the non-null parent class loader - } - - if (c == null) { - // If still not found, then invoke findClass in order - // to find the class. - c = findClass(name); - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - - protected Class findClass(String name) throws ClassNotFoundException { - throw new ClassNotFoundException(name); - } -} -``` - -## 为什么需要双亲委派模型? - -双亲委派模型的好处:可以防止内存中出现多份同样的字节码。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个`java.lang.Object`的同名类并放在`ClassPath`中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的`Object`类,那么类之间的比较结果及类的唯一性将无法保证。 - -## 什么是类加载器,类加载器有哪些? - -- 实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。 - - 主要有一下四种类加载器: - - - **启动类加载器**:用来加载 Java 核心类库,无法被 Java 程序直接引用。 - - **扩展类加载器**:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 - - **系统类加载器**:它根据应用的类路径来加载 Java 类。可通过`ClassLoader.getSystemClassLoader()`获取它。 - - **自定义类加载器**:通过继承`java.lang.ClassLoader`类的方式实现。 - -## 类的实例化顺序? - -1. 父类中的`static`代码块,当前类的`static`代码块 -2. 父类的普通代码块 -3. 父类的构造函数 -4. 当前类普通代码块 -5. 当前类的构造函数 - -## 如何判断一个对象是否存活? - -对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不再被任何途径引用的对象)。判断对象是否存活有两种方法:引用计数法和可达性分析。 - -**引用计数法** - -给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。 - -这种方法很难解决对象之间相互循环引用的问题。比如下面的代码,`obj1` 和 `obj2` 互相引用,这种情况下,引用计数器的值都是1,不会被垃圾回收。 - -```java -public class ReferenceCount { - Object instance = null; - public static void main(String[] args) { - ReferenceCount obj1 = new ReferenceCount(); - ReferenceCount obj2 = new ReferenceCount(); - obj1.instance = obj2; - obj2.instance = obj1; - obj1 = null; - obj2 = null; - } -} -``` - -**可达性分析** - -通过`GC Root`对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到`GC Root`没有任何的引用链相连时,说明这个对象是不可用的。 - -![](http://img.dabin-coder.cn/image/可达性分析0.png) - -## 可作为GC Roots的对象有哪些? - -1. 虚拟机栈中引用的对象 -2. 本地方法栈中Native方法引用的对象 -3. 方法区中类静态属性引用的对象 -4. 方法区中常量引用的对象 - -## 什么情况下类会被卸载? - -需要同时满足以下 3 个条件类才可能会被卸载 : - -- 该类所有的实例都已经被回收。 -- 加载该类的类加载器已经被回收。 -- 该类对应的 `java.lang.Class` 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 - -虚拟机可以对满足上述 3 个条件的类进行回收,但不一定会进行回收。 - -## 强引用、软引用、弱引用、虚引用是什么,有什么区别? - -**强引用**:在程序中普遍存在的引用赋值,类似`Object obj = new Object()`这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 - -**软引用**:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。 - -```java -//软引用 -SoftReference softRef = new SoftReference(str); -``` - -**弱引用**:在进行垃圾回收时,不管当前内存空间足够与否,都会回收只具有弱引用的对象。 - -```java -//弱引用 -WeakReference weakRef = new WeakReference(str); -``` - -**虚引用**:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。**虚引用主要是为了能在对象被收集器回收时收到一个系统通知**。 - -## GC是什么?为什么要GC? - -GC(`Garbage Collection`),垃圾回收,是Java与C++的主要区别之一。作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码。这是因为在Java虚拟机中,存在自动内存管理和垃圾清理机制。对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,保证JVM中的内存空间,防止出现内存泄露和溢出问题。 - -## Minor GC 和 Full GC的区别? - -- **Minor GC**:回收新生代,因为新生代对象存活时间很短,因此 `Minor GC`会频繁执行,执行的速度一般也会比较快。 - -- **Full GC**:回收老年代和新生代,老年代的对象存活时间长,因此 `Full GC` 很少执行,执行速度会比 `Minor GC` 慢很多。 - -## 内存的分配策略? - -**对象优先在 Eden 分配** - -大多数情况下,对象在新生代 `Eden` 上分配,当 `Eden` 空间不够时,触发 `Minor GC`。 - -**大对象直接进入老年代** - -大对象是指需要连续内存空间的对象,最典型的大对象有长字符串和大数组。可以设置JVM参数 `-XX:PretenureSizeThreshold`,大于此值的对象直接在老年代分配。 - -**长期存活的对象进入老年代** - -通过参数 `-XX:MaxTenuringThreshold` 可以设置对象进入老年代的年龄阈值。对象在`Survivor`区每经过一次 `Minor GC`,年龄就增加 1 岁,当它的年龄增加到一定程度,就会被晋升到老年代中。 - -**动态对象年龄判定** - -并非对象的年龄必须达到 `MaxTenuringThreshold` 才能晋升老年代,如果在 `Survivor` 中相同年龄所有对象大小的总和大于 `Survivor` 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需达到 `MaxTenuringThreshold` 年龄阈值。 - -**空间分配担保** - -在发生 `Minor GC` 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 `Minor GC` 是安全的。如果不成立的话虚拟机会查看 `HandlePromotionFailure` 的值是否允许担保失败。如果允许,那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 `Minor GC`;如果小于,或者 `HandlePromotionFailure` 的值为不允许担保失败,那么就要进行一次 `Full GC`。 - -## Full GC 的触发条件? - -对于 Minor GC,其触发条件比较简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 触发条件相对复杂,有以下情况会发生 full GC: - -**调用 System.gc()** - -只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 - -**老年代空间不足** - -老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组、注意编码规范避免内存泄露。除此之外,可以通过 `-Xmn` 参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 `-XX:MaxTenuringThreshold` 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 - -**空间分配担保失败** - -使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。 - -**JDK 1.7 及以前的永久代空间不足** - -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 `java.lang.OutOfMemoryError`。 - - - -## 垃圾回收算法有哪些? - -垃圾回收算法有四种,分别是**标记清除法、标记整理法、复制算法、分代收集算法**。 - -**标记清除算法** - -首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会**产生大量不连续的空间碎片**。 - -![](http://img.dabin-coder.cn/image/标记清除.png) - -**复制清除算法** - -半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 - -特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。 - -**标记整理算法** - -根据老年代的特点提出的一种标记算法,标记过程仍然与`标记-清除`算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。 - -![](http://img.dabin-coder.cn/image/标记整理.png) - -**分类收集算法** - -根据各个年代的特点采用最适当的收集算法。 - -一般将堆分为新生代和老年代。 - -- 新生代使用复制算法 -- 老年代使用标记清除算法或者标记整理算法 - -在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。 - -## 有哪些垃圾回收器? - -垃圾回收器主要分为以下几种:`Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1`。 - -这7种垃圾收集器的特点: - -| 收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 | -| :-------------------: | :--------------: | :-----------: | :----------------: | :----------: | :---------------------------------------: | -| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 | -| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 | -| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 | -| **Parallel Old** | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 | -| **G1** | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS | - -**Serial 收集器** - -**单线程收集器**,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程( `Stop The World` ),直到它收集结束。 - -特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。 - -**ParNew 收集器** - -`Serial`收集器的**多线程版本**,除了使用多线程进行垃圾收集外,其他行为、参数与 `Serial` 收集器基本一致。 - -**Parallel Scavenge 收集器** - -**新生代收集器**,基于**复制清除算法**实现的收集器。特点是**吞吐量优先**,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值(`吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)`)。`Parallel Scavenge` 收集器关注点是**吞吐量,高效率的利用 CPU 资源**。`CMS` 垃圾收集器关注点更多的是**用户线程的停顿时间**。 - -`Parallel Scavenge`收集器提供了两个参数用于**精确控制吞吐量**,分别是控制最大垃圾收集停顿时间的`-XX:MaxGCPauseMillis`参数以及直接设置吞吐量大小的`-XX:GCTimeRatio`参数。 - -- `-XX:MaxGCPauseMillis`参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。 - -- `-XX:GCTimeRatio`参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。 - -**Serial Old 收集器** - -`Serial` 收集器的老年代版本,单线程收集器,使用**标记整理算法**。 - -**Parallel Old 收集器** - -`Parallel Scavenge` 收集器的老年代版本。多线程垃圾收集,使用**标记整理算法**。 - -**CMS 收集器** - -`Concurrent Mark Sweep` ,并发标记清除,追求获取**最短停顿时间**,实现了让**垃圾收集线程与用户线程基本上同时工作**。 - -`CMS` 垃圾回收基于**标记清除算法**实现,整个过程分为四个步骤: - -- 初始标记: 暂停所有用户线程(`Stop The World`),记录直接与 `GC Roots` 直接相连的对象 。 -- 并发标记:从`GC Roots`开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程。 -- 重新标记: 在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程。 -- 并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的。 - -在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,`CMS`收集器的内存回收过程是与用户线程一起并发执行的。 - -**优点**:并发收集,停顿时间短。 - -**缺点**: - -- 标记清除算法导致收集结束有**大量空间碎片**。 -- **产生浮动垃圾**,在并发清理阶段用户线程还在运行,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,`CMS`无法在当次收集中回收它们,只好等到下一次垃圾回收再处理; - -**G1收集器** - -G1垃圾收集器的目标是在不同应用场景中**追求高吞吐量和低停顿之间的最佳平衡**。 - -G1将整个堆分成相同大小的分区(`Region`),有四种不同类型的分区:`Eden、Survivor、Old和Humongous`。分区的大小取值范围为 1M 到 32M,都是2的幂次方。分区大小可以通过`-XX:G1HeapRegionSize`参数指定。`Humongous`区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。 - -![](http://img.dabin-coder.cn/image/g1分区.png) - -G1 收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。 - -**特点**:可以由用户**指定**期望的垃圾收集停顿时间。 - -G1 收集器的回收过程分为以下几个步骤: - -- **初始标记**。暂停所有其他线程,记录直接与 `GC Roots` 直接相连的对象,耗时较短 。 -- **并发标记**。从`GC Roots`开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长,不过可以和用户程序并发执行。 -- **最终标记**。需对其他线程做短暂的暂停,用于处理并发标记阶段对象引用出现变动的区域。 -- **筛选回收**。对各个分区的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全部空间。这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成。 - -## 常用的 JVM 调优的命令都有哪些? - -**jps**:列出本机所有 Java 进程的**进程号**。 - -常用参数如下: - -- `-m` 输出`main`方法的参数 -- `-l` 输出完全的包名和应用主类名 -- `-v` 输出`JVM`参数 - -```java -jps -lvm -//output -//4124 com.zzx.Application -javaagent:E:\IDEA2019\lib\idea_rt.jar=10291:E:\IDEA2019\bin -Dfile.encoding=UTF-8 -``` - -**jstack**:查看某个 Java 进程内的**线程堆栈信息**。使用参数`-l`可以打印额外的锁信息,发生死锁时可以使用`jstack -l pid`观察锁持有情况。 - -```java -jstack -l 4124 | more -``` - -输出结果如下: - -```java -"http-nio-8001-exec-10" #40 daemon prio=5 os_prio=0 tid=0x000000002542f000 nid=0x4028 waiting on condition [0x000000002cc9e000] - java.lang.Thread.State: WAITING (parking) - at sun.misc.Unsafe.park(Native Method) - - parking to wait for <0x000000077420d7e8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) - at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) - at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) - at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) - at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103) - at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31) - at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) - at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) - at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) - at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) - at java.lang.Thread.run(Thread.java:748) - - Locked ownable synchronizers: - - None -``` - -`WAITING (parking)`指线程处于挂起中,在等待某个条件发生,来把自己唤醒。 - -**jstat**:用于查看虚拟机各种**运行状态信息(类装载、内存、垃圾收集等运行数据)**。使用参数`-gcuitl`可以查看垃圾回收的统计信息。 - -```java -jstat -gcutil 4124 - S0 S1 E O M CCS YGC YGCT FGC FGCT GCT - 0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275 -``` - -参数说明: - -- **S0**:`Survivor0`区当前使用比例 -- **S1**:`Survivor1`区当前使用比例 -- **E**:`Eden`区使用比例 -- **O**:老年代使用比例 -- **M**:元数据区使用比例 -- **CCS**:压缩使用比例 -- **YGC**:年轻代垃圾回收次数 -- **FGC**:老年代垃圾回收次数 -- **FGCT**:老年代垃圾回收消耗时间 -- **GCT**:垃圾回收消耗总时间 - -**jmap**:查看**堆内存快照**。通过`jmap`命令可以获得运行中的堆内存的快照,从而可以对堆内存进行离线分析。 - -查询进程4124的堆内存快照,输出结果如下: - -```java ->jmap -heap 4124 -Attaching to process ID 4124, please wait... -Debugger attached successfully. -Server compiler detected. -JVM version is 25.221-b11 - -using thread-local object allocation. -Parallel GC with 6 thread(s) - -Heap Configuration: - MinHeapFreeRatio = 0 - MaxHeapFreeRatio = 100 - MaxHeapSize = 4238344192 (4042.0MB) - NewSize = 88604672 (84.5MB) - MaxNewSize = 1412431872 (1347.0MB) - OldSize = 177733632 (169.5MB) - NewRatio = 2 - SurvivorRatio = 8 - MetaspaceSize = 21807104 (20.796875MB) - CompressedClassSpaceSize = 1073741824 (1024.0MB) - MaxMetaspaceSize = 17592186044415 MB - G1HeapRegionSize = 0 (0.0MB) - -Heap Usage: -PS Young Generation -Eden Space: - capacity = 327155712 (312.0MB) - used = 223702392 (213.33922576904297MB) - free = 103453320 (98.66077423095703MB) - 68.37795697725736% used -From Space: - capacity = 21495808 (20.5MB) - used = 0 (0.0MB) - free = 21495808 (20.5MB) - 0.0% used -To Space: - capacity = 23068672 (22.0MB) - used = 0 (0.0MB) - free = 23068672 (22.0MB) - 0.0% used -PS Old Generation - capacity = 217579520 (207.5MB) - used = 41781472 (39.845916748046875MB) - free = 175798048 (167.65408325195312MB) - 19.20285144484187% used - -27776 interned Strings occupying 3262336 bytes. -``` - -**jinfo**:`jinfo -flags 1`。查看当前的应用JVM参数配置。 - -```java -Attaching to process ID 1, please wait... -Debugger attached successfully. -Server compiler detected. -JVM version is 25.111-b14 -Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=480247808 -XX:MaxNewSize=160038912 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -Command line: -``` - -**查看所有参数**:`java -XX:+PrintFlagsFinal -version`。用于查看最终值,初始值可能被修改掉(查看初始值可以使用java -XX:+PrintFlagsInitial)。 - -```java -[Global flags] - uintx AdaptiveSizeDecrementScaleFactor = 4 {product} - uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} - uintx AdaptiveSizePausePolicy = 0 {product} - uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} - uintx AdaptiveSizePolicyInitializingSteps = 20 {product} - uintx AdaptiveSizePolicyOutputInterval = 0 {product} - uintx AdaptiveSizePolicyWeight = 10 {product} - uintx AdaptiveSizeThroughPutPolicy = 0 {product} - uintx AdaptiveTimeWeight = 25 {product} - bool AdjustConcurrency = false {product} - bool AggressiveOpts = false {product} - .... -``` - - - -## 对象头了解吗? - -Java 内存中的对象由以下三部分组成:**对象头**、**实例数据**和**对齐填充字节**。 - -而对象头由以下三部分组成:**mark word**、**指向类信息的指针**和**数组长度**(数组才有)。 - -`mark word`包含:对象的哈希码、分代年龄和锁标志位。 - -对象的实例数据就是 Java 对象的属性和值。 - -对齐填充字节:因为JVM要求对象占的内存大小是 8bit 的倍数,因此后面有几个字节用于把对象的大小补齐至 8bit 的倍数。 - -**内存对齐的主要作用是:** - -1. 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。 -2. 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。 - -## main方法执行过程 - -以下是示例代码: - -```java -public class Application { - public static void main(String[] args) { - Person p = new Person("大彬"); - p.getName(); - } -} - -class Person { - public String name; - - public Person(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } -} -``` - -执行`main`方法的过程如下: - -1. 编译`Application.java`后得到 `Application.class` 后,执行这个`class`文件,系统会启动一个 `JVM` 进程,从类路径中找到一个名为 `Application.class` 的二进制文件,将 `Application` 类信息加载到运行时数据区的方法区内,这个过程叫做类的加载。 -2. JVM 找到 `Application` 的主程序入口,执行`main`方法。 -3. `main`方法的第一条语句为 `Person p = new Person("大彬") `,就是让 JVM 创建一个`Person`对象,但是这个时候方法区中是没有 `Person` 类的信息的,所以 JVM 马上加载 `Person` 类,把 `Person` 类的信息放到方法区中。 -4. 加载完 `Person` 类后,JVM 在堆中分配内存给 `Person` 对象,然后调用构造函数初始化 `Person` 对象,这个 `Person` 对象持有**指向方法区中的 Person 类的类型信息**的引用。 -5. 执行`p.getName()`时,JVM 根据 p 的引用找到 p 所指向的对象,然后根据此对象持有的引用定位到方法区中 `Person` 类的类型信息的方法表,获得 `getName()` 的字节码地址。 -6. 执行`getName()`方法。 - -## 对象创建过程 - -1. **类加载检查**:当虚拟机遇到一条 `new` 指令时,首先检查是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那先执行类加载。 -2. **分配内存**:在类加载检查通过后,接下来虚拟机将为对象实例分配内存。 -3. **初始化**。分配到的内存空间都初始化为零值,通过这个操作保证了对象的字段可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -4. **设置对象头**。`Hotspot` 虚拟机的对象头包括:存储对象自身的运行时数据(哈希码、分代年龄、锁标志等等)、类型指针和数据长度(数组对象才有),类型指针就是对象指向它的类信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 -5. **按照`Java`代码进行初始化**。 - -## 如何排查 OOM 的问题? - -> 线上JVM必须配置`-XX:+HeapDumpOnOutOfMemoryError` 和`-XX:HeapDumpPath=/tmp/heapdump.hprof`,当OOM发生时自动 dump 堆内存信息到指定目录 - -排查 OOM 的方法如下: - -- 查看服务器运行日志日志,捕捉到内存溢出异常 -- jstat 查看监控JVM的内存和GC情况,评估问题大概出在什么区域 -- 使用MAT工具载入dump文件,分析大对象的占用情况 - - - -**参考资料** - -- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社 - - - -![](http://img.dabin-coder.cn/image/20220612101342.png) diff --git a/Java/Java web.md b/Java/Java web.md deleted file mode 100644 index b92935b..0000000 --- a/Java/Java web.md +++ /dev/null @@ -1,83 +0,0 @@ - - - - -- [servlet](#servlet) -- [jsp](#jsp) -- [Tomcat](#tomcat) - - [tomcat和netty区别](#tomcat%E5%92%8Cnetty%E5%8C%BA%E5%88%AB) -- [跨域](#%E8%B7%A8%E5%9F%9F) - - [同源策略](#%E5%90%8C%E6%BA%90%E7%AD%96%E7%95%A5) - - [CSRF攻击](#csrf%E6%94%BB%E5%87%BB) -- [statement和prepareStatement](#statement%E5%92%8Cpreparestatement) - - - -## servlet - -servlet接口定义的是一套处理网络请求的规范。servlet运行在服务端,由servlet容器管理,用于生成动态的内容(早期的web技术主要用来浏览静态页面)。 - -Servlet是什么? - -- 运行在Servlet容器(如Tomcat)中的Java类 -- 没有main方法,不能独立运行,必须被部署到Servlet容器中,由容器来实例化和调用Servlet的方法 - -servlet生命周期指它从被web服务器加载到它被销毁的整个过程,分三个阶段: -1. 初始化阶段,调用init()方法 -2. 响应客户请求阶段,调用service()方法 -3. 终止阶段,调用destroy()方法 - -servlet容器:负责接收请求,生成servlet实例用于处理请求(调用service方法),然后将servlet生成的响应数据返回给客户端。 - -![](../img/web/servlet-container.jpg) - - - -## jsp - -Java server pages。当有人请求JSP时,服务器会自动帮我们把JSP中的HTML片段和java代码拼接成静态资源响应给浏览器。也就是说JSP运行在服务器端,但最终发给客户端的都已经是转换好的HTML静态页面(在响应体里)。 - -即:**JSP = HTML + Java片段**(各种标签本质上还是Java片段) - - - -## Tomcat - -Tomcat 是由 Apache 开发的一个 Servlet 容器,实现了对 Servlet 和 JSP 的支持。 - -### tomcat和netty区别 - -Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。 - - - -## 跨域 - -当发送请求时,如果浏览器发现是跨源AJAX请求,就自动在头信息之中,添加一个Origin字段。 - -`Origin: http://api.bob.com` - -对于服务端,如果请求头Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP响应。浏览器收到响应后,发现响应头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。 - -如果请求头Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。 - -```java -Access-Control-Allow-Origin: http://api.bob.com -Access-Control-Allow-Credentials: true -Access-Control-Expose-Headers: FooBar -``` - -### 同源策略 - -同domain(或ip),同端口,同协议视为同一个域,一个域内的脚本仅仅具有本域内的权限,也就是本域脚本只能读写本域内的资源,而无法访问其它域的资源。这种安全限制称为同源策略。 - -### CSRF攻击 - -跨域请求有可能被黑客利用来发动 CSRF攻击。CSRF攻击(Cross-site request forgery),跨站请求伪造。攻击者盗用了你的身份,以你的名义发送请求,比如发送邮件,发消息,盗取你的账号,甚至购买商品。 - - - -## statement和prepareStatement -Statement对象每次执行sql,相关数据库都会执行sql语句的编译,prepareStatement是预编译的,支持批处理。 -PreparedStatement是预编译的,对于批量处理可以大大提高效率,也叫JDBC存储过程。 -prepareStatement对象的开销比statement对象开销大,对于一次性操作使用statement更佳。 \ No newline at end of file diff --git "a/Java/Java \347\274\226\347\250\213\346\200\235\346\203\263.md" "b/Java/Java \347\274\226\347\250\213\346\200\235\346\203\263.md" deleted file mode 100644 index 960b2ac..0000000 --- "a/Java/Java \347\274\226\347\250\213\346\200\235\346\203\263.md" +++ /dev/null @@ -1,3916 +0,0 @@ - - - -- [基础知识](#%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86) - - [数据类型](#%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) - - [整型](#%E6%95%B4%E5%9E%8B) - - [浮点类型](#%E6%B5%AE%E7%82%B9%E7%B1%BB%E5%9E%8B) - - [char 类型](#char-%E7%B1%BB%E5%9E%8B) - - [boolean 类型](#boolean-%E7%B1%BB%E5%9E%8B) - - [大数值](#%E5%A4%A7%E6%95%B0%E5%80%BC) - - [操作符](#%E6%93%8D%E4%BD%9C%E7%AC%A6) - - [注释文档](#%E6%B3%A8%E9%87%8A%E6%96%87%E6%A1%A3) - - [代码规范](#%E4%BB%A3%E7%A0%81%E8%A7%84%E8%8C%83) -- [控制执行流程](#%E6%8E%A7%E5%88%B6%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B) - - [switch](#switch) - - [break 和 continue 实现 goto](#break-%E5%92%8C-continue-%E5%AE%9E%E7%8E%B0-goto) -- [初始化和清理](#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%92%8C%E6%B8%85%E7%90%86) - - [成员初始化](#%E6%88%90%E5%91%98%E5%88%9D%E5%A7%8B%E5%8C%96) - - [可变参数列表](#%E5%8F%AF%E5%8F%98%E5%8F%82%E6%95%B0%E5%88%97%E8%A1%A8) -- [访问权限控制](#%E8%AE%BF%E9%97%AE%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6) - - [访问权限修饰词](#%E8%AE%BF%E9%97%AE%E6%9D%83%E9%99%90%E4%BF%AE%E9%A5%B0%E8%AF%8D) -- [复用类](#%E5%A4%8D%E7%94%A8%E7%B1%BB) - - [继承语法](#%E7%BB%A7%E6%89%BF%E8%AF%AD%E6%B3%95) - - [final 关键字](#final-%E5%85%B3%E9%94%AE%E5%AD%97) - - [初始化及类的加载](#%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8A%E7%B1%BB%E7%9A%84%E5%8A%A0%E8%BD%BD) - - [继承与初始化](#%E7%BB%A7%E6%89%BF%E4%B8%8E%E5%88%9D%E5%A7%8B%E5%8C%96) -- [多态](#%E5%A4%9A%E6%80%81) - - [缺陷:“覆盖”私有方法](#%E7%BC%BA%E9%99%B7%E8%A6%86%E7%9B%96%E7%A7%81%E6%9C%89%E6%96%B9%E6%B3%95) - - [域和静态方法](#%E5%9F%9F%E5%92%8C%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95) - - [构造器和多态](#%E6%9E%84%E9%80%A0%E5%99%A8%E5%92%8C%E5%A4%9A%E6%80%81) - - [构造器的调用顺序](#%E6%9E%84%E9%80%A0%E5%99%A8%E7%9A%84%E8%B0%83%E7%94%A8%E9%A1%BA%E5%BA%8F) -- [接口](#%E6%8E%A5%E5%8F%A3) - - [抽象类](#%E6%8A%BD%E8%B1%A1%E7%B1%BB) - - [接口的域](#%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%9F%9F) -- [内部类](#%E5%86%85%E9%83%A8%E7%B1%BB) - - [.this 和 .new](#this-%E5%92%8C-new) - - [匿名内部类](#%E5%8C%BF%E5%90%8D%E5%86%85%E9%83%A8%E7%B1%BB) - - [工厂方法](#%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95) - - [嵌套类](#%E5%B5%8C%E5%A5%97%E7%B1%BB) - - [为什么需要内部类](#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81%E5%86%85%E9%83%A8%E7%B1%BB) - - [局部内部类](#%E5%B1%80%E9%83%A8%E5%86%85%E9%83%A8%E7%B1%BB) - - [内部类标识符](#%E5%86%85%E9%83%A8%E7%B1%BB%E6%A0%87%E8%AF%86%E7%AC%A6) -- [容器](#%E5%AE%B9%E5%99%A8) - - [添加一组元素](#%E6%B7%BB%E5%8A%A0%E4%B8%80%E7%BB%84%E5%85%83%E7%B4%A0) - - [迭代器](#%E8%BF%AD%E4%BB%A3%E5%99%A8) - - [LinkedList](#linkedlist) - - [Set](#set) - - [Map](#map) - - [Queue](#queue) - - [PriorityQueue](#priorityqueue) - - [foreach 和 迭代器](#foreach-%E5%92%8C-%E8%BF%AD%E4%BB%A3%E5%99%A8) - - [适配器方法](#%E9%80%82%E9%85%8D%E5%99%A8%E6%96%B9%E6%B3%95) -- [异常、断言和日志](#%E5%BC%82%E5%B8%B8%E6%96%AD%E8%A8%80%E5%92%8C%E6%97%A5%E5%BF%97) - - [异常分类](#%E5%BC%82%E5%B8%B8%E5%88%86%E7%B1%BB) - - [声明异常](#%E5%A3%B0%E6%98%8E%E5%BC%82%E5%B8%B8) - - [捕获异常](#%E6%8D%95%E8%8E%B7%E5%BC%82%E5%B8%B8) - - [带资源的 try 语句](#%E5%B8%A6%E8%B5%84%E6%BA%90%E7%9A%84-try-%E8%AF%AD%E5%8F%A5) - - [断言](#%E6%96%AD%E8%A8%80) - - [启用和禁用断言](#%E5%90%AF%E7%94%A8%E5%92%8C%E7%A6%81%E7%94%A8%E6%96%AD%E8%A8%80) - - [日志](#%E6%97%A5%E5%BF%97) - - [logback](#logback) -- [字符串](#%E5%AD%97%E7%AC%A6%E4%B8%B2) - - [格式化输出](#%E6%A0%BC%E5%BC%8F%E5%8C%96%E8%BE%93%E5%87%BA) - - [printf() 和 format()](#printf-%E5%92%8C-format) - - [Formatter](#formatter) - - [正则表达式](#%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [基础](#%E5%9F%BA%E7%A1%80) - - [创建正则表达式](#%E5%88%9B%E5%BB%BA%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) - - [Pattern 和 Matcher](#pattern-%E5%92%8C-matcher) - - [扫描输入](#%E6%89%AB%E6%8F%8F%E8%BE%93%E5%85%A5) - - [Scanner 定界符](#scanner-%E5%AE%9A%E7%95%8C%E7%AC%A6) - - [用正则表达式扫描](#%E7%94%A8%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%89%AB%E6%8F%8F) -- [类型信息](#%E7%B1%BB%E5%9E%8B%E4%BF%A1%E6%81%AF) - - [反射](#%E5%8F%8D%E5%B0%84) -- [泛型](#%E6%B3%9B%E5%9E%8B) - - [类型参数的好处](#%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0%E7%9A%84%E5%A5%BD%E5%A4%84) - - [泛型类](#%E6%B3%9B%E5%9E%8B%E7%B1%BB) - - [泛型接口](#%E6%B3%9B%E5%9E%8B%E6%8E%A5%E5%8F%A3) - - [泛型方法](#%E6%B3%9B%E5%9E%8B%E6%96%B9%E6%B3%95) - - [可变参数和泛型方法](#%E5%8F%AF%E5%8F%98%E5%8F%82%E6%95%B0%E5%92%8C%E6%B3%9B%E5%9E%8B%E6%96%B9%E6%B3%95) - - [匿名内部类](#%E5%8C%BF%E5%90%8D%E5%86%85%E9%83%A8%E7%B1%BB-1) - - [泛型擦除](#%E6%B3%9B%E5%9E%8B%E6%93%A6%E9%99%A4) - - [类型变量的限定](#%E7%B1%BB%E5%9E%8B%E5%8F%98%E9%87%8F%E7%9A%84%E9%99%90%E5%AE%9A) - - [擦除的问题](#%E6%93%A6%E9%99%A4%E7%9A%84%E9%97%AE%E9%A2%98) - - [边界](#%E8%BE%B9%E7%95%8C) - - [通配符](#%E9%80%9A%E9%85%8D%E7%AC%A6) - - [上界通配符](#%E4%B8%8A%E7%95%8C%E9%80%9A%E9%85%8D%E7%AC%A6) - - [下界通配符](#%E4%B8%8B%E7%95%8C%E9%80%9A%E9%85%8D%E7%AC%A6) -- [数组](#%E6%95%B0%E7%BB%84) - - [复制数组](#%E5%A4%8D%E5%88%B6%E6%95%B0%E7%BB%84) - - [Arrays 工具](#arrays-%E5%B7%A5%E5%85%B7) - - [数组拷贝](#%E6%95%B0%E7%BB%84%E6%8B%B7%E8%B4%9D) - - [数组的比较](#%E6%95%B0%E7%BB%84%E7%9A%84%E6%AF%94%E8%BE%83) - - [数组元素的比较](#%E6%95%B0%E7%BB%84%E5%85%83%E7%B4%A0%E7%9A%84%E6%AF%94%E8%BE%83) - - [数组排序](#%E6%95%B0%E7%BB%84%E6%8E%92%E5%BA%8F) - - [排序数组查找](#%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84%E6%9F%A5%E6%89%BE) -- [容器深入研究](#%E5%AE%B9%E5%99%A8%E6%B7%B1%E5%85%A5%E7%A0%94%E7%A9%B6) - - [填充容器](#%E5%A1%AB%E5%85%85%E5%AE%B9%E5%99%A8) - - [SortedSet](#sortedset) - - [队列](#%E9%98%9F%E5%88%97) - - [优先级队列](#%E4%BC%98%E5%85%88%E7%BA%A7%E9%98%9F%E5%88%97) - - [双向队列](#%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97) - - [LinkedHashMap](#linkedhashmap) - - [Colletions 工具类](#colletions-%E5%B7%A5%E5%85%B7%E7%B1%BB) - - [排序和查询](#%E6%8E%92%E5%BA%8F%E5%92%8C%E6%9F%A5%E8%AF%A2) - - [只读容器](#%E5%8F%AA%E8%AF%BB%E5%AE%B9%E5%99%A8) - - [Collection 和 Map 的同步控制](#collection-%E5%92%8C-map-%E7%9A%84%E5%90%8C%E6%AD%A5%E6%8E%A7%E5%88%B6) - - [快速报错机制](#%E5%BF%AB%E9%80%9F%E6%8A%A5%E9%94%99%E6%9C%BA%E5%88%B6) - - [Java 1.0/1.1 的容器](#java-1011-%E7%9A%84%E5%AE%B9%E5%99%A8) - - [BitSet](#bitset) -- [Java I/O 系统](#java-io-%E7%B3%BB%E7%BB%9F) - - [输入和输出](#%E8%BE%93%E5%85%A5%E5%92%8C%E8%BE%93%E5%87%BA) - - [InputStream 和 OutputStream](#inputstream-%E5%92%8C-outputstream) - - [Reader 和 Writer](#reader-%E5%92%8C-writer) - - [组合输入输出流过滤器](#%E7%BB%84%E5%90%88%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA%E6%B5%81%E8%BF%87%E6%BB%A4%E5%99%A8) - - [文本输入和输出](#%E6%96%87%E6%9C%AC%E8%BE%93%E5%85%A5%E5%92%8C%E8%BE%93%E5%87%BA) - - [以文本格式存储对象](#%E4%BB%A5%E6%96%87%E6%9C%AC%E6%A0%BC%E5%BC%8F%E5%AD%98%E5%82%A8%E5%AF%B9%E8%B1%A1) - - [字符编码方式](#%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81%E6%96%B9%E5%BC%8F) - - [读写二进制数据](#%E8%AF%BB%E5%86%99%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E6%8D%AE) - - [DataInput 和 DataOutput](#datainput-%E5%92%8C-dataoutput) - - [随机访问文件](#%E9%9A%8F%E6%9C%BA%E8%AE%BF%E9%97%AE%E6%96%87%E4%BB%B6) - - [序列化](#%E5%BA%8F%E5%88%97%E5%8C%96) - - [操作文件](#%E6%93%8D%E4%BD%9C%E6%96%87%E4%BB%B6) - - [目录列表器](#%E7%9B%AE%E5%BD%95%E5%88%97%E8%A1%A8%E5%99%A8) - - [](#) -- [枚举类型](#%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B) - - [基本 enum 特性](#%E5%9F%BA%E6%9C%AC-enum-%E7%89%B9%E6%80%A7) - - [向 enum 添加新方法](#%E5%90%91-enum-%E6%B7%BB%E5%8A%A0%E6%96%B0%E6%96%B9%E6%B3%95) - - [覆盖 enum 的方法](#%E8%A6%86%E7%9B%96-enum-%E7%9A%84%E6%96%B9%E6%B3%95) - - [Switch 语句中的 enum](#switch-%E8%AF%AD%E5%8F%A5%E4%B8%AD%E7%9A%84-enum) - - [EnumSet](#enumset) - - [源码解析](#%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90) - - [EnumMap](#enummap) -- [注解](#%E6%B3%A8%E8%A7%A3) - - [基本语法](#%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95) - - [自定义注解](#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%B3%A8%E8%A7%A3) - - [元注解](#%E5%85%83%E6%B3%A8%E8%A7%A3) - - [编写注解处理器](#%E7%BC%96%E5%86%99%E6%B3%A8%E8%A7%A3%E5%A4%84%E7%90%86%E5%99%A8) - - [注解综合](#%E6%B3%A8%E8%A7%A3%E7%BB%BC%E5%90%88) - - - -## 基础知识 - -### 数据类型 - -#### 整型 - -| 类型 | 存储空间 | -| ----- | -------- | -| int | 4字节 | -| short | 2字节 | -| long | 8字节 | -| byte | 1字节 | - -byte 的取值范围是-128~127。整型的取值范围和运行 Java 代码的机器无关。Java 没有无符号的整型。 - -**数据类型表示:** - -长整型:400L - -十六进制:0xCA - -八进制:020(有前缀0) - -二进制:0b1001(Java 7及以上版本) - -从 Java 7开始,可以为数据字面量添加下划线,如1_000_000,方便易读。Java 编译器会去除这些下划线。 - -#### 浮点类型 - -| 类型 | 存储空间 | -| ------ | -------- | -| float | 4字节 | -| double | 8字节 | - -float 类型的数值会有后缀 F 或者 f (2.01F),没有后缀 F 的浮点数默认为 double 类型(也可以加后缀 D)。浮点数采用二进制系统表示,有些数值会有精度损失,如`System.out.println(2.0 - 1.1)`会输出0.8999999999999999。如果数值计算不允许有任何精度误差,应该使用 BigDecimal。 - -**特殊值** - -正无穷大:Double.POSITIVE_INFINITY - -负无穷大:Double.NEGATIVE_INFINITY - -NaN(不是一个数字):Double.NaN - -#### char 类型 - -Unicode 字符用一个或者两个 char 值描述。Unicode 字符可以表示为十六进制,其范围从/u0000到/uffff。 - -#### boolean 类型 - -布尔类型有两个值:true 和 false。布尔型和整型不能相互转化。Java 语言表达式所操作的 boolean 值,在编译之后都使用Java虚拟机中的int数据类型来代替,而 boolean 数组将会被编码成 Java 虚拟机的 byte 数组,每个元素 boolean 元素占8位”。这样我们可以得出 boolean 类型占了单独使用是4个字节,在数组中又是1个字节。 - -#### 大数值 - -BigInteger 实现了任意精度的整数运算,BigDecimal 实现了任意精度的浮点数运算。 - -```java -BigInteger a = BigInteger.valueOf(100); -BigInteger b = BigInteger.valueOf(100); -BigInteger c = a.add(b); -BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); -``` - -### 操作符 - -**截尾和舍入** - -```java -float num = 0.7f; -//类型转化对数字进行截尾 -System.out.println((int)num); //0 -//四舍五入操作 -System.out.println(Math.round(num)); //1 -``` - -### 注释文档 - -javadoc 用来提取注释的工具。javadoc 只能为 public 和 protected 成员进行文档注释。 - -### 代码规范 - -可以采用将 public 成员放在开头,后面跟着 protect、包访问权限和 private 成员的创建类的形式,方便类的使用者阅读最为重要的部分(public成员)。 - -```java -public class OrganizedByAccess { - public void method1() {} - public void method2() {} - protected void method3() {} - void method4() {} - private method5() {} - private int i; - //... -} -``` - - - -## 控制执行流程 - -### switch - -break 会跳出 switch 语句,没有 break 语句则会一直往下执行。 - -```java -int i = 1; -switch (i) { - case 0: - System.out.println("i = 0"); - break; - case 1: - System.out.println("i = 1"); - break; - case 2: - System.out.println("i = 2"); - break; - default: - System.out.println("i >= 3"); -} -//输出i = 1 - -switch (i) { - case 0: - System.out.println("i = 0"); - //break; - case 1: - System.out.println("i = 1"); - //break; - case 2: - System.out.println("i = 2"); - //break; - default: - System.out.println("i >= 3"); -} -//输出 -//i = 1 -//i = 2 -//i >= 3 -``` - -### break 和 continue 实现 goto - -```java -public class LabeledFor { - public static void main(String[] args) { - int i = 0; - outer: // Can't have statements here - for(; true ;) { // infinite loop - inner: // Can't have statements here - for(; i < 10; i++) { - print("i = " + i); - if(i == 2) { - print("continue"); - continue; - } - if(i == 3) { - print("break"); - i++; - break;//break跳出循环,不会执行递增操作 - } - if(i == 7) { - print("continue outer"); - i++; - continue outer;//continue跳到循环顶部,不会执行递增操作 - } - if(i == 8) { - print("break outer"); - break outer; - } - for(int k = 0; k < 5; k++) { - if(k == 3) { - print("continue inner"); - continue inner; - } - } - } - } - // Can't break or continue to labels here - } -} -``` - - - -## 初始化和清理 - -### 成员初始化 - -局部变量未初始化就使用,会产生编译时错误。类的数据成员是基本类型,它们会有初值(如 int 类型的数据成员初值为0)。 - -### 可变参数列表 - -Java SE5 开始支持可变参数列表。当指定参数时,编译器会将元素列表转换为数组。 - -```java -public class VarArgs { - public static void printArray(Object... args) { - for(Object obj : args) { - System.out.print(obj + " "); - } - System.out.println(); - } - - public static void main(String[] args) { - printArray(new Integer(1), new Integer(3)); - printArray("tyson", "sophia", "tom"); - printArray(new Integer[]{2, 4, 6}); - printArray();//空列表 - } -} -``` - -## 访问权限控制 - -package 语句必须是文件中第一行非注释代码。 - -### 访问权限修饰词 - -1. 包访问权限 - - 默认的访问权限没有任何关键字,通常是指包访问权限,即当前包的所有其他类对当前成员具有访问权限。 - -2. protected - - 继承访问权限,派生类可以访问基类的 protected 元素。同时,protected 也提供包访问权限,即相同包内的其他类也可以访问 protected 元素。 - -3. private - - 通过 private 隐藏了代码细节,类库设计者可以更改类内部工作方式,而不会影响客户端。 - -4. public - - - -示例代码: - -```java -package com.tyson.chapter6_access; -//Cookie.java -public class Cookie { - public Cookie() { - System.out.println("Cookie constructor"); - } - protected void bite() { - System.out.println("bite"); - } -} - -//ChocolateChip.java -public class ChocolateChip extends Cookie { - //私有构造器 - private ChocolateChip() { - System.out.println("chocolate chip constructor"); - } - //通过静态方法创建类 - public static ChocolateChip createChocolateChip() { - return new ChocolateChip(); - } - - public void chomp() { - //访问父类的protected成员 - bite(); - } -} - -class Application { - public static void main(String[] args) { - ChocolateChip chocolateChip = ChocolateChip.createChocolateChip(); - chocolateChip.chomp(); - } -} -``` - - - -## 复用类 - -### 继承语法 - -派生类如果没有指定某个基类的构造器,则会调用基类的默认构造器,若没有默认的构造器(不带参数的构造器),编译器会报错。 - -### final 关键字 - -1. final 数据 - - 基本类型变量用 final 修饰,则该变量是常量,数值不能改变;对象引用用 final 修饰,则一旦该引用被初始化指向一个对象,就不能再把它改为指向另一个对象,不过对象自身可以修改,只是引用不能修改。 - -2. final 参数 - - 将参数指明为 final,则无法在方法中更改参数。 - -```java -public class FinalArguments { - void add(final Num num) { - //num = new Num(1); //不能更改final参数 - num.i++; - } - void add(final int i) { - //i++; //不能更改final参数的值 - } -} - -class Num { - int i; - public Num(int i) { - this.i = i; - } -} -``` - -3. final 方法 - - final 方法不能被重写,使用 final 方法主要是为了防止继承类修改它的含义。过去使用 final 方法效率会高点,现在的 JVM 相比之前有了很大的优化,没必要通过设置 final 来提高效率。 - -4. final 类 - - final 类不能被继承。final 类的所有方法都被隐式指定为 final。JDK 的 String 类就是 final 类。 - -### 初始化及类的加载 - -#### 继承与初始化 - -```java -class Insect { - private int i = printInit("Insect.i initialized"); - protected int j; - Insect() { - System.out.println("i = " + i + ", j = " + j); - j = 39; - } - private static int x1 = - printInit("static Insect.x1 initialized"); - static int printInit(String s) { - System.out.println(s); - return 47; - } -} - -public class Beetle extends Insect { - private int k = printInit("Beetle.k initialized"); - public Beetle() { - System.out.println("k = " + k + ", j = " + j); - } - private static int x2 = - printInit("static Beetle.x2 initialized"); - public static void main(String[] args) { - System.out.println("Beetle constructor"); - Beetle b = new Beetle(); - } -} -/*output -static Insect.x1 initialized -static Beetle.x2 initialized -Beetle constructor -Insect.i initialized -i = 47, j = 0 -Beetle.k initialized -k = 47, j = 39 - */ -``` - - - -## 多态 - -封装把接口和实现分离开来,多态消除类型之间的耦合关系。 - -向上转型:将子类看作是它的基类的过程。子类转型为基类就是在继承图上向上移动。 - -![向上转型](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190402204948375-70203256.png) - -### 缺陷:“覆盖”私有方法 - -```java -public class PrivateOverride { - private void f() { - System.out.println("private f()"); - } - - public static void main(String[] args) { - PrivateOverride po = new Derived(); - po.f(); - } -} - -class Derived extends PrivateOverride { - public void f() { - System.out.println("public f()"); - } -} -/* -private f() - */ -``` - -Derived 类的 f() 实际上是一个全新的方法。基类的 f() 方法在子类 Derived 中不可见,因此不能被重载。 - -### 域和静态方法 - -只有普通的方法调用具备多态性,域和静态方法没有多态性。 - -### 构造器和多态 - -构造器实际上是 static 方法,只不过 static 声明是隐式的,不具备多态性。 - -#### 构造器的调用顺序 - -```java -class Meal { - private int weight = get(); - Meal() { - print("Meal()"); - } - public int get() { - System.out.println("Meal.get()"); - return 1; - } -} - -class Bread { - Bread() { - print("Bread()"); - } -} - -class Cheese { - Cheese() { - print("Cheese()"); - } -} - -class Lettuce { - Lettuce() { - print("Lettuce()"); - } -} - -class Lunch extends Meal { - private int price = food(); - Lunch() { - print("Lunch()"); - } - public int food() { - System.out.println("Lunch.food()"); - return 1; - } -} - -class PortableLunch extends Lunch { - PortableLunch() { - print("PortableLunch()"); - } -} - -public class Sandwich extends PortableLunch { - private Bread b = new Bread(); - private Cheese c = new Cheese(); - private Lettuce l = new Lettuce(); - - public Sandwich() { - print("Sandwich()"); - } - - public static void main(String[] args) { - new Sandwich(); - } -} /* Output: -Meal.get() -Meal() -Lunch.food() -Lunch() -PortableLunch() -Bread() -Cheese() -Lettuce() -Sandwich() -*/ - -``` - -调用顺序: - -1. 按照声明顺序调用基类成员的初始化方法 -2. 基类构造器。从最顶层的基类到最底层的派生类 -3. 按照声明顺序调用成员的初始化方法 -4. 派生类构造器 - - - -## 接口 - -接口和内部类为我们提供了一种将接口与实现分离的方法。 - -### 抽象类 - -从一个抽象类继承,需要为基类中的所有方法提供方法定义。如果没有提供,则派生类也是抽象类。 - -如果有一个类,让其包含任何 abstract 方法都没有实际意义,而我们又想阻止产生这个类的任何对象,这时候可以把它设置成一个没有任何抽象方法的抽象类。 - -### 接口的域 - -接口中的域都自动声明为`public static final`,故接口可以用来创建常量组。Java SE5之前没有提供 enum 实现,可以通过接口实现 enum 的功能。 - -## 内部类 - -内部类允许把一些逻辑相关的类组织在一起,并控制内部类的可视性。内部类可以访问外围类,有些通过编写内部类可以让代码结构更清晰。 - -### .this 和 .new - -使用外部类的名字后面紧跟 .this,可以生成对外部类对象的引用。 - -```java -public class DotThis { - void f() { - System.out.println("DotThis.f()"); - } - public class Inner { - public DotThis outer() { - return DotThis.this; - } - } - public Inner inner() { - return new Inner(); - } - - public static void main(String[] args) { - DotThis dt = new DotThis(); - DotThis.Inner dti = dt.inner(); - dti.outer().f(); - } -} -//output DotThis.f() -``` - -使用 .new 创建内部类的对象。 - -```java -public class DotNew { - public class Inner {} - - public static void main(String[] args) { - DotNew dn = new DotNew(); - DotNew.Inner dni = dn.new Inner(); - } -} -``` - -创建内部类的对象,必须通过外部类的对象来创建,在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会隐式连接到创建它的外部类对象上。但是如果创建的是嵌套类(静态内部类),则不需要创建外部类对象。 - -### 匿名内部类 - -```java -//Wrapping.java -public class Wrapping { - private int i; - - public Wrapping(int x) { - i = x; - } - public int value() { - return i; - } -} - -//Parcel.java -public class Parcel { - public Wrapping wrapping(int x) { - return new Wrapping(x) {//传递构造器参数 - @Override - public int value() { - return super.value() * 47; - } - }; - } - - public static void main(String[] args) { - Parcel p = new Parcel(); - Wrapping w = p.wrapping(8); - System.out.println(w.value()); - } -} -//output 376 -``` - -在匿名类定义字段时,还能够对其执行初始化操作。 - -```java -//Destination.java -public interface Destination { - String readLabel(); -} - -//Parcel1.java -public class Parcel1 { - public Destination destination(final String dest) { - return new Destination() { - private String label = dest; - @Override - public String readLabel() { - return label; - } - }; - } - - public static void main(String[] args) { - Parcel1 p1 = new Parcel1(); - Destination d = p1.destination("Puning"); - System.out.println(d.readLabel()); - } -} -``` - -匿名内部类没有命名构造器,通过实例初始化给匿名内部类创建构造器的效果。 - -```java -abstract class Base { - public Base(int i) { - System.out.println("Base constructor, i = " + i); - } - - public abstract void f(); -} - -public class AnonymousConstructor { - public static Base getBase(int i) { - return new Base(i) { - //构造器的效果 - { - System.out.println("Inside instance initializer"); - } - @Override - public void f() { - System.out.println("In anonymous f()"); - } - }; - } - - public static void main(String[] args) { - Base base = getBase(47); - base.f(); - } -} /* Output: -Base constructor, i = 47 -Inside instance initializer -In anonymous f() -*/ -``` - -#### 工厂方法 - -使用匿名内部类改进工厂方法。 - -```java -interface Game { boolean move();} -interface GameFactory { Game getGame();} - -public class Games { - public static void playGame(GameFactory gameFactory) { - Game game = gameFactory.getGame(); - while(game.move()) { - ; - } - } - - public static void main(String[] args) { - playGame(Checkers.factory); - playGame(Chess.factory); - } -} - -class Chess implements Game { - - private Chess() {} - private int moves = 0; - private static final int MOVES = 4; - - @Override - public boolean move() { - System.out.println("chess move " + moves); - return ++moves != MOVES; - } - - public static GameFactory factory = new GameFactory() { - @Override - public Game getGame() { - return new Chess(); - } - }; -} - -class Checkers implements Game { - private Checkers() {} - private int moves = 0; - private static final int MOVES = 3; - @Override - public boolean move() { - System.out.println("checkers move" + moves); - return ++moves != MOVES; - } - - public static GameFactory factory = new GameFactory() { - @Override - public Game getGame() { - return new Checkers(); - } - }; -} -/* -checkers move0 -checkers move1 -checkers move2 -chess move 0 -chess move 1 -chess move 2 -chess move 3 - */ -``` - -### 嵌套类 - -将内部类声明为 static,即为嵌套类。普通的内部类隐式保存了一个指向外围类对象的引用,而嵌套类没有。所以创建嵌套类的对象,不需要先创建外围类对象。并且嵌套类不能访问非静态的外围类的成员。 - -普通的内部类不能包含 static 字段和 static 方法,也不能包含嵌套类,但嵌套类可以包含这些。 - -```java -public class NestingClass { - public static class StaticInner { - public void dynamicFuc() { - System.out.println("StaticInner动态方法"); - } - public static void staticFuc() { - System.out.println("StaticInner静态方法"); - } - } - - public static void main(String[] args) { - StaticInner si = new StaticInner(); - si.dynamicFuc(); - StaticInner.staticFuc();//直接通过类名调用静态方法 - } -} -/* -StaticInner动态方法 -StaticInner静态方法 - */ -``` - -嵌套类没有.this引用。 - -### 为什么需要内部类 - -使用内部类可以实现多重继承。 - -```java -interface Selector { - boolean end(); - Object current(); - void next(); -} - -public class Sequence { - private Object[] items; - private int next = 0; - - public Sequence(int size) { - items = new Object[size]; - } - - public void add(Object x) { - if (next < items.length) - items[next++] = x; - } - - private class SequenceSelector implements Selector { - private int i = 0; - - public boolean end() { - return i == items.length; - } - public Object current() { - return items[i]; - } - public void next() { - if (i < items.length) i++; - } - } - - public Selector selector() { - return new SequenceSelector(); - } - - public static void main(String[] args) { - Sequence sequence = new Sequence(10); - for (int i = 0; i < 10; i++) - sequence.add(Integer.toString(i)); - Selector selector = sequence.selector(); - while (!selector.end()) { - System.out.print(selector.current() + " "); - selector.next(); - } - } -} /* Output: -0 1 2 3 4 5 6 7 8 9 -*/ -``` - -如果 Sequence.java 不使用内部类,就必须声明 Sequence 是一个 Selector,对于某个特定的 Sequence 只能有一个 Selector。使用内部类的话很容易就可以拥有另一个方法 reverseSelector(),用来生成一个反向遍历序列的 Selector。 - -### 局部内部类 - -```java -interface Counter {int next();} - -public class LocalInnerClass { - private int count = 0; - - Counter getCounter(final String name) { - // A local inner class: - class LocalCounter implements Counter { - public LocalCounter() { - // Local inner class can have a constructor - print("LocalCounter()"); - } - - public int next() { - printnb(name); // Access local final - return count++; - } - } - return new LocalCounter(); - } - - // The same thing with an anonymous inner class: - Counter getCounter2(final String name) { - return new Counter() { - // Anonymous inner class cannot have a named - // constructor, only an instance initializer: - { - print("Counter()"); - } - - public int next() { - printnb(name); // Access local final - return count++; - } - }; - } - - public static void main(String[] args) { - LocalInnerClass lic = new LocalInnerClass(); - Counter c1 = lic.getCounter("Local inner "), - c2 = lic.getCounter2("Anonymous inner "); - print(c1.next()); - print(c2.next()); - } -} /* Output: -LocalCounter() -Counter() -Local inner 0 -Anonymous inner 1 -*/ -``` - -局部内部类可以拥有命名的构造器或者重载构造器,而匿名内部类只能使用实例初始化。如果需要不止一个该内部类对象,那么只能使用局部内部类。 - -### 内部类标识符 - -内部类文件的命名格式:外围类名字加上"$",再加上内部类的名字(如果是匿名类,则是一个数字)。 - -`LocalInnerClass$1LocalCounter.class` - -`LocalInnerClass$1.class` - - - -## 容器 - -![容器分类](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190407155626374-53010434.png) - -### 添加一组元素 - -```java -public class AddingGroups { - public static void main(String[] args) { - Collection nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4)); - Integer[] ints = {5, 6, 7}; - nums.addAll(Arrays.asList(ints)); - Collections.addAll(nums, 8, 9); - Collections.addAll(nums, ints); - - List list = Arrays.asList(1, 2, 3); - list.set(1, 90); - System.out.println(list.getClass()); - //底层是数组java.util.Arrays$ArrayList,不能修改结构,java.lang.UnsupportedOperationException - //list.add(7); - } -} -``` - -`Arrays.asList()`返回类型是`java.util.Arrays$ArrayList`,底层是数组,试图删除或增加元素会抛异常 UnsupportedOperationException。 - -### 迭代器 - -```java -import typeinfo.pets.Pet; -import typeinfo.pets.Pets; -import java.util.Iterator; -import java.util.List; - -public class SimpleIteration { - public static void main(String[] args) { - List pets = Pets.arrayList(8); - Iterator it = pets.iterator(); - while(it.hasNext()) { - Pet p = it.next(); - System.out.print(p.id() + ":" + p + " "); - } - System.out.println(); - for(Pet p : pets) { - System.out.print(p.id() + ":" + p + " "); - } - System.out.println(); - it = pets.iterator(); - for(int i = 0; i < 4; i++) { - it.next(); - it.remove();//删除最近遍历的元素 - } - System.out.println(pets); - } -} -/*output -0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx -0:Rat 1:Manx 2:Cymric 3:Mutt 4:Pug 5:Cymric 6:Pug 7:Manx -[Pug, Cymric, Pug, Manx] - */ -``` - -ListIterator 是一个更为强大的 Iterator 子类型,它只能用于各种 List 类的遍历。ListIterator 可以双向移动。它还可以产生迭代器当前位置前一个和后一个元素的索引,并且可以使用 set() 方法修改最近遍历的元素。通过 listIterator(index) 可以创建指向索引为index的元素的ListIterator。 - -```java -public class ListIteration { - public static void main(String[] args) { - List pets = Pets.arrayList(8); - ListIterator it = pets.listIterator(); - while(it.hasNext()) { - System.out.print(it.next() + ", " + it.nextIndex() + - ", " + it.previousIndex() + "; "); - } - System.out.println(); - while(it.hasPrevious()) { - System.out.print(it.previous().id() + " "); - } - System.out.println(); - System.out.println(pets); - //创建指向索引为index元素处的ListIterator - it = pets.listIterator(3); - while(it.hasNext()) { - it.next(); - it.set(Pets.randomPet()); - } - System.out.println(pets); - } -}/*output -Rat, 1, 0; Manx, 2, 1; Cymric, 3, 2; Mutt, 4, 3; Pug, 5, 4; Cymric, 6, 5; Pug, 7, 6; Manx, 8, 7; -7 6 5 4 3 2 1 0 -[Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx] -[Rat, Manx, Cymric, Cymric, Rat, EgyptianMau, Hamster, EgyptianMau] - */ -``` - -### LinkedList - -```java -public class LinkedListFeatures { - public static void main(String[] args) { - LinkedList pets = - new LinkedList(Pets.arrayList(5)); - print(pets); - // Identical: - print("pets.getFirst(): " + pets.getFirst()); - print("pets.element(): " + pets.element()); - // Only differs in empty-list behavior: - print("pets.peek(): " + pets.peek()); - // Identical; remove and return the first element: - print("pets.remove(): " + pets.remove()); - print("pets.removeFirst(): " + pets.removeFirst()); - // Only differs in empty-list behavior: - print("pets.poll(): " + pets.poll()); - print(pets); - pets.addFirst(new Rat()); - print("After addFirst(): " + pets); - pets.offer(Pets.randomPet()); - print("After offer(): " + pets); - pets.add(Pets.randomPet()); - print("After add(): " + pets); - pets.addLast(new Hamster()); - print("After addLast(): " + pets); - print("pets.removeLast(): " + pets.removeLast()); - } -} /* Output: -[Rat, Manx, Cymric, Mutt, Pug] -pets.getFirst(): Rat -pets.element(): Rat -pets.peek(): Rat -pets.remove(): Rat -pets.removeFirst(): Manx -pets.poll(): Cymric -[Mutt, Pug] -After addFirst(): [Rat, Mutt, Pug] -After offer(): [Rat, Mutt, Pug, Cymric] -After add(): [Rat, Mutt, Pug, Cymric, Pug] -After addLast(): [Rat, Mutt, Pug, Cymric, Pug, Hamster] -pets.removeLast(): Hamster -*/ -``` - -LinkedList 具有实现栈所有功能的所有方法,可以将 LinkedList 作为栈使用: - -```java -public class MyStack { - private LinkedList storage = new LinkedList<>(); - public void push(T t) { - storage.addFirst(t); - } - public T pop() { - return storage.removeFirst(); - } - public T peek() { - return storage.getFirst(); - } - public boolean isEmpty() { - return storage.isEmpty(); - } - @Override - public String toString() { - return storage.toString(); - } -} -``` - -### Set - -Set 不保存重复的元素,插入相同的元素会被忽略。HashSet 存储元素没有顺序;TreeSet 按照升序的方式存储元素;LinkedHashList 使用链表维护元素的插入顺序,并通过散列提供了快速访问能力。 - -```java -public class SortedSetOfInteger { - public static void main(String[] args) { - Random rand = new Random(47); - SortedSet set = new TreeSet<>(); - for (int i = 0; i < 1000; i++) { - set.add(rand.nextInt(30)); - } - System.out.println(set); - } -} -/*output -[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] - */ -``` - -TreeSet 对字母排序是按照字典序,大小写字母会被分到不同的组,如果想按照字母序排序,可以向 TreeSet 构造器传入 String.CASE_INSENTIVE_ORDER 比较器。 - -```java -public class SortedWords { - public static void main(String[] args) { - //往构造器传入String.CASE_INSENTIVE_ORDER 比较器 - Set words = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - String[] strs = {"hello", "world", "tyson", "a"}; - words.addAll(Arrays.asList(strs)); - System.out.println(words); - } -} -/* -[a, hello, tyson, world] - */ -``` - -### Map - -HashMap 设计用来快速访问;TreeMap保持键始终处于升序状态;LinkedHashMap 使用链表维护元素的插入顺序,并通过散列提供了快速访问能力。 - -```java -public class PetMap { - public static void main(String[] args) { - Map petMap = new HashMap<>(); - petMap.put("Tyson", new Cat("cat")); - petMap.put("Sophia", new Dog("dog")); - petMap.put("Tom", new Hamster("hamster")); - - System.out.println(petMap.keySet()); - System.out.println(petMap.values()); - for(String person : petMap.keySet()) { - System.out.println(person + " : " + petMap.get(person)); - } - - Set> entries = petMap.entrySet(); - for(Map.Entry entry : entries) { - System.out.println(entry.getKey() + ": " + entry.getValue()); - entry.setValue(new Cat("cat")); - System.out.println(entry.getKey() + ": " + entry.getValue()); - } - - } -} -/*output -[Tom, Tyson, Sophia] -[Hamster hamster, Cat cat, Dog dog] -Tom : Hamster hamster -Tyson : Cat cat -Sophia : Dog dog -Tom: Hamster hamster -Tom: Cat cat -Tyson: Cat cat -Tyson: Cat cat -Sophia: Dog dog -Sophia: Cat cat - */ -``` - -### Queue - -LinkedList 实现了 Queue 接口,可以作为队列使用。 - -```java -public class QueueDemo { - public static void printQ(Queue queue) { - while(queue.peek() != null) { - System.out.print(queue.remove() + " "); - } - System.out.println(); - } - public static void main(String[] args) { - Queue queue = new LinkedList<>(); - Random rand = new Random(47); - for(int i = 0; i < 10; i++) { - queue.offer(rand.nextInt(i + 10)); - } - printQ(queue); - Queue qc = new LinkedList<>(); - for(char c : "tyson".toCharArray()) { - qc.offer(c); - } - printQ(qc); - } -} -``` - -peek()和 element()都将在不移除的情况下返回对头,peek()方法在队列为空时返回 null,而element()会跑 NoSuchElementExeception 异常。 - -poll()和remove()方法将移除并返回对头,poll()在队列为空时返回 null,而remove()会跑 NoSuchElementExeception 异常。 - -#### PriorityQueue - -Integer、String 和 Character 内建了自然排序,可以与 PriorityQueue 一起工作。要想在 PriorityQueue 中使用自己的类,就必须提供自己的 Comparator。 - -```java -public class PriorityQueueDemo { - public static void main(String[] args) { - PriorityQueue priorityQueue = - new PriorityQueue(); - Random rand = new Random(47); - for(int i = 0; i < 10; i++) - priorityQueue.offer(rand.nextInt(i + 10)); - QueueDemo.printQ(priorityQueue); - - List ints = Arrays.asList(25, 22, 20, - 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25); - priorityQueue = new PriorityQueue(ints); - QueueDemo.printQ(priorityQueue); - priorityQueue = new PriorityQueue( - ints.size(), Collections.reverseOrder()); - priorityQueue.addAll(ints); - QueueDemo.printQ(priorityQueue); - - String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION"; - List strings = Arrays.asList(fact.split("")); - PriorityQueue stringPQ = - new PriorityQueue(strings); - QueueDemo.printQ(stringPQ); - stringPQ = new PriorityQueue( - strings.size(), Collections.reverseOrder()); - stringPQ.addAll(strings); - QueueDemo.printQ(stringPQ); - - Set charSet = new HashSet(); - for(char c : fact.toCharArray()) - charSet.add(c); // Autoboxing - PriorityQueue characterPQ = - new PriorityQueue(charSet); - QueueDemo.printQ(characterPQ); - } -} /* Output: -0 1 1 1 1 1 3 5 8 14 -1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 -25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 - A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W -W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A - A B C D E F H I L N O S T U W -*///:~ -``` - -### foreach 和 迭代器 - -Java SE5引入了 Iterable 接口,该接口包含了一个能够产生 Iterator 的 iterator()方法。任何实现了 Iterable 的类(如Collection)都可以应用于 foreach 语句中。 - -```java -public class IterableClass implements Iterable { - protected String[] words = {"hello", "world", "program"}; - @Override - public Iterator iterator() { - return new Iterator() { - private int index = 0; - @Override - public boolean hasNext() { - return index < words.length; - } - @Override - public String next() { - return words[index++]; - } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - public static void main(String[] args) { - for(String s : new IterableClass()) { - System.out.print(s + " "); - } - } -} -``` - -### 适配器方法 - -添加反向迭代器功能。 - -```java -public class ReversibleArrayList extends ArrayList { - public ReversibleArrayList(Collection c) { super(c); } - public Iterable reversed() { - return new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - int current = size() - 1; - @Override - public boolean hasNext() { return current > -1; } - @Override - public T next() { return get(current--); } - }; - } - }; - } - public static void main(String[] args) { - ReversibleArrayList ral = - new ReversibleArrayList<>(Arrays.asList(new String[]{"hello", "world", "yes"})); - for(String s : ral.reversed()) { - System.out.print(s + " "); - } - } -} -``` - - - -## 异常、断言和日志 - -Java 语言给出了三种处理系统错误的机制:抛出异常、日志和使用断言。 - -### 异常分类 - -所有异常都有 Throwable 继承而来。 - -![异常层次结构](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190420203248534-1858171357.png) - - - -Error 类描述 Java 运行时系统的内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象。Error是程序无法处理的错误。 - -RuntimeException由程序错误导致,如果发生了 Runtime Exception,那一定是你的问题,应该修正程序避免这类异常发生。常见的运行时异常有: - -```java -ClassCastException -IndexOutOfBoundsException -NullPointerException -ArrayStoreException -NumberFormatException -ArithmeticException -``` - -其他 Exception 由具体的环境,如读取的文件不存在、文件为空、sql异常、寻找不存在的 Class 对象等导致的异常。 - -Java 语言规范将派生于 Error 类或 Runtime Exception 类的异常称为非检查(unchecked)异常;其他所有异常称为检查(checked)异常,编译器将会核查是否为所有的检查异常提供异常处理器。 - -### 声明异常 - -一个方法必须声明所有可能抛出的检查异常。任何抛出异常的方法都可能是一个死亡陷阱,如果没有异常处理器捕获这个异常,当前线程就会结束。非检查异常要么不可控,要么应当避免发生。 - -如果在子类覆盖了父类的方法,在子类方法中可以抛出更特定的异常,或者不抛异常。如果父类方法没有抛出异常,则子类只能在方法内捕获所有检查异常,不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。 - -### 捕获异常 - -捕获多个异常可以合并 catch 子句(异常类型不存在子类关系)。 - -```java -try { - // -} catch (FileNotFindException | UnknowHostException e) { - e.printStackTrace(); -} -``` - -解耦合 try/catch 和 try/finally 语句块,这种设计方式不仅代码清晰,而且还会报告 finally 子句出现的错误。 - -```java -InputStream in = ...; -try { - try { - // - } finally { - in.close(); - } -} catch (IOException ex) { - ex.printStackTrace(); -} -``` - -### 带资源的 try 语句 - -```java -try (Scanner in = new Scanner(new FileInputStream("e:\data"), "UTF-8"); - PrintWriter out = new PrinterWriter("out.txt")) { - while (in.hasNext()) { - System.out.println(in.next().toUpperCase()); - } -} -``` - -Java SE7 引入了带资源的 try 语句,当语句正常退出或者有异常,都会调用`in.close()`方法。如果`in.close()`方法也抛出异常,则原来的异常会重新抛出,而close方法抛出的异常会被抑制,通过调用 getSuppressed 方法可以得到从close 方法抛出并被抑制的异常列表。 - -### 断言 - -```java -int x = -1; -if(x < 0) { - throw new IllegalArgumentException("x < 0"); -} -double y = Math.sqrt(x); -``` - -这段检查代码会一直保留在程序,如果程序中含有大量这样的检查,程序运行起来将会很慢。 - -断言允许在测试期间向代码插入一些检查语句。当代码发布时,这些检查语句将会被自动的移走。 - -Java 语言引入了关键字 assert,assert 有两种形式: - -```java -assert condition; -assert condition: expression;//expression用于产生消息字符串 -``` - -这两种形式都会对条件进行检测,如果结果是 false,则会抛出 AssertError 异常。第二种形式中,表达式将会被传入 AssertError 的构造器,并转化成一个消息字符串。 - -断言 x 是一个非负数值: - -```java -assert x >= 0; -assert x >= 0 : x;//将x的值传递给AssertError异常 -``` - -断言只应该用在测试阶段确定程序内部的错误位置。 - -#### 启用和禁用断言 - -默认情况下,断言被禁用,可以在运行程序时用`-eableassertions`或`-ea`选项启用。idea --> run configuration --> vm options 添加 `-ea` 开启断言。 - -`java -enableassertions MyApp` - -启用和禁用断言是类加载器的功能,在启用和禁用断言时不必重新编译程序。当断言被禁用时,类加载器将跳过断言代码,因此不会降低程序的运行速度。 - -在某个类和包使用断言:`java -ea:MyClass -ea:com.tyson.chapter12 MyApp` MyClass 类和 com.tyson.chapter12 包以及子包的所有类都会启用断言。 - -使用选项`-dableassertions`或`-da`禁用某个类和包的断言。 - -### 日志 - -#### logback - -``的三个属性 - -- scan:默认值为true,配置文件发生更改时会重新加载。 -- scanPeriod:设置监控配置文件的时间间隔,默认单位是毫秒。 -- debug:默认值是false,打印logback内部日志信息,实时查看logback运行状态。 - -``的子标签 -root是根``,只有level属性,默认为DEBUG。 -logger关联包或者具体的类到appender,可以定义日志类型、级别。 -Appender主要用于指定日志输出的目的地,如控制台、文件、数据库。 - -``` - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - -``` -日志级别:ERROR>WARN>INFO>DEBUG>TRACE - -Layout用于自定义日志输出格式。 -``` - - - -``` - - - -## 字符串 - -String 对象是不可变的。String 类中会修改 String 的方法都会创建一个全新的 String 对象。 - -### 格式化输出 - -#### printf() 和 format() - -```java -public class SimpleFormat { - public static void main(String[] args) { - int x = 5; - double y = 5.26; - System.out.format("Row 1: [%d %f]\n", x, y); - System.out.printf("Row 1: [%d %f]\n", x, y); - } -} -/*output -Row 1: [5 5.260000] -Row 1: [5 5.260000] - */ -``` - -#### Formatter - -```java -public class FormatterDemo { - public static void main(String[] args) { - Formatter f = new Formatter(System.out); - String item = "peas"; - int quatity = 3; - float price = 5.2f; - //-表示右对齐 - f.format("%-10s %5s %10s\n", "item", "quatity", "price"); - //-表示右对齐,%10.10s指宽度为10,最大输出字符为10 - f.format("%-10.10s %5d %10.2f\n", item, quatity, price); - //String.format() - System.out.println("------string.format()------"); - System.out.println(String.format("%-10.10s %5d %10.2f", item, quatity, price)); - } -} -/* -item quatity price -peas 3 5.20 -------string.format()------ -peas 3 5.20 - */ -``` - -String.format()接受与Formatter.format()一样的参数,它是通过创建一个 Formatter 对象实现的。 - -### 正则表达式 - -#### 基础 - -可能有一个负号在最前面:`-?` - -`\\`在 Java 表示插入一个正则表达式的反斜线 - -表示一个数字:`\\d` - -插入普通的反斜线:`\\\\` - -`+`表示一个或多个之前的表达式 - -可能有一个负号,后面跟着一个或多个数字:`-?\\d+` - -```java -public class IntegerMatch { - public static void main(String[] args) { - System.out.println("-89".matches("-?\\d+")); - System.out.println("+98".matches("-?\\d+")); - //竖直线|代表或操作;+是特殊符号,需转义;括号可以将表达式分组 - System.out.println("+789".matches("(-|\\+)?\\d+")); - } -} -/*output -true -false -true - */ -``` - -String 自带一个正则表达式工具 split()方法,它可以将字符串从正则表达式匹配的地方切开。特殊字符如`.`和`|`需要用反斜杠转义,即`\\.`和`\\|`。 - -```java -public class Splitting { - public static String motto = "All in all, we all have a dream."; - public static void split(String regex) { - System.out.println(Arrays.toString(motto.split(regex))); - } - public static void main(String[] args) { - //.需要转义 - split("\\."); - split(" "); - //\\w+表示一个或多个非单词字符 - split("\\W+"); - //表示字母n后面跟着一个或多个非单词字符 - split("n\\W+"); - } -} -/*output -[All in all, we all have a dream, Thanks] -[All, in, all,, we, all, have, a, dream., Thanks.] -[All, in, all, we, all, have, a, dream, Thanks] -[All i, all, we all have a dream. Thanks.] - */ -``` - -替换功能。 - -```java -public class Replacing { - public static void main(String[] args) { - //小写的w表示字母 - System.out.println(Splitting.motto.replaceFirst("w\\w+", "they")); - System.out.println(Splitting.motto.replaceAll("all|dream", "apple")); - } -} -/*output -All in all, they all have a dream. -All in apple, we apple have a apple. - */ -``` - -#### 创建正则表达式 - -字符类表达式,JDK 文档中 java.util.regex.Pattern 页面有完整的表达式。 - -[jdk8文档](https://docs.oracle.com/javase/8/docs/api/index.html) - -[菜鸟教程正则表达式语法](http://www.runoob.com/java/java-regular-expressions.html) - -**常用的字符类表达式** - -| 字符 | 描述 | -| -------- | ------------------------------------------------------------ | -| . | 匹配除"\r\n"之外的任何单个字符 | -| + | 表示一个或多个之前的表达式,如\\\d+表示一个或多个数字 | -| * | 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,} | -| ? | 零次或一次匹配前面的字符或子表达式。如"do(es)?"匹配"do"或"does"中的"do"。? 等效于 {0,1} | -| [abc] | 包含a/b/c的任何字符(和a\|b\|c作用相同) | -| [^abc] | 除了a/b/c外的任何字符 | -| [a-zA-z] | a-z/A-Z的任何字符 | -| \s | 空白符(空格、tab、换行、换页和回车) | -| \S | 非空白符(\[^\s]) | -| \d | 数字0-9 | -| \D | 非数字(\[^0-9]) | -| \w | 字符[a-zA-Z0-9] | -| \W | 非字符 | - -**边界匹配符** - -| 符号 | 描述 | -| ---- | ---------- | -| ^ | 一行的起始 | -| $ | 一行的结束 | -| \b | 词的边界 | -| \B | 非词的边界 | - -**量词** - -![正则表达式量词](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190409100645126-737003966.png) - -Rudolph.java - -```java -public class Rudolph { - public static void main(String[] args) { - for(String pattern : new String[]{ "Rudolph", - "[rR]udolph", "[rR][aeiou][a-z]ol.*", "R.*" }) - System.out.println("Rudolph".matches(pattern)); - } -} /* Output: -true -true -true -true -*///:~ -``` - -#### Pattern 和 Matcher - -Pattern 和 Matcher 功能较 String 更为强大。Pattern.compile(String regex) 用来编译正则表达式,生成一个。 - -**find()** - -`Matcher.find()`可用来在 CharSequence 中查找多个匹配。find()可以像迭代器一样前向遍历输入的字符串。重载的 find(int index)能够接收一个整数参数,作为搜索起点。 - -```java -public class Finding { - //划分为单词 - public static Pattern WORD_PATTERN = Pattern.compile("\\w+"); - public static void main(String[] args) { - Matcher m = WORD_PATTERN.matcher("Everyone is born equally"); - //find()可用来查找多个匹配 - while(m.find()) { - System.out.print(m.group() + " "); - } - System.out.println(); - int i = 0; - //指定搜索起点 - while(m.find(i++)) { - System.out.print(m.group() + " "); - } - } -} -/*output -Everyone is born equally -Everyone veryone eryone ryone yone one ne e is is s born born orn rn n equally equally qually ually ally lly ly y - */ -``` - -**组** - -组使用括号划分的正则表达式,如`A(B(C))D`,组0是ABCD,组1是BC,组2是C。Matcher 对象提供了一系列方法,用以获取与组相关的信息: - -`public int groupCount()`返回该匹配器的分组数目(**不包含第0组**),如上面例子分组数目为2组(不包含第0组); - -`public String group()`返回前一次匹配操作(如find())的第0组(整个匹配); - -`public String group(int i)`返回前一次匹配操作指定的组号,如果没有匹配到输入字符串的任何部分,将会返回null。 - -```java -public class Groups { - public static final String POEM = - "Twas brillig, and the slithy toves\n" + - "Did gyre and gimble in the wabe.\n" + - "All mimsy were the borogoves,\n" + - "And the mome raths outgrabe.\n\n" + - "Beware the Jabberwock, my son,\n" + - "The jaws that bite, the claws that catch.\n" + - "Beware the Jubjub bird, and shun\n" + - "The frumious Bandersnatch.\n" + - "end\n"; //匹配失败 - //捕获每行最后的三个词 - public static Pattern LAST_THREE_WORD_PATTERN = Pattern.compile("(?m)(\\S+)\\s+((\\S+)\\s+(\\S+))$"); - - public static void main(String[] args) { - Matcher m = LAST_THREE_WORD_PATTERN.matcher(POEM); - while(m.find()) { - //有4个分组,不包括分组0,分组0是整个匹配 - for(int i = 0; i <= m.groupCount(); i++) { - System.out.print("[" + m.group(i) + "]"); - } - System.out.println(); - } - } -} -/* Output: -[the slithy toves][the][slithy toves][slithy][toves] -[in the wabe.][in][the wabe.][the][wabe.] -[were the borogoves,][were][the borogoves,][the][borogoves,] -[mome raths outgrabe.][mome][raths outgrabe.][raths][outgrabe.] -[Jabberwock, my son,][Jabberwock,][my son,][my][son,] -[claws that catch.][claws][that catch.][that][catch.] -[bird, and shun][bird,][and shun][and][shun] -[The frumious Bandersnatch.][The][frumious Bandersnatch.][frumious][Bandersnatch.] -*/ -``` - -默认情况下,^和$仅匹配输入的完整字符串的开始和结束。?m表示多行模式下,^和$分别匹配一行的开始和结束。 - -**start() 与 end()** - -在匹配操作成功之后,start()返回匹配的起始位置的索引,end()返回所匹配字符的最后字符的索引加一的值。 - -**Pattern 标记** - -Pattern 类的 compile()方法还有一个版本,可以接收一个标记参数,以调整匹配的行为: - -`Pattern Pattern.compile(String regex, int flag)` - -flag 来自以下 Pattern 类中的常量: - -![Pattern 标记参数](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190409103507798-1192565494.png) - -可以通过'|'(或)操作符组合多个标记的功能: - -```java -public class ReFlags { - public static Pattern WORD_PATTERN = Pattern.compile("^java", - Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); - public static void main(String[] args) { - Matcher m = WORD_PATTERN.matcher("java has regex\nJava has regex\n" + - "JAVA has pretty good regular expressions\n" + - "Regular expressions are in Java"); - while(m.find()) { - System.out.println(m.group()); - } - } -} -/* Output: -java -Java -JAVA -*/ -``` - -**split()** - -```java -public class SplitDemo { - public static void main(String[] args) { - String input = "This!!unusual use!!of exclamation!!points"; - print(Arrays.toString(Pattern.compile("!!").split(input))); - // 限制分割字符串的数量 - print(Arrays.toString(Pattern.compile("!!").split(input, 3))); - } -} /* Output: -[This, unusual use, of exclamation, points] -[This, unusual use, of exclamation!!points] -*/ -``` - -**替换操作** - -```java -public class TheReplacements { - public static void main(String[] args) throws Exception { - String s = TextFile.read("src/com/tyson/chapter13/string/regex/TheReplacements.java"); - // 匹配上面的注释 - Matcher mInput = Pattern.compile("/\\*!(.*)!\\*/", Pattern.DOTALL).matcher(s); - if (mInput.find()) { - s = mInput.group(1); // 捕捉正则表达式括号,组是括号划分的正则表达式 - } - - // Replace two or more spaces with a single space: - s = s.replaceAll(" {2,}", " "); - // Replace one or more spaces at the beginning of each - // line with no spaces. Must enable MULTILINE mode: - s = s.replaceAll("(?m)^ +", ""); - print(s); - s = s.replaceFirst("[aeiou]", "(VOWEL1)"); - StringBuffer sbuf = new StringBuffer(); - Pattern p = Pattern.compile("[aeiou]"); - Matcher m = p.matcher(s); - // Process the find information as you - // perform the replacements: - while (m.find()) - //将找到的元音字母转化为大写字母 - m.appendReplacement(sbuf, m.group().toUpperCase()); - // Put in the remainder of the text: - m.appendTail(sbuf); - print(sbuf); - } -} /* Output: -Here's a block of text to use as input to -the regular expression matcher. Note that we'll -first extract the block of text by looking for -the special delimiters, then process the -extracted block. -H(VOWEL1)rE's A blOck Of tExt tO UsE As InpUt tO -thE rEgUlAr ExprEssIOn mAtchEr. NOtE thAt wE'll -fIrst ExtrAct thE blOck Of tExt by lOOkIng fOr -thE spEcIAl dElImItErs, thEn prOcEss thE -ExtrActEd blOck. -*/ -``` - -**reset()** - -通过 reset() 方法,可以将 Matcher 对象应用于一个新的字符序列。 - -```java -public class Resetting { - public static void main(String[] args) throws Exception { - Matcher m = Pattern.compile("[frb][aiu][gx]").matcher("fix the rug with bags"); - while (m.find()) - System.out.print(m.group() + " "); - System.out.println(); - m.reset("fix the rig with rags"); - while (m.find()) - System.out.print(m.group() + " "); - } -} /* Output: -fix rug bag -fix rig rag -*/ -``` - -### 扫描输入 - -Java SE5新增了 Scanner 类,Scanner 的构造器可以接受任何类型的输入对象,包括 File 对象、InputStream、String 或者 Readable 对象。Scanner 所有的输入、分词以及翻译都隐藏在不用类型的 next 方法中。普通的 next()方法返回下一个 String 。 - -#### Scanner 定界符 - -默认情况下,Scanner 根据空白字符对输入进行分词,我们可以用正则表达式指定自己所需的定界符。 - -```java -public class ScannerDelimiter { - public static void main(String[] args) { - Scanner sc = new Scanner("12, 42, 85, 23"); - //使用逗号包括逗号前后的空白字符作为定界符 - sc.useDelimiter("\\s*,\\s*"); - while(sc.hasNextInt()) { - System.out.println(sc.nextInt()); - } - } -} -``` - -#### 用正则表达式扫描 - -```java -public class ThreatAnalyzer { - static String threatData = - "58.27.82.161@02/10/2005\n" + - "204.45.234.40@02/11/2005\n" + - "58.27.82.161@02/11/2005\n" + - "58.27.82.161@02/12/2005\n" + - "58.27.82.161@02/12/2005\n" + - "[Next log section with different data format]"; - - public static void main(String[] args) { - Scanner sc = new Scanner(threatData); - String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})"; - while(sc.hasNext(pattern)) { - sc.next(pattern); - MatchResult match = sc.match(); - String ip = match.group(1); - String date = match.group(2); - System.out.format("Thread on %s from %s\n", ip, date); - } - } -} -/*output -Thread on 58.27.82.161 from 02/10/2005 -Thread on 204.45.234.40 from 02/11/2005 -Thread on 58.27.82.161 from 02/11/2005 -Thread on 58.27.82.161 from 02/12/2005 -Thread on 58.27.82.161 from 02/12/2005 - */ -``` - -注意:配合正则表达式进行扫描时,正则表达式不要含有定界符(Scanner 默认定界符是空白符,根据空白符对输入进行分词)。 - - - -## 类型信息 - -通过运行时类型信息可以在程序运行时发现和使用类型信息。 - -`Class.forName("Gum")`类 Gum 没有被加载就加载它。 - -### 反射 - -类方法提取: - -```java -public class typeinfo { - //匹配类似包名的字符串 com.tyson.chapter - public static Pattern p = Pattern.compile("\\w+\\."); - public static void main(String[] args) { - try { - Class c = Class.forName("com.tyson.chapter10.innerclass.LocalInnerClass"); - Method[] methods = c.getMethods(); - for(Method m : methods) { - //去掉包名 - System.out.println(p.matcher(m.toString()).replaceAll("")); - } - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - } -} -/*output -public static void main(String[]) -public final void wait() throws InterruptedException -public final void wait(long,int) throws InterruptedException -public final native void wait(long) throws InterruptedException -public boolean equals(Object) -public String toString() -public native int hashCode() -public final native Class getClass() -public final native void notify() -public final native void notifyAll() - */ -``` - - - -## 泛型 - -泛型实现了参数化类型的概念,使代码可以应用于多种类型。使用泛型编写的程序比使用 Object 变量再进行强制类型转化的程序具有更好的安全性和可读性。 - -### 类型参数的好处 - -Java SE5泛型出现之前,ArrayList 是通过维护一个 Object 数组实现的。 - -```java -public class MyArrayList { - private Object[] elementData; - private int index = 0; - public MyArrayList() { elementData = new Object[8]; } - public Object get(int i) { return elementData[i]; } - public void add(Object o) { elementData[index++] = o; } - - public static void main(String[] args) { - MyArrayList list = new MyArrayList(); - list.add("tyson"); - System.out.println((String)list.get(0)); - list.add(new Cat()); - System.out.println((String)list.get(1)); - } -} -/*output -tyson -Exception in thread "main" java.lang.ClassCastException: typeinfo.pets.Cat cannot be cast to java.lang.String - at com.tyson.chapter15.generics.MyArrayList.main(MyArrayList.java:29) - */ -``` - -当获取值的时候需要进行强制类型转化,并且 add 操作可以放进任何类型的对象,编译和运行都不报错,当获取值进行强制类型转换时,可能会抛异常。 - -### 泛型类 - -```java -public class Holder { - private T a; - public Holder(T a) { this.a = a; } - public void set(T a) { this.a = a; } - public T get() { return a; } - - public static void main(String[] args) { - Holder h = new Holder<>("Tyson"); - System.out.println(h.get()); - } -} -//output Tyson -``` - -### 泛型接口 - -泛型也可以用于接口。 - -```java -//public interface Generator { T next(); } - -public class Fibonacci implements Generator { - private int count = 0; - @Override - public Integer next() { - return fib(count++); - } - public int fib(int n) { - if(n < 2) { return 1; } - return fib(n - 2) + fib(n - 1); - } - - public static void main(String[] args) { - Fibonacci fib = new Fibonacci(); - for(int i = 0; i < 10; i++) { - System.out.print(fib.next() + " "); - } - } -} -//output:1 1 2 3 5 8 13 21 34 55 -``` - -### 泛型方法 - -如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法。对于一个 static 方法而言,无法访问泛型类的类型参数,所以如果 static 方法需要使用泛化能力,就必须使其成为泛型方法。 - -定义泛型方法,只需将泛型参数列表放在返回值之前。 - -```java -public class GenericMethod { - public void f(T t) { System.out.println(t.getClass().getName()); } - - public static void main(String[] args) { - GenericMethod gm = new GenericMethod(); - //可省略,编译器自己可推断出 - gm.f(""); - gm.f(1); - gm.f(1.0); - gm.f(1.0f); - gm.f(gm); - } -} -/*output -java.lang.String -java.lang.Integer -java.lang.Double -java.lang.Float -com.tyson.chapter15.generics.GenericMethod - */ -``` - -GenericMethod 并不是参数化的,只有 f() 拥有参数类型。 - -#### 可变参数和泛型方法 - -向参数个数可变的方法传递一个泛型类型的实例。 - -```java -public class GenericVarArgs { - public static List makeList(T... args) { - List result = new ArrayList<>(); - for(T item : args) { - result.add(item); - } - return result; - } - - public static void main(String[] args) { - List l = makeList("a", "b"); - System.out.println(l); - } -} -//output:[a, b] -``` - -### 匿名内部类 - -```java -public class Customer { - public Customer() {} - public static Generator generator() { - return new Generator() { - @Override - public Customer next() { - return new Customer(); - } - }; - } - - public static void main(String[] args) { - System.out.println(Customer.generator().next()); - } -} -``` - -### 泛型擦除 - -```java -public class ErasedTypeEquvalence { - public static void main(String[] args) { - Class c1 = new ArrayList().getClass(); - Class c2 = new ArrayList().getClass(); - System.out.println(c1 == c2); - } -} -//output:true -``` - -`List`的类型变量没有显式的限定,擦除类型后变成原始的 List,故`List`和`List`是相同的类型。 - -```java -class Manipulator { - private T obj; - public Manipulator(T t) { obj = t; } - public void manipulate() { obj.f(); } -} -``` - -Manipulator 类有显式限定 HasF,故擦除类型后变成: - -```java -class Manipulato { - private HasF obj; - public Manipulator(HasF t) { obj = HasF; } - public void manipulate() { obj.f(); } -} -``` - -### 类型变量的限定 - -给定泛型类的边界,以此告知编译器只能接受遵循这个边界的类型。 - -```java -//HasF.java -public class HasF { - public void f() { - System.out.println("Has.f()"); - } -} - -//Manipulation.java -class Manipulator { - private T obj; - public Manipulator(T t) { obj = t; } - public void manipulate() { obj.f(); } -} - -public class Manipulation { - public static void main(String[] args) { - HasF hf = new HasF(); - Manipulator manipulator = new Manipulator<>(hf); - manipulator.manipulate(); - } -} -//output: Has.f() -``` - -### 擦除的问题 - -```java -class GenericBase { - private T element; - public void set(T arg) { element = arg; } - public T get() { return element; } - - public static void main(String[] args) { - - } -} - -class Derived extends GenericBase {} - -public class ErasureAndInheritance { - public static void main(String[] args) { - Derived d = new Derived(); - d.set(new Object()); - System.out.println(d.get()); - } -} -//output: java.lang.Object@6d6f6e28 -``` - -Derived 继承自 GenericBase,但是没有任何泛型参数,而编译器不会发出任何警告。 - -### 边界 - -在泛型的参数类型上设置限制条件,可以强制规定泛型可以应用的类型,从而可以按照自己的边界类型去调用方法。如果将泛型参数限制为某个类型子集,那么就可以用这些类型子集来调用方法。 - -```java -interface HasColor { Color getColor(); } - -class Colored { - T item; - Colored(T item) { this.item = item; } - T getItem() { return item; } - Color color() { return item.getColor(); } -} - -class Dimension { public int x; } - -//编译错误,应该类在前,接口在后 -//class ColoredDimension {} - -class ColoredDimension { - T item; - ColoredDimension(T item) { this.item = item; } - T getItem() { return item; } - Color color() { return item.getColor(); } - int getX() { return item.x; } -} - -interface Weight { int weight(); } - -//边界继承只能有一个类,可以有多个接口 -class Solid { - T item; - Solid(T item) { this.item = item; } - T getItem() { return item; } - Color color() { return item.getColor(); } - int getX() { return item.x; } - int weight() { return item.weight(); } -} - -class Bounded extends Dimension implements HasColor, Weight { - - @Override - public Color getColor() { return null; } - - @Override - public int weight() { return 0; } -} - -public class BasicBounds { - public static void main(String[] args) { - Solid solid = new Solid<>(new Bounded()); - solid.color(); - solid.getX(); - solid.weight(); - } -} -``` - -通过在继承的每个层次上添加边界限制,消除 BasicBounds.java 代码冗余。 - -```java -class HoldItem { - T item; - HoldItem(T item) { this.item = item; } - T getItem() { return item; } -} - -class Colored2 extends HoldItem { - Colored2(T item) { super(item); } - Color color() { return item.getColor(); } -} - -class ColorDimension2 extends Colored2 { - ColorDimension2(T item) { super(item); } - int getX() { return item.x; } -} - -class Solid2 extends ColorDimension2 { - Solid2(T item) { super(item); } - int weight() { return item.weight(); } -} - -public class InheritBounds { - public static void main(String[] args) { - Solid2 solid2 = new Solid2<>(new Bounded()); - solid2.color(); - solid2.getX(); - solid2.weight(); - } -} -``` - -### 通配符 - -参考自:[泛型超详细解读](https://blog.csdn.net/jeffleo/article/details/52250948) | [通配符的上界通配符和下界通配符](https://blog.csdn.net/hello_worldee/article/details/77934244) - -#### 上界通配符 - -上界< ? extends Class> - -```java -class Fruit { } -class Apple extends Fruit { } -class Orange extends Fruit { } - -public class UpperBound { - public static void main(String[] args) { - List list = new ArrayList<>(); - //编译错误 - //list.add(new Apple()); - //list.add(new Orange()); - //list.add(new Fruit()); - list.add(null); - - list.contains(new Apple()); - list.indexOf(new Apple()); - - System.out.println(list.get(0)); - } -} -``` - -可见,指定了下边界,却不能add任何类型,甚至Object都不行,除了null,因为null代表任何类型。List< ? extends Fruit>可以解读为,“具有任何从Fruit继承的类型”,但实际上,它意味着,它没有指定具体类型。对于编译器来说,当你指定了一个List< ? extends Fruit>,add的参数也变成了“? extends Fruit”。因此编译器并不能了解这里到底需要哪种Fruit的子类型,因此他不会接受任何类型的Fruit。 - -然而,contain和indexof却能执行,这是因为,这两个方法的参数是Object,不涉及任何的通配符,所以编译器允许它调用。 - -list.get(0)能够执行是因为,当item在此list存在时,编译器能够确定他是Apple的子类,所以能够安全获得。 - -#### 下界通配符 - -< ? super Class>表示,指定类的基类。 - -```java -class Apple1 extends Apple {} - -public class LowerBound{ - - public static void main(String[] args) { - List list = new ArrayList<>(); - list.add(new Apple()); - list.add(new Apple1()); - //编译错误 - //list.add(new Fruit()); - - Object apple = list.get(0); - System.out.println(apple); - } -} -//output: com.tyson.chapter15.generics.Apple@6d6f6e28 -``` - -对于super,get返回的是Object,因为编译器不能确定列表中的是Apple的哪个子类,所以只能返回Object。 - -List < ? super Apple>, 代表容器内存放的是Apple的所有父类,所以有多态和上转型,这个容器时可以接受所有Apple父类的子类的(多态的定义:父类可以接受子类型对象)。Apple和Apple1都直接或间接继承了Fruit,所以Apple和Apple1是能够加入List < ? super Apple>这个容器的。 - -list.add(new Fruit())不能添加,是因为容器内存放的是Apple的**所有**父类,正是因为能存放所有,Apple的父类可能有Fruit1, Fruit2, Fruit3 , 所以编译器根本不能识别你要存放哪个Apple的父类,所以不能添加Fruit,因为这不能保证类型安全的原则。这从最后的Object apple = list.get(0)可以看出。 - - - -## 数组 - -数组的 length 是数组的大小,不是实际保存的元素个数。 - -如果不显式初始化,基本类型数组会被自动初始化成初值,对象数组则会被初始化成 null。 - -### 复制数组 - -Java 标准类库提供有 static 方法 System.arraycopy(),用来复制数组比用 for 循环复制要快的多。System.arraycopy() 针对所有类型做了重载。 - -```java -public class CopyArrays { - public static void main(String[] args) { - int[] src = new int[7]; - int[] dest = new int[10]; - Arrays.fill(src, 47); - Arrays.fill(dest, 99); - System.out.println("src = " + Arrays.toString(src)); - System.out.println("dest = " + Arrays.toString(dest)); - System.arraycopy(src, 0, dest, 0, src.length); - System.out.println("dest = " + Arrays.toString(dest)); - } -} -/*output -src = [47, 47, 47, 47, 47, 47, 47] -dest = [99, 99, 99, 99, 99, 99, 99, 99, 99, 99] -dest = [47, 47, 47, 47, 47, 47, 47, 99, 99, 99] - */ -``` - -基本类型数组和对象数组都可以复制。复制对象只是复制对象引用,即浅复制。System.arraycopy() 不会执行自动包装和自动拆包,两个数组需要具有相同的数据类型。 - -### Arrays 工具 - -#### 数组拷贝 - -```java -public class ArrayCopy { - //Arrays 类方法的使用 - public static void main(String[] args) { - int[] arr = {12, 45, 2, 56, 20}; - int[] arrCopy = Arrays.copyOf(arr, arr.length); - System.out.println("arr: " + Arrays.toString(arr)); - System.out.println("arrCopy: " + Arrays.toString(arrCopy)); - //增加数组大小 - arrCopy = Arrays.copyOf(arr, 2 * arr.length); - System.out.println("arr: " + Arrays.toString(arr)); - System.out.println("arrCopy: " + Arrays.toString(arrCopy)); - } -} -/*output -arr: [12, 45, 2, 56, 20] -arrCopy: [12, 45, 2, 56, 20] -arr: [12, 45, 2, 56, 20] -arrCopy: [12, 45, 2, 56, 20, 0, 0, 0, 0, 0] - */ -``` - -`Array.copyOf()`底层是通过`System.arraycopy()`使用的。 - -```java -public static T[] copyOf(U[] original, int newLength, Class newType) { - @SuppressWarnings("unchecked") - T[] copy = ((Object)newType == (Object)Object[].class) - ? (T[]) new Object[newLength] - : (T[]) Array.newInstance(newType.getComponentType(), newLength); - System.arraycopy(original, 0, copy, 0, - Math.min(original.length, newLength)); - return copy; -} -``` - -#### 数组的比较 - -Arrays 类提供了重载后的 equals() 方法,用来比较整个数组。 - -```java -public class ComparingArrays { - public static void main(String[] args) { - int[] a1 = new int[10]; - int[] a2 = new int[10]; - Arrays.fill(a1, 47); - Arrays.fill(a2, 47); - System.out.println(Arrays.equals(a1, a2)); - a2[1] = 0; - System.out.println(Arrays.equals(a1, a2)); - String[] s1 = new String[2]; - Arrays.fill(s1, "hello"); - String[] s2 = { new String("hello"), new String("hello")}; - System.out.println(Arrays.equals(s1, s2)); - } -} -/*output -true -false -true - */ -``` - -#### 数组元素的比较 - -1. 实现`java.lang.Comparable`接口,使得类本身具有比较比较能力。 - - ```java - public class CompType implements Comparable { - int i; - int j; - public CompType(int i, int j) { - this.i = i; - this.j = j; - } - - @Override - public String toString() { - return "CompType{" + - "i=" + i + - ", j=" + j + - '}'; - } - - @Override - public int compareTo(CompType ct) { - return i < ct.i ? -1 : (i == ct.i ? 0 : 1); - } - - private static Random r = new Random(47); - public static Generator generator() { - return new Generator() { - @Override - public CompType next() { - return new CompType(r.nextInt(100), r.nextInt(100)); - } - }; - } - - public static void main(String[] args) { - CompType[] arr = Generated.array(new CompType[4], generator()); - System.out.println("before sorting: " + Arrays.toString(arr)); - Arrays.sort(arr); - System.out.println("after sorting: " + Arrays.toString(arr)); - //反转自然排列顺序 - Arrays.sort(arr, Collections.reverseOrder()); - System.out.println("reverse order: " + Arrays.toString(arr)); - } - } - /*output - before sorting: [CompType{i=58, j=55}, CompType{i=93, j=61}, CompType{i=61, j=29}, CompType{i=68, j=0}] - after sorting: [CompType{i=58, j=55}, CompType{i=61, j=29}, CompType{i=68, j=0}, CompType{i=93, j=61}] - reverse order: [CompType{i=93, j=61}, CompType{i=68, j=0}, CompType{i=61, j=29}, CompType{i=58, j=55}] - */ - ``` - -2. 编写自己的比较器Comparator。 - - ```java - class CompTypeComparator implements Comparator { - - @Override - public int compare(CompType o1, CompType o2) { - return o1.j > o2.j ? 1 : (o1.j == o2.j ? 0 : -1); - } - } - - public class ComparatorTest { - public static void main(String[] args) { - CompType[] arr = Generated.array(new CompType[4], CompType.generator()); - System.out.println("before sorting: " + Arrays.toString(arr)); - Arrays.sort(arr, new CompTypeComparator()); - System.out.println("after sorting: " + Arrays.toString(arr)); - } - } - /*output - before sorting: [CompType{i=58, j=55}, CompType{i=93, j=61}, CompType{i=61, j=29}, CompType{i=68, j=0}] - after sorting: [CompType{i=68, j=0}, CompType{i=61, j=29}, CompType{i=58, j=55}, CompType{i=93, j=61}] - */ - ``` - -#### 数组排序 - -使用内置的排序方法,就可以对基本类型数组进行排序。也可以对对象数组进行排序,只要该对象实现了 Comparable 接口或者具有相关联的 Comparator。 - -```java -public class StringSorting { - public static void main(String[] args) { - String[] strs = Generated.array(new String[5], new RandomGenerator.String(5)); - System.out.println("before sorting: " + Arrays.toString(strs)); - Arrays.sort(strs); - System.out.println("after sorting: " + Arrays.toString(strs)); - Arrays.sort(strs, Collections.reverseOrder()); - System.out.println("reverse order: " + Arrays.toString(strs)); - Arrays.sort(strs, String.CASE_INSENSITIVE_ORDER); - System.out.println("case insensive: " + Arrays.toString(strs)); - } -} -/*output -before sorting: [YNzbr, nyGcF, OWZnT, cQrGs, eGZMm] -after sorting: [OWZnT, YNzbr, cQrGs, eGZMm, nyGcF] -reverse order: [nyGcF, eGZMm, cQrGs, YNzbr, OWZnT] -case insensive: [cQrGs, eGZMm, nyGcF, OWZnT, YNzbr] - */ -``` - -String 排序算法依据词典编排顺序排序。大写字母在前,小写字母在后。Java 标准类库中的排序算法针对正排序做了优化。针对基础类型设计了快速排序,针对对象设计了稳定归并排序。 - -#### 排序数组查找 - -若数组已经排序,既可以使用`Arrays.binarySearch()`执行快速查找。 - -```java -public class ArraySearching { - public static void main(String[] args) { - Generator gen = new RandomGenerator.Integer(1000); - int[] arr = ConvertTo.primitive(Generated.array(new Integer[5], gen)); - Arrays.sort(arr); - System.out.println("after sorting: " + Arrays.toString(arr)); - while(true) { - int r = gen.next(); - int location = Arrays.binarySearch(arr, r); - if(location >= 0) { - System.out.println("location: " + location + "-->" + arr[location]); - break; - } - } - } -} -/*output -after sorting: [258, 555, 693, 861, 961] -location: 3-->861 - */ -``` - -找到目标,`Arrays.binarySearch()`返回值大于或等于0。否则返回负值:`-(插入点)- 1`; - -对象数组使用`Arrays.binarySearch()`需要提供Comparator。 - -```java -public class StringSearch { - public static void main(String[] args) { - String[] strs = Generated.array(new String[6], new RandomGenerator.String(5)); - Arrays.sort(strs, String.CASE_INSENSITIVE_ORDER); - System.out.println(Arrays.toString(strs)); - int index = Arrays.binarySearch(strs, strs[3], String.CASE_INSENSITIVE_ORDER); - System.out.println("index: " + index + "-->" + strs[index]); - } -} -/*output -[cQrGs, eGZMm, JMRoE, nyGcF, OWZnT, YNzbr] -index: 3-->nyGcF - */ -``` - - - -## 容器深入研究 - -### 填充容器 - -```java -public class FillingList { - public static void main(String[] args) { - List list = new ArrayList<>(Collections.nCopies(3, "Tyson")); - System.out.println(list); - Collections.fill(list, "Sophia"); - System.out.println(list); - } -} -/*output -[Tyson, Tyson, Tyson] -[Sophia, Sophia, Sophia] - */ -``` - -### SortedSet - -按对象的比较函数对元素进行排序。用 TreeSet 迭代通常比用 HashSet 要快。 - -```java -public class SortedSetDemo { - public static void main(String[] args) { - SortedSet sortedSet = new TreeSet<>(); - Collections.addAll(sortedSet, "one two three four".split(" ")); - System.out.println(sortedSet); - System.out.println("first: " + sortedSet.first()); - System.out.println("last: " + sortedSet.last()); - Iterator itr = sortedSet.iterator(); - while (itr.hasNext()) { - System.out.print(itr.next() + " "); - } - System.out.println(); - //from包含-to不包含 - System.out.println("subSet: " + sortedSet.subSet("one", "three")); - //小于three的元素 - System.out.println(sortedSet.headSet("three")); - //大于等于one的元素 - System.out.println(sortedSet.tailSet("one")); - } -} -/*output -[four, one, three, two] -first: four -last: two -four one three two -subSet: [one] -[four, one] -[one, three, two] - */ -``` - -### 队列 - -除了并发应用,Queue Java SE5中仅有两个实现 LinkedList 和 PriorityQueue,它们的差异在于排序行为不在性能。 - -#### 优先级队列 - -```java -public class ToDoList extends PriorityQueue { - static class ToDoItem implements Comparable { - private char primary; - private int secondary; - private String item; - - public ToDoItem(String item, char pri, int sec) { - this.item = item; - primary = pri; - secondary = sec; - } - @Override - public int compareTo(ToDoItem o) { - if(primary > o.primary) { - return 1; - } - if(primary == o.primary) { - if(secondary > o.secondary) { - return 1; - } else if (secondary == o.secondary) { - return 0; - } else { - return -1; - } - } - return -1; - } - @Override - public String toString() { - return "ToDoItem{" + - "primary=" + primary + - ", secondary=" + secondary + - ", item='" + item + '\'' + - '}'; - } - } - public void add(String item, char pri, int sec) { - super.add(new ToDoItem(item, pri, sec)); - } - - public static void main(String[] args) { - ToDoList toDoList = new ToDoList(); - toDoList.add("homework", 'b', 3); - toDoList.add("feed dog", 'a', 4); - toDoList.add("lunch", 'a', 1); - while (!toDoList.isEmpty()) { - System.out.println(toDoList.remove()); - } - } -} -/*output -ToDoItem{primary=a, secondary=1, item='lunch'} -ToDoItem{primary=a, secondary=4, item='feed dog'} -ToDoItem{primary=b, secondary=3, item='homework'} - */ -``` - -#### 双向队列 - -可以在队列任何一端添加或移除元素。Java 标准库中没有任何显式的用于双向队列的接口。 - -```java -public class Deque { - private LinkedList deque = new LinkedList<>(); - public void addFirst(T t) { deque.add(t); } - public void addLast(T t) { deque.add(t); } - public T getFirst() { return deque.getFirst(); } - public T getLast() { return deque.getLast(); } - public T removeFirst() { return deque.removeFirst(); } - public T removeLast() { return deque.removeLast(); } - public int size() { return deque.size(); } - @Override - public String toString() { return deque.toString(); } - - public static void main(String[] args) { - Deque deque = new Deque<>(); - for(int i = 20; i < 27; i++) { - deque.addFirst(i); - } - for(int j = 50; j < 55; j++) { - deque.addLast(j); - } - System.out.println(deque); - while(deque.size() != 0) { - System.out.print(deque.removeFirst() + " "); - } - } -} -/*output -[20, 21, 22, 23, 24, 25, 26, 50, 51, 52, 53, 54] -20 21 22 23 24 25 26 50 51 52 53 54 - */ -``` - -### LinkedHashMap - -可以在构造器中设定 LinkedHashMap,使之采用基于访问的最近最少使用(LRU)算法,没有访问过的元素会出现在队列的前面。可用于定期清理元素以节省空间。 - -```java -public class LinkedHashMapDemo { - public static void main(String[] args) { - LinkedHashMap linkedHashMap = new LinkedHashMap<>(new CountingMapData(5)); - System.out.println(linkedHashMap); - linkedHashMap = new LinkedHashMap<>(16, 0.75f, true); - linkedHashMap.putAll(new CountingMapData(5)); - for(int i = 0; i < 3; i++) { - linkedHashMap.get(i); - } - System.out.println(linkedHashMap); - linkedHashMap.get(3); - System.out.println(linkedHashMap); - } -} -/*output -{0=A0, 1=B0, 2=C0, 3=D0, 4=E0} -{3=D0, 4=E0, 0=A0, 1=B0, 2=C0} -{4=E0, 0=A0, 1=B0, 2=C0, 3=D0} - */ -``` - -### Colletions 工具类 - -```java -public class Utilities { - static List list = Arrays.asList("one two three four".split(" ")); - - public static void main(String[] args) { - System.out.println(list); - System.out.println("list disjoint(four)?" + - Collections.disjoint(list, Collections.singletonList("four"))); //不相交 - System.out.println("max: " + Collections.max(list)); - System.out.println("min: " + Collections.min(list)); - System.out.println("max with comparator: " + Collections.max(list, String.CASE_INSENSITIVE_ORDER)); - List subList = Arrays.asList("one two".split(" ")); - System.out.println("indexOfSubList: " + Collections.indexOfSubList(list, subList)); - System.out.println("lastIndexOfSubList: " + Collections.lastIndexOfSubList(list, subList)); - Collections.replaceAll(list, "one", "emm"); - System.out.println("replace all: " + list); - Collections.reverse(list); - System.out.println("reverse: " + list); - Collections.rotate(list, 1); - System.out.println("rotate: " + list); - List source = Arrays.asList("int the matrix".split(" ")); - Collections.copy(list, source); - System.out.println("copy: " + list); - Collections.swap(list, 0, list.size() - 1); - System.out.println("swap: " + list); - Collections.shuffle(list, new Random((47))); - System.out.println("shuffle: " + list); //打乱 - Collections.fill(list, "frequency"); - System.out.println("fill: " + list); - System.out.println("frequency of 'frequency': " + Collections.frequency(list, "frequency")); - List nCopies = Collections.nCopies(3, "nCopies;"); - System.out.println("nCopies: " + nCopies); - System.out.println("list disjoint nCopies? " + Collections.disjoint(list, nCopies)); - Enumeration e = Collections.enumeration(nCopies); - Vector v = new Vector<>(); - while(e.hasMoreElements()) { - v.add(e.nextElement()); - } - ArrayList arrayList = Collections.list(v.elements()); - System.out.println("arrayList: " + arrayList); - } -} -/*output -[one, two, three, four] -list disjoint(four)?false -max: two -min: four -max with comparator: two -indexOfSubList: 0 -lastIndexOfSubList: 0 -replace all: [emm, two, three, four] -reverse: [four, three, two, emm] -rotate: [emm, four, three, two] -copy: [int, the, matrix, two] -swap: [two, the, matrix, int] -shuffle: [two, the, int, matrix] -fill: [frequency, frequency, frequency, frequency] -frequency of 'frequency': 4 -nCopies: [nCopies;, nCopies;, nCopies;] -list disjoint nCopies? true -arrayList: [nCopies;, nCopies;, nCopies;] - */ -``` - -#### 排序和查询 - -```java -public class ListSortSearch { - public static void main(String[] args) { - List list = - new ArrayList(Utilities.list); - list.addAll(Utilities.list); - print(list); - Collections.shuffle(list, new Random(47)); - print("Shuffled: " + list); - // Use a ListIterator to trim off the last elements: - ListIterator it = list.listIterator(4); - while (it.hasNext()) { - it.next(); - it.remove(); - } - print("Trimmed: " + list); - Collections.sort(list); - print("Sorted: " + list); - String key = list.get(3); - int index = Collections.binarySearch(list, key); - print("Location of " + key + " is " + index + - ", list.get(" + index + ") = " + list.get(index)); - Collections.sort(list, String.CASE_INSENSITIVE_ORDER); - print("Case-insensitive sorted: " + list); - key = list.get(3); - index = Collections.binarySearch(list, key, - String.CASE_INSENSITIVE_ORDER); - print("Location of " + key + " is " + index + - ", list.get(" + index + ") = " + list.get(index)); - } -} /* Output: -[one, Two, three, Four, five, six, one, one, Two, three, Four, five, six, one] -Shuffled: [Four, five, one, one, Two, six, six, three, three, five, Four, Two, one, one] -Trimmed: [Four, five, one, one, Two, six, six, three, three, five] -Sorted: [Four, Two, five, five, one, one, six, six, three, three] -Location of six is 7, list.get(7) = six -Case-insensitive sorted: [five, five, Four, one, one, six, six, three, three, Two] -Location of three is 7, list.get(7) = three -*/ -``` - -#### 只读容器 - -```java -public class ReadOnly { - static Collection data = - new ArrayList<>(Countries.names(6)); - - public static void main(String[] args) { - Collection c = Collections.unmodifiableCollection( - new ArrayList<>(data)); - print(c); // Reading is OK - //! c.add("one"); // Can't change it - - List a = Collections.unmodifiableList(new ArrayList<>(data)); - ListIterator lit = a.listIterator(); - print(lit.next()); // Reading is OK - //! lit.add("one"); // Can't change it - - Set s = Collections.unmodifiableSet(new HashSet<>(data)); - print(s); // Reading is OK - //! s.add("one"); // Can't change it - - // For a SortedSet: - Set ss = Collections.unmodifiableSortedSet(new TreeSet<>(data)); - - Map m = Collections.unmodifiableMap( - new HashMap<>(Countries.capitals(6))); - print(m); // Reading is OK - //! m.put("Ralph", "Howdy!"); - - // For a SortedMap: - Map sm = Collections.unmodifiableSortedMap( - new TreeMap<>(Countries.capitals(6))); - } -} -``` - -#### Collection 和 Map 的同步控制 - -```java -public class Synchronization { - public static void main(String[] args) { - Collection c = Collections.synchronizedCollection(new ArrayList<>()); - List list = Collections.synchronizedList(new ArrayList<>()); - Set set = Collections.synchronizedSet(new HashSet<>()); - Set ss = Collections.synchronizedSortedSet(new TreeSet<>()); - Map map = Collections.synchronizedMap(new HashMap<>()); - Map sm = Collections.synchronizedSortedMap(new TreeMap<>()); - } -} -``` - -#### 快速报错机制 - - fast-fail 是 Java 容器的一种保护机制。当多个线程对同一个集合进行操作时,就有可能会产生 fast-fail 事件。例如:当线程a正通过 iterator 遍历集合时,另一个线程b修改了集合的内容(modCount 不等于expectedModCount),那么线程a在遍历的时候会抛出 ConcurrentModificationException,产生 fast-fail 事件。 - -```java -public class FastFail { - public static void main(String[] args) { - Collection c = new ArrayList<>(); - Iterator itr = c.iterator(); - c.add("haha"); - try { - String s = itr.next(); - } catch (ConcurrentModificationException ex) { - ex.printStackTrace(); - } - } -} -``` - -在此例中,应该添加完所有元素后,再获取迭代器。 - -**多线程并发修改容器的方法:** - -- 使用`Colletions.synchronizedList()`方法或在修改集合内容的地方加上 synchronized。这样的话,增删集合内容的同步锁会阻塞遍历操作,影响性能。 -- 使用 CopyOnWriteArrayList 来替换 ArrayList。在对 CopyOnWriteArrayList 进行修改操作的时候,会拷贝一个新的集合,对新的集合进行操作,操作完成后再把引用指向新的集合。 - -### Java 1.0/1.1 的容器 - -#### BitSet - -参考自:[JAVA中BitSet使用](https://blog.csdn.net/xv1356027897/article/details/79518647) - -位图,数据的存在性可以使用bit位上的1或0来表示,一个long型数字占用64位空间,那么一个long型数字就可以保存64个数字的“存在性”状态(true or false)。BitSet内部是一个long[]数组,数组的大小由 BitSet 接收的最大数字决定,这个数组将数字分段表示[0,63],[64,127],[128,191]...。即long[0]用来存储[0,63]这个范围的数字的“存在性”,long[1]用来存储[64,127],依次递推。 - -```java -public class BitSetDemo { - public static void printBitSet(BitSet b) { - System.out.println("BitSet: " + b); - StringBuilder sb = new StringBuilder(); - for(int i = 0; i < b.size(); i++) { - sb.append(b.get(i) ? "1" : "0"); - } - System.out.println("bitset pattern: " + sb); - } - public static void main(String[] args) { - Random rand = new Random(47); - int num = rand.nextInt(20); - BitSet bs = new BitSet(); - bs.set(num); - System.out.println(bs); - System.out.println("num exist? " + bs.get(num)); - System.out.println("size of bitset: " + bs.size()); - System.out.println("the number of bits set: " + bs.cardinality()); - bs.set(num);//重复设置 - System.out.println("set again! num exist? " + bs.get(num)); - bs.clear(num);//清除 - System.out.println("after clearing bit: " + bs); - - bs.set(num + 100);//自动扩充容量 - System.out.println("num+100 exist? " + bs.get(num + 100)); - System.out.println("num-1 exist? " + bs.get(num - 1)); - System.out.println("size of bitset: " + bs.size()); - } -} -/*output -{18} -num exist? true -size of bitset: 64 -the number of bits set: 1 -set again! num exist? true -after clearing bit: {} -num+100 exist? true -num-1 exist? false -size of bitset: 128 - */ -``` - - - -## Java I/O 系统 - -### 输入和输出 - -继承自 InputStream 或 Reader 的类都具有 read() 方法,用于读取单个字节或者字节数组;继承自 OutputStream 或 Writer 的类都含有 write() 方法,用于写单个字节或字节数组。 - -#### InputStream 和 OutputStream - -InputStream 用来表示那些从不同数据源产生输入的类。这些数据源包括:1.字节数组;2.String 对象;3.文件;4.管道;5.一个由其他种类的流组成的序列。 - -InputStream 类有一个抽象方法:`abstract int read()`,这个方法将读入并返回一个字节,或者在遇到输入源结尾时返回-1。 - -OutputStream 决定了输出所要去的目标:字节数组、文件或管道。OutputStream 的 `abstract void write(int b)` 可以向某个输出位置写出一个字节。 - -read() 和 write() 方法在执行时都将阻塞,等待数据被读入或者写出。 - -#### Reader 和 Writer - -字符流是由通过字节流转换得到的,转化过程耗时,而且容易出现乱码问题。I/O 流提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -```java -abstract int read(); -abstract void write(char c); -``` - -### 组合输入输出流过滤器 - -FileInputStream 和 FileOutputStream 可以提供附在磁盘文件上的输入流和输出流。 - -`FileInputStream fin = new FileInputStream("E:/data.txt") //E:\\data.txt` - -通过 java.io.File.separator 可以获得与平台相关的 文件分隔符。 - -FileInputStream 只支持在文件的读写字节,而DataInputStream 只支持数值的读写。 - -实现从文件读取数字,需要将 FileInputStream 和 DataInputStream 组合。 - -```java -FileInputStream fin = new FileInputStream("e:/data.txt"); -DataInputStream din = new DataInputStream(fin); -double x = din.readDouble(); -``` - -使用缓冲机制: - -```java -DataInputStream din = new DataInputStream( - new BufferedInputStream( - new FileInputStream("E:/data.txt") - ) -); -``` - -可回推的输入流: - -```java -PushbackInputStream pin = new PushbackInputStream( - new BufferedInputStream( - new FileInputStream("E:/data.txt") - ) -); - -int b = pin.read(); -if(b != '<') { - pin.unread(b); -} -``` - -从 zip 文件读入数字: - -```java -DataInputStream din = new DataInputStream( - new ZipInputStream( - new FileInputStream("E:/data.zip") - ) -); -``` - -### 文本输入和输出 - -将字节流转化为 Unicode 字符的读入器: - -```java -//Reader in = new InputStreamReader(System.in); -Reader in = new InputStreamReader(new FileInputStream("E:/data.txt"), StandardCharsets.UTF_8); -``` - -将 Unicode 字符转化为字节流的写出器: - -```java -PrintWriter out = new PrintWriter("H:/data.txt", "UTF-8"); -out.print("haha"); -out.flush(); -``` - -默认情况下,自动冲刷机制是禁用的,可以使用`PrintWriter(Writer out, Boolean autoFlush)`启用或禁用自动冲刷机制: - -```java -PrintWriter out = new PrintWriter( - new OutputStreamWriter( - new FileOutputStream("H:/data.txt"), "UTF-8"), - true); -out.print("tyson"); -out.flush(); -``` - -### 以文本格式存储对象 - -```java -public class TextFile { - public static void main(String[] args) throws IOException { - Employee[] staff = new Employee[2]; - DateTimeFormatter ymd = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - //字符串转换成LocalDate类型 - LocalDate ld = LocalDate.parse("2015-11-23", ymd); - staff[0] = new Employee("tyson", 1200.00, ld); - staff[1] = new Employee("tom", 1200.00, ld); - - try (PrintWriter out = new PrintWriter("H:/data.txt", "UTF-8")) { - writeData(staff, out); - } - try (Scanner in = new Scanner( - new FileInputStream("H:/data.txt"), "UTF-8")) { - Employee[] newStaff = readData(in); - - for(Employee e : newStaff) { - System.out.println(e); - } - } - } - - private static void writeData(Employee[] employees, PrintWriter out) throws IOException { - out.println(employees.length); - for(Employee e : employees) { - writeEmployee(out, e); - } - } - - private static Employee[] readData(Scanner in) { - int n = in.nextInt(); - in.nextLine(); - - Employee[] employees = new Employee[n]; - for(int i = 0; i < n ; i++) { - employees[i] = readEmployee(in); - } - - return employees; - } - - public static void writeEmployee(PrintWriter out, Employee e) throws IOException { - out.println(e.getName() + "|" + e.getSalary() + "|" + e.getHireDate()); - } - - public static Employee readEmployee(Scanner in) { - String line = in.nextLine(); - String[] tokens = line.split("\\|"); - String name = tokens[0]; - double salary = Double.parseDouble(tokens[1]); - LocalDate hireDate = LocalDate.parse(tokens[2]); - return new Employee(name, salary, hireDate); - } -} -/*output -Employee{name='tyson', salary=1200.0, hireDate=2015-11-23} -Employee{name='tom', salary=1200.0, hireDate=2015-11-23} - */ -``` - -### 字符编码方式 - -输入和输出流都是用于字节序列,很多情况下,我们希望操作的是字符序列。 - -平台使用的编码方法可以由静态方法`Charset.defaultCharset`返回。 - -将字节数组转化成字符串: - -```java -String str = new String(bytes, StandardCharsets.UTF_8); -``` - -### 读写二进制数据 - -#### DataInput 和 DataOutput - -DataOutput 接口定义了以二进制格式写数组、字符、字符串等的方法,如 writeChars、writeByte、writeInt 和 writeDouble。 - -writeInt 总是将一个整数写出为4字节的二进制数量值,writeDouble 总将一个 double 值写出为8字节的二进制数量值。Java 会将所有的值都按照高位在前的模式写出,这使得 Java 数据文件可以独立于平台。 - -DataInput 接口用于二进制格式读数据,接口定义了如下方法:readInt、readShort、readChar等。 - -DataInputStream 实现了 DataInput 接口,DataOutputStream 实现了 DataOutput 接口: - -```java -DataInputStream in = new DataInputStream(new FileInputStream("e:/data.txt")); -DataOutputStream out = new DataOutputStream(new FileOutputStream("e:/data.txt")); -``` - -#### 随机访问文件 - -RandomAccessFile 类可以在文件中的任何位置查找或写入。RandomAccessFile 类同时实现了 DataInput 和 DataOutput 接口。 - -#### 序列化 - -**保存和加载序列化对象** - -保存对象: - -```java -ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e:/data.txt")); -Employee tyson = new Employee(); -out.writeObject(tyson); -``` - -读回对象: - -```java -ObjectInputStream in = new ObjectInputStream(new FileInputStream("e:/data.txt")); -Employee e = (Employee)in.readObject(); -``` - -其中,Employee 需要实现 Serializable 接口,接口没有任何方法: - -```java -Employee implements Serializable {...} -``` - -ObjectStreamTest.java - -```java -/** - * Copyright (C), 2018-2019 - * FileName: ObjectStream - * Author: Tyson - * Date: 2019/4/25/0025 14:58 - * Description: 对象序列化 - */ -package com.tyson.chapter18.io; - -import java.io.*; - -/** - * @author Tyson - * @ClassName: ObjectStream - * @create 2019/4/25/0025 14:58 - */ -public class ObjectStream { - public static void main(String[] args) throws IOException, ClassNotFoundException { - Employee tyson = new Employee("tyson", 8888.0, "2015-02-15"); - Manager sophia = new Manager("sophia", 6666.0, "2012-03-15"); - sophia.setSecretary(tyson); - Manager tom = new Manager("tom", 5555.0, "2014-03-16"); - tom.setSecretary(tyson); - Employee[] staff = new Employee[3]; - staff[0] = tyson; - staff[1] = sophia; - staff[2] = tom; - - try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e:/data.txt"))) { - out.writeObject(staff); - out.flush(); - } - - try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("e:/data.txt"))) { - Employee[] newStaff = (Employee[]) in.readObject(); - newStaff[1].setSalary(23333.0); - for(Employee e : newStaff) { - System.out.println(e); - } - //原对象sophia不变 - System.out.println(staff[1]); - } - } -} - -class Manager extends Employee { - private Employee secretary; - - public Manager(String name, Double salary, String hireDate) { - super(name, salary, hireDate); - } - - @Override - public String toString() { - return "Manager{" + - "name=" +super.getName() + - ", salary=" +super.getSalary() + - ", hireday=" +super.getHireDate() + - ", secretary=" + secretary + - '}'; - } - - public Employee getSecretary() { - return secretary; - } - - public void setSecretary(Employee secretary) { - this.secretary = secretary; - } -} -``` - -通过关键字 transient 可以将某些不可序列化的域设置成瞬时的域,这些域在对象被序列化时总是被跳过。 - -**使用序列化实现克隆** - -将对象序列化到输出流中,然后将其读回,产生的新对象是原对象的深拷贝。 - -### 操作文件 - -File 类既能代表特定文件的名称,又能代表一个目录下的一组文件的名称。 - -#### 目录列表器 - -获取某个目录下以".java"文件后缀结尾的文件名。 - -```java -class DirFilter implements FilenameFilter { - private Pattern pattern; - public DirFilter(String regex) { - pattern = Pattern.compile(regex); - } - @Override - public boolean accept(File dir, String name) { - return pattern.matcher(name).matches(); - } -} - -public class DirList { - public static void main(String[] args) { - File path = new File("H:" + File.separator + - "java-data" + File.separator + "TIJ4-code" + File.separator + "containers"); - //Java文件后缀".+\\.java$" - String[] list = path.list(new DirFilter("A.+\\.java$")); - Arrays.sort(list); - System.out.println(Arrays.toString(list)); - } -} -/*output -[AssociativeArray.java] - */ -``` - -用匿名内部类实现。 - -```java -public class DirList { - public static void main(String[] args) { - File f = new File("H:" + File.separator + - "java-data" + File.separator + "TIJ4-code" + File.separator + "containers"); - String[] fileNames = f.list(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".java"); - } - }); - System.out.println(Arrays.toString(fileNames)); - } -} -``` - -### - -## 枚举类型 - -### 基本 enum 特性 - -除了不能继承自一个 enum 之外,我们基本上可以将 enum 看做一个常规的类。所有的 enum 都继承自 java.lang.Enum 类,所以 enum 不能再继承其他类。在创建一个新的 enum 时,可以同时实现一个或多个接口。 - -```java -enum Color { RED, GREEN, BLUE } - -public class EnumClass { - public static void main(String[] args) { - for(Color c : Color.values()) { - System.out.println(c + " ordinal: " + c.ordinal()); - System.out.print(c.compareTo(Color.RED) + " "); - System.out.print(c.equals(Color.RED) + " "); - System.out.println(c == Color.RED); - System.out.println(c.getDeclaringClass()); - System.out.println(c.name()); - System.out.println("-------------------------"); - } - for(String s : "RED GREEN BLUE".split(" ")) { - Color c = Enum.valueOf(Color.class, s); - System.out.println(c); - } - } -} -/*output -RED ordinal: 0 -0 true true -class com.tyson.chapter19.enumerated.Color -RED -------------------------- -GREEN ordinal: 1 -1 false false -class com.tyson.chapter19.enumerated.Color -GREEN -------------------------- -BLUE ordinal: 2 -2 false false -class com.tyson.chapter19.enumerated.Color -BLUE -------------------------- -RED -GREEN -BLUE - */ -``` - -values() 是编译器添加的 static 方法,Enum 类中没有 values() 方法。ordinal() 方法返回一个 int 值,这是每个 enum 实例声明时的次序,从0开始。valueOf() 根据给定的名字返回相应的 enum实例,如果不存在给定名字的实例,将会抛出异常。 - -### 向 enum 添加新方法 - -```java -public enum Direction { - WEST("Xizang"), - NORTH("Beijing"), - EAST("Shanghai"), - SOUTH("Hainan");//定义自己的方法,enum实例序列的最后需添加分号 - //属性和方法需要在enum实例之后定义 - private String discription; - private Direction(String discription) { - this.discription = discription; - } - public String getDiscription() { - return discription; - } - - public static void main(String[] args) { - for(Direction dir : Direction.values()) { - System.out.println(dir + ": " + dir.getDiscription()); - } - } -} -/*output -WEST: Xizang -NORTH: Beijing -EAST: Shanghai -SOUTH: Hainan - */ -``` - -### 覆盖 enum 的方法 - -```java -public enum Animal { - CAT, DOG, BIRD, PIG; - @Override - public String toString() { - String id = name(); - String lower = id.substring(1).toLowerCase(); - return id.charAt(0) + lower; - } - - public static void main(String[] args) { - for(Animal a : values()) { - System.out.print(a + " "); - } - } -} -//output: Cat Dog Bird Pig -``` - -### Switch 语句中的 enum - -```java -enum Signal { - GREEN, YELLOW, RED, -} - -public class TrafficLight { - Signal color = Signal.RED; - - public void change() { - switch (color) { - case RED: - color = Signal.GREEN; - break; - case GREEN: - color = Signal.YELLOW; - break; - case YELLOW: - color = Signal.RED; - break; - } - } - @Override - public String toString() { - return "the traffic light is: " + color; - } - - public static void main(String[] args) { - TrafficLight tl = new TrafficLight(); - for (int i = 0; i < 4; i++) { - System.out.println(tl); - tl.change(); - } - } -} -/*output -the traffic light is: RED -the traffic light is: GREEN -the traffic light is: YELLOW -the traffic light is: RED - */ -``` - -### EnumSet - - 如果你想用一个数表示多种状态,那么位运算是一种很好的选择。EnumSet 是通过位运算实现的。它是一个与枚举类型一起使用的专用 Set 实现。枚举set中所有元素都必须来自单个枚举类型(即必须是同类型,且该类型是 Enum 的子类)。 - -```java -public enum Season { - SPRING, SUMMER, AUTUMN, WINTER; - - public static void main(String[] args) { - Set emptyEnumSet = EnumSet.noneOf(Season.class); - System.out.println("EnumSet.noneOf(): " + emptyEnumSet); - emptyEnumSet.add(SPRING); - System.out.println("emptyEnumSet.add(SPRING): " + emptyEnumSet); - Set enumSet = EnumSet.allOf(Season.class); - System.out.println("EnumSet.allOf(): " + enumSet); - emptyEnumSet.addAll(enumSet); - System.out.println("emptyEnumSet.addAll(): " + emptyEnumSet); - Season[] seasons = new Season[emptyEnumSet.size()]; - enumSet.toArray(seasons); - System.out.println("seasons: " + Arrays.toString(seasons)); - } -} -/*output -EnumSet.noneOf(): [] -emptyEnumSet.add(SPRING): [SPRING] -EnumSet.allOf(): [SPRING, SUMMER, AUTUMN, WINTER] -emptyEnumSet.addAll(): [SPRING, SUMMER, AUTUMN, WINTER] -seasons: [SPRING, SUMMER, AUTUMN, WINTER] - */ -``` - -#### 源码解析 - -参考自:[EnumSet源码解析](https://www.jianshu.com/p/f7035c5816b1) - -```java -//EnumSet的容量小于64,创建的是RegularEnumSet,大于64,创建的是JumboEnumSet -public boolean add(E e) { - // 校验枚举类型 - typeCheck(e); - - long oldElements = elements; - elements |= (1L << ((Enum)e).ordinal()); - return elements != oldElements; -} - -/** - * 用于校验枚举类型,位于EnumSet中 - */ -final void typeCheck(E e) { - Class eClass = e.getClass(); - if (eClass != elementType && eClass.getSuperclass() != elementType) - throw new ClassCastException(eClass + " != " + elementType); -} -``` - -每一个枚举元素都有一个属性ordinal,用来表示该元素在枚举类型中的次序或者说下标。 - -![EnumSet add方法](https://img2018.cnblogs.com/blog/1252910/201904/1252910-20190415211843720-460902205.png) - -addAll 方法就是将 elements 上,从低位到枚举长度上的下标值置为1。比如某一个枚举类型共5个元素,而addAll 就是将 elements 的二进制的低5位置为1。 - -```java -void addAll() { - if (universe.length != 0) - elements = -1L >>> -universe.length; -} -``` - -### EnumMap - -EnumMap 是一种特殊的 Map,它要求其中的键必须来自一个 enum。由于 enum 本身的限制,所以 EnumMap 在内部可由数组实现。因此 EnumMap 的速度很快。 - -```java -interface Command { void action(); } -public class EnumMaps { - public static void main(String[] args) { - EnumMap em = new EnumMap<>(AlarmPoints.class); - em.put(KITCHEN, new Command() { - @Override - public void action() { - System.out.println("Kitchen fire!"); - } - }); - em.put(BATHROOM, new Command() { - @Override - public void action() { - System.out.println("Bathroom alert"); - } - }); - for(Map.Entry e : em.entrySet()) { - System.out.print(e.getKey() + ": "); - e.getValue().action(); - } - //获取不存在的值 - try { - em.get(UTILITY).action(); - } catch (Exception ex) { - System.out.println(ex); - } - } -} -/*output -BATHROOM: Bathroom alert -KITCHEN: Kitchen fire! -java.lang.NullPointerException - */ -``` - -与 EnumSet 一样,enum 实例定义时的次序决定了其在 EnumMap 中的顺序。每个 enum 实例都会作为键存放到 EnumMap 中,如果没有为这个键调用 put() 方法来存入相应的值的话,其对应的值为 null。 - - - -## 注解 - -Java SE5内置了三种定义在 java.lang 中的注解:@Override,@Deprecated 和 @SuppressWarnings(关闭不当的编译器警告信息)。 - -### 基本语法 - -```java -@Target(ElementType.METHOD)//应用于什么地方,方法、域等 -@Retention(RetentionPolicy.SOURCE)//注解在哪一个级别可用,SOURCE/CLASS/RUNTIME -public @interface Override { -} -``` - -#### 自定义注解 - -```java -//UserCase.java -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface UserCase { - public int id(); - public String description() default "no description"; -} - -//PasswordUtils.java -public class PasswordUtils { - @UserCase(id = 47, description = "Passwords must contain at least one numeric") - public boolean validatePassword(String password) { - return password.matches("\\w*\\d\\w*"); - } -} -``` - -注解元素(本例的是 id和description)只能是基本类型、String、Class、enum、Annotation 和这些类型的数组。 - -@Retention注解三种取值:**RetentionPolicy.SOURCE**、**RetentionPolicy.CLASS**、**RetentionPolicy.RUNTIME**分别对应:Java源文件(.java文件)---->.class文件---->内存中的字节码。 - -@Target元注解决定了一个注解可以标识到哪些成分上,如标识在在类身上,或者属性身上,或者方法身上等成分,@Target默认值为任何元素(成分)。 - -**Retention注解说明** - -参考自:[注解](https://www.cnblogs.com/xdp-gacl/p/3622275.html) - -当在Java源程序上加了一个注解,这个Java源程序要由javac去编译,javac把java源文件编译成.class文件,在编译成class时可能会把Java源程序上的一些注解给去掉,java编译器在处理java源程序时,可能会认为这个注解没有用了,于是就把这个注解去掉了,那么此时在编译好的class中就找不到注解了, 这是编译器编译java源程序时对注解进行处理的第一种可能情况,假设java编译器在把java源程序编译成class时,没有把java源程序中的注解去掉,那么此时在编译好的class中就可以找到注解,当程序使用编译好的class文件时,需要用类加载器把class文件加载到内存中,class文件中的东西不是字节码,class文件里面的东西由类加载器加载到内存中去,类加载器在加载class文件时,会对class文件里面的东西进行处理,如安全检查,处理完以后得到的最终在内存中的二进制的东西才是字节码,类加载器在把class文件加载到内存中时也有转换,转换时是否把class文件中的注解保留下来,这也有说法,所以说**一个注解的生命周期有三个阶段:java源文件是一个阶段,class文件是一个阶段,内存中的字节码是一个阶段**,javac把java源文件编译成.class文件时,有可能去掉里面的注解,类加载器把.class文件加载到内存时也有可能去掉里面的注解,因此**在自定义注解时就可以使用Retention注解指明自定义注解的生命周期,自定义注解的生命周期是在RetentionPolicy.SOURCE阶段(java源文件阶段),还是在RetentionPolicy.CLASS阶段(class文件阶段),或者是在RetentionPolicy.RUNTIME阶段(内存中的字节码运行时阶段)**,根据**JDK提供的API可以知道默认是在RetentionPolicy.CLASS阶段 (JDK的API写到:the retention policy defaults to RetentionPolicy.CLASS)。** - -### 元注解 - -元、注解负责注解其他的注解。 - -| 注解 | 作用 | -| ----------- | -------------------------------- | -| @Target | 表示该注解可以用于什么地方 | -| @Retention | 表示需要在什么级别保存该注解信息 | -| @Documented | 将此注解包含在 Javadoc 中 | -| @Inherited | 允许子类继承父类的注解 | - -### 编写注解处理器 - -使用注解时,很重要的一部分就是创建和使用注解处理器。 - -```java -public class UseCaseTracker { - public static void trackUseCases(List useCases, Class c) { - for(Method m : c.getDeclaredMethods()) { - UseCase uc = m.getAnnotation(UseCase.class); - if(uc != null) { - System.out.println("found use case: " + uc.id() + " " + uc.description()); - useCases.remove(new Integer(uc.id())); - } - } - for(int i : useCases) { - System.out.println("warning: missing use case-" + i); - } - } - - public static void main(String[] args) { - List useCases = new ArrayList<>(); - Collections.addAll(useCases, 47, 48); - trackUseCases(useCases, PasswordUtils.class); - } -} -/*output -found use case: 47 Passwords must contain at least one numeric -warning: missing use case-48 - */ -``` - -### 注解综合 - -```java -//TrafficLight.java -public enum TrafficLight { - RED, GREEN, YELLOW; -} - -//MetaAnnotation.java -public @interface MetaAnnotation { - String value(); -} - -//MyAnnotation.java -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface MyAnnotation { - String color() default "blue"; - //当只有value属性要设置时,可以省略"value=" - String value(); - int[] arrayAttr() default {1, 2, 3}; - TrafficLight lamp() default TrafficLight.GREEN; - MetaAnnotation annotationAttr() default @MetaAnnotation("tyson"); -} - -//MyAnnotationTracker.java -@MyAnnotation( - color = "red", - value = "MyAnnotation注解用于MyAnnotationTracker类", - arrayAttr = {6, 6, 6}, - lamp = TrafficLight.GREEN, - annotationAttr = @MetaAnnotation("sophia") -) -public class MyAnnotationTracker { - @MyAnnotation("MyAnnotation注解用于main方法") - public static void main(String[] args) { - //如果指定类型的注解存在于此元素上则返回true - if(MyAnnotationTracker.class.isAnnotationPresent(MyAnnotation.class)) { - //获取类的注解 - MyAnnotation myAnnotation = (MyAnnotation)MyAnnotationTracker.class.getAnnotation(MyAnnotation.class); - System.out.println(myAnnotation.color()); - System.out.println(myAnnotation.value()); - System.out.println(Arrays.toString(myAnnotation.arrayAttr())); - System.out.println(myAnnotation.lamp()); - MetaAnnotation ma = myAnnotation.annotationAttr(); - System.out.println(ma.value()); - } - //获取方法的注解 - for(Method m : MyAnnotationTracker.class.getDeclaredMethods()) { - MyAnnotation myAnnotation1 = m.getAnnotation(MyAnnotation.class); - if(myAnnotation1 != null) { - System.out.println(myAnnotation1.value()); - } - } - } -} -/*output -red -MyAnnotation注解用于MyAnnotationTracker类 -[6, 6, 6] -GREEN -sophia -MyAnnotation注解用于main方法 - */ -``` - - - - - - - diff --git "a/Java/Java\345\205\263\351\224\256\345\255\227.md" "b/Java/Java\345\205\263\351\224\256\345\255\227.md" deleted file mode 100644 index 3dc54e5..0000000 --- "a/Java/Java\345\205\263\351\224\256\345\255\227.md" +++ /dev/null @@ -1,205 +0,0 @@ - - - - -- [static](#static) - - [静态变量](#%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F) - - [静态方法](#%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95) - - [静态代码块](#%E9%9D%99%E6%80%81%E4%BB%A3%E7%A0%81%E5%9D%97) - - [静态内部类](#%E9%9D%99%E6%80%81%E5%86%85%E9%83%A8%E7%B1%BB) -- [final](#final) -- [this](#this) -- [super](#super) - - - -## static - -static可以用来修饰类的成员方法、类的成员变量。 - -### 静态变量 - -static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 - -以下例子,age为非静态变量,则p1打印结果是:`Name:zhangsan, Age:10`;若age使用static修饰,则p1打印结果是:`Name:zhangsan, Age:12`,因为static变量在内存只有一个副本。 - -```java -public class Person { - String name; - int age; - - public String toString() { - return "Name:" + name + ", Age:" + age; - } - - public static void main(String[] args) { - Person p1 = new Person(); - p1.name = "zhangsan"; - p1.age = 10; - Person p2 = new Person(); - p2.name = "lisi"; - p2.age = 12; - System.out.println(p1); - System.out.println(p2); - } - /**Output - * Name:zhangsan, Age:10 - * Name:lisi, Age:12 - *///~ -} -``` - -### 静态方法 - -static方法一般称作静态方法。静态方法不依赖于任何对象就可以进行访问,通过类名即可调用静态方法。 - -```java -public class Utils { - public static void print(String s) { - System.out.println("hello world: " + s); - } - - public static void main(String[] args) { - Utils.print("程序员大彬"); - } -} -``` - -### 静态代码块 - -静态代码块只会在类加载的时候执行一次。以下例子,startDate和endDate在类加载的时候进行赋值。 - -```java -class Person { - private Date birthDate; - private static Date startDate, endDate; - static{ - startDate = Date.valueOf("2008"); - endDate = Date.valueOf("2021"); - } - - public Person(Date birthDate) { - this.birthDate = birthDate; - } -} -``` - -### 静态内部类 - -**在静态方法里**,使用⾮静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。⽽静态内部类不需要。 - -```java -public class OuterClass { - class InnerClass { - } - static class StaticInnerClass { - } - public static void main(String[] args) { - // 在静态方法里,不能直接使用OuterClass.this去创建InnerClass的实例 - // 需要先创建OuterClass的实例o,然后通过o创建InnerClass的实例 - // InnerClass innerClass = new InnerClass(); - OuterClass outerClass = new OuterClass(); - InnerClass innerClass = outerClass.new InnerClass(); - StaticInnerClass staticInnerClass = new StaticInnerClass(); - - outerClass.test(); - } - - public void nonStaticMethod() { - InnerClass innerClass = new InnerClass(); - System.out.println("nonStaticMethod..."); - } -} -``` - - - -## final - -1. **基本数据**类型用final修饰,则不能修改,是常量;**对象引用**用final修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身可以修改。 - -2. final修饰的方法不能被子类重写 - -3. final修饰的类不能被继承。 - - - -## this - - `this.属性名称`指访问类中的成员变量,可以用来区分成员变量和局部变量。如下代码所示,`this.name`访问类Person当前实例的变量。 - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-17 00:29 - */ -public class Person { - String name; - int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } -} -``` - -`this.方法名称`用来访问本类的方法。以下代码中,`this.born()`调用类 Person 的当前实例的方法。 - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-17 00:29 - */ -public class Person { - String name; - int age; - - public Person(String name, int age) { - this.born(); - this.name = name; - this.age = age; - } - - void born() { - } -} -``` - - - -## super - -super 关键字用于在子类中访问父类的变量和方法。 - -```java -class A { - protected String name = "大彬"; - - public void getName() { - System.out.println("父类:" + name); - } -} - -public class B extends A { - @Override - public void getName() { - System.out.println(super.name); - super.getName(); - } - - public static void main(String[] args) { - B b = new B(); - b.getName(); - } - /** - * 大彬 - * 父类:大彬 - */ -} -``` - -在子类B中,我们重写了父类的getName()方法,如果在重写的getName()方法中我们要调用父类的相同方法,必须要通过super关键字显式指出。 - diff --git "a/Java/Java\345\237\272\347\241\200.md" "b/Java/Java\345\237\272\347\241\200.md" deleted file mode 100644 index 3a08dc1..0000000 --- "a/Java/Java\345\237\272\347\241\200.md" +++ /dev/null @@ -1,1582 +0,0 @@ - - - - -- [Java概述](#java%E6%A6%82%E8%BF%B0) - - [Java的特点](#java%E7%9A%84%E7%89%B9%E7%82%B9) - - [JKD和JRE](#jkd%E5%92%8Cjre) - - [JDK](#jdk) - - [JRE](#jre) -- [Java基础语法](#java%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95) - - [基本数据类型](#%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B) - - [包装类型](#%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B) - - [包装类缓存](#%E5%8C%85%E8%A3%85%E7%B1%BB%E7%BC%93%E5%AD%98) - - [String](#string) - - [String拼接](#string%E6%8B%BC%E6%8E%A5) - - [关键字](#%E5%85%B3%E9%94%AE%E5%AD%97) - - [static](#static) - - [静态变量](#%E9%9D%99%E6%80%81%E5%8F%98%E9%87%8F) - - [静态方法](#%E9%9D%99%E6%80%81%E6%96%B9%E6%B3%95) - - [静态代码块](#%E9%9D%99%E6%80%81%E4%BB%A3%E7%A0%81%E5%9D%97) - - [静态内部类](#%E9%9D%99%E6%80%81%E5%86%85%E9%83%A8%E7%B1%BB) - - [final](#final) - - [this](#this) - - [super](#super) - - [object常用方法](#object%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95) - - [toString](#tostring) - - [equals](#equals) - - [hashCode](#hashcode) - - [clone](#clone) - - [浅拷贝](#%E6%B5%85%E6%8B%B7%E8%B4%9D) - - [深拷贝](#%E6%B7%B1%E6%8B%B7%E8%B4%9D) - - [getClass](#getclass) - - [wait](#wait) - - [notity](#notity) - - [equals()和hashcode()的关系](#equals%E5%92%8Chashcode%E7%9A%84%E5%85%B3%E7%B3%BB) - - [==和equals区别](#%E5%92%8Cequals%E5%8C%BA%E5%88%AB) -- [面向对象](#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1) - - [面向对象特性](#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%89%B9%E6%80%A7) - - [多态怎么实现](#%E5%A4%9A%E6%80%81%E6%80%8E%E4%B9%88%E5%AE%9E%E7%8E%B0) - - [类与对象](#%E7%B1%BB%E4%B8%8E%E5%AF%B9%E8%B1%A1) - - [属性](#%E5%B1%9E%E6%80%A7) - - [方法](#%E6%96%B9%E6%B3%95) - - [普通方法](#%E6%99%AE%E9%80%9A%E6%96%B9%E6%B3%95) - - [构造方法](#%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95) - - [方法重载](#%E6%96%B9%E6%B3%95%E9%87%8D%E8%BD%BD) - - [方法重写](#%E6%96%B9%E6%B3%95%E9%87%8D%E5%86%99) - - [初始化顺序](#%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A1%BA%E5%BA%8F) -- [接口和抽象类](#%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB) - - [抽象类](#%E6%8A%BD%E8%B1%A1%E7%B1%BB) - - [接口](#%E6%8E%A5%E5%8F%A3) - - [接口与抽象类区别](#%E6%8E%A5%E5%8F%A3%E4%B8%8E%E6%8A%BD%E8%B1%A1%E7%B1%BB%E5%8C%BA%E5%88%AB) -- [反射](#%E5%8F%8D%E5%B0%84) - - [Class类](#class%E7%B1%BB) - - [Field类](#field%E7%B1%BB) - - [Method类](#method%E7%B1%BB) -- [泛型](#%E6%B3%9B%E5%9E%8B) - - [泛型类](#%E6%B3%9B%E5%9E%8B%E7%B1%BB) - - [泛型接口](#%E6%B3%9B%E5%9E%8B%E6%8E%A5%E5%8F%A3) - - [泛型方法](#%E6%B3%9B%E5%9E%8B%E6%96%B9%E6%B3%95) -- [Exception](#exception) - - [Throwable](#throwable) - - [常见的Exception](#%E5%B8%B8%E8%A7%81%E7%9A%84exception) - - [关键字](#%E5%85%B3%E9%94%AE%E5%AD%97-1) -- [IO流](#io%E6%B5%81) - - [InputStream 和 OutputStream](#inputstream-%E5%92%8C-outputstream) - - [Reader 和 Writer](#reader-%E5%92%8C-writer) - - [字符流和字节流的转换](#%E5%AD%97%E7%AC%A6%E6%B5%81%E5%92%8C%E5%AD%97%E8%8A%82%E6%B5%81%E7%9A%84%E8%BD%AC%E6%8D%A2) - - [同步异步](#%E5%90%8C%E6%AD%A5%E5%BC%82%E6%AD%A5) - - [阻塞非阻塞](#%E9%98%BB%E5%A1%9E%E9%9D%9E%E9%98%BB%E5%A1%9E) - - [BIO](#bio) - - [NIO](#nio) - - [AIO](#aio) - - [BIO/NIO/AIO区别](#bionioaio%E5%8C%BA%E5%88%AB) -- [ThreadLocal](#threadlocal) - - [原理](#%E5%8E%9F%E7%90%86) - - [内存泄漏](#%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F) - - [使用场景](#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) -- [线程安全类](#%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%B1%BB) -- [常见操作](#%E5%B8%B8%E8%A7%81%E6%93%8D%E4%BD%9C) - - [排序](#%E6%8E%92%E5%BA%8F) - - [数组操作](#%E6%95%B0%E7%BB%84%E6%93%8D%E4%BD%9C) - - [拷贝](#%E6%8B%B7%E8%B4%9D) - - [数组拷贝](#%E6%95%B0%E7%BB%84%E6%8B%B7%E8%B4%9D) - - [对象拷贝](#%E5%AF%B9%E8%B1%A1%E6%8B%B7%E8%B4%9D) - - [序列化](#%E5%BA%8F%E5%88%97%E5%8C%96) - - [什么情况下需要序列化?](#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E5%BA%8F%E5%88%97%E5%8C%96) - - [如何实现序列化](#%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%BA%8F%E5%88%97%E5%8C%96) - - [serialVersionUID](#serialversionuid) - - [遍历](#%E9%81%8D%E5%8E%86) - - [fast-fail](#fast-fail) - - [fail-safe](#fail-safe) - - [移除集合元素](#%E7%A7%BB%E9%99%A4%E9%9B%86%E5%90%88%E5%85%83%E7%B4%A0) - - - -# Java概述 - -## Java的特点 - -**Java是一门面向对象的编程语言。**面向对象和面向过程是一种软件开发思想。 - -- 面向过程就是分析出解决问题所需要的步骤,然后用函数按这些步骤实现,使用的时候依次调用就可以了。面向对象是把构成问题事务分解成各个对象,分别设计这些对象,然后将他们组装成有完整功能的系统。面向过程只用函数实现,面向对象是用类实现各个功能模块。 - - 例如五子棋,面向过程的设计思路就是首先分析问题的步骤: - 1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。 - 把上面每个步骤用分别的函数来实现,问题就解决了。 - -- 而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为: - 1、黑白双方 - 2、棋盘系统,负责绘制画面 - 3、规则系统,负责判定诸如犯规、输赢等。 - 黑白双方负责接受用户的输入,并告知棋盘系统棋子布局发生变化,棋盘系统接收到了棋子的变化的信息就负责在屏幕上面显示出这种变化,同时利用规则系统来对棋局进行判定。 - -**Java具有平台独立性和移植性。** - -- Java有一句口号:Write once, run anywhere,一次编写、到处运行。这也是Java的魅力所在。而实现这种特性的正是Java虚拟机JVM。已编译的Java程序可以在任何带有JVM的平台上运行。你可以在windows平台编写代码,然后拿到linux上运行。只要你在编写完代码后,将代码编译成.class文件,再把class文件打成Java包,这个jar包就可以在不同的平台上运行了。 - -**Java具有稳健性。** - -- Java是一个强类型语言,它允许扩展编译时检查潜在类型不匹配问题的功能。Java要求显式的方法声明,它不支持C风格的隐式声明。这些严格的要求保证编译程序能捕捉调用错误,这就导致更可靠的程序。 -- 异常处理是Java中使得程序更稳健的另一个特征。异常是某种类似于错误的异常条件出现的信号。使用try/catch/finally语句,程序员可以找到出错的处理代码,这就简化了出错处理和恢复的任务。 - -## JKD和JRE - -JDK和JRE是Java开发和运行工具,其中JDK包含了JRE,而JRE是可以独立安装的。 - -### JDK - -Java Development Kit,JAVA语言的软件工具开发包,是整个JAVA开发的核心,它包含了JAVA的运行(JVM+JAVA类库)环境和JAVA工具。 - -### JRE - -JRE(Java Runtime Environment,Java运行环境):包含JVM标准实现及Java核心类库。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)。 - -JRE是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。 - -# Java基础语法 - -## 基本数据类型 - -- byte,8bit -- char,16bit -- short,16bit -- int,32bit -- float,32bit -- long,64bit -- double,64bit -- boolean,只有两个值:true、false,可以使⽤用 1 bit 来存储,但是具体⼤小没有明确规定。 - -## 包装类型 - -基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值通过自动装箱与拆箱完成。 - -```java -Integer x = 1; // 装箱 调⽤ Integer.valueOf(1) -int y = x; // 拆箱 调⽤了 X.intValue() -``` - -### 包装类缓存 - -使用Integer.valueOf(i)生成Integer,如果 -128 <= i <= 127,则直接从cache取对象返回,不会创建新的对象,避免频繁创建包装类对象。 - -```java - public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) - return IntegerCache.cache[i + (-IntegerCache.low)]; - return new Integer(i); - } -``` - -## String - -String是final类,不可被继承。 - -### String拼接 - -字符串拼接可以使用String用+做拼接,也可以使用StringBuilder和StringBuffer实现,三种方式对比: - -- 底层都是char数组实现的 - -- 字符串拼接性能:StringBuilder > StringBuffer > String -- String 是字符串常量,一旦创建之后该对象是不可更改的,用+对String做拼接操作,实际上是先通过建立StringBuilder,然后调用append()做拼接操作,所以在大量字符串拼接的时候,会频繁创建StringBuilder,性能较差。 -- StringBuilder和StringBuffer的对象是字符串变量,对变量进行操作就是直接对该对象进行修改(修改char[]数组),所以速度要比String快很多。 -- 在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的,StringBuffer中很多方法带有synchronized关键字,可以保证线程安全。 - -## 关键字 - -### static - -static可以用来修饰类的成员方法、类的成员变量。 - -#### 静态变量 - -static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 - -以下例子,age为非静态变量,则p1打印结果是:`Name:zhangsan, Age:10`;若age使用static修饰,则p1打印结果是:`Name:zhangsan, Age:12`,因为static变量在内存只有一个副本。 - -```java -public class Person { - String name; - int age; - - public String toString() { - return "Name:" + name + ", Age:" + age; - } - - public static void main(String[] args) { - Person p1 = new Person(); - p1.name = "zhangsan"; - p1.age = 10; - Person p2 = new Person(); - p2.name = "lisi"; - p2.age = 12; - System.out.println(p1); - System.out.println(p2); - } - /**Output - * Name:zhangsan, Age:10 - * Name:lisi, Age:12 - *///~ -} -``` - -#### 静态方法 - -static方法一般称作静态方法。静态方法不依赖于任何对象就可以进行访问,通过类名即可调用静态方法。 - -```java -public class Utils { - public static void print(String s) { - System.out.println("hello world: " + s); - } - - public static void main(String[] args) { - Utils.print("程序员大彬"); - } -} -``` - -#### 静态代码块 - -静态代码块只会在类加载的时候执行一次。以下例子,startDate和endDate在类加载的时候进行赋值。 - -```java -class Person { - private Date birthDate; - private static Date startDate, endDate; - static{ - startDate = Date.valueOf("2008"); - endDate = Date.valueOf("2021"); - } - - public Person(Date birthDate) { - this.birthDate = birthDate; - } -} -``` - -#### 静态内部类 - -**在静态方法里**,使用⾮静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。⽽静态内部类不需要。 - -```java -public class OuterClass { - class InnerClass { - } - static class StaticInnerClass { - } - public static void main(String[] args) { - // 在静态方法里,不能直接使用OuterClass.this去创建InnerClass的实例 - // 需要先创建OuterClass的实例o,然后通过o创建InnerClass的实例 - // InnerClass innerClass = new InnerClass(); - OuterClass outerClass = new OuterClass(); - InnerClass innerClass = outerClass.new InnerClass(); - StaticInnerClass staticInnerClass = new StaticInnerClass(); - - outerClass.test(); - } - - public void nonStaticMethod() { - InnerClass innerClass = new InnerClass(); - System.out.println("nonStaticMethod..."); - } -} -``` - - - -### final - -1. **基本数据**类型用final修饰,则不能修改,是常量;**对象引用**用final修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身可以修改。 - -2. final修饰的方法不能被子类重写 - -3. final修饰的类不能被继承。 - - - -### this - - `this.属性名称`指访问类中的成员变量,可以用来区分成员变量和局部变量。如下代码所示,`this.name`访问类Person当前实例的变量。 - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-17 00:29 - */ -public class Person { - String name; - int age; - - public Person(String name, int age) { - this.name = name; - this.age = age; - } -} -``` - -`this.方法名称`用来访问本类的方法。以下代码中,`this.born()`调用类 Person 的当前实例的方法。 - -```java -/** - * @description: - * @author: 程序员大彬 - * @time: 2021-08-17 00:29 - */ -public class Person { - String name; - int age; - - public Person(String name, int age) { - this.born(); - this.name = name; - this.age = age; - } - - void born() { - } -} -``` - - - -### super - -super 关键字用于在子类中访问父类的变量和方法。 - -```java -class A { - protected String name = "大彬"; - - public void getName() { - System.out.println("父类:" + name); - } -} - -public class B extends A { - @Override - public void getName() { - System.out.println(super.name); - super.getName(); - } - - public static void main(String[] args) { - B b = new B(); - b.getName(); - } - /** - * 大彬 - * 父类:大彬 - */ -} -``` - -在子类B中,我们重写了父类的getName()方法,如果在重写的getName()方法中我们要调用父类的相同方法,必须要通过super关键字显式指出。 - -## object常用方法 - -Java面试经常会出现的一道题目,Object的常用方法。下面给大家整理一下。 - -object常用方法有:toString()、equals()、hashCode()、clone()等。 - -### toString - -默认输出对象地址。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - public static void main(String[] args) { - System.out.println(new Person(18, "程序员大彬").toString()); - } - //output - //me.tyson.java.core.Person@4554617c -} -``` - -可以重写toString方法,按照重写逻辑输出对象值。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public String toString() { - return name + ":" + age; - } - - public static void main(String[] args) { - System.out.println(new Person(18, "程序员大彬").toString()); - } - //output - //程序员大彬:18 -} -``` - -### equals - -默认比较两个引用变量是否指向同一个对象(内存地址)。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - public static void main(String[] args) { - String name = "程序员大彬"; - Person p1 = new Person(18, name); - Person p2 = new Person(18, name); - - System.out.println(p1.equals(p2)); - } - //output - //false -} -``` - -可以重写equals方法,按照age和name是否相等来判断: - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public boolean equals(Object o) { - if (o instanceof Person) { - Person p = (Person) o; - return age == p.age && name.equals(p.name); - } - return false; - } - - public static void main(String[] args) { - String name = "程序员大彬"; - Person p1 = new Person(18, name); - Person p2 = new Person(18, name); - - System.out.println(p1.equals(p2)); - } - //output - //true -} -``` - -### hashCode - -将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。 - -```java -public class Cat { - public static void main(String[] args) { - System.out.println(new Cat().hashCode()); - } - //out - //1349277854 -} -``` - -### clone - -java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的。Object对象有个clone()方法,实现了对 - -象中各个属性的复制,但它的可见范围是protected的。 - -```java -protected native Object clone() throws CloneNotSupportedException; -``` - -所以实体类使用克隆的前提是: - -- 实现Cloneable接口,这是一个标记接口,自身没有方法,这应该是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常CloneNotSupportedException。 -- 覆盖clone()方法,可见性提升为public。 - -```java -public class Cat implements Cloneable { - private String name; - - @Override - protected Object clone() throws CloneNotSupportedException { - return super.clone(); - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - c.name = "程序员大彬"; - Cat cloneCat = (Cat) c.clone(); - c.name = "大彬"; - System.out.println(cloneCat.name); - } - //output - //程序员大彬 -} -``` - -#### 浅拷贝 - -拷⻉对象和原始对象的引⽤类型引用同⼀个对象。 - -以下例子,Cat对象里面有个Person对象,调用clone之后,克隆对象和原对象的Person引用的是同一个对象,这就是浅拷贝。 - -```java -public class Cat implements Cloneable { - private String name; - private Person owner; - - @Override - protected Object clone() throws CloneNotSupportedException { - return super.clone(); - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - Person p = new Person(18, "程序员大彬"); - c.owner = p; - - Cat cloneCat = (Cat) c.clone(); - p.setName("大彬"); - System.out.println(cloneCat.owner.getName()); - } - //output - //大彬 -} -``` - -#### 深拷贝 - -拷贝对象和原始对象的引用类型引用不同的对象。 - -以下例子,在clone函数中不仅调用了super.clone,而且调用Person对象的clone方法(Person也要实现Cloneable接口并重写clone方法),从而实现了深拷贝。可以看到,拷贝对象的值不会受到原对象的影响。 - -```java -public class Cat implements Cloneable { - private String name; - private Person owner; - - @Override - protected Object clone() throws CloneNotSupportedException { - Cat c = null; - c = (Cat) super.clone(); - c.owner = (Person) owner.clone();//拷贝Person对象 - return c; - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - Person p = new Person(18, "程序员大彬"); - c.owner = p; - - Cat cloneCat = (Cat) c.clone(); - p.setName("大彬"); - System.out.println(cloneCat.owner.getName()); - } - //output - //程序员大彬 -} -``` - -### getClass - -返回此 Object 的运行时类,常用于java反射机制。 - -```java -public class Person { - private String name; - - public Person(String name) { - this.name = name; - } - - public static void main(String[] args) { - Person p = new Person("程序员大彬"); - Class clz = p.getClass(); - System.out.println(clz); - //获取类名 - System.out.println(clz.getName()); - } - /** - * class com.tyson.basic.Person - * com.tyson.basic.Person - */ -} -``` - -### wait - -当前线程调用对象的wait()方法之后,当前线程会释放对象锁,进入等待状态。等待其他线程调用此对象的notify()/notifyAll()唤醒或者等待超时时间wait(long timeout)自动唤醒。线程需要获取obj对象锁之后才能调用 obj.wait()。 - -### notity - -obj.notify()唤醒在此对象上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象上等待的所有线程。 - -> LockSupport.park()/LockSupport.parkNanos(long nanos)/LockSupport.parkUntil(long deadlines),用于阻塞当前线程。对比obj.wait()方法,不需要获得锁就可以让线程进入等待状态,需要通过LockSupport.unpark(Thread thread)唤醒。 - -### equals()和hashcode()的关系 - -equals与hashcode的关系: -1、如果两个对象调用equals比较返回true,那么它们的hashCode值一定要相同; -2、如果两个对象的hashCode相同,它们并不一定相同。 - -hashcode方法主要是用来提升对象比较的效率,先进行hashcode()的比较,如果不相同,那就不必在进行equals的比较,这样就大大减少了equals比较的次数,当比较对象的数量很大的时候能提升效率。 - -之所以重写equals()要重写hashcode(),是为了保证equals()方法返回true的情况下hashcode值也要一致,如果重写了equals()没有重写hashcode(),就会出现两个对象相等但hashcode()不相等的情况。这样,当用其中的一个对象作为键保存到hashMap、hashTable或hashSet中,再以另一个对象作为键值去查找他们的时候,则会查找不到。 - -### ==和equals区别 - -- 对于基本数据类型,==比较的是他们的值。基本数据类型没有equal方法; - -- 对于复合数据类型,==比较的是它们的存放地址(是否是同一个对象)。equals()默认比较地址值,重写的话按照重写逻辑去比较。 - -# 面向对象 - -## 面向对象特性 - -面向对象四大特性:封装,继承,多态,抽象 - -- 封装就是将类的信息隐藏在类内部,不允许外部程序直接访问,而是通过该类的方法实现对隐藏信息的操作和访问。 良好的封装能够减少耦合。 -- 继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性和易维护性。在Java中是单继承的,也就是说一个子类只有一个父类。 -- 多态是同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定的代码。 - 实现多态的三要素:继承、重写、父类引用指向子类对象。 - 静态多态性:通过重载实现,相同的方法有不同的參数列表,可以根据参数的不同,做出不同的处理。 - 动态多态性:在子类中重写父类的方法。运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。 -- 抽象。把客观事物用代码抽象出来。 - -### 多态怎么实现 - -Java提供了编译时多态和运行时多态两种多态机制。编译时多态通过重载实现,根据传入参数不同调用不同的方法。运行时多态通过重写来实现,在子类中重写父类的方法,运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。 - -## 类与对象 - -**类class**对一类事物的描述,是抽象的、概念上的定义。类的结构包括属性和方法。 - -```java -public class Person { - //属性 - private int age; - private String name; - - //构造方法 - public Person(int age, String name) { - this.age = age; - this.name = name; - } -} -``` - -对象是实际存在的该类事物的个体,也称为实例。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - public static void main(String[] args) { - //p为对象 - Person p = new Person(18, "程序员大彬"); - } -} -``` - -在Java中,万物皆对象,一切事物都可以看做对象。上述代码通过new关键字,创建了Person对象,p为对象的引用,p指向所创建的Person对象的内存地址。 - -## 属性 - -属性用来描述对象的状态信息,通常以变量的形式进行定义。变量分为成员变量和局部变量。 - -在类中,方法体之外定义的变量称为成员变量: - -- 成员变量定义在类中,在整个类中都可以被访问 -- 成员变量分为类变量和实例变量,实例变量存在于每个对象所在的堆内存中。实例变量要创建对象后才能访问。 -- 成员变量有默认初始化值 -- 成员变量的权限修饰符可以指定 - -定义在方法内,代码块内的变量称为局部变量: - -- 局部变量存在于栈内存中 -- 局部变量作用范围结束,变量的内存空间回自动释放 -- 局部变量没有默认值,每次必须显示初始化 -- 局部变量声明时不指定权限修饰符 - -## 方法 - -描述的是对象的动作信息,方法也称为函数。 - -### 普通方法 - -普通通方法的语法结构: - -```java -[修饰符列表] 返回值类型 方法名(形参列表){ - //方法体 -} -``` - -定义方法可以将功能代码进行封装。便于该功能进行复用。方法只有被调用才会被执行。 - -### 构造方法 - -构造方法是一种比较特殊的方法,通过构造方法可以创建对象以及初始化实例变量。实例变量没有手动赋值的时候,系统会赋默认值。 - -```java -public class Person { - //属性 - private int age; - private String name; - - //构造方法 - public Person(int age, String name) { - this.age = age; - this.name = name; - } -} -``` - -## 方法重载 - -同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载。参数列表又叫参数签名,包括参数的类型、参数的个数、参数的顺序,只要有一个不同就叫做参数列表不同。 - -重载是面向对象的一个基本特性。 - -```java -public class OverrideTest { - void setPerson() { } - - void setPerson(String name) { - //set name - } - - void setPerson(String name, int age) { - //set name and age - } -} -``` - -## 方法重写 - -方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写。 - -方法重写时, 方法名与形参列表必须一致。 - -如下代码,Person为父类,Student为子类,在Student中重写了dailyTask方法。 - -```java -public class Person { - private String name; - - public void dailyTask() { - System.out.println("work eat sleep"); - } -} - - -public class Student extends Person { - @Override - public void dailyTask() { - System.out.println("study eat sleep"); - } -} -``` - -## 初始化顺序 - -Java中类初始化顺序: - -1. 静态属性,静态代码块。 -2. 普通属性,普通代码块。 -3. 构造方法。 - -```java -public class LifeCycle { - // 静态属性 - private static String staticField = getStaticField(); - - // 静态代码块 - static { - System.out.println(staticField); - System.out.println("静态代码块初始化"); - } - - // 普通属性 - private String field = getField(); - - // 普通代码块 - { - System.out.println(field); - System.out.println("普通代码块初始化"); - } - - // 构造方法 - public LifeCycle() { - System.out.println("构造方法初始化"); - } - - // 静态方法 - public static String getStaticField() { - String statiFiled = "静态属性初始化"; - return statiFiled; - } - - // 普通方法 - public String getField() { - String filed = "普通属性初始化"; - return filed; - } - - public static void main(String[] argc) { - new LifeCycle(); - } - - /** - * 静态属性初始化 - * 静态代码块初始化 - * 普通属性初始化 - * 普通代码块初始化 - * 构造方法初始化 - */ -} -``` - -# 接口和抽象类 - -Java中接口和抽象类的定义语法分别为interface与abstract关键字。 - -## 抽象类 - -首先了解一下抽象方法。抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的格式为: - -```java -abstract void method(); -``` - -抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有未实现的方法,所以不能用抽象类创建对象。 - -抽象类的特点: - -- 抽象类不能被实例化只能被继承; -- 包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法; -- 抽象类中的抽象方法的修饰符只能为public或者protected,默认为public。 - -## 接口 - -接口对行为的抽象。在Java中,定一个接口的形式如下: - -```java -public interface InterfaceName { -} -``` - -接口中可以含有变量和方法。 - -**接口的特点:** - -- 接口中的变量会被隐式指定为public static final类型,而方法会被隐式地指定为public abstract类型; -- 接口中的方法必须是抽象方法,所有的方法不能有具体的实现; -- 一个类可以实现多个接口。 - -**接口的用处** - -接口是对行为的抽象,通过接口可以实现不相关类的相同行为。通过接口可以知道一个类具有哪些行为特性。 - -## 接口与抽象类区别 - -- 语法层面上 - 1)抽象类可以有方法实现,而接口的方法中只能是抽象方法; - 2)抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型; - 3)接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法; - 4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。 - -- 设计层面上的区别 - 1)抽象层次不同。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口只是对类行为进行抽象。继承抽象类是一种"是不是"的关系,而接口实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是具备不具备的关系,比如鸟是否能飞。 - 2) 继承抽象类的是具有相似特点的类,而实现接口的却可以不同的类。 - - 门和警报的例子: - - ```java - class AlarmDoor extends Door implements Alarm { - //code - } - - class BMWCar extends Car implements Alarm { - //code - } - ``` - -# 反射 - -Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,并且能改变它的属性。 - -作用:可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等。需要注意的是反射使用不当会造成很高的资源消耗! - -## Class类 - -在Java中,每定义一个Java class实体都会产生一个Class对象。这个Class对象用于表示这个类的类型信息。 - -获取Class对象的三种方式: - -```java -//1、通过对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object -// 类型的对象,而我不知道你具体是什么类,用这种方法 -  Person p1 = new Person(); -  Class c1 = p1.getClass(); - -//2、直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高 -// 这说明任何一个类都有一个隐含的静态成员变量 class -  Class c2 = Person.class; - -//3、通过 Class 对象的 forName() 静态方法来获取,用的最多, -// 但可能抛出 ClassNotFoundException 异常 -  Class c3 = Class.forName("com.ys.reflex.Person"); -``` - -Class 类提供了一些方法,可以获取成员变量、成员方法、接口、超类、构造方法: - -```java -  getName():获得类的完整名字。 -  getFields():获得类的public类型的属性。 -  getDeclaredFields():获得类的所有属性。包括private 声明的和继承类 -  getMethods():获得类的public类型的方法。 -  getDeclaredMethods():获得类的所有方法。包括private 声明的和继承类 -  getMethod(String name, Class[] parameterTypes):获得类的特定方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。 -  getConstructors():获得类的public类型的构造方法。 -  getConstructor(Class[] parameterTypes):获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型。 -  newInstance():通过类的不带参数的构造方法创建这个类的一个对象。 -``` - -## Field类 - -Field提供了类和接口中字段的信息,通过Field类可以动态访问这些字段。下图是Field类提供的一些方法。 - -![field-method](http://img.dabin-coder.cn/image/field-method.png) - -## Method类 - -Method类位于 java.lang.reflect 包中,主要用于在程序运行状态中,动态地获取方法信息。 - -Method类常用的方法: - -1. getAnnotation(Class annotationClass):如果该方法对象存在指定类型的注解,则返回该注解,否则返回null。 -2. getName():返回方法对象名称。 -3. isAnnotationPresent(Class annotationClass):如果该方法对象上有指定类型的注解,则返回true,否则为false。 -4. getDeclaringClass ():返回该方法对象表示的方法所在类的Class对象。 -5. getParameters():返回一个参数对象数组,该数组表示该方法对象的所有参数。 -6. getReturnType():返回一个Class对象,该Class对象表示该方法对象的返回对象,会擦除泛型。 - -# 泛型 - -泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。编译时会进行类型擦除。在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 - -## 泛型类 - -泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。 - -泛型类示例: - -```java -//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 -//在实例化泛型类时,必须指定T的具体类型 -public class Generic{ - //key这个成员变量的类型为T,T的类型由外部指定 - private T key; - - public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 - this.key = key; - } - - public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 - return key; - } -} -``` - -泛型类的使用: - -```java -@Slf4j -public class GenericTest { - public static void main(String[] args) { - //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型 - //传入的实参类型需与泛型的类型参数类型相同,即为Integer. - Generic genericInteger = new Generic(666); - - //传入的实参类型需与泛型的类型参数类型相同,即为String. - Generic genericString = new Generic("程序员大彬"); - log.info("泛型测试: key is {}", genericInteger.getKey()); - log.info("泛型测试: key is {}", genericString.getKey()); - } - - /** - * output - * 23:51:55.519 [main] INFO com.tyson.generic.GenericTest - 泛型测试: key is 666 - * 23:51:55.526 [main] INFO com.tyson.generic.GenericTest - 泛型测试: key is 程序员大彬 - */ -} -``` - -## 泛型接口 - -泛型接口与泛型类的定义及使用基本相同。 - -```java -//定义一个泛型接口 -public interface Generator { - public T next(); -} -``` - -## 泛型方法 - -泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。 - -```java -@Slf4j -public class GenericMethod { - - /** - * 泛型方法的基本介绍 - * @param t 传入的泛型实参 - * @return T 返回值为T类型 - * 说明: - * 1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。 - * 2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 - * 3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 - * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 - */ - public void genericMethod(T t) { - log.info(t.toString()); - } - - public static void main(String[] args) { - GenericMethod genericMethod = new GenericMethod(); - genericMethod.genericMethod("程序员大彬"); - genericMethod.genericMethod(666); - } - - /** - * output - * 23:59:11.906 [main] INFO com.tyson.generic.GenericMethod - 程序员大彬 - * 23:59:11.912 [main] INFO com.tyson.generic.GenericMethod - 666 - */ -} -``` - - - -# Exception - -在 JAVA 语言中,将程序执行中发生的不正常情况称为异常。异常是程序经常会出现的情况,发现错误的最佳时机是在编译阶段,也就是运行程序之前。 - -所有异常都继承了 Throwable。 - -## Throwable - -Throwable类是Error和Exception的父类,只有继承于Throwable的类或者其子类才能被抛出。Throwable分为两类: - -![exception](http://img.dabin-coder.cn/image/exception.png) - -- Error:JVM 无法解决的严重问题,如栈溢出(StackOverflowError)、内存溢出(OOM)等。程序无法处理的错误。 - - 栈溢出:如下代码递归调用main,最终会抛出StackOverflowError。 - - ```java - public class Test { - public static void main(String[] args) { - main(args); - } - /** - * Exception in thread "main" java.lang.StackOverflowError - */ - } - ``` - - 内存溢出OOM:下面的代码中,新建了一个数组,数组长度是 1G,又因为是 Integer 包装类型,一个元素占 4 个字节,所以,这个数组占用内存 4GB。最后,堆内存空间不足,抛出了 OOM。 - - ```java - public class Test { - public static void main(String[] args) { - Integer[] arr = new Integer[1024 * 1024 * 1024]; - } - /** - * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space - */ - } - ``` - -- Exception:其它因编程错误或偶然的外在因素导致的一般性问题。可以在代码中进行处理。如:空指针异常、数组下标越界等。 - -unchecked exception包括RuntimeException和Error类,其他所有异常称为检查(checked)异常。 - -运行时异常和非运行时异常(checked)的区别: - -1. RuntimeException由程序错误导致,应该修正程序避免这类异常发生。 -2. checked Exception由具体的环境(读取的文件不存在或文件为空或sql异常)导致的异常。必须进行处理,不然编译不通过,可以catch或者throws。 - -## 常见的Exception - -常见的RuntimeException: - -```java -ClassCastException //类型转换异常 -IndexOutOfBoundsException //数组越界异常 -NullPointerException //空指针 -ArrayStoreException //数组存储异常 -NumberFormatException //数字格式化异常 -ArithmeticException //数学运算异常 -``` - -unchecked Exception: - -``` -NoSuchFieldException //反射异常,没有对应的字段 -ClassNotFoundException //类没有找到异常 -IllegalAccessException //安全权限异常,可能是反射时调用了private方法 -``` - -## 关键字 - -- **throw**:用于抛出一个具体的异常对象。 - -- **throws**:用在方法签名中,用于声明该方法可能抛出的异常。子类方法抛出的异常范围更加小,或者根本不抛异常。 - -- **try**:用于监听。将可能抛出异常的代码放在try语句块之中,当try语句块内发生异常时,异常就被抛出。 -- **catch**:用于捕获异常。 -- **finally**:finally语句块总是会被执行。它主要用于回收在try块里打开的资源(如数据库连接、网络连接和磁盘文件)。 - -如下代码示例,除以0抛出异常,发生异常之后的代码不会执行,直接跳到catch语句块执行,最后执行finally语句块。 - -```java -public class ExceptionTest { - public static void main(String[] args) { - try { - int i = 1 / 0; - System.out.println(i + 1); - } catch (Exception e) { - System.out.println(e.getMessage()); - } finally { - System.out.println("run finally..."); - } - } - /** - * / by zero - * run finally... - */ -} -``` - - - -# IO流 - -Java IO流的核心就是对文件的操作,对于字节 、字符类型的输入和输出流。IO流主要分为两大类,字节流和字符流。字节流可以处理任何类型的数据,如图片,视频等,字符流只能处理字符类型的数据。 - -![io](http://img.dabin-coder.cn/image/io.jpg) - -> 图片参考:[Java io学习整理](https://zhuanlan.zhihu.com/p/25418336) - -## InputStream 和 OutputStream - -InputStream 用来表示那些从不同数据源产生输入的类。这些数据源包括:1.字节数组;2.String 对象;3.文件;4.管道;5.一个由其他种类的流组成的序列。 - -InputStream 类有一个抽象方法:`abstract int read()`,这个方法将读入并返回一个字节,或者在遇到输入源结尾时返回-1。 - -OutputStream 决定了输出所要去的目标:字节数组、文件或管道。OutputStream 的 `abstract void write(int b)` 可以向某个输出位置写出一个字节。 - -read() 和 write() 方法在执行时都将阻塞,等待数据被读入或者写出。 - -常用的字节流有FileInputStream、FileOutputStream、ObjectInputStream、ObjectOutputStream。 - -## Reader 和 Writer - -字符流是由通过字节流转换得到的,转化过程耗时,而且容易出现乱码问题。I/O 流提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -```java -abstract int read(); -abstract void write(char c); -``` - -## 字符流和字节流的转换 - -InputStreamReader:字节到字符的转换,可对读取到的字节数据经过指定编码转换成字符。 - -OutputStreamWriter:字符到字节的转换,可对读取到的字符数据经过指定编码转换成字节。 - -## 同步异步 - -同步:发出一个调用时,在没有得到结果之前,该调用就不返回。 - -异步:在调用发出后,被调用者返回结果之后会通知调用者,或通过回调函数处理这个调用。 - -## 阻塞非阻塞 - -阻塞和非阻塞关注的是线程的状态。 - -阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会恢复运行。 - -非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 - -> 举个例子,理解下同步、阻塞、异步、非阻塞的区别: -> -> 同步就是烧开水,要自己来看开没开;异步就是水开了,然后水壶响了通知你水开了(回调通知)。阻塞是烧开水的过程中,你不能干其他事情,必须在旁边等着;非阻塞是烧开水的过程里可以干其他事情。 - -## BIO - -同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。 - -![bio](http://img.dabin-coder.cn/image/bio.png) - -## NIO - -NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。 - -![nio](http://img.dabin-coder.cn/image/nio.png) - -NIO与IO区别: - -- IO是面向流的,NIO是面向缓冲区的; -- IO流是阻塞的,NIO流是不阻塞的; -- NIO有选择器,而IO没有。 - -Buffer:Buffer用于和Channel交互。从Channel中读取数据到Buffer里,从Buffer把数据写入到Channel。 - -Channel:NIO 通过Channel(通道) 进行读写。通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。 - -Selector:使用更少的线程来就可以来处理通道了,相比使用多个线程,避免了线程上下文切换带来的开销。 - -## AIO - -异步非阻塞 IO。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。 - -## BIO/NIO/AIO区别 - -同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。 - -同步非阻塞IO: 客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。用户进程发起一个IO操作以后,可做其它事情,但用户进程需要轮询IO操作是否完成,这样造成不必要的CPU资源浪费。 - -异步非阻塞IO: 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。用户进程发起一个IO操作,然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。 - - - -# ThreadLocal -线程本地变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。 - -## 原理 - -每个线程都有一个ThreadLocalMap(ThreadLocal内部类),Map中元素的键为ThreadLocal,而值对应线程的变量副本。 - -![image-20200715234257808](../img/threadlocal.png) - -调用threadLocal.set()-->调用getMap(Thread)-->返回当前线程的ThreadLocalMap-->map.set(this, value),this是ThreadLocal - -```java -public void set(T value) { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) - map.set(this, value); - else - createMap(t, value); -} - -ThreadLocalMap getMap(Thread t) { - return t.threadLocals; -} - -void createMap(Thread t, T firstValue) { - t.threadLocals = new ThreadLocalMap(this, firstValue); -} -``` -调用get()-->调用getMap(Thread)-->返回当前线程的ThreadLocalMap-->map.getEntry(this),返回value - -```java - public T get() { - Thread t = Thread.currentThread(); - ThreadLocalMap map = getMap(t); - if (map != null) { - ThreadLocalMap.Entry e = map.getEntry(this); - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - return setInitialValue(); - } -``` - -threadLocals的类型ThreadLocalMap的键为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,如longLocal和stringLocal。 - -``` -public class ThreadLocalDemo { - ThreadLocal longLocal = new ThreadLocal<>(); - - public void set() { - longLocal.set(Thread.currentThread().getId()); - } - public Long get() { - return longLocal.get(); - } - - public static void main(String[] args) throws InterruptedException { - ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); - threadLocalDemo.set(); - System.out.println(threadLocalDemo.get()); - - Thread thread = new Thread(() -> { - threadLocalDemo.set(); - System.out.println(threadLocalDemo.get()); - } - ); - - thread.start(); - thread.join(); - - System.out.println(threadLocalDemo.get()); - } -} -``` -ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。 - -## 内存泄漏 - -每个Thread都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。GC的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用Thread对象节省资源,这也就导致了Thread对象的生命周期比较长,这样便一直存在一条强引用链的关系:Thread --> ThreadLocalMap-->Entry-->Value,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。 - -![image-20200715235804982](../img/threadlocal-oom.png) - -解决⽅法:每次使⽤完ThreadLocal就调⽤它的remove()⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。 - -```java -currentTime.set(System.currentTimeMillis()); -result = joinPoint.proceed(); -Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get()); -currentTime.remove(); -``` - -## 使用场景 - -ThreadLocal 适用场景:每个线程需要有自己单独的实例,且需要在多个方法中共享实例,即同时满足实例在线程间的隔离与方法间的共享。比如Java web应用中,每个线程有自己单独的 Session 实例,就可以使用ThreadLocal来实现。 - - - -# 线程安全类 - -线程安全:代码段在多线程下执行和在单线程下执行能获得一样的结果。 -线程安全类:线程安全的类其方法是同步的,每次只能有一个线程访问,效率较低。 -- vector:比arraylist多了个同步化机制,效率较低 -- stack:堆栈类,继承自vector -- hashtable:hashtable不允许插入空值,hashmap允许 -- enumeration:枚举,相当于迭代器 -- StringBuffer - -Iterator和Enumeration的重要区别: -- Enumeration为vector/hashtable等类提供遍历接口,Iterator为ArrayList/HashMap提供遍历接口。 -- Enumeration只能读集合中的数据,不能删除。 -- Enumeration是先进后出,而Iterator是先进先出。 -- Enumeration不支持fast-fail机制,不会抛ConcurrentModificationException。 - - - -# 常见操作 - -## 排序 - -数组 - -```java -Arrays.sort(jdArray, (int[] jd1, int[] jd2) -> {return jd1[0] - jd2[0];}); -``` - - - -## 数组操作 - -数组遍历 - -```java -Arrays.asList(array).stream().forEach(System.out::println); -``` - -数组排序 - -```java -Arrays.sort(players,(String s1,String s2)->(s1.compareTo(s2))); -``` - -集合转数组: - -```java -//List --> Array -List list = new ArrayList<>(); -list.add(1); -Integer[] arr = list.toArray(new Integer[list.size()]); -``` - -数组转集合: - -```java -//Array --> List -String[] array = {"java", "c"}; -List list = Arrays.asList(array); -``` - -该方法存在一定的弊端,返回的list是Arrays里面的一个静态内部类(数组的视图),对list的操作会反映在原数组上,而且list是定长的,不支持add、remove操作。 - -该ArrayList并非java.util.ArrayList,而是 java.util.Arrays.ArrayList.ArrayList(T[]),该类并未实现add、remove方法,因此在使用时存在局限性。 - -代替方案: - -```java -List list = new ArrayList(Arrays.asList(array)); -``` - - - -## 拷贝 - -### 数组拷贝 - -```java -System.arraycopy(Object src, int srcPos, Object dest, int desPos, int length) -Arrays.copyOf(originalArr, length) //length为拷贝的长度 -Arrays.copyOfRange(originalArr, from, to); //from包含,to不包含 -``` - -二维数组拷贝: - -```java -int[][] arr = {{1, 2},{3, 4}}; -int[][] newArr = new int[2][2]; -for(int i = 0; i < arr.length; i++) { - newArr[i] = arr[i].clone(); -} -``` - -### 对象拷贝 - -实现对象克隆有两种方式: - -1. 实现Cloneable接口并重写Object类中的clone()方法; - -2. 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。 - -实现cloneable接口,重写clone方法。 - -```java -public class Dog implements Cloneable { - private String id; - private String name; - - public Dog(String id, String name) { - this.id = id; - this.name = name; - } - - // 省略 getter 、 setter 以及 toString 方法 - - @Override - public Dog clone() throws CloneNotSupportedException { - Dog dog = (Dog) super.clone(); - - return dog; - } -} -``` - -使用: - -```java -Dog dog1 = new Dog("1", "Dog1"); -Dog dog2 = dog1.clone(); - -dog2.setName("Dog1 changed"); - -System.out.println(dog1); // Dog{id='1', name='Dog1'} -System.out.println(dog2); // Dog{id='1', name='Dog1 changed'} -``` - -如果一个类引用了其他类,引用的类也需要实现cloneable接口,比较麻烦。可以将所有的类都实现Serializable接口,通过序列化反序列化实现对象的深度拷贝。 - -## 序列化 - -序列化:把内存中的对象转换为字节序列的过程称为对象的序列化。 - -### 什么情况下需要序列化? - -当你想把的内存中的对象状态保存到一个文件中或者数据库中时候; -当你想在网络上传送对象的时候; - -### 如何实现序列化 - -实现Serializable接口即可。序列化的时候(如objectOutputStream.writeObject(user)),会判断user是否实现了Serializable(obj instanceof Serializable),如果对象没有实现Serializable接口,在序列化的时候会抛出NotSerializableException异常。 - -### serialVersionUID - -serialVersionUID 是 Java 为每个序列化类产生的版本标识,可用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。类的serialVersionUID的默认值完全依赖于Java编译器的实现。当完成序列化之后,此时对对象进行修改,由编译器生成的serialVersionUID会改变,这样反序列化的时候会报错。可以在序列化对象中添加 serialVersionUID,固定版本号,这样即便序列化对象做了修改,版本都是一致的,就能进行反序列化了。 - -## 遍历 - -### fast-fail - -fast-fail是Java集合的一种错误机制。当多个线程对同一个集合进行操作时,就有可能会产生fast-fail事件。 -例如:当线程a正通过iterator遍历集合时,另一个线程b修改了集合的内容,此时modCount(记录集合操作过程的修改次数)会加1,不等于expectedModCount,那么线程a访问集合的时候,就会抛出ConcurrentModificationException,产生fast-fail事件。边遍历边修改集合也会产生fast-fail事件。 - -解决方法: - -- 使用Colletions.synchronizedList方法或在修改集合内容的地方加上synchronized。这样的话,增删集合内容的同步锁会阻塞遍历操作,影响性能。 -- 使用CopyOnWriteArrayList来替换ArrayList。在对CopyOnWriteArrayList进行修改操作的时候,会拷贝一个新的数组,对新的数组进行操作,操作完成后再把引用移到新的数组。 - -### fail-safe - -fail-safe允许在遍历的过程中对容器中的数据进行修改。因为采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先copy原有集合内容,在拷贝的集合上进行遍历。 - -由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发`ConcurrentModificationException`。 - -java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用。常见的的使用fail-safe方式遍历的容器有`ConcerrentHashMap`和`CopyOnWriteArrayList`等。 - -fail-safe机制有两个问题:(1)需要复制集合,产生大量的无效对象,内存开销大;(2)不能访问到修改后的内容 。 - -### 移除集合元素 - -遍历时安全的移除集合中的元素,要使用遍历器Iterator和iterator.remove()方法。next()必须在remove()之前调用。 - -``` -ArrayList list = new ArrayList(Arrays.asList("a","b","c","d")); -Iterator iter = list.iterator(); -while(iter.hasNext()){ - String s = iter.next(); - if(s.equals("a")){ - iter.remove(); - } -} -``` - - - diff --git "a/Java/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" "b/Java/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" deleted file mode 100644 index 5381c9e..0000000 --- "a/Java/Java\345\271\266\345\217\221\347\274\226\347\250\213.md" +++ /dev/null @@ -1,392 +0,0 @@ - - - - -- [共享对象](#%E5%85%B1%E4%BA%AB%E5%AF%B9%E8%B1%A1) - - [非原子的64位操作](#%E9%9D%9E%E5%8E%9F%E5%AD%90%E7%9A%8464%E4%BD%8D%E6%93%8D%E4%BD%9C) - - [this 引用逸出](#this-%E5%BC%95%E7%94%A8%E9%80%B8%E5%87%BA) - - [安全的对象构造过程](#%E5%AE%89%E5%85%A8%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%9E%84%E9%80%A0%E8%BF%87%E7%A8%8B) - - [ThreadLocal](#threadlocal) -- [容器](#%E5%AE%B9%E5%99%A8) - - [间接的迭代操作](#%E9%97%B4%E6%8E%A5%E7%9A%84%E8%BF%AD%E4%BB%A3%E6%93%8D%E4%BD%9C) - - [ConcurrentHashMap](#concurrenthashmap) - - [同步工具类](#%E5%90%8C%E6%AD%A5%E5%B7%A5%E5%85%B7%E7%B1%BB) - - [信号量](#%E4%BF%A1%E5%8F%B7%E9%87%8F) - - [缓存系统](#%E7%BC%93%E5%AD%98%E7%B3%BB%E7%BB%9F) -- [任务执行](#%E4%BB%BB%E5%8A%A1%E6%89%A7%E8%A1%8C) - - [Executor 框架](#executor-%E6%A1%86%E6%9E%B6) - - [延迟任务](#%E5%BB%B6%E8%BF%9F%E4%BB%BB%E5%8A%A1) - - [携带结果的 Callable 和 Future](#%E6%90%BA%E5%B8%A6%E7%BB%93%E6%9E%9C%E7%9A%84-callable-%E5%92%8C-future) - - [为任务设置时限](#%E4%B8%BA%E4%BB%BB%E5%8A%A1%E8%AE%BE%E7%BD%AE%E6%97%B6%E9%99%90) - - [取消与关闭](#%E5%8F%96%E6%B6%88%E4%B8%8E%E5%85%B3%E9%97%AD) - - [任务取消](#%E4%BB%BB%E5%8A%A1%E5%8F%96%E6%B6%88) - - [阻塞和中断](#%E9%98%BB%E5%A1%9E%E5%92%8C%E4%B8%AD%E6%96%AD) - - - -## 共享对象 - -### 非原子的64位操作 - -在多线程程序使用共享且可变的64位数据类型的变量是不安全的。 - - - -### this 引用逸出 - -参考自:[this 引用逸出](https://www.cnblogs.com/whatisjava/archive/2013/05/29/3106336.html) - -实例化 ThisEscape 对象时,会调用 source 的 registerListener 方法,这时便启动了一个线程,而且这个线程持有了 ThisEscape 对象(调用了对象的 doSomething 方法),但此时 ThisEscape 对象却没有实例化完成(还没有返回一个引用),所以我们说,此时造成了一个 this 引用逸出,即还没有完成的实例化 ThisEscape 对象的动作,却已经暴露了对象的引用。其他线程访问还没有构造好的对象,可能会造成意料不到的问题。 - -```java -public class ThisEscape { -  public ThisEscape(EventSource source) { -    source.registerListener(new EventListener() { -      public void onEvent(Event e) { -        doSomething(e); -      } -    }); -  } - -  void doSomething(Event e) { -  } - -  interface EventSource { -    void registerListener(EventListener e); -  } - -  interface EventListener { -    void onEvent(Event e); -  } - -  interface Event { -  } -} -``` - -### 安全的对象构造过程 - -使用工厂方法来防止 this 引用在构造过程中逸出。 - -```java -public class SafeListener { -  private final EventListener listener; - -  private SafeListener() { -    listener = new EventListener() { -      public void onEvent(Event e) { -        doSomething(e); -      } -    }; -  } - -  public static SafeListener newInstance(EventSource source) { -    SafeListener safe = new SafeListener(); -    source.registerListener(safe.listener); -    return safe; -  } - -  void doSomething(Event e) { -  } - -  interface EventSource { -    void registerListener(EventListener e); -  } - -  interface EventListener { -    void onEvent(Event e); -  } - -  interface Event { -  } - } -``` - -构造好了 SafeListener 对象(通过构造器构造)之后,才启动了监听线程,也就确保了构造完成之后再使用SafeListener对象。 - -### ThreadLocal - -当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 -每个线程都有一个ThreadLocalMap(ThreadLocal内部类),Map中元素的键为ThreadLocal,而值对应线程的变量副本。 -调用set()-->调用getMap(Thread)-->返回当前线程的ThreadLocalMap-->map.set(this, value),this是ThreadLocal。 -调用get()-->调用getMap(Thread)-->返回当前线程的ThreadLocalMap-->map.getEntry(this),返回value。 - -```java -public class ThreadLocalDemo { - ThreadLocal longLocal = new ThreadLocal<>(); - - public void set() { - longLocal.set(Thread.currentThread().getId()); - } - - public String get() { - return Thread.currentThread().getName() + ": " + longLocal.get(); - } - - public static void main(String[] args) throws InterruptedException { - ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo(); - threadLocalDemo.set(); - System.out.println(threadLocalDemo.get()); - - Thread thread = new Thread(() -> { - threadLocalDemo.set(); - System.out.println(threadLocalDemo.get()); - } - ); - - thread.start(); - thread.join(); - - System.out.println(threadLocalDemo.get()); - } -} -/*output -main: 1 -Thread-0: 11 -main: 1 - */ -``` - -## 容器 - -### 间接的迭代操作 - -调用容器的 toString 方法会迭代容器。容器的 hashcode 和 equals 方法也会间接的进行迭代操作。 - -```java -public class HiddenIterator { - private final Set set = new HashSet<>(); - //... - public void print() { - System.out.println("set: " + set); - } -} -``` - -### ConcurrentHashMap -多线程环境下,使用Hashmap进行put操作会引起死循环。 -CocurrentHashMap利用锁分段技术增加了锁的数目,从而使争夺同一把锁的线程的数目得到控制。 -锁分段技术就是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 -ConcurrentHashMap调用get的时候不加锁,原因是node数组成员val和指针next是用volatile修饰的,更改后的值会立刻刷新到主存中,保证了可见性,node数组table也用volatile修饰,保证在运行过程对其他线程具有可见性。 - -JDK1.7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,使用volatile来保证可见性,当要统计size时,比较统计前后modCount是否发生变化。如果没有变化,则直接返回size。否则,需要依次锁住所有的Segment来计算。jdk1.7中ConcurrentHashmap中,当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能。 - -jdk1.8不采用segment而采用Node,锁住Node来实现减小锁粒度。当链表长度过长时,Node会转换成TreeNode。 - -put 操作会对当前的table进行无条件自循环直到put成功,可以分成以下流程来概述: -1)如果没有初始化就先调用 initTable 方法来进行初始化过程 -2)如果没有hash冲突就直接CAS插入 -3)如果还在进行扩容操作就先进行扩容 -4)如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入 - -5)如果该链表的数量大于阈值8,就要先转换成黑红树的结构 -6)如果添加成功就调用 addCount 方法统计size,并且检查是否需要扩容 - -### 同步工具类 - -#### 信号量 - -信号量 Semaphore 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。 - -通过信号量实现有界的HashSet: - -```java -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Semaphore; - -public class BoundedHashSet { - private final Set set; - private final Semaphore sem; - - public BoundedHashSet(int bound) { - set = Collections.synchronizedSet(new HashSet<>()); - sem = new Semaphore(bound); - } - - public boolean add(T t) throws InterruptedException { - sem.acquire(); - boolean wasAdded = false; - try { - wasAdded = set.add(t); - return wasAdded; - } finally { - if (!wasAdded) { - sem.release(); - } - } - } - - public boolean remove(Object o) { - boolean wasRemoved = set.remove(o); - if (wasRemoved) { - sem.release(); - } - return wasRemoved; - } -} -``` - -### 缓存系统 - -假设有一个高计算开销的 compute 函数,我们可以将计算结果保存在 Map 中,调用 compute 时先检查 Map 是否存在需要的结果。使用 ConturrentHashMap 可以提高系统的并发能力。假如两个线程同时调用 compute ,则相同的数据会被计算多次,使用 FutureTask 可以避免这个问题。 - -```java -interface Computable { - V compute(A arg) throws InterruptedException; -} - -public class Memorizer implements Computable { - private final ConcurrentMap> cache = new ConcurrentHashMap<>(); - private final Computable c; - public Memorizer(Computable c) { - this.c = c; - } - @Override - public V compute(A arg) throws InterruptedException { - while (true) { - Future f = cache.get(arg); - if (f == null) { - Callable eval = new Callable() { - @Override - public V call() throws InterruptedException { - return c.compute(arg); - } - }; - FutureTask ft = new FutureTask<>(eval); - //先放进缓存,再进行计算;若线程x正在计算某个值,而线程y刚好正在查找这个值,则线程y会等待x的计算结果 - f = cache.putIfAbsent(arg, ft); - //避免两个线程同一时间调用compute计算相同的值 - if (f == null) { - f = ft; - ft.run(); - } - } - try { - return f.get(); - } catch (CancellationException ex) { - cache.remove(arg, f); - } catch (ExecutionException ex) { - ex.printStackTrace(); - } - } - } -} -``` - -## 任务执行 - -### Executor 框架 - -1.5后引入的 Executor 框架的最大优点是把任务的提交和执行解耦。Executor 是任务执行的抽象,,使用 Runnable 或 Callable 来表示任务。 - -```java -public class TaskExecutionWebServer { - private static final int NTHREADS = 100; - private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); - - public static void main(String[] args) throws IOException { - ServerSocket socket = new ServerSocket(80); - while (true) { - final Socket connection = socket.accept(); - Runnable task = new Runnable() { - @Override - public void run() { - //handleRequest(connection); - } - }; - //将任务提交到工作队列 - exec.execute(task); - } - } -} -``` - - - -#### 延迟任务 - -Timer 类负责延迟任务和周期任务,然后 Timer 存在一些缺陷。现在一般使用 ScheduledThreadPoolExecutor 来代替它,通过 ScheduledThreadPoolExecutor 的构造函数或者 Executors.newScheduledThreadPool 工厂方法来创建该类的对象(不推荐,见阿里编码规范)。 - -#### 携带结果的 Callable 和 Future - -当提交一个Callable对象给ExecutorService,将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。而 get 方法的行为取决于任务的状态(尚未开始、正在运行、已完成)。任务已完成,那么 get 会立即返回或者抛出异常;任务没有完成,get 将一直阻塞直到任务完成;任务抛出异常,那么 get 会将异常封装成 ExecutionException 重新抛出;任务被取消,那么 get 将抛出 CancellationExeception,此时通过 getCause 可以获取被封装的初始异常。 - - -#### 为任务设置时限 - -Future.get() 支持时间限制,当结果可用时,它将直接返回,如果在指定时间内没有计算出结果,那么将抛出 TimeoutException。 - -```java -Page renderPageWithAd() throws InterruptedException { - long endNanos = System.nanoTime() + TIME_BUDGET; - Future f = exec.submit(new FetchAdTask()); - //等待广告同时显示页面 - Page page = renderPageBody(); - Ad ad; - try { - //只等待指定的时间 - long timeLeft = endNanos - System.nanoTime(); - ad = f.get(timeLeft, NANOSECONDES); - } catch (ExecutionException e) { - ad = DEFAULT_AD; - } catch (TimeoutException e) { - ad = DEFAULT_AD; - //超时则取消任务 - f.cancel(true); - } - page.setAd(ad); - return page; -} -``` - -### 取消与关闭 - -Java 没有提供任何机制来安全的终止线程,但它提供了中断机制,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。 - - -#### 任务取消 - -给任务设置某个“请求取消”的标志,而任务将定期查看该标志,如果设置了该标志,那么任务将提前结束。 - -#### 阻塞和中断 - -如果任务中调用了一个阻塞方法,那么任务有可能永远不会检查取消标志,因此永远不会结束。通过中断机制可以避免这个问题。一些特殊的阻塞库的方法支持中断。 - -线程可能受到阻塞的原因:等待 IO 操作,等待获得锁,等待从 Thread.sleep 方法醒来,或是等待另一个线程的计算结果。阻塞方法可能会抛出 InterruptedException。当在代码中调用了一个会抛出 InterruptedException 的方法时,这个方法也就变成了阻塞方法,需要处理中断异常。阻塞方法必须等待某个不受它控制的事件发生后才能继续执行。 - -每个线程都有一个 boolean 类型的中断状态,当中断线程时,这个线程的中断状态会被设置为 true。在 Thread 中包含了中断线程以及查询线程中断状态的方法。 - -```java -public class Thread { - public void interrupt() {} //中断目标线程 - public boolean isInterrupted() {} //返回目标线程的中断状态 - public static boolean interrupted() {}//清除当前线程中断状态的唯一方法,并返回它之前的值 -} -``` - -阻塞方法如 Thread.sleep 和 Object.wait 等,都会检查线程何时中断,并且在发生中断时提前返回。它们在响应中断时执行的操作包含:清除中断状态,抛出 InterruptedException,表示阻塞操作由于中断而提前结束。**调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息**。对中断操作的正确理解是:它并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻自己中断。 - -阻塞方法必须处理对中断的响应,有两种处理方法: - -1. 传递 InterruptedException 给方法的调用者。 -2. 恢复中断。有时不能抛出 InterruptedException,如代码是 Runnable 一部分时,必须捕获 InterruptedException,并通过调用当前线程上的 interrupt 方法恢复中断状态,这样在高层代码就看到引发了一个中断。 - -```java -public class TaskRunnable implements Runnable { - BlockingQueue queue; - ... - public void run() { - try { - processTask(queue.take()); - } catch (InterruptedException e) { - //恢复被中断的状态,不然会丢失线程被中断的证据 - Thread.currentThread().interrupt(); - } - } -} -``` - - - diff --git "a/Java/Object\347\261\273\345\270\270\347\224\250\346\226\271\346\263\225.md" "b/Java/Object\347\261\273\345\270\270\347\224\250\346\226\271\346\263\225.md" deleted file mode 100644 index e78bb55..0000000 --- "a/Java/Object\347\261\273\345\270\270\347\224\250\346\226\271\346\263\225.md" +++ /dev/null @@ -1,241 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Object常用方法](#object%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95) - - [toString](#tostring) - - [equals](#equals) - - [hashCode](#hashcode) - - [clone](#clone) - - [浅拷贝](#%E6%B5%85%E6%8B%B7%E8%B4%9D) - - [深拷贝](#%E6%B7%B1%E6%8B%B7%E8%B4%9D) - - - -## Object常用方法 - -Java面试经常会出现的一道题目,Object的常用方法。下面给大家整理一下。 - -object常用方法有:toString()、equals()、hashCode()、clone()等。 - -### toString - -默认输出对象地址。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - public static void main(String[] args) { - System.out.println(new Person(18, "程序员大彬").toString()); - } - //output - //me.tyson.java.core.Person@4554617c -} -``` - -可以重写toString方法,按照重写逻辑输出对象值。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public String toString() { - return name + ":" + age; - } - - public static void main(String[] args) { - System.out.println(new Person(18, "程序员大彬").toString()); - } - //output - //程序员大彬:18 -} -``` - -### equals - -默认比较两个引用变量是否指向同一个对象(内存地址)。 - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - public static void main(String[] args) { - String name = "程序员大彬"; - Person p1 = new Person(18, name); - Person p2 = new Person(18, name); - - System.out.println(p1.equals(p2)); - } - //output - //false -} -``` - -可以重写equals方法,按照age和name是否相等来判断: - -```java -public class Person { - private int age; - private String name; - - public Person(int age, String name) { - this.age = age; - this.name = name; - } - - @Override - public boolean equals(Object o) { - if (o instanceof Person) { - Person p = (Person) o; - return age == p.age && name.equals(p.name); - } - return false; - } - - public static void main(String[] args) { - String name = "程序员大彬"; - Person p1 = new Person(18, name); - Person p2 = new Person(18, name); - - System.out.println(p1.equals(p2)); - } - //output - //true -} -``` - -### hashCode - -将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。 - -```java -public class Cat { - public static void main(String[] args) { - System.out.println(new Cat().hashCode()); - } - //out - //1349277854 -} -``` - -### clone - -java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的。Object对象有个clone()方法,实现了对 - -象中各个属性的复制,但它的可见范围是protected的。 - -```java -protected native Object clone() throws CloneNotSupportedException; -``` - -所以实体类使用克隆的前提是: - -- 实现Cloneable接口,这是一个标记接口,自身没有方法,这应该是一种约定。调用clone方法时,会判断有没有实现Cloneable接口,没有实现Cloneable的话会抛异常CloneNotSupportedException。 -- 覆盖clone()方法,可见性提升为public。 - -```java -public class Cat implements Cloneable { - private String name; - - @Override - protected Object clone() throws CloneNotSupportedException { - return super.clone(); - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - c.name = "程序员大彬"; - Cat cloneCat = (Cat) c.clone(); - c.name = "大彬"; - System.out.println(cloneCat.name); - } - //output - //程序员大彬 -} -``` - -下面介绍下浅拷贝和深拷贝。 - -#### 浅拷贝 - -拷⻉对象和原始对象的引⽤类型引用同⼀个对象。 - -以下例子,Cat对象里面有个Person对象,调用clone之后,克隆对象和原对象的Person引用的是同一个对象,这就是浅拷贝。 - -```java -public class Cat implements Cloneable { - private String name; - private Person owner; - - @Override - protected Object clone() throws CloneNotSupportedException { - return super.clone(); - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - Person p = new Person(18, "程序员大彬"); - c.owner = p; - - Cat cloneCat = (Cat) c.clone(); - p.setName("大彬"); - System.out.println(cloneCat.owner.getName()); - } - //output - //大彬 -} -``` - -#### 深拷贝 - -拷贝对象和原始对象的引用类型引用不同的对象。 - -以下例子,在clone函数中不仅调用了super.clone,而且调用Person对象的clone方法(Person也要实现Cloneable接口并重写clone方法),从而实现了深拷贝。可以看到,拷贝对象的值不会受到原对象的影响。 - -```java -public class Cat implements Cloneable { - private String name; - private Person owner; - - @Override - protected Object clone() throws CloneNotSupportedException { - Cat c = null; - c = (Cat) super.clone(); - c.owner = (Person) owner.clone();//拷贝Person对象 - return c; - } - - public static void main(String[] args) throws CloneNotSupportedException { - Cat c = new Cat(); - Person p = new Person(18, "程序员大彬"); - c.owner = p; - - Cat cloneCat = (Cat) c.clone(); - p.setName("大彬"); - System.out.println(cloneCat.owner.getName()); - } - //output - //程序员大彬 -} -``` - diff --git "a/Java/\345\271\266\345\217\221.md" "b/Java/\345\271\266\345\217\221.md" deleted file mode 100644 index 65ea9c9..0000000 --- "a/Java/\345\271\266\345\217\221.md" +++ /dev/null @@ -1,1166 +0,0 @@ - - - - -- [线程池](#%E7%BA%BF%E7%A8%8B%E6%B1%A0) - - [线程池原理](#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86) - - [线程池大小](#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%A4%A7%E5%B0%8F) - - [关闭线程池](#%E5%85%B3%E9%97%AD%E7%BA%BF%E7%A8%8B%E6%B1%A0) -- [executor框架](#executor%E6%A1%86%E6%9E%B6) - - [简介](#%E7%AE%80%E4%BB%8B) - - [ThreadPoolExecutor实例](#threadpoolexecutor%E5%AE%9E%E4%BE%8B) - - [Runnable和Callable的区别](#runnable%E5%92%8Ccallable%E7%9A%84%E5%8C%BA%E5%88%AB) - - [Future和FutureTask](#future%E5%92%8Cfuturetask) - - [execute()和submit()](#execute%E5%92%8Csubmit) - - [常用的线程池](#%E5%B8%B8%E7%94%A8%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0) - - [FixedThreadPool](#fixedthreadpool) - - [SingleThreadExecutor](#singlethreadexecutor) - - [CachedThreadPool](#cachedthreadpool) - - [ScheduledThreadPoolExecutor](#scheduledthreadpoolexecutor) - - [编码规范](#%E7%BC%96%E7%A0%81%E8%A7%84%E8%8C%83) -- [JMM](#jmm) -- [进程线程](#%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B) - - [线程状态](#%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81) - - [中断](#%E4%B8%AD%E6%96%AD) - - [常见方法](#%E5%B8%B8%E8%A7%81%E6%96%B9%E6%B3%95) - - [join](#join) - - [yield](#yield) - - [sleep](#sleep) - - [wait()和sleep()的区别](#wait%E5%92%8Csleep%E7%9A%84%E5%8C%BA%E5%88%AB) - - [创建线程的方法](#%E5%88%9B%E5%BB%BA%E7%BA%BF%E7%A8%8B%E7%9A%84%E6%96%B9%E6%B3%95) -- [线程间通信](#%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1) - - [volatile](#volatile) - - [synchronized](#synchronized) - - [等待通知机制](#%E7%AD%89%E5%BE%85%E9%80%9A%E7%9F%A5%E6%9C%BA%E5%88%B6) -- [锁](#%E9%94%81) - - [synchronized](#synchronized-1) - - [释放锁](#%E9%87%8A%E6%94%BE%E9%94%81) - - [实现原理](#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) - - [锁的状态](#%E9%94%81%E7%9A%84%E7%8A%B6%E6%80%81) - - [ReentrantLock](#reentrantlock) - - [原理](#%E5%8E%9F%E7%90%86) - - [ReentrantLock和synchronized区别](#reentrantlock%E5%92%8Csynchronized%E5%8C%BA%E5%88%AB) - - [锁的分类](#%E9%94%81%E7%9A%84%E5%88%86%E7%B1%BB) - - [公平锁与非公平锁](#%E5%85%AC%E5%B9%B3%E9%94%81%E4%B8%8E%E9%9D%9E%E5%85%AC%E5%B9%B3%E9%94%81) - - [共享式与独占式锁](#%E5%85%B1%E4%BA%AB%E5%BC%8F%E4%B8%8E%E7%8B%AC%E5%8D%A0%E5%BC%8F%E9%94%81) - - [悲观锁与乐观锁](#%E6%82%B2%E8%A7%82%E9%94%81%E4%B8%8E%E4%B9%90%E8%A7%82%E9%94%81) - - [CAS](#cas) -- [并发工具](#%E5%B9%B6%E5%8F%91%E5%B7%A5%E5%85%B7) - - [CountDownLatch](#countdownlatch) - - [CyclicBarrier](#cyclicbarrier) - - [CyclicBarrier和CountDownLatch区别](#cyclicbarrier%E5%92%8Ccountdownlatch%E5%8C%BA%E5%88%AB) - - [Semaphore](#semaphore) -- [原子类](#%E5%8E%9F%E5%AD%90%E7%B1%BB) - - [基本类型原子类](#%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E5%8E%9F%E5%AD%90%E7%B1%BB) - - [数组类型原子类](#%E6%95%B0%E7%BB%84%E7%B1%BB%E5%9E%8B%E5%8E%9F%E5%AD%90%E7%B1%BB) - - [引用类型原子类](#%E5%BC%95%E7%94%A8%E7%B1%BB%E5%9E%8B%E5%8E%9F%E5%AD%90%E7%B1%BB) -- [AQS](#aqs) - - [原理](#%E5%8E%9F%E7%90%86-1) -- [Condition](#condition) - - [实现原理](#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86-1) -- [其他](#%E5%85%B6%E4%BB%96) - - [Daemon Thread](#daemon-thread) -- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - - -> 首先给大家分享一个github仓库,上面放了**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -> -> github地址:https://github.com/Tyson0314/java-books -> -> 如果github访问不了,可以访问gitee仓库。 -> -> gitee地址:https://gitee.com/tysondai/java-books - -## 线程池 - -**使用线程池的好处**: - -- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 -- **提高线程的可管理性**。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。 - -``` -public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler); -``` -### 线程池原理 - -创建新的线程需要获取全局锁,通过这种设计可以尽量避免获取全局锁,当 ThreadPoolExecutor 完成预热之后(当前运行的线程数大于等于 corePoolSize),提交的大部分任务都会被放到 BlockingQueue。 - -![](http://img.dabin-coder.cn/image/thread-pool.png) - -ThreadPoolExecutor 的通用构造函数: - -``` -public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler); -``` - -- corePoolSize:当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。 - -- maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过 keepAliveTime 之后,就应该退出,避免资源浪费。 - -- BlockingQueue:存储等待运行的任务。 - -- keepAliveTime:**非核心线程**空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。 - -- TimeUnit:时间单位 - - ```java - TimeUnit.DAYS - TimeUnit.HOURS - TimeUnit.MINUTES - TimeUnit.SECONDS - TimeUnit.MILLISECONDS - TimeUnit.MICROSECONDS - TimeUnit.NANOSECONDS - ``` -- ThreadFactory:每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。 - - ```java - public class MyThreadFactory implements ThreadFactory { - private final String poolName; - - public MyThreadFactory(String poolName) { - this.poolName = poolName; - } - - public Thread newThread(Runnable runnable) { - return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程 - } - } - ``` - -- RejectedExecutionHandler:当队列和线程池都满了时,根据拒绝策略处理新任务。 - - ```java - AbortPolicy:默认的策略,直接抛出RejectedExecutionException - DiscardPolicy:不处理,直接丢弃 - DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务 - CallerRunsPolicy:由调用线程处理该任务 - ``` - -### 线程池大小 - -如果线程池线程数量太小,当有大量请求需要处理,系统响应比较慢影响体验,甚至会出现任务队列大量堆积任务导致OOM。 - -如果线程池线程数量过大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换(cpu给线程分配时间片,当线程的cpu时间片用完后保存状态,以便下次继续运行),从而增加线程的执行时间,影响了整体执行效率。 - -**CPU 密集型任务(N+1)**: 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 - -**I/O 密集型任务(2N)**: 系统会用大部分的时间来处理 I/O 操作,而线程等待 I/O 操作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N。 - -### 关闭线程池 - -shutdown(): - -将线程池状态置为`SHUTDOWN`,并不会立即停止: - -- 停止接收外部提交的任务 -- 内部正在跑的任务和队列里等待的任务,会执行完 -- 等到第二步完成后,才真正停止 - -shutdownNow(): - -将线程池状态置为`STOP`。企图立即停止,事实上不一定: - -- 跟shutdown()一样,先停止接收外部提交的任务 -- 忽略队列里等待的任务 -- 尝试将正在跑的任务中断(不一定中断成功,取决于任务响应中断的逻辑) -- 返回未执行的任务列表 - - - -## executor框架 - -1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。当提交一个Callable对象给ExecutorService,将得到一个Future对象,调用Future对象的get方法等待执行结果。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。 - -### 简介 - -executor框架由3部分组成:任务、任务的执行、异步计算的结果 - -- 任务。需要实现的接口:Runnable和Callable接口。 -- 任务的执行。ExecutorService 是一个接口,用于定义线程池,调用它的 execute(Runnable)或者 submit(Runnable/Callable)执行任务。ExecutorService接口继承于Executor,有两个实现类`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。 -- 异步计算的结果。包括future接口和实现future接口的FutureTask,调用future.get()会阻塞当前线程直到任务完成,future.cancel()可以取消执行任务。 - -### ThreadPoolExecutor实例 - -使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。 - -```java -public class ThreadPoolExecutorDemo { - private static final int CORE_POOL_SIZE = 5; - private static final int MAX_POOL_SIZE = 10; - private static final int QUEUE_CAPACITY = 100; - private static final long KEEP_ALIVE_TIME = 1L; - - public static void main(String[] args) throws ExecutionException, InterruptedException { - ThreadPoolExecutor executor = new ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, - new ArrayBlockingQueue<>(QUEUE_CAPACITY), - new ThreadPoolExecutor.CallerRunsPolicy() - ); - - for (int i = 0; i < 10; i++) { - Callable worker = () -> { - System.out.println(Thread.currentThread().getName()); - return "ok"; - }; - Future f = executor.submit(worker); - f.get(); - } - executor.shutdown(); - while (!executor.isTerminated()) { - } - System.out.println("Finished all threads"); - } -} -``` - -### Runnable和Callable的区别 - -Runnable 任务执行后不能返回值或者抛出异常。Callable 任务执行后可以返回值或抛出异常。 - -``` -Executors.callable(Runnable task);//runnable转化为callable -ExecutorService.execute(Runnable); -ExecutorService.submit(Runnable/Callable);//submit callable任务有返回值 - -//返回值是泛型参数V -public interface Callable { - V call() throws Exception; -} -``` - -### Future和FutureTask - -Future 可以获取任务执行的结果、取消任务。调用 future.get()会阻塞当前线程直到任务返回结果。 - -```java -public interface Future { - boolean cancel(boolean mayInterruptIfRunning); - - boolean isCancelled(); - - boolean isDone(); - - V get() throws InterruptedException, ExecutionException; - - V get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException; -} -``` - -FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 实现了 Runnable 和 Future\ 接口。 - -### execute()和submit() - -`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。 - -`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout, TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,无论任务是否执行完。 - -### 常用的线程池 - -常见的线程池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。这几个都是 ExecutorService (线程池)实例。 - -#### FixedThreadPool - -固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。 - -``` -public static ExecutorService newFixedThreadPool(int nThreads) { - return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); -} -``` - -使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。 - -maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。 - -keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。 - -适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,**在任务比较多的时候会导致 OOM。** - -#### SingleThreadExecutor - -只有一个线程的线程池。 - -``` -public static ExecutionService newSingleThreadExecutor() { - return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); -} -``` - -使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。 - -适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。**在任务比较多的时候也是会导致 OOM。** - -#### CachedThreadPool - -根据需要创建新线程的线程池。 - -``` -public static ExecutorService newCachedThreadPool() { - return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); -} -``` - -如果主线程提交任务的速度高于线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 - -使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,`SynchronousQueue.offer(Runnable task)`提交的任务会被空闲线程处理,否则会创建新的线程处理任务。 - -适用场景:用于并发执行大量短期的小任务。`CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,**可能会创建大量线程,从而导致 OOM。** - -#### ScheduledThreadPoolExecutor - -在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比如`quartz`。 - -使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,时间早的任务先被执行(即`ScheduledFutureTask` 的 `time` 变量小的先执行),如果time相同则先提交的任务会被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。 - -执行周期任务步骤: - -1. 线程从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask`的 time 大于等于当前系统的时间; -2. 执行这个 `ScheduledFutureTask`; -3. 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间; -4. 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。 - -![](http://img.dabin-coder.cn/image/scheduled-task.jpg) - -适用场景:周期性执行任务的场景,需要限制线程数量的场景。 - -#### 编码规范 - -阿里巴巴编码规约不允许使用Executors去创建线程池,而是通过ThreadPoolExecutor的方式手动创建线程池,这样子使用者会更加明确线程池的运行机制,避免资源耗尽的风险。 - -Executors 创建线程池对象的弊端: - -FixedThreadPool和SingleThreadPool。允许请求队列长度为 Integer.MAX_VALUE,可能堆积大量请求,从而导致OOM。 - -CachedThreadPool。创建的线程池允许的最大线程数是Integer.MAX_VALUE,当添加任务的速度大于线程池处理任务的速度,可能会创建大量的线程,消耗资源,甚至导致OOM。 - -![](http://img.dabin-coder.cn/image/executors-ali.png) - -正确示例(阿里巴巴编码规范): - -```java -//正例1 -ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); -//Common Thread Pool -ExecutorService pool = new ThreadPoolExecutor(5, 200, -0L, TimeUnit.MILLISECONDS, //0L keepAliveTime -new LinkedBlockingQueue(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); - -pool.execute(()-> System.out.println(Thread.currentThread().getName())); -pool.shutdown();//gracefully shutdown - -//正例2 -ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, //corePoolSize threadFactory -    new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build()); -``` - -## JMM - -Java内存模型:线程之间的共享变量存储在主内存里,每个线程都有自己私有的本地内存,本地内存保存了共享变量的副本,线程对变量的操作都在本地内存中进行,不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方本地内存中的变量,线程之间值的传递都需要通过主内存来完成。 - -线程a和线程b要想进行数据交换一般要经过下面的步骤: - -1. 线程a把本地内存a中的更新过的共享变量刷新到主内存中去。 -2. 线程b到主内存中去读取线程a刷新过的共享变量,然后复制一份到本地内存b中去。 - -![](http://img.dabin-coder.cn/image/image-20210909233258929.png) - -本地内存是JMM的一个抽象概念,并不真实存在,它包括缓存、写缓冲区、寄存器以及其他硬件和编译器优化。 - -## 进程线程 - -进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。 -线程是比进程更小的执行单位,它是在一个进程中独立的控制流,一个进程可以启动多个线程,每条线程并行执行不同的任务。 - -### 线程状态 - -初始(NEW):线程被构建,还没有调用 start()。 - -运行(RUNNABLE):包括操作系统的就绪和运行两种状态。 - -阻塞(BLOCKED):一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒。线程被阻塞会释放CPU,不释放内存。 - -等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 - -超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。 - -终止(TERMINATED):表示该线程已经执行完毕。 - -![](http://img.dabin-coder.cn/image/image-20210909235618175.png) - -> 图片来源:Java并发编程的艺术 - -### 中断 - -线程中断即线程运行过程中被其他线程给打断了,它与 stop 最大的区别是:stop 是由系统强制终止线程,而线程中断则是给目标线程发送一个中断信号,如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止,具体是否退出或者执行其他逻辑取决于目标线程。 - -线程中断三个重要的方法: - -**1、java.lang.Thread#interrupt** - -调用目标线程的interrupt()方法,给目标线程发一个中断信号,线程被打上中断标记。 - -**2、java.lang.Thread#isInterrupted()** - -判断目标线程是否被中断,不会清除中断标记。 - -**3、java.lang.Thread#interrupted** - -判断目标线程是否被中断,会清除中断标记。 - -```java -private static void test2() { - Thread thread = new Thread(() -> { - while (true) { - Thread.yield(); - - // 响应中断 - if (Thread.currentThread().isInterrupted()) { - System.out.println("Java技术栈线程被中断,程序退出。"); - return; - } - } - }); - thread.start(); - thread.interrupt(); -} -``` - -### 常见方法 - -#### join - -Thread.join(),在main中创建了thread线程,在main中调用了thread.join()/thread.join(long millis),main线程放弃cpu控制权,线程进入WAITING/TIMED_WAITING状态,等到thread线程执行完才继续执行main线程。 - -```java -public final void join() throws InterruptedException { - join(0); -} -``` - -#### yield - -Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。 - -```java -public static native void yield(); //static方法 -``` - -#### sleep - -Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,让出cpu资源,但不释放对象锁,指定时间到后又恢复运行。作用:给其它线程执行机会的最佳方式。 - -```java -public static native void sleep(long millis) throws InterruptedException;//static方法 -``` - -#### wait()和sleep()的区别 - -相同点: - -1. 使当前线程暂停运行,把机会交给其他线程 -2. 任何线程在等待期间被中断都会抛出InterruptedException - -不同点: - -1. wait() 是Object超类中的方法;而sleep()是线程Thread类中的方法 -2. 对锁的持有不同,wait()会释放锁,而sleep()并不释放锁 -3. 唤醒方法不完全相同,wait() 依靠notify或者notifyAll 、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒 -4. 调用obj.wait()需要先获取对象的锁,而 Thread.sleep()不用 - -### 创建线程的方法 - -- 通过扩展Thread类来创建多线程 -- 通过实现Runnable接口来创建多线程,可实现线程间的资源共享 -- 实现Callable接口,通过FutureTask接口创建线程。 -- 使用Executor框架来创建线程池。 - -**继承 Thread 创建线程**代码如下。run()方法是由jvm创建完操作系统级线程后回调的方法,不可以手动调用,手动调用相当于调用普通方法。 - -```java -/** - * @author: 程序员大彬 - * @time: 2021-09-11 10:15 - */ -public class MyThread extends Thread { - public MyThread() { - } - - @Override - public void run() { - for (int i = 0; i < 10; i++) { - System.out.println(Thread.currentThread() + ":" + i); - } - } - - public static void main(String[] args) { - MyThread mThread1 = new MyThread(); - MyThread mThread2 = new MyThread(); - MyThread myThread3 = new MyThread(); - mThread1.start(); - mThread2.start(); - myThread3.start(); - } -} -``` - -**Runnable 创建线程代码**: - -```java -/** - * @author: 程序员大彬 - * @time: 2021-09-11 10:04 - */ -public class RunnableTest { - public static void main(String[] args){ - Runnable1 r = new Runnable1(); - Thread thread = new Thread(r); - thread.start(); - System.out.println("主线程:["+Thread.currentThread().getName()+"]"); - } -} - -class Runnable1 implements Runnable{ - @Override - public void run() { - System.out.println("当前线程:"+Thread.currentThread().getName()); - } -} -``` - -实现Runnable接口比继承Thread类所具有的优势: - -1. 资源共享,适合多个相同的程序代码的线程去处理同一个资源 -2. 可以避免java中的单继承的限制 -3. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类 - -**Callable 创建线程代码**: - -```java -/** - * @author: 程序员大彬 - * @time: 2021-09-11 10:21 - */ -public class CallableTest { - public static void main(String[] args) { - Callable1 c = new Callable1(); - - //异步计算的结果 - FutureTask result = new FutureTask<>(c); - - new Thread(result).start(); - - try { - //等待任务完成,返回结果 - int sum = result.get(); - System.out.println(sum); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - -} - -class Callable1 implements Callable { - - @Override - public Integer call() throws Exception { - int sum = 0; - - for (int i = 0; i <= 100; i++) { - sum += i; - } - return sum; - } -} -``` - -**使用 Executor 创建线程代码**: - -```java -/** - * @author: 程序员大彬 - * @time: 2021-09-11 10:44 - */ -public class ExecutorsTest { - public static void main(String[] args) { - //获取ExecutorService实例,生产禁用,需要手动创建线程池 - ExecutorService executorService = Executors.newCachedThreadPool(); - //提交任务 - executorService.submit(new RunnableDemo()); - } -} - -class RunnableDemo implements Runnable { - @Override - public void run() { - System.out.println("大彬"); - } -} -``` - - - -## 线程间通信 - -### volatile - -volatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。 - -1. 当对volatile变量进行写操作的时候,JVM会向处理器发送一条LOCK前缀的指令,将该变量所在缓存行的数据写回系统内存。 -2. 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。 - -MESI(缓存一致性协议):当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。 - -volatile关键字的两个作用: - -1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 -2. 禁止进行指令重排序。 - -指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入`内存屏障`指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。 - -### synchronized - -保证线程对变量访问的可见性和排他性。synchronized 详细内容见下文锁部分。 - -### 等待通知机制 - -wait/notify为 Object 对象的方法,调用wait/notify需要先获得对象的锁。对象调用wait之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的notify()方法后,等待线程并不会立即从wait返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从wait()方法返回,即从wait方法返回前提是线程获得锁。 - -等待通知机制依托于同步机制,目的是确保等待线程从wait方法返回时能感知到通知线程对对象的变量值的修改。 - - - -## 锁 - -### synchronized - -较常用的用于保证线程安全的方式。当一个线程获取到锁时,其他线程都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程才有机会获取到锁。 - -- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 -- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁(类的字节码文件) -- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁 - -获取了类锁的线程和获取了对象锁的线程是不冲突的。 - -#### 释放锁 - -当方法或者代码块执行完毕后会自动释放锁,不需要做任何的操作。 -当一个线程执行的代码出现异常时,其所持有的锁会自动释放。 - -#### 实现原理 - -synchronized通过对象内部的监视器锁(monitor)实现。每个对象都有一个monitor,当对象的monitor被持有时,则它处于锁定的状态。 - -**代码块的同步**是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处或异常处。 - -```java -public class SynchronizedDemo { - public void method() { - synchronized (this) { - System.out.println("method start"); - } - } -} -``` - -线程访问同步块时,先执行monitorenter指令时尝试获取monitor,过程如下: - -1. 如果monitor的进入数entry count为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 -2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。 -3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。 - -线程退出同步块时会执行monitorexit指令,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor。 - -Synchronized底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。 - -![](http://img.dabin-coder.cn/image/synchronized-block.png) - -**方法的同步**不是通过添加monitorenter和monitorexit指令来完成,而是在其常量池中添加了ACC_SYNCHRONIZED标识符。JVM就是根据该标识符来实现方法的同步的:当线程调用方法时,会先检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,说明此方法是同步方法,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他线程无法再获得同一个monitor对象。 - -```java -public class SynchronizedMethod { - public synchronized void method() { - System.out.println("Hello World!"); - } -} -``` - -![](http://img.dabin-coder.cn/image/synchronized-method.png) - -#### 锁的状态 - -Synchronized是通过对象内部的监视器来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间。这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量级锁。 - -JDK1.6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 - -synchronized锁主要存在四种状态,依次是:偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 - -- 偏向锁:当线程访问同步块并获取锁时,会在对象头和锁记录中存储锁偏向的线程id,以后该线程进入和退出同步块时,只需简单测试一下对象头的mark word中是否存储着指向当前线程的偏向锁,如果测试成功,则线程获取锁成功,否则,需再测试一下mark word中偏向锁标识是否是1,是的话则使用CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。 - **偏向锁偏向于第一个获得它的线程,如果程序运行过程,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行的开销,因为轻量级锁的获取及释放使用了多次CAS原子指令,而偏向锁只在置换ThreadID的时候使用一次CAS原子指令。当存在锁竞争的时候,偏向锁会升级为轻量级锁。** - 适用场景:在锁无竞争的情况下使用,在线程没有执行完同步代码之前,没有其它线程去竞争锁,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,会做很多额外操作,导致性能下降。 - -- 轻量级锁 - 加锁过程:线程执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的mark word复制到锁记录(displaced mark word)中,然后线程尝试使用cas将对象头的mark word替换为指向锁记录的指针。如果成功,则当前线程获得锁,否则表示有其他线程竞争锁,当前线程便尝试使用自旋来获得锁。当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。 - 解锁过程:使用原子的cas操作将displaced mark word替换回到对象头,如果成功则解锁成功,否则表明有锁竞争,锁会膨胀成重量级锁。 - - **在没有多线程竞争的前提下,使用轻量级锁可以减少传统的重量级锁使用操作系统互斥量(申请互斥锁)产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级锁将很快膨胀为重量级锁!** - -- 重量级锁:当一个线程获取到锁时,其他线程都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程才有机会获取到锁。 - - synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,保证了可见性。 - -- 自旋锁:一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程比较浪费资源。自旋锁就是让该线程等待一段时间,执行一段无意义的循环,不会被立即挂起,看持有锁的线程是否会很快释放锁。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,这样反而会带来性能上的浪费。所以自旋的次数必须要有一个限度,如果自旋超过了限定次数仍然没有获取到锁,则应该被挂起。 - -- 自适应自旋锁:JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 - -- 锁消除:虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。 - -- 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗,使用锁粗化减少锁操作的开销。 - -### ReentrantLock - -重入锁,支持一个线程对资源的重复加锁。该锁的还支持设置获取锁时的公平和非公平性。 - -使用lock时需要在try finally块进行解锁: - -```java -public static final Object get(String key) { - r.lock(); - try { - return map.get(key); - } finally { - r.unlock(); - } -} -``` - -#### 原理 - -ReentrantLock是通过组合自定义同步器来实现锁的获取与释放。当线程尝试获取同步状态时,首先判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。获取同步状态失败,则该线程会被构造成node节点放到AQS同步队列中。 - -如果锁被获取了n次,那么前n-1次 tryRelease(int releases)方法必须返回false,第n次调用tryRelease()之后,同步状态完全释放(值为0),才会返回true。 - -### ReentrantLock和synchronized区别 - -1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。 -2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。 -3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。 -4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。 -5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。 - -### 锁的分类 - -#### 公平锁与非公平锁 - -按照线程访问顺序获取对象锁。synchronized 是非公平锁, Lock 默认是非公平锁,可以设置为公平锁,公平锁会影响性能。 - -```java -public ReentrantLock() { - sync = new NonfairSync(); -} - -public ReentrantLock(boolean fair) { - sync = fair ? new FairSync() : new NonfairSync(); -} -``` - -#### 共享式与独占式锁 - -共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。 - -#### 悲观锁与乐观锁 - -悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,synchronized 和 ReentrantLock 属于悲观锁。 - -乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是CAS。 - -乐观锁一般来说有以下2种方式: - -1. 使用数据版本记录机制实现,这是乐观锁最常用的一种实现方式。给数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的version字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。 -2. 使用时间戳。数据库表增加一个字段,字段类型使用时间戳(timestamp),和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。 - -适用场景: - -- 悲观锁适合写操作多的场景。 -- 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。 - -##### CAS - -CAS全称 Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS 在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock 内部的 AQS 和原子类内部都使用了 CAS。 - -CAS算法涉及到三个操作数: - -- 需要读写的内存值 V。 -- 进行比较的值 A。 -- 要写入的新值 B。 - -只有当 V 的值等于 A 时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。 - -以 AtomicInteger 为例,AtomicInteger 的 getAndIncrement()方法底层就是CAS实现,关键代码是 `compareAndSwapInt(obj, offset, expect, update)`,其含义就是,如果`obj`内的`value`和`expect`相等,就证明没有其他线程改变过这个变量,那么就更新它为`update`,如果不相等,那就会继续重试直到成功更新值。 - -CAS 三大问题: - -1. **ABA问题**。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从`A-B-A`变成了`1A-2B-3A`。 - - JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类型。 - -2. **循环时间长开销大**。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。 - -3. **只能保证一个共享变量的原子操作**。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。 - - Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。 - - - -## 并发工具 - -在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段。 - -### CountDownLatch - -CountDownLatch用于某个线程等待其他线程**执行完任务**再执行,与thread.join()功能类似。常见的应用场景是开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。 - -``` -public class CountDownLatchDemo { - static final int N = 4; - static CountDownLatch latch = new CountDownLatch(N); - - public static void main(String[] args) throws InterruptedException { - - for(int i = 0; i < N; i++) { - new Thread(new Thread1()).start(); - } - - latch.await(1000, TimeUnit.MILLISECONDS); //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行;等待timeout时间后count值还没变为0的话就会继续执行 - System.out.println("task finished"); - } - - static class Thread1 implements Runnable { - - @Override - public void run() { - try { - System.out.println(Thread.currentThread().getName() + "starts working"); - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - latch.countDown(); - } - } - } -} -``` - -运行结果: - -```java -Thread-0starts working -Thread-1starts working -Thread-2starts working -Thread-3starts working -task finished -``` - -### CyclicBarrier - -CyclicBarrier(同步屏障),用于一组线程互相等待到某个状态,然后这组线程再**同时**执行。 - -```java -public CyclicBarrier(int parties, Runnable barrierAction) { -} -public CyclicBarrier(int parties) { -} -``` - -参数parties指让多少个线程或者任务等待至某个状态;参数barrierAction为当这些线程都达到某个状态时会执行的内容。 - -```java -public class CyclicBarrierTest { - // 请求的数量 - private static final int threadCount = 10; - // 需要同步的线程数量 - private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5); - - public static void main(String[] args) throws InterruptedException { - // 创建线程池 - ExecutorService threadPool = Executors.newFixedThreadPool(10); - - for (int i = 0; i < threadCount; i++) { - final int threadNum = i; - Thread.sleep(1000); - threadPool.execute(() -> { - try { - test(threadNum); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (BrokenBarrierException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - }); - } - threadPool.shutdown(); - } - - public static void test(int threadnum) throws InterruptedException, BrokenBarrierException { - System.out.println("threadnum:" + threadnum + "is ready"); - try { - /**等待60秒,保证子线程完全执行结束*/ - cyclicBarrier.await(60, TimeUnit.SECONDS); - } catch (Exception e) { - System.out.println("-----CyclicBarrierException------"); - } - System.out.println("threadnum:" + threadnum + "is finish"); - } - -} -``` - -运行结果如下,可以看出CyclicBarrier是可以重用的: - -```java -threadnum:0is ready -threadnum:1is ready -threadnum:2is ready -threadnum:3is ready -threadnum:4is ready -threadnum:4is finish -threadnum:3is finish -threadnum:2is finish -threadnum:1is finish -threadnum:0is finish -threadnum:5is ready -threadnum:6is ready -... -``` - -当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。 - -### CyclicBarrier和CountDownLatch区别 - -CyclicBarrier 和 CountDownLatch 都能够实现线程之间的等待。 - -CountDownLatch用于某个线程等待其他线程**执行完任务**再执行。CyclicBarrier用于一组线程互相等待到某个状态,然后这组线程再**同时**执行。 -CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可用于处理更为复杂的业务场景。 - -### Semaphore - -Semaphore类似于锁,它用于控制同时访问特定资源的线程数量,控制并发线程数。 - -``` -public class SemaphoreDemo { - public static void main(String[] args) { - final int N = 7; - Semaphore s = new Semaphore(3); - for(int i = 0; i < N; i++) { - new Worker(s, i).start(); - } - } - - static class Worker extends Thread { - private Semaphore s; - private int num; - public Worker(Semaphore s, int num) { - this.s = s; - this.num = num; - } - - @Override - public void run() { - try { - s.acquire(); - System.out.println("worker" + num + " using the machine"); - Thread.sleep(1000); - System.out.println("worker" + num + " finished the task"); - s.release(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } -} -``` - -运行结果如下,可以看出并非按照线程访问顺序获取资源的锁,即 - -```java -worker0 using the machine -worker1 using the machine -worker2 using the machine -worker2 finished the task -worker0 finished the task -worker3 using the machine -worker4 using the machine -worker1 finished the task -worker6 using the machine -worker4 finished the task -worker3 finished the task -worker6 finished the task -worker5 using the machine -worker5 finished the task -``` - - - -## 原子类 - -### 基本类型原子类 - -使用原子的方式更新基本类型 - -- AtomicInteger:整型原子类 -- AtomicLong:长整型原子类 -- AtomicBoolean :布尔型原子类 - -AtomicInteger 类常用的方法: - -```java -public final int get() //获取当前的值 -public final int getAndSet(int newValue)//获取当前的值,并设置新的值 -public final int getAndIncrement()//获取当前的值,并自增 -public final int getAndDecrement() //获取当前的值,并自减 -public final int getAndAdd(int delta) //获取当前的值,并加上预期的值 -boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update) -public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -AtomicInteger 类主要利用 CAS (compare and swap) 保证原子操作,从而避免加锁的高开销。 - -### 数组类型原子类 - -使用原子的方式更新数组里的某个元素 - -- AtomicIntegerArray:整形数组原子类 -- AtomicLongArray:长整形数组原子类 -- AtomicReferenceArray :引用类型数组原子类 - -AtomicIntegerArray 类常用方法: - -```java -public final int get(int i) //获取 index=i 位置元素的值 -public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue -public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增 -public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减 -public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值 -boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update) -public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -### 引用类型原子类 - -- AtomicReference:引用类型原子类 -- AtomicStampedReference:带有版本号的引用类型原子类。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 -- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来 - - - -## AQS - -AQS定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。 - -### 原理 - -AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。 - -```java -private volatile int state;//共享变量,使用volatile修饰保证线程可见性 -``` - -同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享 )构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。 - -![](http://img.dabin-coder.cn/image/image-20210910000456607.png) - - - -## Condition - -任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,使用这些方法的前提是已经获取对象的锁,和 synchronized 配合使用。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式。Condition是依赖Lock对象。 - -```java -Lock lock = new ReentrantLock(); -Condition condition = lock.newCondition(); -public void conditionWait() throws InterruptedException { - lock.lock(); - try { - condition.await(); - } finally { - lock.unlock(); - } -} -public void conditionSignal() throws InterruptedException { - lock.lock(); - try { - condition.signal(); - } finally { - lock.unlock(); - } -} -``` - -一般将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁进入等待队列。其他线程调用Condition对象的signal()方法,唤醒等待队列首节点的线程。 - -### 实现原理 - -每个Condition对象都包含着一个等待队列,如果一个线程成功获取了锁之后调用了Condition.await()方法,那么该线程将会释放同步状态、唤醒同步队列中的后继节点,然后构造成节点加入等待队列。只有当线程再次获取Condition相关联的锁之后,才能从await()方法返回。 - -![](http://img.dabin-coder.cn/image/condition-await.png) - -> 图片来源:Java并发编程的艺术 - -在Object的监视器模型上,一个对象拥有一个同步队列和等待队列。Lock通过AQS实现,AQS可以有多个Condition,所以Lock拥有一个同步队列和多个等待队列。 - -![](http://img.dabin-coder.cn/image/condition-wait-queue.png) - -> 图片来源:Java并发编程的艺术 - -线程获取了锁之后,调用Condition的signal()方法,会将等待队列的队首节点移到同步队列中,然后该节点的线程会尝试去获取同步状态。成功获取同步状态之后,线程将await()方法返回。 - -![](http://img.dabin-coder.cn/image/condition-signal.png) - -> 图片来源:Java并发编程的艺术 - - - -## 其他 - -### Daemon Thread - -在Java中有两类线程: - -- User Thread(用户线程) -- Daemon Thread(守护线程) - -只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。 - -Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是垃圾收集。 - -将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。 - - - -## 参考资料 - -[线程中断](https://zhuanlan.zhihu.com/p/45667127) - -[synchronized实现原理](https://www.cnblogs.com/paddix/p/5367116.html) - -[指令重排导致单例模式失效](https://blog.csdn.net/jiyiqinlovexx/article/details/50989328) - - - -> 本文已经收录到github仓库,此仓库用于分享Java相关知识总结,包括Java基础、MySQL、Spring Boot、MyBatis、Redis、RabbitMQ、计算机网络、数据结构与算法等等,欢迎大家提pr和star! -> -> github地址:https://github.com/Tyson0314/Java-learning -> -> 如果github访问不了,可以访问gitee仓库。 -> -> gitee地址:https://gitee.com/tysondai/Java-learning diff --git "a/Java/\351\233\206\345\220\210.md" "b/Java/\351\233\206\345\220\210.md" deleted file mode 100644 index d626f12..0000000 --- "a/Java/\351\233\206\345\220\210.md" +++ /dev/null @@ -1,479 +0,0 @@ - - - - -- [ArrayList 简介](#arraylist-%E7%AE%80%E4%BB%8B) - - [Arraylist 和 Vector 的区别](#arraylist-%E5%92%8C-vector-%E7%9A%84%E5%8C%BA%E5%88%AB) - - [Arraylist 与 LinkedList 区别](#arraylist-%E4%B8%8E-linkedlist-%E5%8C%BA%E5%88%AB) -- [Map](#map) - - [HashMap](#hashmap) - - [hash算法](#hash%E7%AE%97%E6%B3%95) - - [resize](#resize) - - [put](#put) - - [红黑树](#%E7%BA%A2%E9%BB%91%E6%A0%91) - - [HashMap和HashTable](#hashmap%E5%92%8Chashtable) - - [LinkedHashMap](#linkedhashmap) - - [TreeMap](#treemap) -- [Set](#set) -- [Queue](#queue) -- [Iterator](#iterator) - - [ListIterator](#listiterator) -- [并发容器](#%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8) - - [ConcurrentHashMap](#concurrenthashmap) - - [put](#put-1) - - [扩容](#%E6%89%A9%E5%AE%B9) - - [ConcurrentHashMap 和 Hashtable](#concurrenthashmap-%E5%92%8C-hashtable) - - [CopyOnWrite](#copyonwrite) - - [ConcurrentLinkedQueue](#concurrentlinkedqueue) - - [阻塞队列](#%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97) - - [方法](#%E6%96%B9%E6%B3%95) - - [JDK提供的阻塞队列](#jdk%E6%8F%90%E4%BE%9B%E7%9A%84%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97) - - [原理](#%E5%8E%9F%E7%90%86) - - - -## ArrayList 简介 - -`ArrayList` 的底层是动态数组,它的容量能动态增长。在添加大量元素前,应用可以使用`ensureCapacity`操作增加 `ArrayList` 实例的容量。ArrayList 继承了 AbstractList ,并实现了 List 接口。 - -### Arraylist 和 Vector 的区别 - -1. ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。 -2. Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为操作Vector效率比较低。 - -### Arraylist 与 LinkedList 区别 - -1. ArrayList基于动态数组实现;LinkedList基于链表实现。 -2. 对于随机index访问的get和set方法,ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。 -3. 新增和删除元素,LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。 - -## Map - -以 Map 结尾的类都实现了 Map 接口,其他所有的类都实现了 Collection 接口。 - -![](http://img.dabin-coder.cn/image/Java-Collections.jpeg) - -### HashMap - -HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。 - -#### hash算法 - -Hash算法:取key的hashCode值、高位运算、取模运算。 - -``` -h=key.hashCode() //第一步 取hashCode值 -h^(h>>>16) //第二步 高位参与运算,减少冲突 -return h&(length-1); //第三步 取模运算 -``` - -在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:这么做可以在数组比较小的时候,也能保证考虑到高低位都参与到Hash的计算中,可以减少冲突,同时不会有太大的开销。 - -#### resize - -1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。 - -1.7链表新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,1.8采用了尾插法,避免了这种情况的发生。 - -原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据`e.hash & (oldCap - 1) == 0`判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。 - -#### put - -put方法流程: - -1. 如果table没有初始化就先进行初始化过程 -2. 使用hash算法计算key的索引 -3. 判断索引处有没有存在元素,没有就直接插入 -4. 如果索引处存在元素,则遍历插入,有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入 -5. 链表的数量大于阈值8,就要转换成红黑树的结构 -6. 添加成功后会检查是否需要扩容 - -![](http://img.dabin-coder.cn/image/hashmap-put.png) - -> 参考链接: -> -> http://www.importnew.com/20386.html -> -> https://www.cnblogs.com/yangming1996/p/7997468.html -> -> https://coolshell.cn/articles/9606.htm(HashMap 死循环) - - - -#### 红黑树 - -为什么使用红黑树而不使用AVL树? - -ConcurrentHashMap 在put的时候会加锁,使用红黑树插入速度更快,可以减少等待锁释放的时间。红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。 - -### HashMap和HashTable - -HashMap和Hashtable都实现了Map接口 - -1. HashMap可以接受为null的键值(key)和值(value),key为null的键值对放在下标为0的头结点的链表中,而Hashtable则不行。 -2. HashMap是非线程安全的,HashTable是线程安全的。Jdk1.5提供了ConcurrentHashMap,它是HashTable的替代。 -3. Hashtable很多方法是同步方法,在单线程环境下它比HashMap要慢。 -4. 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。 - -### LinkedHashMap - -HashMap是无序的,迭代HashMap所得到元素的顺序并不是它们最初放到HashMap的顺序,即不能保持它们的插入顺序。 - -LinkedHashMap继承于HashMap,是HashMap和LinkedList的融合体,具备两者的特性。每次put操作都会将entry插入到双向链表的尾部。 - -![linkedhashmap](http://img.dabin-coder.cn/image/linkedhashmap.png) - -### TreeMap - -TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。 - -```java -public class TreeMap - extends AbstractMap - implements NavigableMap, Cloneable, java.io.Serializable { -} -``` - -TreeMap 的继承结构: - -![](http://img.dabin-coder.cn/image/image-20210905215046510.png) - -**TreeMap的特点:** - -1. TreeMap是有序的key-value集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的Comparator进行排序。 -2. TreeMap继承了AbstractMap,实现了NavigableMap接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。如floorEntry()、ceilingEntry()分别返回小于等于、大于等于给定键关联的Map.Entry()对象,不存在则返回null。lowerKey()、floorKey、ceilingKey、higherKey()只返回关联的key。 - -## Set - -HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。 - -```java -public class HashSet - extends AbstractSet - implements Set, Cloneable, java.io.Serializable { - static final long serialVersionUID = -5024744406713321676L; - - private transient HashMap map; //基于HashMap实现 - //... -} -``` - - - -## Queue - -ArrayDeque实现了双端队列,内部使用循环数组实现,默认大小为16。 - -特点: - -1. 在两端添加、删除元素的效率较高 - -2. 根据元素内容查找和删除的效率比较低。 - -3. 没有索引位置的概念,不能根据索引位置进行操作。 - -ArrayDeque和LinkedList都实现了Deque接口,如果只需要从两端进行操作,ArrayDeque效率更高一些。如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除(LinkedList有相应的 api,如add(int index, E e)),则应该选LinkedList。 - -ArrayDeque和LinkedList都是线程不安全的,可以使用Collections工具类中synchronizedXxx()转换成线程同步。 - -## Iterator - -Iterator模式用同一种逻辑来遍历集合。它可以把访问逻辑从不同类型的集合类中抽象出来,不需要了解集合内部实现便可以遍历集合元素,集合是数组还是链表实现的无所谓,统一使用 Iterator 提供的接口去遍历。 - -主要有三个方法:hasNext()、next()和remove()。 - -```java - for(Iterator it = c.iterater(); it.hasNext(); ) { ... } -``` - -### ListIterator - -Iterator的增强版,增加了以下功能: - -1. ListIterator有add()方法,可以向List中添加对象。 - -2. hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。 -3. nextIndex()和previousIndex()定位索引位置。 -4. set() 实现对象的修改。 - -```java -public interface ListIterator extends Iterator { - boolean hasNext(); - - E next(); - - boolean hasPrevious(); - - E previous(); - - int nextIndex(); - - int previousIndex(); - - void remove(); - - void set(E var1); - - void add(E var1); -} -``` - - - -## 并发容器 - -JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 - -- **ConcurrentHashMap:** 线程安全的 HashMap -- **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector. -- **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。 -- **BlockingQueue:** 阻塞队列接口,JDK 内部通过链表、数组等方式实现了这个接口。非常适合用于作为数据共享的通道。 -- **ConcurrentSkipListMap:** 跳表的实现。使用跳表的数据结构进行快速查找。 - -### ConcurrentHashMap - -多线程环境下,使用Hashmap进行put操作会引起死循环,应该使用支持多线程的 ConcurrentHashMap。 - -```java -private static final int MAXIMUM_CAPACITY = 1 << 30; -private static final int DEFAULT_CAPACITY = 16; -static final int TREEIFY_THRESHOLD = 8; -static final int UNTREEIFY_THRESHOLD = 6; -static final int MIN_TREEIFY_CAPACITY = 64; -static final int MOVED = -1; // 表示正在转移 -static final int TREEBIN = -2; // 表示已经转换成树 -static final int RESERVED = -3; // hash for transient reservations -static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash -transient volatile Node[] table;//默认没初始化的数组,用来保存元素 -private transient volatile Node[] nextTable;//转移的时候用的数组 -/** - * 用来控制表初始化和扩容的,默认值为0,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75 - * 当为负的时候,说明表正在初始化或扩张, - * -1表示初始化 - * -(1+n) n:表示活动的扩张线程 - */ -private transient volatile int sizeCtl; -​```JDK1.7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度。Segment继承了ReentrantLock,所以它就是一种可重入锁。默认分配16个segment,允许16个线程并发执行。Segment维护了一个HashEntry**数组**,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。当要统计size时,比较统计前后modCount是否发生变化。如果没有变化,则直接返回size。否则,需要依次锁住所有的Segment来计算。当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能。 -``` - -JDK1.8 ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,相比1.7锁定HashEntry数组,锁粒度更小,支持更高的并发量。当链表长度过长时,Node会转换成TreeNode,提高查找速度。 - -#### put - -在put的时候需要锁住Segment,保证并发安全。调用get的时候不加锁,因为node数组成员val和指针next是用volatile修饰的,更改后的值会立刻刷新到主存中,保证了可见性,node数组table也用volatile修饰,保证在运行过程对其他线程具有可见性。 - -```java -transient volatile Node[] table; - -static class Node implements Map.Entry { - volatile V val; - volatile Node next; -} -``` - -put 操作流程: - -1. 如果table没有初始化就先进行初始化过程 -2. 使用hash算法计算key的位置 -3. 如果这个位置为空则直接CAS插入,如果不为空的话,则取出这个节点来 -4. 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制 -5. 如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作,这里有两种情况,一种是链表形式就直接遍历到尾端插入或者覆盖掉相同的key,一种是红黑树就按照红黑树结构插入 -6. 链表的数量大于阈值8,就会转换成红黑树的结构或者进行扩容(table长度小于64) -7. 添加成功后会检查是否需要扩容 - -#### 扩容 - -数组扩容transfer方法中会设置一个步长,表示一个线程处理的数组长度,最小值是16。在一个步长范围内只有一个线程会对其进行复制移动操作。 - -#### ConcurrentHashMap 和 Hashtable - -区别: - -1. Hashtable通过使用synchronized修饰方法的方式来实现多线程同步,因此,Hashtable的同步会锁住整个数组。在高并发的情况下,性能会非常差。ConcurrentHashMap采用了更细粒度的锁来提高在并发情况下的效率。注:Synchronized容器(同步容器)也是通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。 -2. Hashtable默认的大小为11,当达到阈值后,每次按照下面的公式对容量进行扩充:newCapacity = oldCapacity * 2 + 1。ConcurrentHashMap默认大小是16,扩容时容量扩大为原来的2倍。 - -ConcurrentHashMap 和 Hashtable 的key和value不能为null。 - -HashMap.java 部分源码: - -```java - static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//key为null时,hash值为0 - } -``` - -ConcurrentHashMap.java 部分源码: - -```java - /** Implementation for put and putIfAbsent */ - final V putVal(K key, V value, boolean onlyIfAbsent) { - if (key == null || value == null) throw new NullPointerException(); - int hash = spread(key.hashCode()); - int binCount = 0; - ...... - } -``` - -ConcurrentHashmap和Hashtable都支持并发,当你通过get(k)获取对应的value时,如果获取到的是null时,无法判断是key 对应的 value 为 null,还是这个 key 从来没有做过映射,在多线程里面是模糊不清的,所以不让put null。HashMap用于非并发场景,可以通过contains(key)来判断是否存在key。而支持并发的Map在调用m.get(key)之后,再调用m.contains(key),两个调用之间可能有其他线程删除了key,得到的结果不准确,产生多线程安全问题。因此ConcurrentHashMap 和 Hashtable 的key和value不能为null。 - -### CopyOnWrite - -写时复制。当我们往容器添加元素时,不直接往容器添加,而是先将当前容器进行复制,复制出一个新的容器,然后往新的容器添加元素,添加完元素之后,再将原容器的引用指向新容器。这样做的好处就是可以对CopyOnWrite容器进行并发的读而不需要加锁,因为当前容器不会被修改。 - -```java - public boolean add(E e) { - final ReentrantLock lock = this.lock; - lock.lock(); //add方法需要加锁 - try { - Object[] elements = getArray(); - int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1); //复制新数组 - newElements[len] = e; - setArray(newElements); //原容器的引用指向新容器 - return true; - } finally { - lock.unlock(); - } - } -``` - -从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。 - -CopyOnWriteArrayList中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向CopyOnWriteArrayList添加数据,还是可以读到旧的数据。 - -**缺点:** - -- 内存占用问题。由于CopyOnWrite的写时复制机制,在进行写操作的时候,内存里会同时驻扎两个对象的内存。 -- CopyOnWrite容器不能保证数据的实时一致性,可能读取到旧数据。 - -### ConcurrentLinkedQueue - -非阻塞队列。高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,通过 CAS 操作实现。 - -如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。适合在对性能要求相对较高,同时有多个线程对队列进行读写的场景。 - -**非阻塞队列中的几种主要方法:** -add(E e) : 将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常; -remove() :移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常; -offer(E e) :将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false; -poll() :移除并获取队首元素,若成功,则返回队首元素;否则返回null; -peek() :获取队首元素,若成功,则返回队首元素;否则返回null - -对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。 - -### 阻塞队列 - -阻塞队列是java.util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。BlockingQueue 适合用于作为数据共享的通道。 - -使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。 - -阻塞队列和一般的队列的区别就在于: - -1. 多线程支持,多个线程可以安全的访问队列 -2. 阻塞操作,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线程就会阻塞直到队列不满。 - -#### 方法 - -| 方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 | -| ------------- | --------- | ---------- | -------- | ------------------ | -| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) | -| 移除方法 | remove() | poll() | take() | poll(time,unit) | -| 检查方法 | element() | peek() | 不可用 | 不可用 | - -#### JDK提供的阻塞队列 - -JDK 7 提供了7个阻塞队列,如下 - -1、**ArrayBlockingQueue** - -有界阻塞队列,底层采用数组实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不能保证线程访问队列的公平性,参数`fair`可用于设置线程是否公平访问队列。为了保证公平性,通常会降低吞吐量。 - -```java -private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true);//fair -``` - -2、**LinkedBlockingQueue** - -LinkedBlockingQueue是一个用单向链表实现的有界阻塞队列,可以当做无界队列也可以当做有界队列来使用。通常在创建 LinkedBlockingQueue 对象时,会指定队列最大的容量。此队列的默认和最大长度为`Integer.MAX_VALUE`。此队列按照先进先出的原则对元素进行排序。与 ArrayBlockingQueue 相比起来具有更高的吞吐量。 - -3、**PriorityBlockingQueue** - -支持优先级的**无界**阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现`compareTo()`方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数`Comparator`来进行排序。 - -PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会**自动扩容**。 - -PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。 - -4、**DelayQueue** - -支持延时获取元素的无界阻塞队列。队列使用PriorityBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。 - -5、**SynchronousQueue** - -不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。支持公平访问队列。 - -SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。 - -6、**LinkedTransferQueue** - -由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法。 - -transfer方法:如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。 - -tryTransfer方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。 - -#### 原理 - -JDK使用通知模式实现阻塞队列。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。 - -ArrayBlockingQueue使用Condition来实现: - -```java -private final Condition notEmpty; -private final Condition notFull; - -public ArrayBlockingQueue(int capacity, boolean fair) { - if (capacity <= 0) - throw new IllegalArgumentException(); - this.items = new Object[capacity]; - lock = new ReentrantLock(fair); - notEmpty = lock.newCondition(); - notFull = lock.newCondition(); -} - -public E take() throws InterruptedException { - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - while (count == 0) // 队列为空时,阻塞当前消费者 - notEmpty.await(); - return dequeue(); - } finally { - lock.unlock(); - } -} - -public void put(E e) throws InterruptedException { - checkNotNull(e); - final ReentrantLock lock = this.lock; - lock.lockInterruptibly(); - try { - while (count == items.length) - notFull.await(); - enqueue(e); - } finally { - lock.unlock(); - } -} - -private void enqueue(E x) { - final Object[] items = this.items; - items[putIndex] = x; - if (++putIndex == items.length) - putIndex = 0; - count++; - notEmpty.signal(); // 队列不为空时,通知消费者获取元素 -} -``` - diff --git a/README.md b/README.md index 21e4c06..77dfbb0 100644 --- a/README.md +++ b/README.md @@ -4,205 +4,406 @@ 大彬,**非科班自学Java**,校招斩获京东、携程、华为等多家互联网中大厂offer。作为一名转码选手,深感这一路的不易,**半年的自学经历**,彻底改变了我的职业生涯。坚持分享自学Java经历、计算机知识、Java后端技术和面试经验等,希望能帮助到更多的小伙伴,**我踩过的坑你们不要再踩**。 -[点击此处](https://zhuanlan.zhihu.com/p/395162772) 查看我的**自学路线**。 +[点击此处](https://topjavaer.cn/learning-resources/java-learn-guide.html) 查看我的**自学路线**。 -# 仓库简介 +# 面试网站 + +大彬自己搭建了一个**小破站**,将**本仓库所有的面试题**都整理到小破站了,欢迎大家访问~ + +网站地址:https://topjavaer.cn + +![](http://img.topjavaer.cn/img/image-20221030165150524.png) + +# 仓库相关 + +## 简介 **本仓库用于分享互联网大厂高频面试题、Java核心知识总结,包括Java基础、并发、MySQL、Springboot、MyBatis、Redis、RabbitMQ等等,面试必备!** **面试专题相关的文章已经整理成PDF,需要的小伙伴可以自行下载**:[Java高频面试题PDF](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) -如果你是Java初学者,不知道下一步该学什么内容,可以看下我总结的**2021年最新的[Java学习路线](https://zhuanlan.zhihu.com/p/395162772)**。如果喜欢看视频学习,可以参考这个:[播放量1000万+!B站最值得学习的Java视频教程](https://zhuanlan.zhihu.com/p/397533240)。 +如果你是Java初学者,不知道下一步该学什么内容,可以看下我最新整理的[Java学习路线](https://topjavaer.cn/learning-resources/java-learn-guide.html)。如果喜欢看视频学习,可以参考这个:[播放量1000万+!B站最值得学习的Java视频教程](https://zhuanlan.zhihu.com/p/397533240)。 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的**公众号『 程序员大彬 』**,后台回复『 PDF 』可以**下载最新版本的大厂高频面试题目PDF版本**。 -
+

个人公众号


- 微信交流群 -公众号 - 知乎 - 牛客网 - 掘金 - 免费PDF + 微信交流群 +公众号 + 知乎 + 牛客网 + 掘金 + 免费PDF

+## 贡献指南 + +欢迎各位小伙伴参与本仓库的维护工作,如果你发现有以下问题,可以直接提交**issue**或者**pull request**: + +- 笔记存在笔误(手动码字在所难免,欢迎提pr订正) +- 笔记内容存在错误 +- 知识点欠缺,不够完善 +- ... + +当然不止以上这些问题,只要你觉得有**待改善**的地方,都可以提出你的建议(提交**issue**或者**pull request**) + +参与贡献的小伙伴,希望你可以**遵守以下规范**: + +- [如何在 Github 上规范的提交 PR](https://juejin.cn/post/7167734674167447582) +- [如何向开源社区提issue](https://github.com/seajs/seajs/issues/545) + # 简历很重要 -[简历投递之后总是石沉大海?](https://zhuanlan.zhihu.com/p/406982597) +- [23套精美简历模板](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247489358&idx=1&sn=dd1b91f115438c29a4215c674b8761e4&chksm=ce98ea08f9ef631e2c8361269f28c01db73eca1ff5b91ba0a0ec8c9a6b9fdcb46a5e16c57020#rd) +- [简历投递之后总是石沉大海?](https://topjavaer.cn/campus-recruit/resume.html) # 精选资源 -- [200多本经典的计算机书籍](https://github.com/Tyson0314/java-books) -- [谷歌师兄刷题笔记](https://t.1yb.co/A6id)(推荐 :+1:) -- [BAT大佬总结的刷题手册](https://t.1yb.co/yMbo)(推荐 :+1:) -- [Java优质项目推荐](https://www.zhihu.com/question/325011850/answer/2257046656) -- [优质视频教程推荐](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd)(推荐 :+1:) - -# 大厂面试系列 - -1. [字节跳动一面面经](https://mp.weixin.qq.com/s/RH-SunzjqUTTx8HWaCmCcw) -2. [别再问我Java List八股文了!](https://mp.weixin.qq.com/s/doyy_GYGWoH_YHgyMijStA) -3. [腾讯面试,你真的懂HTTP吗?](https://mp.weixin.qq.com/s/kC7XRBfO7Z5hZcX6Dz2viw) -4. [美团二面面经,最后竟然有惊喜?](https://mp.weixin.qq.com/s/3HvOtTU29HGALqmeeOZNWw) -5. [Java多线程,被面试官难倒了!](https://mp.weixin.qq.com/s/tv8pOLaS6hpwgbKOB9w0Zw) -6. [京东二面,Redis为什么这么快?](https://mp.weixin.qq.com/s/S3vN5T9HpziRd2s5ysLaSg) -7. [MySQL索引,给我整不会了!](https://mp.weixin.qq.com/s/Q5CrDlNInpnckJaBQSrA7w) -8. [别再问我Java虚拟机八股文了!](https://mp.weixin.qq.com/s/npo5-VqQt5sqZiSwPv6LVw) -9. [计算机网络,问傻了!](https://mp.weixin.qq.com/s/WXcMLa_tdxpRLhO4U8LHIQ) -10. [Spring这几道题你会吗?](https://mp.weixin.qq.com/s/DtgYRFfOQxQdtQosCU-6aw) -10. [面向对象编程?](https://mp.weixin.qq.com/s/M8jDnLat61YAbM1-jIhJIA) -10. [Java内功深厚?](https://mp.weixin.qq.com/s/v_kWSHX9GMS_aoqfwUMKsg) -10. [面试初级Java开发,面试官问我MySQL架构?](https://mp.weixin.qq.com/s/JvDZCk4IecmaEYfFsRhpjQ) -10. [手写红黑树?](https://mp.weixin.qq.com/s/yznh_IfMg4hWqU62U-t9GQ) -10. [面完阿里,直接入职!](https://mp.weixin.qq.com/s/49QJ1FzaGTe-_54PT8_8jA) -10. [华为面经](https://mp.weixin.qq.com/s/KmjwoG7pNvAHiX1UNnef6g) +- [200多本经典的计算机书籍,收藏吧](https://github.com/Tyson0314/java-books) +- [谷歌师兄刷题笔记,支持Java、C++、Go三种语言!](https://t.1yb.co/A6id)(推荐 :+1:) +- [刷题必备!BAT大佬总结的刷题手册!](https://t.1yb.co/yMbo)(推荐 :+1:) +- [Github 上爆火的各种硬核技术学习路线思维导图 :star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494513&idx=1&sn=de1a7cf0b5580840cb8ad4a96e618866&chksm=ce9b1637f9ec9f212d054018598b96b5277f7733fac8f985d8dae0074c8446a2cad8e43ba739#rd) +- [图解操作系统、网络、计算机组成PDF下载!那些让你起飞的计算机基础知识 :star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494510&idx=1&sn=b19d9e07321b8fca9129fe0d8403a426&chksm=ce9b1628f9ec9f3e7d45a6db8389ee2813864a9ca692238d29b139c35ccb01b08155bc2da358#rd) +- [白嫖真的香!15个Java优质项目](https://www.zhihu.com/question/325011850/answer/2257046656) +- [免费分享!字节大佬推荐的优质视频教程](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd)(推荐 :+1:) +- [玩转ChatGPT手册限时免费分享:star:](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494344&idx=1&sn=d16f51e8bd3424f63e4fb6a5aa5ca4db&chksm=ce9b178ef9ec9e9841c7a049e4da0843c291b96f463e87190a6bf344c7022194ee393b695751#rd) -# Java +# 经验分享 -## 基础 +- [工作一年想要跳槽,不知道应该怎么准备?](https://topjavaer.cn/career-plan/how-to-prepare-job-hopping.html) +- [工作3年半,最近岗位有变动,有点迷茫](https://topjavaer.cn/career-plan/4-years-reflect.html) +- [对于java开发和大数据开发,24年秋招的话选择哪个方向会比较合适呢?](https://topjavaer.cn/career-plan/java-or-bigdata.html) +- [四年程序员生涯的反思](https://topjavaer.cn/career-plan/4-years-reflect.html) +- [在国企做开发,是什么样的体验](https://topjavaer.cn/career-plan/guoqi-programmer.html) +- [工作两年多,技术水平没有很大提升,该怎么办](https://topjavaer.cn/zsxq/question/2-years-tech-no-upgrade.html) +- [24届校招,Java开发和大数据开发怎么选](https://topjavaer.cn/zsxq/question/java-or-bigdata.html) +- [新人如何快速的熟悉新项目](https://topjavaer.cn/zsxq/question/familiarize-new-project-qucikly.html) -1. [**Java面试题精选**](Java/Java基础面试题.md) **精心整理的大厂Java面试题目,附有详细答案** (推荐 :+1:) -2. [Java8 新特性总结](Java/Java8.md) +# 副业指南 -## 集合 +- [一些接单平台](https://topjavaer.cn/zsxq/article/sideline-guide.html) -1. [Java集合高频面试题](Java/Java集合面试题.md)(**牛客点赞200+!推荐** :+1:) +# 面试前准备 -## 并发 +- [我建议你这样去刷题](https://topjavaer.cn/campus-recruit/leetcode-guide.html) +- [项目经验怎么回答?](https://topjavaer.cn/campus-recruit/project-experience.html) +- [没有项目经验,怎么办?](https://topjavaer.cn/campus-recruit/lack-project-experience.html) +- [你在项目里遇到的最大困难是什么,如何解决的?](https://topjavaer.cn/campus-recruit/lbiggest-difficulty.html) +- [面试官问你的职业规划是怎样的?该怎么回答](https://topjavaer.cn/campus-recruit/interview-question-career-plan.html) +- [面试官问你有什么要问我的吗?该怎么回答](https://topjavaer.cn/campus-recruit/question-ask-me.html) +- [HR问目前拿到哪几个offer了,怎么回答好?](https://topjavaer.cn/campus-recruit/hr-ask-offers.html) -1. [Java并发高频面试题(精华版)](Java/Java并发面试题.md) **精心整理的大厂Java并发编程面试题目,附有详细答案** (推荐 :+1:) +# Java + +- [Java高频面试题总结](https://topjavaer.cn/java/java-basic.html) (推荐 :+1:) +- [Java集合高频面试题](https://topjavaer.cn/java/java-collection.html)(推荐 :+1:) +- [Java并发高频面试题](https://topjavaer.cn/java/java-concurrent.html) (推荐 :+1:) +- [JVM高频面试题](https://topjavaer.cn/java/jvm.html)(推荐 :+1:) +- [Tomcat基础知识点总结](https://topjavaer.cn/web/tomcat.html) + +**Java重要知识点** + +- [Java8新特性总结](https://topjavaer.cn/java/java8-all.html) +- [Java19新特性](https://topjavaer.cn/advance/excellent-article/19-java19.html) +- [Java Stream常见用法汇总](https://topjavaer.cn/advance/excellent-article/26-java-stream.html) +- [泛型中的T、E、K、V,是什么含义?](https://topjavaer.cn/advance/excellent-article/24-generic.html) +- [面试官:反射是如何影响性能的?](https://topjavaer.cn/java/basic/reflect-affect-permance.html) +- [面试官:详细说说你对序列化的理解?](https://topjavaer.cn/java/basic/serialization.html) +- [感受 lambda 之美](https://mp.weixin.qq.com/s/xwvdtWdFbvmUYaRAAkIhvA) +- [try-catch 捕获异常会影响性能吗?](https://mp.weixin.qq.com/s/iZAB3XzBCoKaJMW6X2jmzA) + +**JVM重要知识点** + +- [美团面试:熟悉哪些JVM调优参数?](https://topjavaer.cn/advance/excellent-article/9-jvm-optimize-param.html) +- [一次简单的JVM调优,拿去写到简历里](https://topjavaer.cn/advance/excellent-article/5-jvm-optimize.html) +- [阿里排错神器--Arthas](https://topjavaer.cn/advance/excellent-article/23-arthas-intro.html) +- [Java堆内存是线程共享的?](https://topjavaer.cn/java/jvm/jvm-heap-memory-share.html) +- [面试官:你工作中做过 JVM 调优吗?怎么做的?](https://mp.weixin.qq.com/s/mwZ5qiBt-xlxy8N3Ya2SvQ) +- [JVM调优几款好用的内存分析工具](https://mp.weixin.qq.com/s/bSgNk6roybRp2buCvwfomw) -## JVM +**Java并发重要知识点** -1. [【大厂面试】——JVM高频面试题](Java/JVM高频面试题.md)(推荐 :+1:) +- [说一说多线程常见锁的策略](https://mp.weixin.qq.com/s/t0SK4fMF7D_zY_1zlTz6jA) +- [8 种异步实现方式](https://mp.weixin.qq.com/s/2lgGj878MQoD-siZvG473g) +- [CompletableFuture 异步多线程](https://mp.weixin.qq.com/s/gRnXInZznCyZE0ZQ9ByfIg) # 数据库 ## MySQL -1. [【大厂面试】—— MySQL高频面试题50道](数据库/MySQL高频面试题.md)(**知乎1k+收藏,推荐** :+1:) -2. [图解索引下推](https://mp.weixin.qq.com/s/W1XQYmihtSdbLWQKeNwZvQ)(推荐 :+1:) -3. [MySQL执行计划](数据库/MySQL执行计划.md)(推荐 :+1:) +- [MySQL高频面试题50道](https://topjavaer.cn/database/mysql.html)(**知乎1k+收藏,推荐** :+1:) +- [MySQL锁高频面试题](https://topjavaer.cn/database/mysql-lock.html) + +**重要知识点**: + +- [MySQL执行计划详解](https://topjavaer.cn/database/mysql-execution-plan.html)(推荐 :+1:) +- [图解索引下推](https://mp.weixin.qq.com/s/W1XQYmihtSdbLWQKeNwZvQ)(推荐 :+1:) +- [MySQL最大建议行数 2000w,靠谱吗?](https://topjavaer.cn/advance/excellent-article/12-mysql-table-max-rows.html) +- [order by是怎么工作的?](https://topjavaer.cn/advance/excellent-article/13-order-by-work.html) +- [8种最坑SQL语法](https://topjavaer.cn/advance/excellent-article/7-sql-optimize.html) +- [为什么说数据库连接很消耗资源](https://topjavaer.cn/advance/excellent-article/18-db-connect-resource.html) +- [SELECT COUNT(*) 会造成全表扫描?](https://topjavaer.cn/advance/excellent-article/25-select-count-slow-query.html) +- [MySQL中的 distinct 和 group by 哪个效率更高?](https://mp.weixin.qq.com/s/jPUjKl81Es3bbtGoqdVDxg) +- [MySQL慢查询之慢 SQL 定位、日志分析与优化方案](https://mp.weixin.qq.com/s/XpEfv0M_ArMa69fnXugWig) +- [MySQL 上亿大表如何优化?](https://mp.weixin.qq.com/s/YSlhVJYp9AhR_UZEJKH1Vg) +- [字节一面:select......for update会锁表还是锁行?](https://mp.weixin.qq.com/s/FW6y8UXVDODG2ViiiWKfYQ) +- [面试官:从 MySQL 读取 100w 数据进行处理,应该怎么做?](https://mp.weixin.qq.com/s/a8vgtTvdgAU6E9xOfm18nw) +- [面试官:int(1) 和 int(10) 有什么区别?](https://mp.weixin.qq.com/s/0P1R2JqTWuPvmqEA2ttj_w) +- [1000万的数据,怎么查询?](https://mp.weixin.qq.com/s/WJmwxDGg6fOfV6hJ300Diw) +- [新同事竟然不懂 where 1=1 是什么意思?](https://mp.weixin.qq.com/s/DjocMG-lE4Swsq2gTvl_7g) ## Redis -1. [【大厂面试】——Redis30问](Redis/Redis面试题.md)(牛客高赞,推荐 :+1:) -2. [Redis分布式锁(推荐 :+1:)](Redis/Redis分布式锁.md) -4. [缓存穿透、缓存雪崩、缓存击穿](Redis/缓存穿透、缓存雪崩、缓存击穿.md) +- [Redis高频面试题总结](https://topjavaer.cn/redis/redis.html)(牛客高赞,推荐 :+1:) + +**重要知识点**: + +- [Redis如何实现库存扣减操作和防止被超卖?](https://topjavaer.cn/advance/excellent-article/1-redis-stock-minus.html) +- [Redis持久化详解](https://topjavaer.cn/redis/article/redis-duration.html) +- [为什么Redis 6.0 引入多线程](https://topjavaer.cn/redis/article/redis-multi-thread.html) +- [缓存和数据库一致性问题,看这篇就够了](https://topjavaer.cn/redis/article/cache-db-consistency.html) +- [Redis 集群模式的工作原理](https://topjavaer.cn/redis/article/redis-cluster-work.html) +- [面试官问:你们项目中用Redis来干什么?](https://mp.weixin.qq.com/s/eAKajEByV1P4eOF9J1hiNA) +- [MySQL和Redis如何保持数据一致性?](https://mp.weixin.qq.com/s/0t3ZZpwwczFfrgbbvrTJRw) + +## ElasticSearch + +- [ElasticSearch高频面试题](https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg) + +## MongoDB + +- [MongoDB高频面试题](https://topjavaer.cn/database/mongodb.html) # 框架 ## Spring -1. [Spring高频面试题30道](框架/Spring面试题.md)(推荐 :+1:) -3. [Spring用到哪些设计模式?](框架/Spring用到哪些设计模式.md) +- [Spring高频面试题30道](https://topjavaer.cn/framework/spring.html)(推荐 :+1:) + +**重要知识点**: + +- [Spring为何需要三级缓存解决循环依赖,而不是二级缓存?](https://topjavaer.cn/advance/excellent-article/6-spring-three-cache.html) +- [@Transactional事务注解详解](https://topjavaer.cn/advance/excellent-article/2-spring-transaction.html) +- [一文彻底搞懂Spring事务传播行为](https://topjavaer.cn/framework/spring/transaction-propagation.html) +- [15个Spring扩展点](https://mp.weixin.qq.com/s/q2ZLXxAM0AC7sDlq6kDA4Q) ## Spring Boot -[Spring Boot总结](框架/SpringBoot实战.md) +- [Spring Boot总结](框架/SpringBoot实战.html) -[SpringBoot面试题总结](框架/SpringBoot面试题总结.md) +- [SpringBoot面试题总结](https://topjavaer.cn/framework/springboot.html) -## Spring MVC +**重要知识点**: + +- [SpringBoot自动装配原理](https://topjavaer.cn/advance/excellent-article/3-springboot-auto-assembly.html) +- [SpringBoot如何解决跨域问题](https://topjavaer.cn/framework/springboot/springboot-cross-domain.html) +- [SpringBoot项目启动优化实践](https://mp.weixin.qq.com/s/-WtrN3jD8pVXTHQ-kpwqQA) +- [SpringBoot实现电子文件签字+合同系统](https://topjavaer.cn/framework/springboot/springboot-contract.html) -[Spring MVC入门知识点](框架/SpringMVC.md) +## Spring MVC -[Spring MVC面试题总结](框架/SpringMVC面试题.md) +[Spring MVC面试题总结](https://topjavaer.cn/framework/springmvc.html) ## Mybatis -[Mybatis入门知识点](框架/深入浅出Mybatis技术原理与实战.md) +[Mybatis入门知识点](框架/深入浅出Mybatis技术原理与实战.html) -[Mybatis面试题总结](框架/Mybatis面试题.md) +[Mybatis面试题总结](https://topjavaer.cn/framework/mybatis.html) ## SpringCloud -[SpringCloud总结](框架/SpringCloud微服务实战.md) +[SpringCloud面试题](https://topjavaer.cn/framework/springcloud-interview.html)(推荐 :+1:) -## Netty +[SpringCloud总结](https://topjavaer.cn/framework/springcloud-overview.html) -[Netty实战笔记](框架/netty实战.md) +## Zookeeper -# 消息队列 +- [Zookeeper面试题](https://topjavaer.cn/zookeeper/zk.html) +- [Zookeeper有哪些使用场景?](https://topjavaer.cn/zookeeper/zk-usage.html) -## RabbitMQ +## Netty -1. [消息队列面试题](消息队列/消息队列面试题.md) -1. [RabbitMQ核心知识总结](消息队列/RabbitMQ.md) (推荐 :+1:) -2. [死信队列](消息队列/死信队列.md) +[Netty实战笔记](https://topjavaer.cn/framework/netty-overview.html) # 计算机网络 -[【大厂面试】—— 计算机网络常见面试题总结](计算机基础/网络/计算机网络高频面试题.md) (**知乎1k+收藏!推荐 :+1:**) +- [计算机网络常见面试题总结](https://topjavaer.cn/computer-basic/network.html) (**知乎1k+收藏!推荐 :+1:**) +- [TCP常见面试题总结](https://topjavaer.cn/computer-basic/tcp.html) -[session和cookie详解](计算机基础/网络/session和cookie.md) +**重要知识点**: + +- [有了HTTP,为啥还要用RPC](https://topjavaer.cn/advance/excellent-article/15-http-vs-rpc.html) # 数据结构与算法 -[如何高效的刷LeetCode?](https://www.zhihu.com/question/280279208/answer/2377906738) +- [常见数据结构总结](https://topjavaer.cn/computer-basic/data-structure.html) +- [如何高效的刷LeetCode?](https://www.zhihu.com/question/280279208/answer/2377906738) +- [120道Leetcode题解(高频)](https://topjavaer.cn/leetcode/hot120/) -[7种常见的排序算法Java代码实现](计算机基础/数据结构与算法/常见的排序算法Java代码实现.md) +# 设计模式 -[二叉树前序、中序、后序、层序遍历代码实现](计算机基础/数据结构与算法/二叉树前序、中序、后序、层序遍历代码实现.md) +[字节跳动大佬总结的设计模式PDF](https://t.1yb.co/y96J) -[常见数据结构总结](计算机基础/数据结构与算法/数据结构.md) +[设计模式的六大原则](https://topjavaer.cn/advance/design-pattern/1-principle.html) -# 设计模式 +常见的**设计模式**详解: -[字节跳动大佬总结的设计模式PDF](https://t.1yb.co/y96J) +- [设计模式之单例模式](https://topjavaer.cn/advance/design-pattern/2-singleton.html) +- [设计模式之工厂模式](https://topjavaer.cn/advance/design-pattern/3-factory.html) +- [设计模式之模板模式](https://topjavaer.cn/advance/design-pattern/4-template.html) +- [设计模式之策略模式](https://topjavaer.cn/advance/design-pattern/5-strategy.html) +- [设计模式之责任链模式](https://topjavaer.cn/advance/design-pattern/6-chain.html) +- [设计模式之迭代器模式](https://topjavaer.cn/advance/design-pattern/7-iterator.html) +- [设计模式之装饰器模式](https://topjavaer.cn/advance/design-pattern/8-decorator.html) +- [设计模式之适配器模式](https://topjavaer.cn/advance/design-pattern/9-adapter.html) +- [设计模式之观察者模式](https://topjavaer.cn/advance/design-pattern/10-observer.html) +- [设计模式之代理模式](https://topjavaer.cn/advance/design-pattern/11-proxy.html) +- [设计模式之建造者模式](https://topjavaer.cn/advance/design-pattern/12-builder.html) -[设计模式总结](其他/设计模式.md) +**设计模式优质文章** -# 工具 +- [代码越写越乱?那是因为你没用责任链](https://mp.weixin.qq.com/s/TpB5bmNwlcJj-XZo5iXXtg) -[Git 超详细总结!](工具/progit2.md)(推荐 :+1:) +# 分布式 -# 其他精选文章 +- [微服务面试题](https://topjavaer.cn/advance/distributed/4-micro-service.html) +- [RPC面试题](https://topjavaer.cn/advance/distributed/3-rpc.html) +- [分布式事务总结](https://topjavaer.cn/advance/distributed/6-distributed-transaction.html) -1. [记一次OOM问题排查](https://mp.weixin.qq.com/s/40C1OdtfGJSrecBoL9Mjbg) -2. [推荐一款画图神器](https://mp.weixin.qq.com/s/vDtWku41rpeux8ZhZyA81w) -3. [有哪些值得推荐的Java书籍?](https://mp.weixin.qq.com/s/MUB07APbyDH2-iftGDdGlA) -4. [这款MarkDown神器,要收费啦!](https://mp.weixin.qq.com/s/aCEi-4vb3kNPF0-E4rB5Kg) -5. [在B站学Java](https://mp.weixin.qq.com/s/_xfT2Tr6rT8qSAFrtRQ3wQ) -6. [你见过这样使用Github的吗?](https://mp.weixin.qq.com/s/U_AItugf8KaUPyIun_xagg) +**优质文章**: +- [全局唯一ID生成方案](https://topjavaer.cn/advance/distributed/1-global-unique-id.html) +- [分布式架构演进](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) +- [新一代分布式任务调度框架](https://topjavaer.cn/advance/excellent-article/22-distributed-scheduled-task.html) +- [分布式锁怎么实现?](https://topjavaer.cn/distributed/article/distributed-lock.html) +# 高并发 -**本仓库持续更新中,欢迎star和pr~** +- [限流算法总结](https://topjavaer.cn/advance/concurrent/1-current-limiting.html) +- [负载均衡](https://topjavaer.cn/advance/concurrent/2-load-balance.html) +- [限流的几种方案](https://topjavaer.cn/advance/excellent-article/17-limit-scheme.html) +- [面试官:如何保证接口幂等性?一口气说了12种方法!](https://topjavaer.cn/advance/excellent-article/8-interface-idempotent.html) +# 消息队列 + +1. [消息队列面试题](https://topjavaer.cn/message-queue/mq.html) +2. [RabbitMQ面试题总结](https://topjavaer.cn/message-queue/rabbitmq.html) (推荐 :+1:) +3. [Kafka面试题总结](https://topjavaer.cn/message-queue/kafka.html) (推荐 :+1:) + +**重要知识点:** + +- [消息队列常见的使用场景](https://topjavaer.cn/advance/excellent-article/27-mq-usage.html) +- [如何从0到1设计消息队列](https://topjavaer.cn/advance/system-design/16-mq-design.html) + +# 海量数据场景题 + +1. [大数据中 TopK 问题的常用套路](https://topjavaer.cn/mass-data/8-topk-template.html) +2. [统计不同电话号码的个数](https://topjavaer.cn/mass-data/1-count-phone-num.html) +3. [出现频率最高的100个词](https://topjavaer.cn/mass-data/2-find-hign-frequency-word.html) +4. [查找两个大文件共同的URL](https://topjavaer.cn/mass-data/3-find-same-url.html) +5. [如何在100亿的数据中找到中位数](https://topjavaer.cn/mass-data/4-find-mid-num.html) +6. [找出最热门的查询串](https://topjavaer.cn/mass-data/5-find-hot-string.html) +7. [如何找出排名前500的数字](https://topjavaer.cn/mass-data/6-top-500-num.html) +8. [如何按照 query 的频度排序?](https://topjavaer.cn/mass-data/7-query-frequency-sort.html) +9. [5亿个数的大文件怎么排序?](https://topjavaer.cn/mass-data/9-sort-500-million-large-files.html) + +# 系统设计 + +- [扫码登录](https://topjavaer.cn/advance/system-design/1-scan-code-login.html) +- [订单超时未支付自动取消](https://topjavaer.cn/advance/system-design/2-order-timeout-auto-cancel.html) +- [短链系统设计](https://topjavaer.cn/advance/system-design/3-short-url.html) +- [超卖问题](https://topjavaer.cn/advance/system-design/4-oversold.html) +- [秒杀系统设计](https://topjavaer.cn/advance/system-design/5-second-kill.html) +- [秒杀系统设计的5个要点](https://topjavaer.cn/advance/system-design/12-second-kill-5-pointmd) +- [微信红包系统如何设计?](https://topjavaer.cn/advance/system-design/6-wechat-redpacket-design.html) +- [如何把一个文件较快的发送到100w个服务器?](https://topjavaer.cn/advance/system-design/7-file-send.html) +- [如何用 Redis 统计用户访问量?](https://topjavaer.cn/advance/system-design/10-pdd-visit-statistics) +- [实时订阅推送设计](https://topjavaer.cn/advance/system-design/11-realtime-subscribe-push.html) +- [权限系统设计方案](https://topjavaer.cn/advance/system-design/13-permission-system.html) +- [如何设计一个抢红包系统](https://topjavaer.cn/advance/system-design/15-red-packet.html) +- [如何从0到1设计消息队列](https://topjavaer.cn/advance/system-design/16-mq-design.html) +- [购物车系统怎么设计?](https://topjavaer.cn/advance/system-design/17-shopping-car.html) +- [如何设计一个注册中心?](https://topjavaer.cn/advance/system-design/18-register-center.html) +- [如何设计一个高并发系统?](https://topjavaer.cn/advance/system-design/19-high-concurrent-system-design.html) +- [分库分表平滑迁移](https://topjavaer.cn/advance/system-design/20-sharding-smooth-migration.html) +- [10w级别数据Excel导入优化](https://topjavaer.cn/advance/system-design/21-excel-import.html) +- [从3s到25ms!看看人家的接口优化技巧](https://mp.weixin.qq.com/s/vDD_FT6re249HlPvgR9TRw) + +# 安全 + +- [什么是JWT?](https://topjavaer.cn/advance/excellent-article/16-what-is-jwt.html) +- [单点登录(SSO)设计与实现](https://topjavaer.cn/advance/system-design/8-sso-design.html) + +# 大厂面经汇总 + +- [字节跳动](https://topjavaer.cn/campus-recruit/interview/1-byte-and-dance.html) +- [腾讯](https://topjavaer.cn/campus-recruit/interview/2-tencent.html) +- [百度](https://topjavaer.cn/campus-recruit/interview/3-baidu.html) +- [阿里](https://topjavaer.cn/campus-recruit/interview/4-ali.html) +- [快手](https://topjavaer.cn/campus-recruit/interview/5-kuaishou.html) +- [美团](https://topjavaer.cn/campus-recruit/interview/6-meituan.html) +- [shopee](https://topjavaer.cn/campus-recruit/interview/7-shopee.html) +- [京东](https://topjavaer.cn/campus-recruit/interview/8-jingdong.html) +- [华为](https://topjavaer.cn/campus-recruit/interview/9-huawei.html) +- [网易](https://topjavaer.cn/campus-recruit/interview/10-netease.html) + +# 优质文章 + +- [干掉“重复代码”的技巧有哪些](https://topjavaer.cn/advance/excellent-article/4-remove-duplicate-code.html) +- [大文件上传时如何做到秒传?](https://topjavaer.cn/advance/excellent-article/10-file-upload.html) +- [架构的演进](https://topjavaer.cn/advance/excellent-article/14-architect-forward.html) +- [8种架构模式](https://topjavaer.cn/advance/excellent-article/11-8-architect-pattern.html) +- [几种常见的架构模式](https://topjavaer.cn/advance/excellent-article/20-architect-pattern.html) +- [线上接口很慢怎么办?](https://topjavaer.cn/practice/service-performance-optimization.html) +- [不要再封装各种 Util 工具类了,这个神级框架值得拥有!](https://mp.weixin.qq.com/s/7VuxBrBcXsAoykcJyNRsvQ) +- [怎样写出优雅的代码?](https://mp.weixin.qq.com/s/ph2pH4O1G_6YScGITaiJwg) +- [BitMap牛逼在哪里?](https://mp.weixin.qq.com/s/jfRCHHh2D6wMAeyD7XLKxg) +- [什么是雪花算法?啥原理?附 Java 实现!](https://mp.weixin.qq.com/s/1Kx55x3fYUs9afpeAzIUOg) +# 工具 + +- [Git 高频面试题总结](https://topjavaer.cn/tools/git.html) +- [Git 超详细总结!](https://topjavaer.cn/tools/git-overview.html)(推荐 :+1:) +- [Linux 常用命令总结!](https://topjavaer.cn/tools/linux-overview.html) +- [Docker 基础总结!](https://topjavaer.cn/tools/docker-overview.html) +- [Maven 基础总结!](https://topjavaer.cn/tools/maven-overview.html) +- [Nginx 高频面试题](https://topjavaer.cn/tools/nginx.html) # 交流 如果想进**技术、面试交流群**,可以扫描下方二维码加我微信,**备注加群**,我拉你进群,群里有BAT大佬,互相学习~ -
+

+ + # 赞赏 如果觉得**本仓库**对您有帮助的话,可以请大彬**喝一杯咖啡**(小伙伴们赞赏的时候可以备注下哦~) -| 微信 | 支付宝 | -| ----------------------------------------------------------- | ------------------------------------------------------------ | -| ![](http://img.dabin-coder.cn/image/微信收款.png) | ![](http://img.dabin-coder.cn/image/支付宝赞赏码.png) | +| 微信 | 支付宝 | +| --------------------------------------------- | ------------------------------------------------- | +| ![](http://img.topjavaer.cn/img/微信收款.png) | ![](http://img.topjavaer.cn/img/支付宝赞赏码.png) | 每笔赞赏我会在下面记录下来,感谢你们,我会更加努力,砥砺前行~ -| 日期 | 来源 | **用户** | **金额** | 备注 | -| ---------- | ------------ | -------- | -------- | ------ | -| 2021.11.19 | 微信收款码 | *张 | 6.66元 | 支持! | -| 2021.11.25 | 支付宝收款码 | *海 | 1元 | | -| 2021.12.10 | 微信收款码 | 浩*y | 10元 | | -| 2021.12.15 | 微信收款码 | biubiu* | 6.66元 | 好 | -| 2022.02.17 | 微信收款码 | *齐 | 8元 | | -| 2022.05.03 | 微信收款码 | *哈 | 2元 | | - - +| 日期 | 来源 | **用户** | **金额** | 备注 | +| ---------- | ------------ | -------- | -------- | ------------------------ | +| 2021.11.19 | 微信收款码 | *张 | 6.66元 | 支持! | +| 2021.11.25 | 支付宝收款码 | *海 | 1元 | | +| 2021.12.10 | 微信收款码 | 浩*y | 10元 | | +| 2021.12.15 | 微信收款码 | biubiu* | 6.66元 | 好 | +| 2022.02.17 | 微信收款码 | *齐 | 8元 | | +| 2022.05.03 | 微信收款码 | *哈 | 2元 | | +| 2022.06.12 | 微信收款码 | *可 | 8.8元 | | +| 2022.10.19 | 微信收款码 | *斌 | 10元 | 支持一下,希望能持续更新 | +| 2022.11.16 | 支付宝收款码 | *雄 | 2元 | | +| 2022.12.02 | 微信收款码 | *军 | 5元 | | diff --git "a/Redis/\347\274\223\345\255\230\347\251\277\351\200\217\343\200\201\347\274\223\345\255\230\351\233\252\345\264\251\343\200\201\347\274\223\345\255\230\345\207\273\347\251\277.md" "b/Redis/\347\274\223\345\255\230\347\251\277\351\200\217\343\200\201\347\274\223\345\255\230\351\233\252\345\264\251\343\200\201\347\274\223\345\255\230\345\207\273\347\251\277.md" deleted file mode 100644 index ca08607..0000000 --- "a/Redis/\347\274\223\345\255\230\347\251\277\351\200\217\343\200\201\347\274\223\345\255\230\351\233\252\345\264\251\343\200\201\347\274\223\345\255\230\345\207\273\347\251\277.md" +++ /dev/null @@ -1,60 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [缓存穿透](#%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F) -- [缓存雪崩](#%E7%BC%93%E5%AD%98%E9%9B%AA%E5%B4%A9) -- [缓存击穿](#%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF) - - - -# 缓存穿透 - -缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,如果从DB查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。 - -1. 缓存空值,不会查数据库 -2. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。 - -布隆可以看成数据库的缩略版,用来判定是否存在值。启动的时候过滤器是要全表扫描的,数据库数据发生变化的时候会更新布隆过滤器。 - -布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询redis和数据库。 - -![](E:/project/java/learn/Java-learning/img/bloom-filter.jpg) - -> 图片来源网络 - - - -# 缓存雪崩 - -缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重挂掉。 - -解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。 - -# 缓存击穿 - -缓存击穿:大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。 - -解决方法:加互斥锁(redis分布式锁或者使用ReentrantLock),第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。 - -```java -public String get(key) { - String value = redis.get(key); - if (value == null) { //代表缓存值过期 - String key_mutex = "mutext:key:" + key; - //设置30s的超时,防止del操作失败的时候,下次缓存过期一直不能load db - if (redis.set(key_mutex, 1, 'NX', 'PX', 30000) == 1) { //代表设置成功 - value = db.get(key); - redis.set(key, value, expire_secs); - redis.del(key_mutex); - } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可 - sleep(50); - get(key); //重试 - } - } else { - return value; - } - } -``` - -SETNX:只有不存在的时候才设置,可以利用它来实现锁的效果。 \ No newline at end of file diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 0000000..63869db --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,86 @@ +import {defineUserConfig} from "vuepress"; +import theme from "./theme"; + +const {searchPlugin} = require('@vuepress/plugin-search') + +import { gitPlugin } from '@vuepress/plugin-git' + + +export default defineUserConfig({ + lang: "zh-CN", + title: "大彬", + description: "Java学习、面试指南,涵盖大部分 Java 程序员所需要掌握的核心知识", + base: "/", + dest: './public', + theme, + // 是否开启默认预加载 js + shouldPrefetch: (file, type) => false, + + head: [ + //meta + ["meta", { name: "robots", content: "all" }], + ["meta", {name: "author", content: "大彬"}], + [ + "meta", + { + "http-equiv": "Cache-Control", + content: "no-cache, no-store, must-revalidate", + }, + ], + ["meta", { "http-equiv": "Pragma", content: "no-cache" }], + ["meta", { "http-equiv": "Expires", content: "0" }], + ['meta', {name: 'baidu-site-verification', content: 'code-mtJaPDeFwy'}], + // ['meta', { name: 'google-site-verification', content: 'eGgkbT6uJR-WQeSkhhcB6RbnZ2RtF5poPf1ai-Fgmy8' }], + ['meta', {name: 'keywords', content: 'Java,Spring,Mybatis,SpringMVC,Springboot,编程,程序员,MySQL,Redis,系统设计,分布式,RPC,高可用,高并发,场景设计,Java面试'}], + [ + 'script', {}, ` + var _hmt = _hmt || []; + (function() { + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?f9b36644dd9e756e508a77f272a63e07"; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + })(); + ` + ], + ], + + plugins: [ + searchPlugin({ + // 配置项 + }), + gitPlugin({ + createdTime: false, + updatedTime: false, + contributors: false, + }), + ], + //plugins: [ + // copyright({ + // disableCopy: true, + // global: true, + // disableSelection: true, + // author: "大彬", + // license: "MIT", + // hostname: "https://www.topjavaer.cn", + // }), + // [ + // 'vuepress-plugin-baidu-autopush' + // ], + // sitemapPlugin({ + // // 配置选项 + // hostname: "https:www.topjavaer.cn" + // }), + // photoSwipePlugin({ + // // 你的选项 + // }), + // readingTimePlugin({ + // // 你的选项 + // }), + // [ + // nprogressPlugin(), + // ], + // //['@vuepress/nprogress'], + //], +}) +; diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js new file mode 100644 index 0000000..42e09d7 --- /dev/null +++ b/docs/.vuepress/enhanceApp.js @@ -0,0 +1,13 @@ +export default ({router}) => { + router.beforeEach((to, from, next) => { + //对每个页面点击添加百度统计 + if(typeof _hmt!='undefined'){ + if (to.path) { + _hmt.push(['_trackPageview', to.fullPath]); + } + } + + // continue + next(); + }) +}; diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts new file mode 100644 index 0000000..d2fcdc3 --- /dev/null +++ b/docs/.vuepress/navbar.ts @@ -0,0 +1,418 @@ +import {navbar} from "vuepress-theme-hope"; + +export default navbar([ + + //"/", + //"/home", + { + text: "主页", + link: "/", + icon: "home", + }, + //{ + // text: "校招", + // icon: "campus", + // children: [ + // {text: "校招分享", link: "/campus-recruit/share"}, + // {text: "简历应该这么写", link: "/campus-recruit/resume.md"}, + // {text: "项目经验介绍", link: "/campus-recruit/project-experience.md"}, + // {text: "编程语言", link: "/campus-recruit/program-language"}, + // {text: "面经总结", link: "/campus-recruit/interview/"}, + // {text: "秋招内推", link: "https://docs.qq.com/sheet/DYW9ObnpobXNRTXpq"}, + // ], + //}, + { + text: "学习圈", + icon: "zsxq", + link: "/zsxq/introduce.md", + }, + { + text: "面试指南", + icon: "java", + children: [ + { + text: "Java", + children: [ + {text: "基础", link: "/java/java-basic.md", icon: "jihe"}, + {text: "集合", link: "/java/java-collection.md", icon: "fuwuqi"}, + {text: "并发", link: "/java/java-concurrent.md", icon: "bingfa"}, + {text: "JVM", link: "/java/jvm.md", icon: "xuniji"}, + {text: "Java8", link: "/java/java8", icon: "java"}, + {text: "Tomcat", link: "/web/tomcat.md", icon: "TOMCAT"}, + ] + }, + { + text: "框架", + children: [ + {text: "Spring面试题", link: "/framework/spring.md", icon: "bxl-spring-boot"}, + {text: "SpringMVC面试题", link: "/framework/springmvc.md", icon: "pingtai"}, + {text: "Mybatis面试题", link: "/framework/mybatis.md", icon: "wendang"}, + {text: "SpringBoot面试题", link: "/framework/springboot.md", icon: "bxl-spring-boot"}, + {text: "SpringCloud详解", link: "/framework/springcloud/", icon: "jihe"}, + {text: "SpringCloud面试题", link: "/framework/springcloud-interview.md", icon: "yun"}, + {text: "ZooKeeper面试题", link: "/zookeeper/zk.md", icon: "Zookeeper"}, + {text: "Netty详解", link: "/framework/netty/", icon: "fuwuqi"}, + ] + }, + { + text: "消息队列", + children: [ + {text: "消息队列面试题", link: "/message-queue/mq.md", icon: "xiaoxiduilie"}, + {text: "RabbitMQ面试题", link: "/message-queue/rabbitmq.md", icon: "amqpxiaoxiduilie"}, + {text: "Kafka面试题", link: "/message-queue/kafka.md", icon: "Kafka"}, + ] + }, + { + text: "关系型数据库", + children: [ + //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, + {text: "MySQL基础", link: "/database/mysql-basic/", icon: "jihe"}, + {text: "MySQL面试题", link: "/database/mysql.md", icon: "mysql"}, + {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md", icon: "chayan"}, + ] + }, + { + text: "非关系型数据库", + children: [ + {text: "Redis基础", link: "/redis/redis-basic/", icon: "jihe"}, + {text: "Redis面试题", link: "/redis/redis.md", icon: "Redis"}, + {text: "MongoDB面试题", link: "/database/mongodb.md", icon: "MongoDB"}, + {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg", icon: "elastic"}, + ] + }, + { + text: "计算机基础", + icon: "computer", + children: [ + {text: "网络", link: "/computer-basic/network.md", icon: "wangluo3"}, + {text: "TCP专题", link: "/computer-basic/tcp.md", icon: "wangluo1"}, + {text: "操作系统", link: "/computer-basic/operate-system.md", icon: "os"}, + {text: "算法", link: "/computer-basic/algorithm.md", icon: "suanfa"}, + {text: "LeetCode题解", link: "/leetcode/hot120", icon: "leetcode"}, + {text: "数据结构", link: "/computer-basic/data-structure.md", icon: "datastruct"}, + //{ + // text: "关系型数据库", + // children: [ + // //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, + // {text: "MySQL基础", link: "/database/mysql-basic/"}, + // {text: "MySQL面试题", link: "/database/mysql.md"}, + // {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md"}, + // ] + //}, + //{ + // text: "非关系型数据库", + // children: [ + // {text: "Redis基础", link: "/redis/redis-basic/"}, + // {text: "Redis面试题", link: "/redis/redis.md"}, + // {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg"}, + // ] + //}, + ] + }, + ] + }, + //{ + // text: "计算机基础", + // icon: "computer", + // children: [ + // {text: "网络", link: "/computer-basic/network.md"}, + // {text: "操作系统", link: "/computer-basic/operate-system.md"}, + // {text: "算法", link: "/computer-basic/algorithm.md"}, + // {text: "LeetCode题解", link: "/leetcode/hot120"}, + // {text: "数据结构", link: "/computer-basic/data-structure.md"}, + // //{ + // // text: "关系型数据库", + // // children: [ + // // //{text: "MySQL基础", children: ["/database/mysql-basic/"],}, + // // {text: "MySQL基础", link: "/database/mysql-basic/"}, + // // {text: "MySQL面试题", link: "/database/mysql.md"}, + // // {text: "MySQL执行计划详解", link: "/database/mysql-execution-plan.md"}, + // // ] + // //}, + // //{ + // // text: "非关系型数据库", + // // children: [ + // // {text: "Redis基础", link: "/redis/redis-basic/"}, + // // {text: "Redis面试题", link: "/redis/redis.md"}, + // // {text: "ElasticSearch面试题", link: "https://mp.weixin.qq.com/s/Ffb8NDgavf9QAWYBm0qAVg"}, + // // ] + // //}, + // ] + //}, + { + text: "进阶之路", + icon: "win", + children: [ + { + text: "海量数据", + children: [ + {text: "统计不同号码的个数", link: "/mass-data/1-count-phone-num.md", icon: "phoneno"}, + {text: "出现频率最高的100个词", link: "/mass-data/2-find-hign-frequency-word.md", icon: "datastruct"}, + {text: "查找两个大文件共同的URL", link: "/mass-data/3-find-same-url.md", icon: "wenben"}, + {text: "如何在100亿数据中找到中位数?", link: "/mass-data/4-find-mid-num.md", icon: "bingfa"}, + {text: "如何查询最热门的查询串?", link: "/mass-data/5-find-hot-string.md", icon: "query"}, + {text: "如何找出排名前 500 的数?", link: "/mass-data/6-top-500-num.md", icon: "rank"}, + {text: "如何按照 query 的频度排序?", link: "/mass-data/7-query-frequency-sort.md", icon: "frequency"}, + {text: "大数据中 TopK 问题的常用套路", link: "/mass-data/8-topk-template.md", icon: "bigdata"}, + ] + }, + { + text: "系统设计", + //link: "/advance/system-design/README.md", + //children: [ + // {text: "扫码登录设计", link: "/advance/system-design/1-scan-code-login.md"}, + // {text: "超时订单自动取消", link: "/advance/system-design/2-order-timeout-auto-cancel.md"}, + // {text: "短链系统设计", link: "/advance/system-design/3-short-url.md"}, + // {text: "微信红包系统如何设计?", link: "/advance/system-design/6-wechat-redpacket-design.md"}, + // {text: "单点登录设计与实现", link: "/advance/system-design/8-sso-design.md"}, + //] + children: [ + {text: "扫码登录设计", link: "/advance/system-design/1-scan-code-login.md", icon: "scan"}, + {text: "超时订单自动取消", link: "/advance/system-design/2-order-timeout-auto-cancel.md", icon: "timeout"}, + {text: "短链系统设计", link: "/advance/system-design/README.md", icon: "lianjie"}, + {text: "微信红包系统如何设计?", link: "/advance/system-design/README.md", icon: "hongbao"}, + {text: "单点登录设计与实现", link: "/advance/system-design/README.md", icon: "login"}, + {text: "如何用 Redis 统计用户访问量?", link: "/advance/system-design/README.md", icon: "visit"}, + {text: "实时订阅推送设计与实现", link: "/advance/system-design/README.md", icon: "tongzhi"}, + {text: "如何设计一个抢红包系统", link: "/advance/system-design/README.md", icon: "hongbao1"}, + {text: "购物车系统怎么设计?", link: "/advance/system-design/README.md", icon: "shopcar"}, + {text: "如何设计一个注册中心?", link: "/advance/system-design/README.md", icon: "zhuce"}, + {text: "如何设计一个高并发系统?", link: "/advance/system-design/README.md", icon: "xitong"}, + {text: "10w级别数据Excel导入怎么优化?", link: "/advance/system-design/README.md", icon: "excel"}, + ] + }, + { + text: "分布式", + icon: "distribute", + children: [ + {text: "全局唯一ID", link: "/advance/distributed/1-global-unique-id.md", icon: "quanju"}, + {text: "分布式锁", link: "/advance/distributed/2-distributed-lock.md", icon: "lock"}, + {text: "RPC", link: "/advance/distributed/3-rpc.md", icon: "call"}, + {text: "微服务", link: "/advance/distributed/4-micro-service.md", icon: "weifuwu"}, + {text: "分布式架构", link: "/advance/distributed/5-distibuted-arch.md", icon: "jiagou"}, + {text: "分布式事务", link: "/advance/distributed/6-distributed-transaction.md", icon: "transaction"}, + ] + }, + { + text: "高并发", + children: [ + {text: "限流", link: "/advance/concurrent/1-current-limiting.md", icon: "bingfa"}, + {text: "负载均衡", link: "/advance/concurrent/2-load-balance.md", icon: "balance"}, + ], + }, + { + text: "设计模式", + icon: "win", + children: [ + {text: "设计模式详解", link: "/advance/design-pattern/", icon: "design"}, + ], + }, + { + text: "优质文章", + children: [ + {text: "优质文章汇总", link: "/advance/excellent-article", icon: "wenzhang"}, + ] + }, + ] + }, + + { + text: "源码解读", + icon: "source", + children: [ + { + text: "Spring", + children: [ + {text: "整体架构", link: "/source/spring/1-architect.md", icon: "book"}, + {text: "IOC 容器基本实现", link: "/source/spring/2-ioc-overview", icon: "book"}, + {text: "IOC默认标签解析(上)", link: "/source/spring/3-ioc-tag-parse-1", icon: "book"}, + {text: "IOC默认标签解析(下)", link: "/source/spring/4-ioc-tag-parse-2", icon: "book"}, + {text: "IOC之自定义标签解析", link: "/source/spring/5-ioc-tag-custom.md", icon: "book"}, + {text: "IOC-开启 bean 的加载", link: "/source/spring/6-bean-load", icon: "book"}, + {text: "IOC之bean创建", link: "/source/spring/7-bean-build", icon: "book"}, + {text: "IOC属性填充", link: "/source/spring/8-ioc-attribute-fill", icon: "book"}, + {text: "IOC之循环依赖处理", link: "/source/spring/9-ioc-circular-dependency", icon: "book"}, + {text: "IOC之bean 的初始化", link: "/source/spring/10-bean-initial", icon: "book"}, + {text: "ApplicationContext容器refresh过程", link: "/source/spring/11-application-refresh", icon: "book"}, + {text: "AOP的使用及AOP自定义标签", link: "/source/spring/12-aop-custom-tag", icon: "book"}, + {text: "创建AOP代理之获取增强器", link: "/source/spring/13-aop-proxy-advisor", icon: "book"}, + {text: "AOP代理的生成", link: "/source/spring/14-aop-proxy-create", icon: "book"}, + {text: "AOP目标方法和增强方法的执行", link: "/source/spring/15-aop-advice-create", icon: "book"}, + {text: "@Transactional注解的声明式事物介绍", link: "/source/spring/16-transactional", icon: "book"}, + {text: "Spring事务是怎么通过AOP实现的?", link: "/source/spring/17-spring-transaction-aop", icon: "book"}, + {text: "事务增强器", link: "/source/spring/18-transaction-advice", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring/19-transaction-rollback-commit", icon: "book"}, + ] + }, + { + text: "SpringMVC", + children: [ + {text: "文件上传和拦截器", link: "/source/spring-mvc/1-overview", icon: "book"}, + {text: "导读篇", link: "/source/spring-mvc/2-guide", icon: "book"}, + {text: "场景分析", link: "/source/spring-mvc/3-scene", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring-mvc/4-fileupload-interceptor", icon: "book"}, + ] + }, + { + text: "MyBatis(更新中)", + children: [ + {text: "整体架构", link: "/source/mybatis/1-overview", icon: "book"}, + {text: "反射模块", link: "/source/mybatis/2-reflect", icon: "book"}, + ] + }, + + ] + }, + //{ + // text: "场景题", + // icon: "design", + // children: [ + // { + // text: "海量数据", + // children: [ + // {text: "统计不同号码的个数", link: "/mass-data/count-phone-num.md"}, + // {text: "出现频率最高的100个词", link: "/mass-data/find-hign-frequency-word.md"}, + // ] + // }, + // { + // text: "系统设计", + // children: [ + // {text: "扫码登录设计", link: "/system-design/scan-code-login.md"}, + // ] + // }, + // ] + //}, + { + text: "工具", + icon: "tool", + children: [ + { + text: "开发工具", + children: [ + {text: "Git详解", link: "/tools/git/", icon: "git1"}, + {text: "Maven详解", link: "/tools/maven/", icon: "jihe"}, + {text: "Docker详解", link: "/tools/docker/", icon: "docker1"}, + {text: "Linux常用命令", link: "/tools/linux", icon: "linux"}, + {text: "Nginx面试题", link: "https://mp.weixin.qq.com/s/SKKEeYxif0wWJo6n57rd6A", icon: "nginx"}, + ] + }, + { + text: "在线工具", + children: [ + {text: "json", link: "https://www.json.cn/"}, + {text: "base64编解码", link: "https://c.runoob.com/front-end/693/"}, + {text: "时间戳转换", link: "https://www.beijing-time.org/shijianchuo/"}, + {text: "unicode转换", link: "https://www.fulimama.com/unicode/"}, + {text: "正则表达式", link: "https://www.sojson.com/regex/"}, + {text: "md5加密", link: "https://www.toolkk.com/tools/md5-encrypt"}, + {text: "流程图工具", link: "https://app.diagrams.net/"}, + {text: "二维码", link: "https://cli.im/"}, + {text: "文本比对", link: "https://c.runoob.com/front-end/8006/"}, + ] + }, + { + text: "编程利器", + children: [ + {text: "markdown编辑器", link: "/tools/typora-overview.md", icon: "markdown"}, + ] + }, + ] + }, + { + text: "珍藏资源", + icon: "collection", + children: [ + { + text: "学习资源", + children: [ + {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books", icon: "book"}, + {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md", icon: "leetcode"}, + {text: "技术学习路线思维导图", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494513&idx=1&sn=de1a7cf0b5580840cb8ad4a96e618866&chksm=ce9b1637f9ec9f212d054018598b96b5277f7733fac8f985d8dae0074c8446a2cad8e43ba739#rd", icon: "route"}, + {text: "图解操作系统、网络、计算机系列", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494510&idx=1&sn=b19d9e07321b8fca9129fe0d8403a426&chksm=ce9b1628f9ec9f3e7d45a6db8389ee2813864a9ca692238d29b139c35ccb01b08155bc2da358#rd", icon: "computer"}, + {text: "优质视频教程", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247487149&idx=1&sn=aa883c9f020945d3f210550bd688c7d0&chksm=ce98f3ebf9ef7afdae0b37c4d0751806b0fbbf08df783fba536e5ec20ec6a6e1512198dc6206&token=104697471&lang=zh_CN#rd", icon: "video"}, + {text: "ChatGPT手册", link: "https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494344&idx=1&sn=d16f51e8bd3424f63e4fb6a5aa5ca4db&chksm=ce9b178ef9ec9e9841c7a049e4da0843c291b96f463e87190a6bf344c7022194ee393b695751#rd", icon: "ai"}, + ] + }, + { + text: "学习路线", + children: [ + {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md", icon: "java"}, + {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md", icon: "jisuanji"}, + ] + }, + + ] + }, + { + text: "关于", + icon: "about", + children: [ + {text: "关于我", link: "/about/introduce.md", icon: "wode"}, + {text: "网站日记", link: "/other/site-diary.md", icon: "riji"}, + {text: "联系我", link: "/about/contact.md", icon: "lianxi"}, + {text: "留言区", link: "/other/leave-a-message.md", icon: "liuyan"}, + //{ + // text: "学习资源", + // children: [ + // {text: "计算机经典电子书PDF", link: "https://github.com/Tyson0314/java-books"}, + // {text: "Leetcode刷题笔记", link: "/learning-resources/leetcode-note.md"}, + // ] + //}, + //{ + // text: "学习路线", + // children: [ + // {text: "Java学习路线", link: "/learning-resources/java-learn-guide.md"}, + // {text: "CS学习路线", link: "/learning-resources/cs-learn-guide.md"}, + // ] + //}, + ] + }, + + + //{ text: "Guide", icon: "creative", link: "/guide/" }, + //{ + // text: "Posts", + // icon: "edit", + // prefix: "/posts/", + // children: [ + // { + // text: "Articles 1-4", + // icon: "edit", + // prefix: "article/", + // children: [ + // { text: "Article 1", icon: "edit", link: "article1" }, + // { text: "Article 2", icon: "edit", link: "article2" }, + // "article3", + // "article4", + // ], + // }, + // { + // text: "Articles 5-12", + // icon: "edit", + // children: [ + // { + // text: "Article 5", + // icon: "edit", + // link: "article/article5", + // }, + // { + // text: "Article 6", + // icon: "edit", + // link: "article/article6", + // }, + // "article/article7", + // "article/article8", + // ], + // }, + // { text: "Article 9", icon: "edit", link: "article9" }, + // { text: "Article 10", icon: "edit", link: "article10" }, + // "article11", + // "article12", + // ], + //}, + //{ + // text: "Theme Docs", + // icon: "note", + // link: "https://vuepress-theme-hope.github.io/v2/", + //}, +]); diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts new file mode 100644 index 0000000..283d1b5 --- /dev/null +++ b/docs/.vuepress/sidebar.ts @@ -0,0 +1,179 @@ +import {sidebar} from "vuepress-theme-hope"; + +const {getChildren} = require("./vuepress-sidebar-auto/vuepress-sidebar-auto"); + +export default sidebar({ + "/database/mysql-basic/": [{ + text: "MySQL基础", + collapsable: false, + children: getChildren('./docs/database', 'mysql-basic'), + // children: ["1-data-type"], + }, + ], + "/redis/redis-basic/": [ + { + text: "Redis基础", + collapsable: false, + children: getChildren('./docs/redis', 'redis-basic'), + }, + ], + "/advance/design-pattern/": [ + { + text: "设计模式", + collapsable: false, + children: getChildren('./docs/advance', 'design-pattern'), + }, + ], + "/tools/docker/": [ + { + text: "Docker基础", + collapsable: false, + children: getChildren('./docs/tools', 'docker'), + }, + ], + "/tools/git/": [ + { + text: "Git基础", + collapsable: false, + children: getChildren('./docs/tools', 'git'), + }, + ], + "/leetcode/hot120": [ + { + text: "LeetCode题解", + collapsable: false, + children: getChildren('./docs/leetcode', 'hot120'), + }, + ], + "/tools/maven/": [ + { + text: "Maven基础", + collapsable: false, + children: getChildren('./docs/tools', 'maven'), + }, + ], + "/framework/netty/": [ + { + text: "Netty基础", + collapsable: false, + children: getChildren('./docs/framework', 'netty'), + }, + ], + "/framework/springcloud/": [ + { + text: "SpringCloud基础", + collapsable: false, + children: getChildren('./docs/framework', 'springcloud'), + }, + ], + "/java/java8/": [ + { + text: "java8新特性", + collapsable: false, + children: getChildren('./docs/java', 'java8'), + }, + ], + "/campus-recruit/interview/": [ + { + text: "面经合集", + collapsable: true, + children: getChildren('./docs/campus-recruit', 'interview'), + }, + ], + "/advance/excellent-article": [ + { + text: "优质文章汇总", + collapsable: false, + children: getChildren('./docs/advance', 'excellent-article'), + }, + ], + "/advance/concurrent": [ + { + text: "高并发", + collapsable: false, + children: getChildren('./docs/advance', 'concurrent'), + }, + ], + "/tools/linux/": [ + { + text: "linux常用命令", + collapsable: false, + children: getChildren('./docs/tools', 'linux'), + }, + ], + "/campus-recruit/program-language/": [ + { + text: "编程语言", + collapsable: true, + children: getChildren('./docs/campus-recruit', 'program-language'), + }, + ], + //"/advance/system-design": [ + // { + // text: "系统设计", + // collapsable: false, + // children: getChildren('./docs/advance', 'system-design'), + // }, + //], + "/campus-recruit/share": [ + { + text: "校招分享", + collapsable: false, + children: getChildren('./docs/campus-recruit', 'share'), + }, + ], + // "/mass-data": [ + // { + // text: "海量数据", + // collapsable: false, + // // children: getChildren('./docs', '/mass-data'), + // children: [ + // {text: "统计不同号码的个数", link: "/mass-data/1-count-phone-num.md"} + // ] + // }, + // ], + + //'/': "auto", //不能放在数组第一个,否则会导致右侧栏无法使用 + //"/", + //"/home", + //"/slide", + //{ + // icon: "creative", + // text: "Guide", + // prefix: "/guide/", + // link: "/guide/", + // children: "structure", + //}, + //{ + // text: "Articles", + // icon: "note", + // prefix: "/posts/", + // children: [ + // { + // text: "Articles 1-4", + // icon: "note", + // collapsable: true, + // prefix: "article/", + // children: ["article1", "article2", "article3", "article4"], + // }, + // { + // text: "Articles 5-12", + // icon: "note", + // children: [ + // { + // text: "Articles 5-8", + // icon: "note", + // collapsable: true, + // prefix: "article/", + // children: ["article5", "article6", "article7", "article8"], + // }, + // { + // text: "Articles 9-12", + // icon: "note", + // children: ["article9", "article10", "article11", "article12"], + // }, + // ], + // }, + // ], + //}, +}); diff --git a/docs/.vuepress/styles/index.scss b/docs/.vuepress/styles/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/docs/.vuepress/styles/palette.scss b/docs/.vuepress/styles/palette.scss new file mode 100644 index 0000000..e69de29 diff --git a/docs/.vuepress/theme.ts b/docs/.vuepress/theme.ts new file mode 100644 index 0000000..f62dc4a --- /dev/null +++ b/docs/.vuepress/theme.ts @@ -0,0 +1,168 @@ +import {hopeTheme} from "vuepress-theme-hope"; +import navbar from "./navbar"; +import sidebar from "./sidebar"; + +export default hopeTheme({ + hostname: "https://www.topjavaer.cn", + + author: { + name: "大彬", + url: "https://www.topjavaer.cn", + }, + + iconAssets: "//at.alicdn.com/t/c/font_3573089_ladvogz6xzq.css", + iconPrefix: "iconfont icon-", + //iconAssets: "iconfont", + + darkmode: "toggle", + + fullscreen: false, + + logo: "/logo.svg", + + repo: "Tyson0314/Java-learning", + + docsDir: "docs", + + // navbar + navbar: navbar, + + // sidebar + sidebar: sidebar, + headerDepth: 5, + collapsable: true, + + displayFooter: true, + prevLink: true, + nextLink: true, + + // footer: '粤ICP备2022005190号-2 |' + + // '关于网站', + + footer: '粤ICP备2022005190号-2', + + pageInfo: ["Author", "Original", "Date", "Category", "Tag", "ReadingTime"], + + blog: { + description: "非科班自学转码选手,校招拿了多家互联网大厂offer", + intro: "https://mp.weixin.qq.com/s/84ZDT5d9TIbnyg-jeRKIIA", + medias: { + Github: "https://github.com/Tyson0314", + Gitee: "https://gitee.com/tysondai", + ZhiHu: "https://www.zhihu.com/people/dai-shu-bin-13", + }, + }, + + // encrypt: { + // config: { + // "/guide/encrypt.html": ["1234"], + // }, + // }, + + plugins: { + blog: { + autoExcerpt: true, + }, + + // If you don't need comment feature, you can remove following option + // The following config is for demo ONLY, if you need comment feature, please generate and use your own config, see comment plugin documentation for details. + // To avoid disturbing the theme developer and consuming his resources, please DO NOT use the following config directly in your production environment!!!!! + comment: { + /** + * Using Giscus + */ + provider: "Giscus", + repo: "Tyson0314/topjavaer", + repoId: "R_kgDOHxs_3g", + category: "Announcements", + categoryId: "DIC_kwDOHxs_3s4CQpxA", +// + /** + * Using Twikoo + */ + // provider: "Twikoo", + // envId: "https://twikoo.ccknbc.vercel.app", +// + /** + * Using Waline + */ + // provider: "Waline", + // serverURL: "https://vuepress-theme-hope-comment.vercel.app", + }, + + mdEnhance: { + enableAll: true, + presentation: { + plugins: ["highlight", "math", "search", "notes", "zoom"], + }, + }, + + //myplugin + copyright: { + disableCopy: false, + global: true, + author: "大彬", + //license: "MIT", + hostname: "https://www.topjavaer.cn", + }, + baiduAutoPush: {}, + sitemapPlugin: { + // 配置选项 + hostname: "https://www.topjavaer.cn" + }, + photoSwipePlugin: { + // 你的选项 + }, + readingTimePlugin: {}, + + nprogressPlugin: {}, + + //searchPlugin: { + // appId: "xxx", + // apiKey: "xxxx", + // indexName: "topjavaer.cn", + // locales: { + // "/": { + // placeholder: "搜索文档", + // translations: { + // button: { + // buttonText: "搜索文档", + // buttonAriaLabel: "搜索文档", + // }, + // modal: { + // searchBox: { + // resetButtonTitle: "清除查询条件", + // resetButtonAriaLabel: "清除查询条件", + // cancelButtonText: "取消", + // cancelButtonAriaLabel: "取消", + // }, + // startScreen: { + // recentSearchesTitle: "搜索历史", + // noRecentSearchesText: "没有搜索历史", + // saveRecentSearchButtonTitle: "保存至搜索历史", + // removeRecentSearchButtonTitle: "从搜索历史中移除", + // favoriteSearchesTitle: "收藏", + // removeFavoriteSearchButtonTitle: "从收藏中移除", + // }, + // errorScreen: { + // titleText: "无法获取结果", + // helpText: "你可能需要检查你的网络连接", + // }, + // footer: { + // selectText: "选择", + // navigateText: "切换", + // closeText: "关闭", + // searchByText: "搜索提供者", + // }, + // noResultsScreen: { + // noResultsText: "无法找到相关结果", + // suggestedQueryText: "你可以尝试查询", + // }, + // }, + // }, + // }, + // }, + // }, + }, +}) +; diff --git a/docs/.vuepress/vuepress-sidebar-auto/vuepress-sidebar-auto.js b/docs/.vuepress/vuepress-sidebar-auto/vuepress-sidebar-auto.js new file mode 100644 index 0000000..29d0ca4 --- /dev/null +++ b/docs/.vuepress/vuepress-sidebar-auto/vuepress-sidebar-auto.js @@ -0,0 +1,92 @@ +//侧边栏 +// const autosidebar = require('vuepress-auto-sidebar-doumjun') +const fs = require('fs') +const path = require('path') + +/** + * 过滤所要导航的文件 + * 文件名 包含.md 但 不包含 README */ +function checkFileType(path) { + return path.includes(".md")&&(!path.includes("README")); +} + +/** + * 格式化文件路径*/ +function prefixPath(basePath, dirPath) { + let index = basePath.indexOf("/") +// 去除一级目录地址 + basePath = basePath.slice(index, path.length) +// replace用于处理windows电脑的路径用\表示的问题 + return path.join(basePath, dirPath).replace(/\\/g, "/") +} + +/** + * 截取文档路径*/ +function getPath(path,ele) { + let item=prefixPath(path,ele); + //if (item.split('/')[6]) { + // return item.split('/')[3] + '/' + item.split('/')[4]+ '/' + item.split('/')[5]+ '/' + item.split('/')[6] + //}else if (item.split('/')[5]) { + // return item.split('/')[3] + '/' + item.split('/')[4]+ '/' + item.split('/')[5] + //}else if (item.split('/')[4]) { + // return item.split('/')[3] + '/' + item.split('/')[4] + //} else { + // return item.split('/')[3] + //} + + if (item.split('/')[6]) { + return item.split('/')[4]+ '/' + item.split('/')[5]+ '/' + item.split('/')[6] + }else if (item.split('/')[5]) { + return item.split('/')[4]+ '/' + item.split('/')[5] + }else { + return item.split('/')[4] + } +} + +/** + * 递归获取分组信息并排序*/ +function getGroupChildren(path,ele,root) { + let pa = fs.readdirSync(path + "/" + ele + "/"); + let palist=pa; + // console.log("pathlist", pa) + pa = palist.sort(function (a, b) { + //console.log("a " + a, a.replace(".md", "").match(/^[^-]/)) + //console.log("b " + b, b.replace(".md", "").match(/^[^-]/)) + let num1 = a.split("-")[0] + let num2 = b.split("-")[0] + //console.log("num1", num1, "num2", num2) + //console.log(num1 - num2) + return num1 - num2 + //return a.replace(".md", "").match(/^[^-]/) - b.replace(".md", "").match(/^[^-]/) + //return a.replace(".md", "").match(/[^-]*$/) - b.replace(".md", "").match(/[^-]*$/) + }); + // console.log("after sort ", pa) + pa.forEach(function (item, index) { + let info = fs.statSync(path + "/" + ele + "/" + item); + if (info.isDirectory()) { + let children = []; + let group = {}; + group.title = item.split('-')[1]; + group.collapsable = true; + group.sidebarDepth = 4; + getGroupChildren(path + "/" + ele, item, children); + group.children=children; + root.push(group); + } else { + if (checkFileType(item)) { + root.push(getPath(path + "/" + ele, item)); + } + } + }) +} +/** + * 初始化*/ +function getChildren(path,ele){ + var root=[] + getGroupChildren(path,ele,root); + return root; +} + +// console.log("demo", getChildren('./docs','database/mysql-basic')) + +module.exports = {getChildren: getChildren}; diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9f8ba39 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,81 @@ +--- +home: true +icon: home +title: 主页 +heroImage: /logo.png +heroText: 程序员大彬 +tagline: 优质的编程学习网站 +actions: + - text: 开始阅读 + link: /java/java-basic.html + type: primary + - text: 学习圈子💡 + link: /zsxq/introduce.html + type: primary +features: + - title: 经典计算机书籍 + icon: repo + details: 大彬精心整理200本经典计算机书籍 + link: https://github.com/Tyson0314/java-books + - title: Leetcode算法笔记 + icon: note + details: 谷歌师兄整理的Leetcode算法笔记 + link: learning-resources/leetcode-note.html +projects: + - name: java-books + desc: 经典计算机书籍电子版 + link: https://github.com/Tyson0314/java-books + icon: /assets/img/vuepress.png + + - name: Leetcode算法手册 + desc: Leetcode算法手册 + link: learning-resources/leetcode-note.html + icon: /assets/img/vuepress-hope-logo.svg + + +--- + + + +大彬是**非科班**出身,大三开始自学Java,校招斩获**京东、携程、华为**等offer。作为一名转码选手,深感这一路的不易。 + +**希望我的分享可以帮助更多的小伙伴,我踩过的坑你们不要再踩**! + +[](http://img.topjavaer.cn/img/微信群.png) +[](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247488751&idx=1&sn=507e27534b6ea5f4b3771b30e1fcf57e&chksm=ce98e9a9f9ef60bfbf1370899b49181bae5247e5935714f7ad9e3d06c0028a25c0bfc34d4441#rd) +[](https://space.bilibili.com/1729916794) +[](https://www.zhihu.com/people/dai-shu-bin-13) +[](https://github.com/Tyson0314/java-books) + +## 面试手册电子版 + +本网站所有内容已经汇总成**PDF电子版**,**PDF电子版**在我的[**学习圈**](zsxq/introduce.md)可以获取~ + +## 学习路线 + +![](http://img.topjavaer.cn/img/20220530232715.png) + +> 微信搜索【程序员大彬】,回复【学习路线】获取**高清图** + +## 交流群 + +学习路上,难免遇到很多坑,为方便大家交流求职和技术问题,我建了**求职&技术交流群**,在群里可以讨论技术、面试相关问题,也可以获得阿里、字节等大厂的内推机会! + +交流群学习氛围很浓厚,截个图给大家感受下。 + + + +![](http://img.topjavaer.cn/img/交流群2.png) + + + +感兴趣的小伙伴可以扫描下方的二维码**加我微信**,**备注加群**,我拉你进群,一起学习成长! + +![](http://img.topjavaer.cn/img/微信加群.png) + +## 参与贡献 + +1. 如果您对本项目有任何建议或发现文中内容有误的,欢迎提交 issues 进行指正。 +2. 对于文中我没有涉及到知识点,欢迎提交 PR。 +3. 如果您有文章推荐请以 markdown 格式到邮箱 `1713476357@qq.com`, +[中文技术文档的写作规范指南](https://github.com/ruanyf/document-style-guide)。 diff --git a/docs/about/contact.md b/docs/about/contact.md new file mode 100644 index 0000000..680a8ad --- /dev/null +++ b/docs/about/contact.md @@ -0,0 +1,28 @@ +--- +sidebar: heading +--- + +## 联系我 + +如果有什么疑问或者建议,欢迎添加大彬微信进行交流~ + +
+

+
+ + +## 交流群 + +学习路上,难免遇到很多坑,为方便大家交流求职和技术问题,我建了**求职&技术交流群**,在群里可以讨论技术、面试相关问题,也可以获得阿里、字节等大厂的内推机会! + +交流群学习氛围很浓厚,截个图给大家感受下。 + + + +![](http://img.topjavaer.cn/img/交流群2.png) + + + +感兴趣的小伙伴可以扫描下方的二维码**加我微信**,**备注加群**,我拉你进群,一起学习成长! + +![](http://img.topjavaer.cn/img/202306241420398.png) diff --git a/docs/about/introduce.md b/docs/about/introduce.md new file mode 100644 index 0000000..7067e6e --- /dev/null +++ b/docs/about/introduce.md @@ -0,0 +1,66 @@ +--- +sidebar: heading + +--- + +大家好,我是大彬~ + +我是非科班出身,本科学的不是计算机,大四开始自学Java,并且找到了中大厂的offer。自学路上遇到不少问题,每天晚上都是坚持到一两点才睡觉,**最终也拿到了30w的offer**。 + +![](http://img.topjavaer.cn/img/image-20211206000941636.png) + +下面来说说自己的经历吧(附自学路线)。 + +## 接触编程 + +大学以前基本没碰过电脑,家里没电脑,也没去过网吧。高中的计算机课程,期末作业要完成一个自我介绍的PPT,也不会做,最后直接抄同桌的作业(复制粘贴都不会。。还得同桌教,捂脸)。 + +高考完一个月后,买了电脑,真正开始使用上了电脑。 + +大一上学期的时候,系里开了一门C语言的课程,这也是我第一次接触编程。教材是英文的,刚开始学还是挺头大的。每次课程作业,周围的同学都是一顿复制粘贴,我也一样嘿嘿。 + +记得在讲指针那一章的时候,听的一头雾水。稍微走神,回过头来,已经不知道讲的是啥了。 + +后面系里开设了兴趣小组,因为平时比较闲,也想着去捣鼓点东西,就去参加了。刚开始的时候,什么都不懂,老师推荐我学一下51单片机,拿了一本厚厚的51单片机的书籍,跟着书里的demo敲了一遍,发现了新天地!原来编程这么有意思! + +![](https://pic2.zhimg.com/80/v2-2ac0759cc48ed0d17b9ce46a13bb0f1e_720w.jpg) + +记得第一次跑出流水灯的时候,那叫一个激动啊,满满的都是成就感!后面也写了一些电机、红外遥控等demo。从那以后,激发了我学习编程的兴趣。 + +到了大二,辅导员在群里发布全国电子设计大赛的信息,参赛题跟四轴飞行器相关,那段时间对四轴飞行器比较感兴趣,于是约了两个小伙伴一块参加。距离比赛时间只有一个月,在那一个月的时间里,每天都是早出晚归,吃饭的时候还在想着哪一块代码出了bug。虽然最后没能获奖,但是在这个过程中,学到很多知识,编程能力也有了很大的提升。 + +## 决定转码 + +转眼间,大三开学,开始纠结考研还是工作,思考了一周时间,也进了系里的实验室体验了一把研究生生活,最后还是听从内心的想法,决定直接找工作。 + +我咨询了本专业的师兄师姐们往年的就业情况,他们大部分人还是找了互联网方向的工作。有一个在传统行业的师兄,也劝我投互联网公司的岗位,因为在传统行业加班也不少,但是工资贼低。。最后决定转行程序员,找后端相关的工作。 + +那么学习哪一种语言呢?当时有三个选择:c++,Java,python。 + +那段时间python比较火,但是经过一番深思熟虑之后,还是选择了Java。为什么选择Java呢? + +很简单,市场需求大,学习难度适中。相比科班同学来说,我缺乏系统的计算机基础知识,而距离秋招也只有不到一年时间,所以还是选择学习难度低一点的Java。 + +## 闭关自学 + +确定方向后,便开始制定学习路线。不得不说,Java要学的东西是真的多。。 + +自学期间遇到挺多问题,比如一些环境配置问题,有时候搞上好几天,很打击积极性。中途也有很多次怀疑自己的水平,是不是不适合干编程,差点就放弃了。幸好最后还是坚持了下来。 + +半年多的时间,除了平时上课,其他时间就是在图书馆。周末或者节假日,每天都是7点起床,八点到图书馆开始学习,到了晚上十点,图书馆闭馆,才回宿舍,每天都是图书馆最后走的一批。回到宿舍,洗完澡,继续肝到十二点多(卷王!)。 + +![img](https://pic3.zhimg.com/80/v2-25f6523ffc0ea6982c576b75458695dc_720w.jpg) + +> 很多人在问,大三才开始自学Java,来的及吗? 我觉得,还是看个人的投入程度和学习能力。有些人自学能力强一点,每天可以投入10小时及以上的时间去学习,那完全没问题。 + +自学过程还是挺辛苦的,要耐得住寂寞,最最重要的还是得坚持! + +我根据自己的自学经历,整理了一些学习过程中踩坑总结的经验,希望自学的小伙伴可以少走弯路: + +- **注重实践**,不要只是埋头看书,一定要多动手写代码。 +- 刚开始自学的时候,可以不用太深究细节,不然可能会怀疑自己的学习能力。等到后面有了一定的基础,回过头来重新回顾,可能会恍然大悟,没有当初想的那么难。 +- 可以适当加一些[交流群](https://topjavaer.cn/about/contact.html#%E4%BA%A4%E6%B5%81%E7%BE%A4),遇到不懂的知识点,多与其他人交流。 + +## 自学路线 + +具体学习路线[点这里](https://topjavaer.cn/learning-resources/java-learn-guide.html#%E8%87%AA%E5%AD%A6%E8%B7%AF%E7%BA%BF) \ No newline at end of file diff --git a/docs/advance/concurrent/1-current-limiting.md b/docs/advance/concurrent/1-current-limiting.md new file mode 100644 index 0000000..95d9136 --- /dev/null +++ b/docs/advance/concurrent/1-current-limiting.md @@ -0,0 +1,154 @@ +--- +sidebar: heading +title: 限流算法介绍 +category: 实践经验 +tag: + - 并发 +head: + - - meta + - name: keywords + content: 限流算法,令牌桶算法,漏桶算法,时间窗口算法,队列法 + - - meta + - name: description + content: Java常见面试题总结,让天下没有难背的八股文! +--- + +# 限流算法 + +大多数情况下,我们不需要自己实现一个限流系统,但限流在实际应用中是一个非常微妙、有很多细节的系统保护手段,尤其是在高流量时,了解你所使用的限流系统的限流算法,将能很好地帮助你充分利用该限流系统达到自己的商业需求和目的,并规避一些使用限流系统可能带来的大大小小的问题。 + +## 令牌桶算法 + +令牌桶(token bucket)算法,指的是设计一个容器(即“桶”),由某个组件持续运行往该容器中添加令牌(token),令牌可以是简单的数字、字符或组合,也可以仅仅是一个计数,然后每个请求进入系统时,需要从桶中领取一个令牌,所有请求都必须有令牌才能进入后端系统。当令牌桶空时,拒绝请求;当令牌桶满时,不再往其中添加新的令牌。 +令牌桶算法的架构如图1所示: + +![](http://img.topjavaer.cn/img/限流1.png) + +令牌桶算法的实现逻辑如下: + +首先会有一个定义的时间窗口的访问次数阈值,例如每天1000人,每秒5个请求之类,限流系统一般最小粒度是秒,再小就会因为实现和性能的原因而变得不准确或不稳定,假设是T秒内允许N个请求,那么令牌桶算法则会使令牌添加组件每T秒往令牌桶中添加N个令牌。 + +其次,令牌桶需要有一个最大值M,当令牌添加组件检测到令牌桶中已经有M个令牌时,剩余的令牌会被丢弃。反映到限流系统中,可以认为是当前系统允许的瞬时最大流量,但不是持续最大流量。例如令牌桶中的令牌最大数量是100个,每秒钟会往其中添加10个新令牌,当令牌满的时候,突然出现100 TPS的流量,这时候是可以承受的,但是假如连续两秒的100 TPS流量就不行,因为令牌添加速度是一秒10个,添加速度跟不上使用速度。 + +因此,凡是使用令牌桶算法的限流系统,我们都会注意到它在配置时要求两个参数: + +- 平均阈值(rate或average) +- 高峰阈值(burst或peak) + + +通过笔者的介绍,读者应该意识到,令牌桶算法的高峰阈值是有特指的,并不是指当前限流系统允许的最高流量。因为这一描述可能会使人认为只要低于该阈值的流量都可以,但事实上不是这样,因为只要高于添加速度的流量持续一段时间都会出现问题。 + +反过来说,令牌桶算法的限流系统不容易计算出它支持的最高流量,因为它能实时支持的最高流量取决于那整个时间段内的流量变化情况即令牌存量,而不是仅仅取决于一个瞬时的令牌量。 + +最后,当有组件请求令牌的时候,令牌桶会随机挑选一个令牌分发出去,然后将该令牌从桶中移除。注意,此时令牌桶不再做别的操作,令牌桶永远不会主动要求令牌添加组件补充新的令牌。 + +令牌桶算法有一个同一思想、方向相反的变种,被称为漏桶(leaky bucket)算法,它是令牌桶的一种改进,在商业应用中非常广泛。 + +漏桶算法的基本思想,是将请求看作水流,用一个底下有洞的桶盛装,底下的洞漏出水的速率是恒定的,所有请求进入系统的时候都会先进入这个桶,并慢慢由桶流出交给后台服务。桶有一个固定大小,当水流量超过这个大小的时候,多余的请求都会被丢弃。 + +漏桶算法的架构如图所示: + +![](http://img.topjavaer.cn/img/限流2.png) + +漏桶算法的实现逻辑如下: + +- 首先会有一个容器存放请求,该容器有一个固定大小M,所有请求都会被先存放到该容器中。 +- 该容器会有一个转发逻辑,该转发以每T秒N个请求的速率循环发生。 +- 当容器中请求数已经达到M个时,拒绝所有新的请求。 + + +因此同样地,漏桶算法的配置也需要两个值:平均值(rate)和峰值(burst)。只是平均值这时候是用来表示漏出的请求数量,峰值则是表示桶中可以存放的请求数量。 + +注意:漏桶算法和缓冲的限流思想不是一回事! + +同样是将请求放在一个容器中,漏桶算法和缓冲不是一个用途,切不可搞混,它们的区别如下: + +- 漏桶算法中,存在桶中的请求会以恒定的速率被漏给后端业务服务器,而缓冲思想中,放在缓冲区域的请求只有等到后端服务器空闲下来了,才会被发出去。 +- 漏桶算法中,存在桶中的请求是原本就应该被系统处理的,是系统对外界宣称的预期,不应该被丢失,而缓冲思想中放在缓冲区域的请求仅仅是对意外状况的尽量优化,并没有任何强制要求这些请求可以被处理。 + + +漏桶算法和令牌桶算法在思想上非常接近,而实现方向恰好相反,它们有如下的相同和不同之处: + +- 令牌桶算法以固定速率补充可以转发的请求数量(令牌),而漏桶算法以固定速率转发请求; +- 令牌桶算法限制数量的是预算数,漏桶算法限制数量的是实际请求数; +- 令牌桶算法在有爆发式增长的流量时可以一定程度上接受,漏桶算法也是,但当流量爆发时,令牌桶算法会使业务服务器直接承担这种流量,而漏桶算法的业务服务器感受到的是一样的速率变化。 + + +因此,通过以上比较,我们会发现漏桶算法略优于令牌桶算法,因为漏桶算法对流量控制更平滑,而令牌桶算法在设置的数值范围内,会将流量波动忠实地转嫁到业务服务器头上。 + +漏桶算法在Nginx和分布式的限流系统例如Redis的限流功能中都有广泛应用,是目前业界最流行的算法之一。 + +## 时间窗口算法 + +时间窗口算法是比较简单、基础的限流算法,由于它比较粗略,不适合大型、流量波动大或者有更精细的流量控制需求的网站。 + +时间窗口算法根据确定时间窗口的方式,可以分为两种: + +- 固定时间窗口算法 +- 滑动时间窗口算法 + + +固定时间窗口算法最简单,相信如果让初次接触限流理念的读者去快速设计实现一个限流系统的话,也可以很快想到这种算法。这种算法即固定一个时间段内限定一个请求阈值,没有达到则让请求通过,达到数量阈值了就拒绝请求。步骤如下: + +- 先确定一个起始时间点,一般就是系统启动的时间。 +- 从起始时间点开始,根据我们的需求,设置一个最大值M,开始接受请求并从0开始为请求计数。 +- 在时间段T内,请求计数超过M时,拒绝所有剩下的请求。 +- 超过时间段T后,重置计数。 + + +固定时间窗口算法的思路固然简单,但是它的逻辑是有问题的,它不适合流量波动大和有精细控制流量需求的服务。让我们看以下例子: + +假设我们的时间段T是1秒,请求最大值是10,在第一秒内,请求数量分布是第500毫秒时有1个请求,第800毫秒时有9个请求,如图3所示: + +![](http://img.topjavaer.cn/img/限流3.png) + +这是对于第一秒而言,这个请求分布是合理的。 + +此时第二秒的第200毫秒(即两秒中的第1200毫秒)内,又来了10个请求,如图4所示: + +![](http://img.topjavaer.cn/img/限流4.png) + +单独看第二秒依然是合理的,但是两个时间段连在一起的时候,就出现了问题,如图5所示: + +![](http://img.topjavaer.cn/img/限流5.png) + +从500毫秒到1200毫秒,短短700毫秒的时间内后端服务器就接收了20个请求,这显然违背了一开始我们希望1秒最多10个的初衷。这种远远大于预期流量的流量加到后端服务器头上,是会造成不可预料的后果的。因此,人们改进了固定窗口的算法,将其改为检查任何一个时间段都不超过请求数量阈值的时间窗口算法:滑动时间窗口算法。 + +滑动时间窗口算法要求当请求进入系统时,回溯过去的时间段T,找到其中的请求数量,然后决定是否接受当前请求,因此,滑动时间窗口算法需要记录时间段T内请求到达的时间点,逻辑如图6所示: + +![](http://img.topjavaer.cn/img/限流6.png) + +解释如下: + +1、确定一个起始时间点,一般就是系统启动的时间,并记录该点为时间窗口的开始点。然后创建一个空的列表作为时间窗口内请求进入的时间戳记录。 + +2、当请求到来时,使用当前时间戳比较它是否在时间窗口起始点加上T时间段(从开始点到开始点+T就是时间窗口)内。 + +- 如果在,则查看当前时间窗口内记录的所有请求的数量: + - 如果超过,则拒绝请求。 + - 如果没有,则将该请求加入到时间戳记录中,并将请求交给后端业务服务器。 +- 如果不在,则查看时间戳记录,将时间戳最久远的记录删除,然后将时间窗口的开始点更新为第二久远的记录时间,然后回到步骤2,再次检查时间戳是否在时间窗口内。 + + +滑动时间窗口尽管有所改进,但依然不能很好应对某个时间段内突发大量请求,而令牌桶和漏桶算法就由于允许指定平均请求率和最大瞬时请求率,它比时间窗口算法控制更精确。 + +时间窗口算法可以通过多时间窗口来改进。例如,可以设置一个1秒10 TPS的时间窗口限流和一个500毫秒5 TPS的时间窗口限流,二者同时运行,如此就可以保证更精确的限流控制。 + +## 队列法 + +队列法与漏桶算法很类似,都是将请求放入到一个区域,然后业务服务器从中提取请求,但是队列法采用的是完全独立的外部系统,而不是依附于限流系统。队列法的架构如图7所示: + +![](http://img.topjavaer.cn/img/限流7.png) + +与漏桶算法相比,队列法的优势如下: + +- 由业务逻辑层决定请求收取的速度。限流系统即队列不需要再关注流量的设置(例如T是多少,N是多少,M又是多少等等),只需要专注保留发送的请求,而业务服务器由于完全掌控消息的拉取,可以根据自身条件决定请求获取的速度,更加自由。 +- 完全将业务逻辑层保护起来,并且可以增加服务去消费这些请求。这一手段将业务服务器完全隐藏在了客户端后面,由队列去承担所有流量,也可以更好地保护自身不受到恶意流量的攻击。 +- 队列可以使用更健壮、更成熟的服务,这些服务比限流系统复杂,但能够承受大得多的流量。例如,业务服务器使用的是像阿里云或者AWS这样的消息队列的话,业务服务器就不用担心扩容的问题了,只要请求对实时性的要求不高。业务服务器由于使用了云服务,队列一端的扩容不用担心,而由于消息是自由决定拉取频率和处理速度,自身的扩容压力也就不那么大了。 + + +但队列法最大的缺陷,就是服务器不能直接与客户端沟通,因此只适用于客户端令业务服务器执行任务且不要求响应的用例,所有客户端需要有实质响应的服务都不能使用。例如,业务服务器提供的服务是消息发送服务,那么这种模式就可以的,但如果客户端是请求某些用户信息,那这种方式就完全不可行了。 + + + +> 本文摘录自《深入浅出大型网站架构设计》 diff --git a/docs/advance/concurrent/2-load-balance.md b/docs/advance/concurrent/2-load-balance.md new file mode 100644 index 0000000..6e94c80 --- /dev/null +++ b/docs/advance/concurrent/2-load-balance.md @@ -0,0 +1,179 @@ +--- +sidebar: heading +title: 高可用——负载均衡 +category: 实践经验 +tag: + - 并发 +head: + - - meta + - name: keywords + content: 高可用,负载均衡 + - - meta + - name: description + content: Java常见面试题总结,让天下没有难背的八股文! +--- + +# 高可用——负载均衡 + +## **一、 什么是负载均衡?** + +**什么是负载均衡?** + +记得第一次接触 Nginx 是在实验室,那时候在服务器部署网站需要用 Nginx 。Nginx 是一个服务组件,用来反向代理、负载平衡和 HTTP 缓存等。那么这里的 负载均衡 是什么? + +负载均衡(LB,Load Balance),是一种技术解决方案。用来在多个资源(一般是服务器)中分配负载,达到最优化资源使用,避免过载。 + + + +![](http://img.topjavaer.cn/img/负载均衡1.jpg) + + + +资源,相当于每个服务实例的执行操作单元,负载均衡就是将大量的数据处理操作分摊到多个操作单元进行执行,用来解决互联网分布式系统的大流量、高并发和高可用的问题。那什么是高可用呢? + +## **二、什么是高可用?** + +**首先了解什么是高可用?** + +这是 CAP 定理是分布式系统的基础,也是分布式系统的 3 个指标: + +1. Consistency(一致性) +2. Availability(可用性) +3. Partition tolerance(分区容错性) + +那高可用(High Availability)是什么?高可用,简称 HA,是系统一种特征或者指标,通常是指,提供一定性能上的服务运行时间,高于平均正常时间段。反之,消除系统服务不可用的时间。 + +衡量系统是否满足高可用,就是当一台或者多台服务器宕机的时候,系统整体和服务依然正常可用。 + +举个例子,一些知名的网站保证 4 个 9 以上的可用性,也就是可用性超过 99.99%。那 0.01% 就是所谓故障时间的百分比。比如电商网站有赞,服务不可用会造成商家损失金钱和用户。那么在提高可用性基础上同时,对系统宕机和服务不可用会有补偿。 + + + +![](http://img.topjavaer.cn/img/负载均衡2.jpg) + + + +比如下单服务,可以使用带有负载均衡的多个下单服务实例,代替单一的下单服务实例,即使用冗余的方式来提高可靠性。 + +总而言之,负载均衡(Load Balance)是分布式系统架构设计中必须考虑的因素之一。一般通过负载均衡,冗余同一个服务实例的方式,解决分布式系统的大流量、高并发和高可用的问题。负载均衡核心关键:在于是否分配均匀。 + +## **三、常见的负载均衡案例** + + + +![](http://img.topjavaer.cn/img/负载均衡3.jpg) + + + +场景1:微服务架构中,网关路由到具体的服务实例 hello: + +- 两个相同的服务实例 hello service ,一个端口 8000 ,另一个端口 8082 +- 通过 Kong 的负载均衡 LB 功能,让请求均匀的分发到两个 hello 服务实例 +- Kong 的负载均衡策略算法很多:默认 weighted-round-robin 算法,还有 consumer: consumer id 作为 hash 算法输入值等 + + + +![](http://img.topjavaer.cn/img/负载均衡4.jpg) + + + +场景2:微服务架构中,A 服务调用 B 服务的集群。通过了 Ribbon 客户端负载均衡组件: + +- 负载均衡策略算法并不高级,最简单的是随机选择和轮循 + +## **四、互联网分布式系统解决方案** + + + +![](http://img.topjavaer.cn/img/负载均衡5.jpg) + + + +常见的互联网分布式系统架构分为几层,一般如下: + +- 客户端层:比如用户浏览器、APP 端 +- 反向代理层:技术选型 Nignx 或者 F5 等 +- Web 层:前后端分离场景下, Web 端可以用 NodeJS 、 RN 、Vue +- 业务服务层:用 Java 、Go,一般互联网公司,技术方案选型就是 SC 或者 Spring Boot + Dubbo 服务化 +- 数据存储层:DB 选型 MySQL ,Cache 选型 Redis ,搜索选型 ES 等 + +一个请求从第 1 层到第 4 层,层层访问都需要负载均衡。即每个上游调用下游多个业务方的时候,需要均匀调用。这样整体系统来看,就比较负载均衡 + +**第 1 层:客户端层 -> 反向代理层 的负载均衡** + +客户端层 -> 反向代理层的负载均衡如何实现呢? + +答案是:DNS 的轮询。 DNS 可以通过 A (Address,返回域名指向的 IP 地址)设置多个 IP 地址。比如这里访问 [http://bysocket.com](https://link.zhihu.com/?target=http%3A//bysocket.com) 的 DNS 配置了 ip1 和 ip2 。为了反向代理层的高可用,至少会有两条 A 记录。这样冗余的两个 ip 对应的 nginx 服务实例,防止单点故障。 + +每次请求 [http://bysocket.com](https://link.zhihu.com/?target=http%3A//bysocket.com) 域名的时候,通过 DNS 轮询,返回对应的 ip 地址,每个 ip 对应的反向代理层的服务实例,也就是 nginx 的外网ip。这样可以做到每一个反向代理层实例得到的请求分配是均衡的。 + +**第 2 层:反向代理层 -> Web 层 的负载均衡** + +反向代理层 -> Web 层 的负载均衡如何实现呢? + +是通过反向代理层的负载均衡模块处理。比如 nginx 有多种均衡方法: + +1. 请求轮询。请求按时间顺序,逐一分配到 web 层服务,然后周而复始。如果 web 层服务 down 掉,自动剔除 + +```text +upstream web-server { +server ip3; +server ip4; +} +``` + +ip 哈希。按照 ip 的哈希值,确定路由到对应的 web 层。只要是用户的 ip 是均匀的,那么请求到 Web 层也是均匀的。 + +1. 还有个好处就是同一个 ip 的请求会分发到相同的 web 层服务。这样每个用户固定访问一个 web 层服务,可以解决 session 的问题。 + +```text +upstream web-server { +ip_hash; +server ip3; +server ip4; +} +``` + +1. weight 权重 、 fair、url_hash 等 + +**第 3 层:Web 层 -> 业务服务层 的负载均衡** + +Web 层 -> 业务服务层 的负载均衡如何实现呢? + +比如 Dubbo 是一个服务治理方案,包括服务注册、服务降级、访问控制、动态配置路由规则、权重调节、负载均衡。其中一个特性就是智能负载均衡:内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。 + +为了避免避免单点故障和支持服务的横向扩容,一个服务通常会部署多个实例,即 Dubbo 集群部署。会将多个服务实例成为一个服务提供方,然后根据配置的随机负载均衡策略,在20个 Provider 中随机选择了一个来调用,假设随机到了第7个 Provider。LoadBalance 组件从提供者地址列表中,使用均衡策略,选择选一个提供者进行调用,如果调用失败,再选另一台调用。 + +Dubbo内置了4种负载均衡策略: + +- RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。 +- RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。 +- LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 +- ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。 + +同样,因为业务的需要,也可以实现自己的负载均衡策略 + +**第 4 层:业务服务层 -> 数据存储层 的负载均衡** + +数据存储层的负载均衡,一般通过 DBProxy 实现。比如 MySQL 分库分表。 + +当单库或者单表访问太大,数据量太大的情况下,需要进行垂直拆分和水平拆分两个维度。比如水平切分规则: + +- Range 、 时间 +- hash 取模,订单根据店铺ID 等 + +但伴随着这块的负载会出现下面的问题,需要解决: + +- 分布式事务 +- 跨库 join 等 + +现状分库分表的产品方案很多:当当 sharding-jdbc、阿里的 Cobar 等 + +## **五、小结** + +对外看来,负载均衡是一个系统或软件的整体。对内看来,层层上下游调用。只要存在调用,就需要考虑负载均衡这个因素。所以负载均衡(Load Balance)是分布式系统架构设计中必须考虑的因素之一。考虑主要是如何让下游接收到的请求是均匀分布的: + +- 第 1 层:客户端层 -> 反向代理层 的负载均衡。通过 DNS 轮询 +- 第 2 层:反向代理层 -> Web 层 的负载均衡。通过 Nginx 的负载均衡模块 +- 第 3 层:Web 层 -> 业务服务层 的负载均衡。通过服务治理框架的负载均衡模块 +- 第 4 层:业务服务层 -> 数据存储层 的负载均衡。通过数据的水平分布,数据均匀了,理论上请求也会均匀。比如通过买家ID分片类似 diff --git a/docs/advance/concurrent/README.md b/docs/advance/concurrent/README.md new file mode 100644 index 0000000..b495d0c --- /dev/null +++ b/docs/advance/concurrent/README.md @@ -0,0 +1,4 @@ +## 高并发专题(更新中) + +- [限流算法](./1-current-limiting.md) +- [负载均衡](./2-load-balance.md) diff --git a/docs/advance/design-pattern-all.md b/docs/advance/design-pattern-all.md new file mode 100644 index 0000000..c440514 --- /dev/null +++ b/docs/advance/design-pattern-all.md @@ -0,0 +1,819 @@ +## 设计模式的六大原则 + +- 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口。 +- 里氏替换原则:基类可以被子类替换,使用抽象类继承,不使用具体类继承。 +- 依赖倒转原则:要依赖于抽象,不要依赖于具体,针对接口编程,不针对实现编程。 +- 接口隔离原则:使用多个隔离的接口,比使用单个接口好,建立最小的接口。 +- 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系。 +- 合成复用原则:尽量使用合成/聚合,而不是使用继承。 + +## 单例模式 + +需要对实例字段使用线程安全的延迟初始化,使用双重检查锁定的方案;需要对静态字段使用线程安全的延迟初始化,使用静态内部类的方案。 + +### 饿汉模式 + +JVM在类的初始化阶段,会执行类的静态方法。在执行类的初始化期间,JVM会去获取Class对象的锁。这个锁可以同步多个线程对同一个类的初始化。 + +饿汉模式只在类加载的时候创建一次实例,没有多线程同步的问题。单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。 + +``` +public class Singleton { + private static Singleton instance = new Singleton(); + private Singleton() {} + public static Singleton newInstance() { + return instance; + } +} +``` +### 双重检查锁定 + +双重校验锁先判断 instance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。 + +instance使用static修饰的原因:getInstance为静态方法,因为静态方法的内部不能直接使用非静态变量,只有静态成员才能在没有创建对象时进行初始化,所以返回的这个实例必须是静态的。 + +``` +public class Singleton { + private static volatile Singleton instance = null; //volatile + private Singleton(){} + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` +为什么两次判断`instance == null`: + +| Time | Thread A | Thread B | +| ---- | -------------------- | -------------------- | +| T1 | 检查到`instance`为空 | | +| T2 | | 检查到`instance`为空 | +| T3 | | 初始化对象`A` | +| T4 | | 返回对象`A` | +| T5 | 初始化对象`B` | | +| T6 | 返回对象`B` | | + +`new Singleton()`会执行三个动作:分配内存空间、初始化对象和对象引用指向内存地址。 + +```java +memory = allocate();  // 1:分配对象的内存空间 +ctorInstance(memory);  // 2:初始化对象 +instance = memory;   // 3:设置instance指向刚分配的内存地址 +``` + +由于指令重排优化的存在,导致初始化对象和将对象引用指向内存地址的顺序是不确定的。在某个线程创建单例对象时,会为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的是未初始化的对象,程序就会出错。volatile 可以禁止指令重排序,保证了先初始化对象再赋值给instance变量。 + +| Time | Thread A | Thread B | +| :--- | :----------------------- | :--------------------------------------- | +| T1 | 检查到`instance`为空 | | +| T2 | 获取锁 | | +| T3 | 再次检查到`instance`为空 | | +| T4 | 为`instance`分配内存空间 | | +| T5 | 将`instance`指向内存空间 | | +| T6 | | 检查到`instance`不为空 | +| T7 | | 访问`instance`(此时对象还未完成初始化) | +| T8 | 初始化`instance` | | + +### 静态内部类 + +它与饿汉模式一样,也是利用了类初始化机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。 + +![](http://img.topjavaer.cn/img/singleton-class-init.png) + +基于类初始化的方案的实现代码更简洁。 + +``` +public class Instance { + private static class InstanceHolder { + public static Instance instance = new Instance(); + } + private Instance() {} + public static Instance getInstance() { + return InstanceHolder.instance ;  // 这里将导致InstanceHolder类被初始化 + } +} +``` +但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。 + +## 工厂模式 + +工厂模式是用来封装对象的创建。 + +### 简单工厂模式 + +把实例化的操作单独放到一个类中,这个类就成为简单工厂类,让简单工厂类来决定应该用哪个具体子类来实例化,这样做能把客户类和具体子类的实现解耦,客户类不再需要知道有哪些子类以及应当实例化哪个子类。 + +```java + public class ShapeFactory { + public static final String TAG = "ShapeFactory"; + public static Shape getShape(String type) { + Shape shape = null; + if (type.equalsIgnoreCase("circle")) { + shape = new CircleShape(); + } else if (type.equalsIgnoreCase("rect")) { + shape = new RectShape(); + } else if (type.equalsIgnoreCase("triangle")) { + shape = new TriangleShape(); + } + return shape; + } + } +``` + +优点:只需要一个工厂创建对象,代码量少。 + +缺点:系统扩展困难,新增产品需要修改工厂逻辑,当产品较多时,会造成工厂逻辑过于复杂,不利于系统扩展和维护。 + +### 工厂方法模式 + +针对不同的对象提供不同的工厂。每个对象都有一个与之对应的工厂。 + +```java +public interface Reader { + void read(); +} + +public class JpgReader implements Reader { + @Override + public void read() { + System.out.print("read jpg"); + } +} + +public class PngReader implements Reader { + @Override + public void read() { + System.out.print("read png"); + } +} + +public interface ReaderFactory { + Reader getReader(); +} + +public class JpgReaderFactory implements ReaderFactory { + @Override + public Reader getReader() { + return new JpgReader(); + } +} + +public class PngReaderFactory implements ReaderFactory { + @Override + public Reader getReader() { + return new PngReader(); + } +} +``` + +客户端通过子类来指定创建对应的对象。 + +```java +ReaderFactory factory=new JpgReaderFactory(); +Reader reader=factory.getReader(); +reader.read(); +``` + +优点:增加新的产品类时无须修改现有系统,只需增加新产品和对应的工厂类即可。 + +### 抽象工厂模式 + +抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 + +多了一层抽象,减少了工厂的数量(HpMouseFactory和HpKeyboFactory合并为HpFactory)。 + +![](http://img.topjavaer.cn/img/抽象工厂.png) + + + +## 模板模式 + +模板模式:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。 这种类型的设计模式属于行为型模式。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 + +**模板模式**主要由抽象模板(Abstract Template)角色和具体模板(Concrete Template)角色组成。 + +- 抽象模板(Abstract Template): 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤;定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。 +- 具体模板(Concrete Template): 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤;每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。 + +示例图如下: + +![](http://img.topjavaer.cn/img/模板方法.jpg) + +以游戏为例。创建一个抽象类,它的模板方法被设置为 final,这样它就不会被重写。 + +```java +public abstract class Game { + abstract void initialize(); + abstract void startPlay(); + abstract void endPlay(); + + //模板 + public final void play(){ + //初始化游戏 + initialize(); + + //开始游戏 + startPlay(); + + //结束游戏 + endPlay(); + } +} +``` + +Football类: + +```java +public class Football extends Game { + + @Override + void endPlay() { + System.out.println("Football Game Finished!"); + } + + @Override + void initialize() { + System.out.println("Football Game Initialized! Start playing."); + } + + @Override + void startPlay() { + System.out.println("Football Game Started. Enjoy the game!"); + } +} +``` + +使用Game的模板方法 play() 来演示游戏的定义方式。 + +```java +public class TemplatePatternDemo { + public static void main(String[] args) { + + Game game = new Cricket(); + game.play(); + System.out.println(); + game = new Football(); + game.play(); + } +} +``` + +**模板模式优点** : + +1. 封装不变部分,扩展可变部分。 +2. 提取公共代码,便于维护。 +3. 行为由父类控制,子类实现。 + +**模板模式缺点**: + +- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。 + +## 策略模式 + +策略模式(Strategy Pattern)属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。 + +其主要目的是通过定义相似的算法,替换if else 语句写法,并且可以随时相互替换。 + +**策略模式**主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。 + +- 环境角色(Context):持有一个策略类的引用,提供给客户端使用。 +- 抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。 +- 具体策略角色(ConcreteStrategy):包装了相关的算法或行为。 + +示例图如下: + +![](http://img.topjavaer.cn/img/策略模式.png) + +以计算器为例,如果我们想得到两个数字相加的和,我们需要用到“+”符号,得到相减的差,需要用到“-”符号等等。虽然我们可以通过字符串比较使用if/else写成通用方法,但是计算的符号每次增加,我们就不得不加在原先的方法中进行增加相应的代码,如果后续计算方法增加、修改或删除,那么会使后续的维护变得困难。 + +但是在这些方法中,我们发现其基本方法是固定的,这时我们就可以通过策略模式来进行开发,可以有效避免通过if/else来进行判断,即使后续增加其他的计算规则也可灵活进行调整。 + +首先定义一个抽象策略角色,并拥有一个计算的方法。 + +```java +interface CalculateStrategy { + int doOperation(int num1, int num2); +} +``` + +然后再定义加减乘除这些具体策略角色并实现方法。代码如下: + +```java +class OperationAdd implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 + num2; + } +} + +class OperationSub implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 - num2; + } +} + +class OperationMul implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 * num2; + } +} + +class OperationDiv implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 / num2; + } +} +``` + +最后在定义一个环境角色,提供一个计算的接口供客户端使用。代码如下: + +```java +class CalculatorContext { + private CalculateStrategy strategy; + + public CalculatorContext(CalculateStrategy strategy) { + this.strategy = strategy; + } + + public int executeStrategy(int num1, int num2) { + return strategy.doOperation(num1, num2); + } +} +``` + +测试代码如下: + +```java +public static void main(String[] args) { + int a=4,b=2; + CalculatorContext context = new CalculatorContext(new OperationAdd()); + System.out.println("a + b = "+context.executeStrategy(a, b)); + + CalculatorContext context2 = new CalculatorContext(new OperationSub()); + System.out.println("a - b = "+context2.executeStrategy(a, b)); + + CalculatorContext context3 = new CalculatorContext(new OperationMul()); + System.out.println("a * b = "+context3.executeStrategy(a, b)); + + CalculatorContext context4 = new CalculatorContext(new OperationDiv()); + System.out.println("a / b = "+context4.executeStrategy(a, b)); +} +``` + +**策略模式优点:** + +- 扩展性好,可以在不修改对象结构的情况下,为新的算法进行添加新的类进行实现; +- 灵活性好,可以对算法进行自由切换; + +**策略模式缺点:** + +- 使用策略类变多,会增加系统的复杂度。; +- 客户端必须知道所有的策略类才能进行调用; + +**使用场景:** + +- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为; + 一个系统需要动态地在几种算法中选择一种; +- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现; + +## 责任链模式 + +为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。 + +在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。 + +**责任链模式是一种对象行为型模式,其主要优点如下。** + +1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。 +2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。 +3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。 +4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。 +5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。 + +代码实现如下: + +请假条类 + +```java +public class LeaveRequest { + //姓名 + private String name; + // 请假天数 + private int num; + // 请假内容 + private String content; + + public LeaveRequest(String name, int num, String content) { + this.name = name; + this.num = num; + this.content = content; + } + + public String getName() { + return name; + } + + public int getNum() { + return num; + } + + public String getContent() { + return content; + } + +} +``` + +处理类 + +```java +public abstract class Handler { + protected final static int NUM_ONE = 1; + protected final static int NUM_THREE = 3; + protected final static int NUM_SEVEN = 7; + + //该领导处理的请假天数区间 + private int numStart; + private int numEnd; + + + //领导上还有领导 + private Handler nextHandler; + + //设置请假天数范围 + public Handler(int numStart) { + this.numStart = numStart; + } + + //设置请假天数范围 + public Handler(int numStart, int numEnd) { + this.numStart = numStart; + this.numEnd = numEnd; + } + + //设置上级领导 + public void setNextHandler(Handler nextHandler) { + this.nextHandler = nextHandler; + } + + //提交请假条 + public final void submit(LeaveRequest leaveRequest) { + if (this.numStart == 0) { + return; + } + //请假天数达到领导处理要求 + if (leaveRequest.getNum() >= this.numStart) { + this.handleLeave(leaveRequest); + + //如果还有上级 并且请假天数超过当前领导的处理范围 + if (this.nextHandler != null && leaveRequest.getNum() > numEnd) { + //继续提交 + this.nextHandler.submit(leaveRequest); + } else { + System.out.println("流程结束!!!"); + } + } + } + + //各级领导处理请假条方法 + protected abstract void handleLeave(LeaveRequest leave); + +} +``` + +小组长类 + +```java +public class GroupLeader extends Handler { + //1-3天的假 + public GroupLeader() { + super(Handler.NUM_ONE, Handler.NUM_THREE); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("小组长审批通过:同意!"); + } +} +``` + +部门经理类 + +```java +public class Manager extends Handler { + //3-7天的假 + public Manager() { + super(Handler.NUM_THREE, Handler.NUM_SEVEN); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("部门经理审批通过:同意!"); + } +} +``` + +总经理类 + +```java +public class GeneralManager extends Handler{ + //7天以上的假 + public GeneralManager() { + super(Handler.NUM_THREE, Handler.NUM_SEVEN); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("总经理审批通过:同意!"); + } +} +``` + +测试类 + +```java +public class Client { + public static void main(String[] args) { + //请假条 + LeaveRequest leave = new LeaveRequest("小庄", 3, "出去旅游"); + + //各位领导 + Manager manager = new Manager(); + GroupLeader groupLeader = new GroupLeader(); + GeneralManager generalManager = new GeneralManager(); + + /* + * 小组长上司是经理 经理上司是总经理 + */ + groupLeader.setNextHandler(manager); + manager.setNextHandler(generalManager); + + //提交 + groupLeader.submit(leave); + + } +} +``` + +应用场景: + +1. 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。 +2. 可动态指定一组对象处理请求,或添加新的处理者。 +3. 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。 + +[参考链接](https://segmentfault.com/a/1190000040450513) + +## 迭代器模式 + +提供一种方法顺序访问一个聚合对象中的各个元素, 而又不暴露其内部的表示。 + +把在元素之间游走的责任交给迭代器,而不是聚合对象。 + +**应用实例:**JAVA 中的 iterator。 + +**优点:** 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。 + +**缺点:**由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 + +**使用场景:** 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。 + +**迭代器模式在JDK中的应用** + +ArrayList的遍历: + +```java +Iterator iter = null; + +System.out.println("ArrayList:"); +iter = arrayList.iterator(); +while (iter.hasNext()) { + System.out.print(iter.next() + "\t"); +} +``` + + + +## 装饰模式 + +装饰者模式(decorator pattern):动态地将责任附加到对象上, 若要扩展功能, 装饰者提供了比继承更有弹性的替代方案。 + +装饰模式以对客户端透明的方式拓展对象的功能,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不创造更多子类的情况下,将对象的功能加以扩展。 + +比如设置FileInputStream,先用BufferedInputStream装饰它,再用自己写的LowerCaseInputStream过滤器去装饰它。 + +``` +InputStream in = new LowerCaseInputStream( + new BufferedInputStream( + new FileInputStream("test.txt"))); +``` +在装饰模式中的角色有: + +- 抽象组件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。 +- 具体组件(ConcreteComponent)角色:定义一个将要接收附加责任的类。 +- 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。 +- 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。 + +## 适配器模式 +适配器模式将现成的对象通过适配变成我们需要的接口。 适配器让原本接口不兼容的类可以合作。 + +适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。 + +对象适配器模式通过组合对象进行适配。 + +![](http://img.topjavaer.cn/img/适配器对象适配,png) + +类适配器通过继承来完成适配。 + +![](http://img.topjavaer.cn/img/适配器-类继承,png) + +适配器模式的**优点**: + +1. 更好的复用性。系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。 +2. 更好的扩展性。在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。 + +## 观察者模式 + +定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。 + +主题(Subject)是被观察的对象,而其所有依赖者(Observer)称为观察者。 + +多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。其作用是让主题对象和观察者松耦合。 + +观察者模式所涉及的角色有: + +- 抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。 +- 具体主题(ConcreteSubject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。 +- 抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。 +- 具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。 + + + +## 代理模式 + +代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。 + +### 静态代理 + +静态代理:代理类在编译阶段生成,程序运行前就已经存在,在编译阶段将通知织入Java字节码中。 + +缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。同时,一旦接口增加方法,目标对象与代理对象都要维护。 + +### 动态代理 + +动态代理:代理类在程序运行时创建,在内存中临时生成一个代理对象,在运行期间对业务方法进行增强。 + +**JDK动态代理** + +JDK实现代理只需要使用newProxyInstance方法: + +```java +static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h ) +``` + +三个入参: + +- ClassLoader loader:指定当前目标对象使用的类加载器 +- Class[] interfaces:目标对象实现的接口的类型 +- InvocationHandler:当代理对象调用目标对象的方法时,会触发事件处理器的invoke方法() + +示例代码: + +``` +public class DynamicProxyDemo { + + public static void main(String[] args) { + //被代理的对象 + MySubject realSubject = new RealSubject(); + + //调用处理器 + MyInvacationHandler handler = new MyInvacationHandler(realSubject); + + MySubject subject = (MySubject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), + realSubject.getClass().getInterfaces(), handler); + + System.out.println(subject.getClass().getName()); + subject.rent(); + } +} + +interface MySubject { + public void rent(); +} +class RealSubject implements MySubject { + + @Override + public void rent() { + System.out.println("rent my house"); + } +} +class MyInvacationHandler implements InvocationHandler { + + private Object subject; + + public MyInvacationHandler(Object subject) { + this.subject = subject; + } + + @Override + public Object invoke(Object object, Method method, Object[] args) throws Throwable { + System.out.println("before renting house"); + //invoke方法会拦截代理对象的方法调用 + Object o = method.invoke(subject, args); + System.out.println("after rentint house"); + return o; + } +} +``` + +## 建造者模式 + +建造者模式:封装一个对象的构造过程,并允许按步骤构造。 + +有两种形式:传统建造者模式和传统建造者模式变种。 + +传统建造者模式: + +```java +public class Computer { + private final String cpu;//必须 + private final String ram;//必须 + private final int usbCount;//可选 + private final String keyboard;//可选 + private final String display;//可选 + + private Computer(Builder builder){ + this.cpu=builder.cpu; + this.ram=builder.ram; + this.usbCount=builder.usbCount; + this.keyboard=builder.keyboard; + this.display=builder.display; + } + public static class Builder{ + private String cpu;//必须 + private String ram;//必须 + private int usbCount;//可选 + private String keyboard;//可选 + private String display;//可选 + + public Builder(String cup,String ram){ + this.cpu=cup; + this.ram=ram; + } + public Builder setDisplay(String display) { + this.display = display; + return this; + } + //set... + public Computer build(){ + return new Computer(this); + } + } +} + +public class ComputerDirector { + public void makeComputer(ComputerBuilder builder){ + builder.setUsbCount(); + builder.setDisplay(); + builder.setKeyboard(); + } +} +``` + +传统建造者模式变种,链式调用: + +```java +public class LenovoComputerBuilder extends ComputerBuilder { + private Computer computer; + public LenovoComputerBuilder(String cpu, String ram) { + computer=new Computer(cpu,ram); + } + @Override + public void setUsbCount() { + computer.setUsbCount(4); + } + //... + @Override + public Computer getComputer() { + return computer; + } +} + +Computer computer=new Computer.Builder("因特尔","三星") + .setDisplay("三星24寸") + .setKeyboard("罗技") + .setUsbCount(2) + .build(); +``` + diff --git a/docs/advance/design-pattern/1-principle.md b/docs/advance/design-pattern/1-principle.md new file mode 100644 index 0000000..873ad1f --- /dev/null +++ b/docs/advance/design-pattern/1-principle.md @@ -0,0 +1,24 @@ +--- +sidebar: heading +title: 设计模式的六大原则 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 设计模式的六大原则,设计模式,设计模式面试题 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 设计模式的六大原则 + +- 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口。 +- 里氏替换原则:基类可以被子类替换,使用抽象类继承,不使用具体类继承。 +- 依赖倒转原则:要依赖于抽象,不要依赖于具体,针对接口编程,不针对实现编程。 +- 接口隔离原则:使用多个隔离的接口,比使用单个接口好,建立最小的接口。 +- 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系。 +- 合成复用原则:尽量使用合成/聚合,而不是使用继承。 + diff --git a/docs/advance/design-pattern/10-observer.md b/docs/advance/design-pattern/10-observer.md new file mode 100644 index 0000000..138cd8a --- /dev/null +++ b/docs/advance/design-pattern/10-observer.md @@ -0,0 +1,138 @@ +--- +sidebar: heading +title: 设计模式之观察者模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 观察者模式,设计模式,观察者 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 观察者模式 + +**观察者模式(Observer)**,又叫**发布-订阅模式(Publish/Subscribe)**,定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。UML结构图如下: + +![](http://img.topjavaer.cn/img/观察者1.png) + +其中,Subject类是主题,它把所有对观察者对象的引用文件存在了一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供了一个接口,可以增加和删除观察者对象;Observer类是抽象观察者,为所有的具体观察者定义一个接口,在得到主题的通知时更新自己;ConcreteSubject类是具体主题,将有关状态存入具体观察者对象,在具体主题内部状态改变时,给所有登记过的观察者发出通知;ConcreteObserver是具体观察者,实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协同。 + +### 主题Subject + +首先定义一个观察者数组,并实现增、删及通知操作。它的职责很简单,就是定义谁能观察,谁不能观察,用Vector是线程同步的,比较安全,也可以使用ArrayList,是线程异步的,但不安全。 + +```java +public class Subject { + + //观察者数组 + private Vector oVector = new Vector<>(); + + //增加一个观察者 + public void addObserver(Observer observer) { + this.oVector.add(observer); + } + + //删除一个观察者 + public void deleteObserver(Observer observer) { + this.oVector.remove(observer); + } + + //通知所有观察者 + public void notifyObserver() { + for(Observer observer : this.oVector) { + observer.update(); + } + } + +} +``` + +### 抽象观察者Observer + +观察者一般是一个接口,每一个实现该接口的实现类都是具体观察者。 + +```java +public interface Observer { + //更新 + public void update(); +} +``` + +### 具体主题 + +继承Subject类,在这里实现具体业务,在具体项目中,该类会有很多变种。 + +```java +public class ConcreteSubject extends Subject { + + //具体业务 + public void doSomething() { + //... + super.notifyObserver(); + } + +} +``` + +### 具体观察者 + +实现Observer接口。 + +```java +public class ConcreteObserver implements Observer { + + @Override + public void update() { + System.out.println("收到消息,进行处理"); + } + +} +``` + +### Client客户端 + +首先创建一个被观察者,然后定义一个观察者,将该被观察者添加到该观察者的观察者数组中,进行测试。 + +```java +public class Client { + + public static void main(String[] args) { + //创建一个主题 + ConcreteSubject subject = new ConcreteSubject(); + //定义一个观察者 + Observer observer = new ConcreteObserver(); + //观察 + subject.addObserver(observer); + //开始活动 + subject.doSomething(); + } + +} +``` + +运行结果如下: + +```java +收到消息,进行处理 +``` + +### 观察者模式的优点 + +- 观察者和被观察者是抽象耦合的 +- 建立了一套触发机制 + +### 观察者模式的缺点 + +- 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间 +- 如果观察者和观察目标间有循环依赖,可能导致系统崩溃 +- 没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的 + +### 观察者模式的使用场景 + +- 关联行为场景 +- 事件多级触发场景 +- 跨系统的消息变换场景,如消息队列的处理机制 diff --git a/docs/advance/design-pattern/11-proxy.md b/docs/advance/design-pattern/11-proxy.md new file mode 100644 index 0000000..8342c6c --- /dev/null +++ b/docs/advance/design-pattern/11-proxy.md @@ -0,0 +1,210 @@ +--- +sidebar: heading +title: 设计模式之代理模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 代理模式,设计模式,静态代理,动态代理 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 代理模式 + +代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。 + +代理模式一般有三种角色: + +- 抽象(Subject)角色,该角色是真实主题和代理主题的共同接口,以便在任何可以使用真实主题的地方都可以使用代理主题。 +- 代理(Proxy Subject)角色,也叫做委托类、代理类,该角色负责控制对真实主题的引用,负责在需要的时候创建或删除真实主题对象,并且在真实主题角色处理完毕前后做预处理和善后处理工作。 +- 真实(Real Subject)角色:该角色也叫做被委托角色、被代理角色,是业务逻辑的具体执行者。 + +## 静态代理 + +静态代理:代理类在编译阶段生成,程序运行前就已经存在,在编译阶段将通知织入Java字节码中。 + +以租房为例,我们一般用租房软件、找中介或者找房东。这里的中介就是代理者。 + +首先定义一个提供了租房方法的接口。 + +```java +public interface IRentHouse { + void rentHouse(); +} +``` + +定义租房的实现类 + +```java +public class RentHouse implements IRentHouse { + @Override + public void rentHouse() { + System.out.println("租了一间房子。。。"); + } +} +``` + +我要租房,房源都在中介手中,所以找中介 + +```java +public class IntermediaryProxy implements IRentHouse { + + private IRentHouse rentHouse; + + public IntermediaryProxy(IRentHouse irentHouse){ + rentHouse = irentHouse; + } + + @Override + public void rentHouse() { + System.out.println("交中介费"); + rentHouse.rentHouse(); + System.out.println("中介负责维修管理"); + } +} +``` + +这里中介也实现了租房的接口。 + +再main方法中测试 + +```java +public class Main { + + public static void main(String[] args){ + //定义租房 + IRentHouse rentHouse = new RentHouse(); + //定义中介 + IRentHouse intermediary = new IntermediaryProxy(rentHouse); + //中介租房 + intermediary.rentHouse(); + } +} +``` + +返回信息 + +```text +交中介费 +租了一间房子。。。 +中介负责维修管理 +``` + +这就是静态代理,因为中介这个代理类已经事先写好了,只负责代理租房业务。 + +缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。同时,一旦接口增加方法,目标对象与代理对象都要维护。 + +## 动态代理 + +动态代理:代理类在程序运行时创建,在内存中临时生成一个代理对象,在运行期间对业务方法进行增强。 + +**JDK动态代理** + +JDK实现代理只需要使用newProxyInstance方法: + +```java +static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h ) +``` + +三个入参: + +- ClassLoader loader:指定当前目标对象使用的类加载器 +- Class[] interfaces:目标对象实现的接口的类型 +- InvocationHandler:当代理对象调用目标对象的方法时,会触发事件处理器的invoke方法() + +还是以租房为例。 + +现在的中介不仅仅是有租房业务,同时还有卖房、家政、维修等得业务,只是我们就不能对每一个业务都增加一个代理,就要提供通用的代理方法,这就要通过动态代理来实现了。 + +中介的代理方法做了一下修改: + +```java +public class IntermediaryProxy implements InvocationHandler { + + + private Object obj; + + public IntermediaryProxy(Object object){ + obj = object; + } + + /** + * 调用被代理的方法 + * @param proxy + * @param method + * @param args + * @return + * @throws Throwable + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object result = method.invoke(this.obj, args); + return result; + } + +} +``` + +在这里实现InvocationHandler接口,此接口是JDK提供的动态代理接口,对被代理的方法提供代理。其中invoke方法是接口InvocationHandler定义必须实现的, 它完成对真实方法的调用。动态代理是根据被代理的接口生成所有的方法,也就是说给定一个接口,动态代理就会实现接口下所有的方法。通过 InvocationHandler接口, 所有方法都由该Handler来进行处理, 即所有被代理的方法都由 InvocationHandler接管实际的处理任务。 + +这里增加一个卖房的业务,代码和租房代码类似。 + +main方法测试: + +```java +public static void main(String[] args){ + + IRentHouse rentHouse = new RentHouse(); + //定义一个handler + InvocationHandler handler = new IntermediaryProxy(rentHouse); + //获得类的class loader + ClassLoader cl = rentHouse.getClass().getClassLoader(); + //动态产生一个代理者 + IRentHouse proxy = (IRentHouse) Proxy.newProxyInstance(cl, new Class[]{IRentHouse.class}, handler); + proxy.rentHouse(); + + ISellHouse sellHouse = new SellHouse(); + InvocationHandler handler1 = new IntermediaryProxy(sellHouse); + ClassLoader classLoader = sellHouse.getClass().getClassLoader(); + ISellHouse proxy1 = (ISellHouse) Proxy.newProxyInstance(classLoader, new Class[]{ISellHouse.class}, handler1); + proxy1.sellHouse(); + +} +``` + +在main方法中我们用到了Proxy这个类的方法, + +```java +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) +``` + +InvocationHandler 是一个接口,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。 + +```java +public interface InvocationHandler { + + public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +} +``` + +InvocationHandler 内部只是一个 invoke() 方法,正是这个方法决定了怎么样处理代理传递过来的方法调用。 + +因为,Proxy 动态产生的代理会调用 InvocationHandler 实现类,所以 InvocationHandler 是实际执行者。 + +## 总结 + +1. 静态代理,代理类需要自己编写代码写成。 +2. 动态代理,代理类通过 Proxy.newInstance() 方法生成。 +3. JDK实现的代理中不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。CGLib可以不需要接口。 +4. 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。 + + + +> 参考链接:https://zhuanlan.zhihu.com/p/72644638 diff --git a/docs/advance/design-pattern/12-builder.md b/docs/advance/design-pattern/12-builder.md new file mode 100644 index 0000000..1f894fe --- /dev/null +++ b/docs/advance/design-pattern/12-builder.md @@ -0,0 +1,335 @@ +--- +sidebar: heading +title: 设计模式之建造者模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 建造者模式,设计模式,建造者,生成器模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 建造者模式 + +Builder 模式中文叫作建造者模式,又叫生成器模式,它属于对象创建型模式,是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。 + +在建造者模式中,有如下4种角色: + +- Product:产品角色 +- Builder:抽象建造者,定义产品接口 +- ConcreteBuilder:具体建造者,实现Builder定义的接口,并且返回组装好的产品 +- Director:指挥者,属于这里面的老大,你需要生产什么产品都直接找它。 + +## 建造者模式应用举例 + +### 家装 + +家装不管是精装还是简装,它的流程都相对固定,所以它适用于建造者模式。我们就以家装为例,一起来学习了解一下建造者模式。下图是家装建造者模式简单的 UML 图。 + +### 1、家装对象类 + +```java +/** + * 家装对象类 + */ +public class House { + // 买家电 + private String jiadian; + + // 买地板 + private String diban; + // 买油漆 + private String youqi; + + public String getJiadian() { + return jiadian; + } + + public void setJiadian(String jiadian) { + this.jiadian = jiadian; + } + + public String getDiban() { + return diban; + } + + public void setDiban(String diban) { + this.diban = diban; + } + + public String getYouqi() { + return youqi; + } + + public void setYouqi(String youqi) { + this.youqi = youqi; + } + + @Override + public String toString() { + return "House{" + + "jiadian='" + jiadian + '\'' + + ", diban='" + diban + '\'' + + ", youqi='" + youqi + '\'' + + '}'; + } +} +``` + +### 2、抽象建造者 Builder 类 + +```java +/** + * 抽象建造者 + */ +public interface HouseBuilder { + // 买家电 + void doJiadian(); + // 买地板 + void doDiBan(); + // 买油漆 + void doYouqi(); + + House getHouse(); +} +``` + +### 3、具体建造者-简装建造者类 + +```java +/** + * 简装创建者 + */ +public class JianzhuangBuilder implements HouseBuilder { + + private House house = new House(); + + @Override + public void doJiadian() { + house.setJiadian("简单家电就好"); + } + + @Override + public void doDiBan() { + house.setDiban("普通地板"); + } + + @Override + public void doYouqi() { + house.setYouqi("污染较小的油漆就行"); + } + + @Override + public House getHouse() { + return house; + } +} +``` + +### 4、具体建造者-精装建造者类 + +```text +/** + * 精装创建者 + */ +public class jingzhuangBuilder implements HouseBuilder { + + private House house = new House(); + + @Override + public void doJiadian() { + house.setJiadian("二话不说,最好的"); + } + + @Override + public void doDiBan() { + house.setDiban("二话不说,实木地板"); + } + + @Override + public void doYouqi() { + house.setYouqi("二话不说,给我来0污染的"); + } + + @Override + public House getHouse() { + return house; + } +} +``` + +### 5、指挥官-家装公司类 + +```java +/** + * 家装公司,值需要告诉他精装还是简装 + */ +public class HouseDirector { + + public House builder(HouseBuilder houseBuilder){ + houseBuilder.doDiBan(); + houseBuilder.doJiadian(); + houseBuilder.doYouqi(); + return houseBuilder.getHouse(); + } +} +``` + +### 6、测试 + +```java +public class App { + public static void main(String[] args) { + house(); + } + + public static void house(){ + HouseDirector houseDirector = new HouseDirector(); + // 简装 + JianzhuangBuilder jianzhuangBuilder = new JianzhuangBuilder(); + System.out.println("我要简装"); + System.out.println(houseDirector.builder(jianzhuangBuilder)); + + // 精装 + jingzhuangBuilder jingzhuangBuilder = new jingzhuangBuilder(); + System.out.println("我要精装"); + System.out.println(houseDirector.builder(jingzhuangBuilder)); + + } +} +``` + +输出结果 + +```java +我要简装 +House{jiadian="简单家电就好",diban='普通地板 ,youqi= 污染较小的油漆就行' +我要精装 +House{jiadian='二话不说,最好的',diban='二话不说,实木地板,yougi='二话不说,给我来0污染的'} +``` + +我们以家装为例,实现了两个具体的建造者,一个简装建造者、一个精装建造者。我们只需要告诉家装公司,我是需要简装还是精装,他会去帮我们安排,我不需要知道里面具体的细节。 + +### 对象构建 + +在日常开发中,你是不是会经常看到下面这种代码: + +```java +return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.curry.springbootswagger.controller")) + .paths(PathSelectors.any()) + .build(); +``` + +是不是很优美?学会了 Builder 模式之后,你也可以通过这种方式进行对象构建。它是通过变种的 Builder 模式实现的。先不解释了,我们先用 Builder 模式来实现跟上述的对象构建,使用学生类为例。 + +学生对象代码: + +```java +public class Student { + + private String name; + + private int age; + + private int num; + + private String email; + + // 提供一个静态builder方法 + public static Student.Builder builder() { + return new Student.Builder(); + } + // 外部调用builder类的属性接口进行设值。 + public static class Builder{ + private String name; + + private int age; + + private int num; + + private String email; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder age(int age) { + this.age = age; + return this; + } + + public Builder num(int num) { + this.num = num; + return this; + } + + public Builder email(String email) { + this.email = email; + return this; + } + + public Student build() { + // 将builder对象传入到学生构造函数 + return new Student(this); + } + } + // 私有化构造器 + private Student(Builder builder) { + name = builder.name; + age = builder.age; + num = builder.num; + email = builder.email; + } + + @Override + public String toString() { + return "Student{" + + "name='" + name + '\'' + + ", age=" + age + + ", num=" + num + + ", email='" + email + '\'' + + '}'; + } +} +``` + +调用代码: + +```java +public static void student(){ + Student student = Student.builder().name("平头哥").num(1).age(18).email("平头哥@163.com").build(); + System.out.println(student); + } +``` + +可以看到,变种 Builder 模式包括以下内容: + +- 在要构建的类内部创建一个静态内部类 Builder +- 静态内部类的参数与构建类一致 +- 构建类的构造参数是 静态内部类,使用静态内部类的变量一一赋值给构建类 +- 静态内部类提供参数的 setter 方法,并且返回值是当前 Builder 对象 +- 最终提供一个 build 方法构建一个构建类的对象,参数是当前 Builder 对象 + +可能你会说,这种写法实现太麻烦了,确实需要我们写很多额外的代码,好在前辈们已经开发出了`lombok`来拯救我们,我们只需要引入`lombok`插件,然后在实体类上添加`@Builder`注解,你就可以使用 Builder 模式构建对象了。 + +## 建造者模式的优缺点 + +### 优点 + +- 在建造者模式中, 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象 +- 每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象 +- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程 +- 增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则” + +### 缺点 + +- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。 +- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。 diff --git a/docs/advance/design-pattern/2-singleton.md b/docs/advance/design-pattern/2-singleton.md new file mode 100644 index 0000000..3d536d5 --- /dev/null +++ b/docs/advance/design-pattern/2-singleton.md @@ -0,0 +1,187 @@ +--- +sidebar: heading +title: 设计模式之单例模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 单例模式,设计模式,单例 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 单例模式 + +单例模式(Singleton),目的是为了保证在一个进程中,某个类有且仅有一个实例。 + +由于这个类只有一个实例,所以不能让调用方使用`new Xxx()`来创建实例。所以,单例的构造方法必须是`private`,这样就防止了调用方自己创建实例。 + +单例模式的实现需要**三个必要的条件**: + +1. 单例类的**构造函数**必须是**私有的**,这样才能将类的创建权控制在类的内部,从而使得类的外部不能创建类的实例。 +2. 单例类通过一个**私有的静态变量**来存储其唯一实例。 +3. 单例类通过提供一个**公开的静态方法**,使得外部使用者可以访问类的唯一实例。 + +另外,实现单例类时,还需要考虑三个问题: + +- 创建单例对象时,是否**线程安全**。 +- 单例对象的创建,是否**延时加载**。 +- 获取单例对象时,是否需要**加锁**。 + +下面介绍几种实现单例模式的方式。 + +## 饿汉模式 + +JVM在类的初始化阶段,会执行类的静态方法。在执行类的初始化期间,JVM会去获取Class对象的锁。这个锁可以同步多个线程对同一个类的初始化。 + +饿汉模式只在类加载的时候创建一次实例,没有多线程同步的问题。单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。 + +``` +public class Singleton { + private static Singleton instance = new Singleton(); + private Singleton() {} + public static Singleton newInstance() { + return instance; + } +} +``` +饿汉式单例的**优点**: + +- 单例对象的创建是**线程安全**的; +- 获取单例对象时**不需要加锁**。 + +饿汉式单例的**缺点**:单例对象的创建,不是**延时加载**。 + +## 懒汉式 + +与饿汉式思想不同,懒汉式支持延时加载,将对象的创建延迟到了获取对象的时候。不过为了线程安全,在获取对象的操作需要加锁,这就导致了低性能。 + +```java +public class Singleton { + private static final Singleton instance; + + private Singleton () {} + + public static synchronized Singleton getInstance() { + if (instance == null) { + instance = new Singleton(); + } + + return instance; + } +} +``` + +上述代码加的锁只有在第一次创建对象时有用,而之后每次获取对象,其实是不需要加锁的(双重检查锁定优化了这个问题)。 + +懒汉式单例**优点**: + +- 对象的创建是线程安全的。 +- 支持延时加载。 + +懒汉式单例**缺点**: + +- 获取对象的操作被加上了锁,影响了并发性能。 + +## 双重检查锁定 + +双重检查锁定将懒汉式中的 `synchronized` 方法改成了 `synchronized` 代码块。如下: + +``` +public class Singleton { + private static volatile Singleton instance = null; //volatile + private Singleton(){} + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } +} +``` +双重校验锁先判断 instance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。 + +instance使用static修饰的原因:getInstance为静态方法,因为静态方法的内部不能直接使用非静态变量,只有静态成员才能在没有创建对象时进行初始化,所以返回的这个实例必须是静态的。 + +为什么两次判断`instance == null`: + +| Time | Thread A | Thread B | +| ---- | -------------------- | -------------------- | +| T1 | 检查到`instance`为空 | | +| T2 | | 检查到`instance`为空 | +| T3 | | 初始化对象`A` | +| T4 | | 返回对象`A` | +| T5 | 初始化对象`B` | | +| T6 | 返回对象`B` | | + +`new Singleton()`会执行三个动作:分配内存空间、初始化对象和对象引用指向内存地址。 + +```java +memory = allocate();  // 1:分配对象的内存空间 +ctorInstance(memory);  // 2:初始化对象 +instance = memory;   // 3:设置instance指向刚分配的内存地址 +``` + +由于指令重排优化的存在,导致初始化对象和将对象引用指向内存地址的顺序是不确定的。在某个线程创建单例对象时,会为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的是未初始化的对象,程序就会出错。volatile 可以禁止指令重排序,保证了先初始化对象再赋值给instance变量。 + +| Time | Thread A | Thread B | +| :--- | :----------------------- | :--------------------------------------- | +| T1 | 检查到`instance`为空 | | +| T2 | 获取锁 | | +| T3 | 再次检查到`instance`为空 | | +| T4 | 为`instance`分配内存空间 | | +| T5 | 将`instance`指向内存空间 | | +| T6 | | 检查到`instance`不为空 | +| T7 | | 访问`instance`(此时对象还未完成初始化) | +| T8 | 初始化`instance` | | + +双重检查锁定单例**优点**: + +- 对象的创建是线程安全的。 +- 支持延时加载。 +- 获取对象时不需要加锁。 + +## 静态内部类 + +它与饿汉模式一样,也是利用了类初始化机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。 + +基于类初始化的方案的实现代码更简洁。 + +``` +public class Instance { + private static class InstanceHolder { + public static Instance instance = new Instance(); + } + private Instance() {} + public static Instance getInstance() { + return InstanceHolder.instance ;  // 这里将导致InstanceHolder类被初始化 + } +} +``` +如上述代码,`InstanceHolder` 是一个静态内部类,当外部类 `Instance` 被加载的时候,并不会创建 `InstanceHolder` 实例对象。 + +只有当调用 `getInstance()` 方法时,`InstanceHolder` 才会被加载,这个时候才会创建 `Instance`。`Instance` 的唯一性、创建过程的线程安全性,都由 JVM 来保证。 + +静态内部类单例**优点**: + +- 对象的创建是线程安全的。 +- 支持延时加载。 +- 获取对象时不需要加锁。 + +## 枚举 + +用枚举来实现单例,是最简单的方式。这种实现方式通过 **Java 枚举**类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。 + +```java +public enum Singleton { + INSTANCE; // 该对象全局唯一 +} +``` + diff --git a/docs/advance/design-pattern/3-factory.md b/docs/advance/design-pattern/3-factory.md new file mode 100644 index 0000000..df2b20c --- /dev/null +++ b/docs/advance/design-pattern/3-factory.md @@ -0,0 +1,310 @@ +--- +sidebar: heading +title: 设计模式之工厂模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 工厂模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 工厂模式 + +工厂模式是用来封装对象的创建。工厂模式有三种,它们分别是简单工厂模式,工厂方法模式以及抽象工厂模式,通常我们所说的工厂模式指的是工厂方法模式。 + +下面分别介绍下这三种工厂模式。 + +## 简单工厂模式 + +简单工厂模式的定义:定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。 + +由于只有一个工厂类,所以工厂类中创建的对象不能太多,否则工厂类的业务逻辑就太复杂了,其次由于工厂类封装了对象的创建过程,所以客户端应该不关心对象的创建。 + +适用场景: + +(1)需要创建的对象较少。 + +(2)客户端不关心对象的创建过程。 + +下面看一个具体的实例。 + +创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw()方法用于绘图。 + +```java +public interface Shape { + void draw(); +} +``` + +编写具体的图形,每种图形都实现Shape 接口。 + +```java +public class CircleShape implements Shape { + + public CircleShape() { + System.out.println( "CircleShape: created"); + } + + @Override + public void draw() { + System.out.println( "draw: CircleShape"); + } + +} + +public class RectShape implements Shape { + public RectShape() { + System.out.println( "RectShape: created"); + } + + @Override + public void draw() { + System.out.println( "draw: RectShape"); + } + +} + +... +``` + +工厂类的具体实现: + +```java + public class ShapeFactory { + public static final String TAG = "ShapeFactory"; + public static Shape getShape(String type) { + Shape shape = null; + if (type.equalsIgnoreCase("circle")) { + shape = new CircleShape(); + } else if (type.equalsIgnoreCase("rect")) { + shape = new RectShape(); + } else if (type.equalsIgnoreCase("triangle")) { + shape = new TriangleShape(); + } + return shape; + } + } +``` + +在这个工厂类中通过传入不同的type可以new不同的形状,返回结果为Shape 类型,这个就是简单工厂核心的地方了。 + +客户端使用: + +```java +Shape shape= ShapeFactory.getShape("circle"); +shape.draw(); +``` + +通过给ShapeFactory传入不同的参数就实现了各种形状的绘制。 + +简单工厂模式的**优点**:只需要一个工厂创建对象,代码量少。 + +简单工厂模式的**缺点**:系统扩展困难,新增产品需要修改工厂逻辑,当产品较多时,会造成工厂逻辑过于复杂,不利于系统扩展和维护。 + +## 工厂方法模式 + +工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。每个对象都有一个与之对应的工厂。 + +下面看一个具体的实例。 + +设计一个这样的图片加载类,它具有多个图片加载器,用来加载jpg,png,gif格式的图片,每个加载器都有一个read方法,用于读取图片。 + +首先完成图片加载器的设计,编写一个加载器的公共接口。 + +```java +public interface Reader { + void read(); +} +``` + +各个图片加载器的实现如下: + +```java +public class JpgReader implements Reader { + @Override + public void read() { + System.out.print("read jpg"); + } +} + +public class PngReader implements Reader { + @Override + public void read() { + System.out.print("read png"); + } +} +``` + +定义一个抽象的工厂接口ReaderFactory,通过getReader()方法返回我们的Reader 类。 + +```java +public interface ReaderFactory { + Reader getReader(); +} +``` + +为每个图片加载器都提供一个工厂类,这些工厂类实现了ReaderFactory 。 + +```java +public class JpgReaderFactory implements ReaderFactory { + @Override + public Reader getReader() { + return new JpgReader(); + } +} + +public class PngReaderFactory implements ReaderFactory { + @Override + public Reader getReader() { + return new PngReader(); + } +} + +public class GifReaderFactory implements ReaderFactory { + @Override + public Reader getReader() { + return new GifReader(); + } +} +``` + +客户端通过子类来指定创建对应的对象。 + +```java +ReaderFactory factory=new JpgReaderFactory(); +Reader reader=factory.getReader(); +reader.read(); +``` + +和简单工厂对比一下,最根本的区别在于,简单工厂只有一个统一的工厂类,而工厂方法是针对每个要创建的对象都会提供一个工厂类,这些工厂类都实现了一个工厂基类(本例中的ReaderFactory )。 + +优点:增加新的产品类时无须修改现有系统,只需增加新产品和对应的工厂类即可。 + +## 抽象工厂模式 + +抽象工厂模式创建的是对象家族,也就是很多对象而不是一个对象,并且这些对象是相关的,也就是说必须一起创建出来。而工厂方法模式只是用于创建一个对象,这和抽象工厂模式有很大不同。 + +下面举一个实例帮助大家理解。 + +现在需要做一款跨平台的游戏,需要兼容Android,IOS两个移动操作系统,该游戏针对每个系统都设计了一套操作控制器(OperationController)和界面控制器(UIController),下面通过抽象工厂模式完成这款游戏的架构设计。 + +新建两个抽象产品接口。 + +```java +//抽象操作控制器 +public interface OperationController { + void control(); +} + +//抽象界面控制器 +public interface UIController { + void display(); +} +``` + +然后完成这两个系统平台的具体操作控制器和界面控制器。 + +```java +//Android +public class AndroidOperationController implements OperationController { + @Override + public void control() { + System.out.println("AndroidOperationController"); + } +} + +public class AndroidUIController implements UIController { + @Override + public void display() { + System.out.println("AndroidInterfaceController"); + } +} + + +//IOS +public class IosOperationController implements OperationController { + @Override + public void control() { + System.out.println("IosOperationController"); + } +} + +public class IosUIController implements UIController { + @Override + public void display() { + System.out.println("IosInterfaceController"); + } +} +``` + +下面定义一个抽象工厂,该工厂需要可以创建OperationController和UIController + +```java +public interface SystemFactory { + public OperationController createOperationController(); + public UIController createInterfaceController(); +} +``` + +在各平台具体的工厂类中完成操作控制器和界面控制器的创建过程。 + +```java +//android +public class AndroidFactory implements SystemFactory { + @Override + public OperationController createOperationController() { + return new AndroidOperationController(); + } + + @Override + public UIController createInterfaceController() { + return new AndroidUIController(); + } +} + +//IOS +public class IosFactory implements SystemFactory { + @Override + public OperationController createOperationController() { + return new IosOperationController(); + } + + @Override + public UIController createInterfaceController() { + return new IosUIController(); + } +} +``` + +客户端调用如下: + +```java +SystemFactory mFactory; +UIController interfaceController; +OperationController operationController; + +//Android +mFactory=new AndroidFactory(); +//IOS +//mFactory=new IosFactory(); + +interfaceController=mFactory.createInterfaceController(); +operationController=mFactory.createOperationController(); +interfaceController.display(); +operationController.control(); +``` + +针对不同平台只通过创建不同的工厂对象就完成了操作和UI控制器的创建。 + +**适用场景:** + +(1)和工厂方法一样客户端不需要知道它所创建的对象的类。 + +(2)需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。 + +(3)系统结构稳定,不会频繁的增加对象。(因为一旦增加就需要修改原有代码,不符合开闭原则) diff --git a/docs/advance/design-pattern/4-template.md b/docs/advance/design-pattern/4-template.md new file mode 100644 index 0000000..f678096 --- /dev/null +++ b/docs/advance/design-pattern/4-template.md @@ -0,0 +1,98 @@ +--- +sidebar: heading +title: 设计模式之模板模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 模板模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + + +# 模板模式 + +模板模式:一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。 这种类型的设计模式属于行为型模式。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 + +**模板模式**主要由抽象模板(Abstract Template)角色和具体模板(Concrete Template)角色组成。 + +- 抽象模板(Abstract Template): 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤;定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。 +- 具体模板(Concrete Template): 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤;每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。 + +示例图如下: + +![](http://img.topjavaer.cn/img/模板方法.jpg) + +以游戏为例。创建一个抽象类,它的模板方法被设置为 final,这样它就不会被重写。 + +```java +public abstract class Game { + abstract void initialize(); + abstract void startPlay(); + abstract void endPlay(); + + //模板 + public final void play(){ + //初始化游戏 + initialize(); + + //开始游戏 + startPlay(); + + //结束游戏 + endPlay(); + } +} +``` + +Football类: + +```java +public class Football extends Game { + + @Override + void endPlay() { + System.out.println("Football Game Finished!"); + } + + @Override + void initialize() { + System.out.println("Football Game Initialized! Start playing."); + } + + @Override + void startPlay() { + System.out.println("Football Game Started. Enjoy the game!"); + } +} +``` + +使用Game的模板方法 play() 来演示游戏的定义方式。 + +```java +public class TemplatePatternDemo { + public static void main(String[] args) { + + Game game = new Cricket(); + game.play(); + System.out.println(); + game = new Football(); + game.play(); + } +} +``` + +**模板模式优点** : + +1. 封装不变部分,扩展可变部分。 +2. 提取公共代码,便于维护。 +3. 行为由父类控制,子类实现。 + +**模板模式缺点**: + +- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。 + diff --git a/docs/advance/design-pattern/5-strategy.md b/docs/advance/design-pattern/5-strategy.md new file mode 100644 index 0000000..263a42b --- /dev/null +++ b/docs/advance/design-pattern/5-strategy.md @@ -0,0 +1,126 @@ +--- +sidebar: heading +title: 设计模式之策略模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 策略模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 策略模式 + +策略模式(Strategy Pattern)属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。 + +其主要目的是通过定义相似的算法,替换if else 语句写法,并且可以随时相互替换。 + +**策略模式**主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。 + +- **环境角色**(Context):持有一个策略类的引用,提供给客户端使用。 +- **抽象策略角色**(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。 +- **具体策略角色**(ConcreteStrategy):包装了相关的算法或行为。 + +示例图如下: + +![](http://img.topjavaer.cn/img/策略模式.png) + +以计算器为例,如果我们想得到两个数字相加的和,我们需要用到“+”符号,得到相减的差,需要用到“-”符号等等。虽然我们可以通过字符串比较使用if/else写成通用方法,但是计算的符号每次增加,我们就不得不加在原先的方法中进行增加相应的代码,如果后续计算方法增加、修改或删除,那么会使后续的维护变得困难。 + +但是在这些方法中,我们发现其基本方法是固定的,这时我们就可以通过策略模式来进行开发,可以有效避免通过if/else来进行判断,即使后续增加其他的计算规则也可灵活进行调整。 + +首先定义一个抽象策略角色,并拥有一个计算的方法。 + +```java +interface CalculateStrategy { + int doOperation(int num1, int num2); +} +``` + +然后再定义加减乘除这些具体策略角色并实现方法。代码如下: + +```java +class OperationAdd implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 + num2; + } +} + +class OperationSub implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 - num2; + } +} + +class OperationMul implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 * num2; + } +} + +class OperationDiv implements CalculateStrategy { + @Override + public int doOperation(int num1, int num2) { + return num1 / num2; + } +} +``` + +最后在定义一个环境角色,提供一个计算的接口供客户端使用。代码如下: + +```java +class CalculatorContext { + private CalculateStrategy strategy; + + public CalculatorContext(CalculateStrategy strategy) { + this.strategy = strategy; + } + + public int executeStrategy(int num1, int num2) { + return strategy.doOperation(num1, num2); + } +} +``` + +测试代码如下: + +```java +public static void main(String[] args) { + int a=4,b=2; + CalculatorContext context = new CalculatorContext(new OperationAdd()); + System.out.println("a + b = "+context.executeStrategy(a, b)); + + CalculatorContext context2 = new CalculatorContext(new OperationSub()); + System.out.println("a - b = "+context2.executeStrategy(a, b)); + + CalculatorContext context3 = new CalculatorContext(new OperationMul()); + System.out.println("a * b = "+context3.executeStrategy(a, b)); + + CalculatorContext context4 = new CalculatorContext(new OperationDiv()); + System.out.println("a / b = "+context4.executeStrategy(a, b)); +} +``` + +**策略模式优点:** + +- 扩展性好,可以在不修改对象结构的情况下,为新的算法进行添加新的类进行实现; +- 灵活性好,可以对算法进行自由切换; + +**策略模式缺点:** + +- 使用策略类变多,会增加系统的复杂度。; +- 客户端必须知道所有的策略类才能进行调用; + +**使用场景:** + +- 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为; + 一个系统需要动态地在几种算法中选择一种; +- 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现; + diff --git a/docs/advance/design-pattern/6-chain.md b/docs/advance/design-pattern/6-chain.md new file mode 100644 index 0000000..dbe5599 --- /dev/null +++ b/docs/advance/design-pattern/6-chain.md @@ -0,0 +1,235 @@ +--- +sidebar: heading +title: 设计模式之责任链模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 责任链模式,设计模式 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 责任链模式 + +## 定义 + +为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。 + +在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。 + +## 优点 + +**责任链模式是一种对象行为型模式,其主要优点如下。** + +1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。 +2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。 +3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。 +4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。 +5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。 + +## 缺点 + +1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。 +2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。 +3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。 + +## 责任链模式的结构 + +1. 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。 +2. 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。 +3. 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。 + +## 代码实现 + +比如要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。 + +代码实现如下: + +请假条类 + +```java +public class LeaveRequest { + //姓名 + private String name; + // 请假天数 + private int num; + // 请假内容 + private String content; + + public LeaveRequest(String name, int num, String content) { + this.name = name; + this.num = num; + this.content = content; + } + + public String getName() { + return name; + } + + public int getNum() { + return num; + } + + public String getContent() { + return content; + } + +} +``` + +处理类 + +```java +public abstract class Handler { + protected final static int NUM_ONE = 1; + protected final static int NUM_THREE = 3; + protected final static int NUM_SEVEN = 7; + + //该领导处理的请假天数区间 + private int numStart; + private int numEnd; + + + //领导上还有领导 + private Handler nextHandler; + + //设置请假天数范围 + public Handler(int numStart) { + this.numStart = numStart; + } + + //设置请假天数范围 + public Handler(int numStart, int numEnd) { + this.numStart = numStart; + this.numEnd = numEnd; + } + + //设置上级领导 + public void setNextHandler(Handler nextHandler) { + this.nextHandler = nextHandler; + } + + //提交请假条 + public final void submit(LeaveRequest leaveRequest) { + if (this.numStart == 0) { + return; + } + //请假天数达到领导处理要求 + if (leaveRequest.getNum() >= this.numStart) { + this.handleLeave(leaveRequest); + + //如果还有上级 并且请假天数超过当前领导的处理范围 + if (this.nextHandler != null && leaveRequest.getNum() > numEnd) { + //继续提交 + this.nextHandler.submit(leaveRequest); + } else { + System.out.println("流程结束!!!"); + } + } + } + + //各级领导处理请假条方法 + protected abstract void handleLeave(LeaveRequest leave); + +} +``` + +小组长类 + +```java +public class GroupLeader extends Handler { + //1-3天的假 + public GroupLeader() { + super(Handler.NUM_ONE, Handler.NUM_THREE); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("小组长审批通过:同意!"); + } +} +``` + +部门经理类 + +```java +public class Manager extends Handler { + //3-7天的假 + public Manager() { + super(Handler.NUM_THREE, Handler.NUM_SEVEN); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("部门经理审批通过:同意!"); + } +} +``` + +总经理类 + +```java +public class GeneralManager extends Handler{ + //7天以上的假 + public GeneralManager() { + super(Handler.NUM_THREE, Handler.NUM_SEVEN); + } + + @Override + protected void handleLeave(LeaveRequest leave) { + System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "!"); + System.out.println("总经理审批通过:同意!"); + } +} +``` + +测试类 + +```java +public class Client { + public static void main(String[] args) { + //请假条 + LeaveRequest leave = new LeaveRequest("小庄", 3, "出去旅游"); + + //各位领导 + Manager manager = new Manager(); + GroupLeader groupLeader = new GroupLeader(); + GeneralManager generalManager = new GeneralManager(); + + /* + * 小组长上司是经理 经理上司是总经理 + */ + groupLeader.setNextHandler(manager); + manager.setNextHandler(generalManager); + + //提交 + groupLeader.submit(leave); + + } +} +``` + +输出结果: + +```java +小庄请假3天,出去旅游! +小组长审批通过: 同意! +流程结束!!! +``` + +## 应用场景 + +1. 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。 +2. 可动态指定一组对象处理请求,或添加新的处理者。 +3. 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。 + + + +> [参考链接](https://segmentfault.com/a/1190000040450513) + diff --git a/docs/advance/design-pattern/7-iterator.md b/docs/advance/design-pattern/7-iterator.md new file mode 100644 index 0000000..e2fabbd --- /dev/null +++ b/docs/advance/design-pattern/7-iterator.md @@ -0,0 +1,45 @@ +--- +sidebar: heading +title: 设计模式之迭代器模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 迭代器模式,设计模式,迭代器 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 迭代器模式 + +提供一种方法顺序访问一个聚合对象中的各个元素, 而又不暴露其内部的表示。 + +把在元素之间游走的责任交给迭代器,而不是聚合对象。 + +**应用实例:**JAVA 中的 iterator。 + +**优点:** 1、它支持以不同的方式遍历一个聚合对象。 2、迭代器简化了聚合类。 3、在同一个聚合上可以有多个遍历。 4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。 + +**缺点:**由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。 + +**使用场景:** 1、访问一个聚合对象的内容而无须暴露它的内部表示。 2、需要为聚合对象提供多种遍历方式。 3、为遍历不同的聚合结构提供一个统一的接口。 + +**迭代器模式在JDK中的应用** + +ArrayList的遍历: + +```java +Iterator iter = null; + +System.out.println("ArrayList:"); +iter = arrayList.iterator(); +while (iter.hasNext()) { + System.out.print(iter.next() + "\t"); +} +``` + + + diff --git a/docs/advance/design-pattern/8-decorator.md b/docs/advance/design-pattern/8-decorator.md new file mode 100644 index 0000000..f010b2b --- /dev/null +++ b/docs/advance/design-pattern/8-decorator.md @@ -0,0 +1,188 @@ +--- +sidebar: heading +title: 设计模式之装饰模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 装饰模式,设计模式,装饰者 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 装饰者模式 + +装饰者模式(decorator pattern):动态地将责任附加到对象上, 若要扩展功能, 装饰者提供了比继承更有弹性的替代方案。 + +装饰模式以对客户端透明的方式拓展对象的功能,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不创造更多子类的情况下,将对象的功能加以扩展。 + +比如设置FileInputStream,先用BufferedInputStream装饰它,再用自己写的LowerCaseInputStream过滤器去装饰它。 + +``` +InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("test.txt"))); +``` +在装饰模式中的角色有: + +- **抽象组件**(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。 +- **具体组件**(ConcreteComponent)角色:定义一个将要接收附加责任的类。 +- **抽象装饰**(Decorator)角色:持有一个组件(Component)对象的实例,并定义一个与抽象组件接口一致的接口。 +- **具体装饰**(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。 + +**代码实例** + +LOL、王者荣耀等类Dota游戏中,每次英雄升级都会附加一个额外技能点学习技能,这个就类似装饰模式为已有类动态附加额外的功能。 + +具体的英雄就是ConcreteComponent,技能栏就是装饰器Decorator,每个技能就是ConcreteDecorator。 + +新建一个接口: + +```java +public interface Hero { + //学习技能 + void learnSkills(); +} +``` + +创建接口的实现类(具体英雄盲僧): + +```java +//ConcreteComponent 具体英雄盲僧 +public class BlindMonk implements Hero { + + private String name; + + public BlindMonk(String name) { + this.name = name; + } + + @Override + public void learnSkills() { + System.out.println(name + "学习了以上技能!"); + } +} +``` + +装饰角色: + +```java +//Decorator 技能栏 +public abstract class Skills implements Hero { + + //持有一个英雄对象接口 + private Hero hero; + + public Skills(Hero hero) { + this.hero = hero; + } + + @Override + public void learnSkills() { + if(hero != null) + hero.learnSkills(); + } +} +``` + +具体装饰角色: + +```java +//ConreteDecorator 技能:Q +public class Skill_Q extends Skills{ + + private String skillName; + + public Skill_Q(Hero hero,String skillName) { + super(hero); + this.skillName = skillName; + } + + @Override + public void learnSkills() { + System.out.println("学习了技能Q:" +skillName); + super.learnSkills(); + } +} + +//ConreteDecorator 技能:W +public class Skill_W extends Skills{ + + private String skillName; + + public Skill_W(Hero hero,String skillName) { + super(hero); + this.skillName = skillName; + } + + @Override + public void learnSkills() { + System.out.println("学习了技能W:" + skillName); + super.learnSkills(); + } +} +//ConreteDecorator 技能:E +public class Skill_E extends Skills{ + + private String skillName; + + public Skill_E(Hero hero,String skillName) { + super(hero); + this.skillName = skillName; + } + + @Override + public void learnSkills() { + System.out.println("学习了技能E:"+skillName); + super.learnSkills(); + } +} +//ConreteDecorator 技能:R +public class Skill_R extends Skills{ + + private String skillName; + + public Skill_R(Hero hero,String skillName) { + super(hero); + this.skillName = skillName; + } + + @Override + public void learnSkills() { + System.out.println("学习了技能R:" +skillName ); + super.learnSkills(); + } +} +``` + +客户端: + +```java +//客户端:召唤师 +public class Player { + public static void main(String[] args) { + //选择英雄 + Hero hero = new BlindMonk("李青"); + + Skills skills = new Skills(hero); + Skills r = new Skill_R(skills,"猛龙摆尾"); + Skills e = new Skill_E(r,"天雷破/摧筋断骨"); + Skills w = new Skill_W(e,"金钟罩/铁布衫"); + Skills q = new Skill_Q(w,"天音波/回音击"); + //学习技能 + q.learnSkills(); + } +} +``` + +输出: + +```java +学习了技能Q:天音波/回音击 +学习了技能W:金钟罩/铁布衫 +学习了技能E:天雷破/摧筋断骨 +学习了技能R:猛龙摆尾 +李青学习了以上技能! +``` + diff --git a/docs/advance/design-pattern/9-adapter.md b/docs/advance/design-pattern/9-adapter.md new file mode 100644 index 0000000..919c173 --- /dev/null +++ b/docs/advance/design-pattern/9-adapter.md @@ -0,0 +1,33 @@ +--- +sidebar: heading +title: 设计模式之适配器模式 +category: 设计模式 +tag: + - 设计模式 +head: + - - meta + - name: keywords + content: 适配器模式,设计模式,适配器 + - - meta + - name: description + content: 设计模式常见面试题总结,让天下没有难背的八股文! +--- + +# 适配器模式 +适配器模式将现成的对象通过适配变成我们需要的接口。 适配器让原本接口不兼容的类可以合作。 + +适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。 + +对象适配器模式通过组合对象进行适配。 + +![](http://img.topjavaer.cn/img/image-20221224093518386.png) + +类适配器通过继承来完成适配。 + +![](http://img.topjavaer.cn/img/image-20221224093534895.png) + +适配器模式的**优点**: + +1. 更好的复用性。系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。 +2. 更好的扩展性。在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。 + diff --git a/docs/advance/design-pattern/README.md b/docs/advance/design-pattern/README.md new file mode 100644 index 0000000..e96d9f8 --- /dev/null +++ b/docs/advance/design-pattern/README.md @@ -0,0 +1,31 @@ +--- +title: 设计模式 +icon: design +date: 2022-08-07 +category: 设计模式 +star: true +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +**本专栏是大彬学习设计模式基础知识的学习笔记,如有错误,可以在评论区指出**~ + +## 设计模式详解 + +- [设计模式的六大原则](./1-principle.md) +- [单例模式](./2-singleton.md) +- [工厂模式](./3-factory.md) +- [模板模式](./4-template.md) +- [策略模式](./5-strategy.md) +- [责任链模式](./6-chain.md) +- [迭代器模式](./7-iterator.md) +- [装饰模式](./8-decorator.md) +- [适配器模式](./9-adapter.md) +- [观察者模式](./10-observer.md) +- [代理模式](./11-proxy.md) +- [建造者模式](./12-builder.md) diff --git "a/\345\210\206\345\270\203\345\274\217/\345\205\250\345\261\200\345\224\257\344\270\200ID.md" b/docs/advance/distributed/1-global-unique-id.md similarity index 95% rename from "\345\210\206\345\270\203\345\274\217/\345\205\250\345\261\200\345\224\257\344\270\200ID.md" rename to docs/advance/distributed/1-global-unique-id.md index 0871efa..7812e4a 100644 --- "a/\345\210\206\345\270\203\345\274\217/\345\205\250\345\261\200\345\224\257\344\270\200ID.md" +++ b/docs/advance/distributed/1-global-unique-id.md @@ -1,10 +1,27 @@ +--- +sidebar: heading +title: 全局唯一ID生成方案 +category: 分布式 +tag: + - 分布式ID +head: + - - meta + - name: keywords + content: 分布式ID,分布式,唯一ID生成方案 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# 全局唯一ID生成方案 + 传统的单体架构的时候,我们基本是单库然后业务单表的结构。每个业务表的ID一般我们都是从1增,通过`AUTO_INCREMENT=1`设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性。 -![](http://img.dabin-coder.cn/image/20220508235751.png) +![](http://img.topjavaer.cn/img/20220508235751.png) 如上图,如果第一个订单存储在 DB1 上则订单 ID 为1,当一个新订单又入库了存储在 DB2 上订单 ID 也为1。我们系统的架构虽然是分布式的,但是在用户层应是无感知的,重复的订单主键显而易见是不被允许的。那么针对分布式系统如何做到主键唯一性呢? -# UUID +## UUID UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID是由一组32位数的16进制数字所构成,所以UUID理论上的总数为 1632=2128,约等于 3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。 @@ -52,7 +69,7 @@ public static void main(String[] args) { - 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置。 - 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能,可以查阅 Mysql 索引原理 B+树的知识。 -# 数据库生成 +## 数据库生成 是不是一定要基于外界的条件才能满足分布式唯一ID的需求呢,我们能不能在我们分布式数据库的基础上获取我们需要的ID? @@ -65,13 +82,13 @@ public static void main(String[] args) { 假设有三台机器,则DB1中order表的起始ID值为1,DB2中order表的起始值为2,DB3中order表的起始值为3,它们自增的步长都为3,则它们的ID生成范围如下图所示: -![](http://img.dabin-coder.cn/image/20220509000004.png) +![](http://img.topjavaer.cn/img/20220509000004.png) 通过这种方式明显的优势就是依赖于数据库自身不需要其他资源,并且ID号单调自增,可以实现一些对ID有特殊要求的业务。 但是缺点也很明显,首先它强依赖DB,当DB异常时整个系统不可用。虽然配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。还有就是ID发号性能瓶颈限制在单台MySQL的读写性能。 -# 使用redis实现 +## 使用redis实现 Redis实现分布式唯一ID主要是通过提供像 *INCR* 和 *INCRBY* 这样的自增原子命令,由于Redis自身的单线程的特点所以能保证生成的 ID 肯定是唯一有序的。 @@ -83,7 +100,7 @@ Redis 实现分布式全局唯一ID,它的性能比较高,生成的数据是 当然现在Redis的使用性很普遍,所以如果其他业务已经引进了Redis集群,则可以资源利用考虑使用Redis来实现。 -# 雪花算法-Snowflake +## 雪花算法-Snowflake Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将 64-bit位分割成多个部分,每个部分代表不同的含义。而 Java中64bit的整数是Long类型,所以在 Java 中 SnowFlake 算法生成的 ID 就是 long 来存储的。 @@ -94,7 +111,7 @@ Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划 这样的划分之后相当于在一毫秒一个数据中心的一台机器上可产生4096个有序的不重复的ID。但是我们 IDC 和机器数肯定不止一个,所以毫秒内能生成的有序ID数是翻倍的。 -![](http://img.dabin-coder.cn/image/20220508235958.png) +![](http://img.topjavaer.cn/img/20220508235958.png) Snowflake 的[Twitter官方原版](https://github.com/twitter/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala)是用Scala写的,对Scala语言有研究的同学可以去阅读下,以下是 Java 版本的写法。 @@ -290,7 +307,7 @@ public static void main(String[] args) { 很多其他类雪花算法也是在此思想上的设计然后改进规避它的缺陷,后面介绍的百度 UidGenerator 和 美团分布式ID生成系统 Leaf 中snowflake模式都是在 snowflake 的基础上演进出来的。 -# 百度-UidGenerator +## 百度-UidGenerator [百度的 UidGenerator](https://github.com/baidu/uid-generator) 是百度开源基于Java语言实现的唯一ID生成器,是在雪花算法 snowflake 的基础上做了一些改进。UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略,适用于docker等虚拟化环境下实例自动重启、漂移等场景。 @@ -303,7 +320,7 @@ UidGenerator 依然是以划分命名空间的方式将 64-bit位分割成多个 - 中间的 workId (数据中心+工作机器,可以其他组成方式)则由 22-bit位组成,可表示 2^22 = 4194304个工作ID。 - 最后由13-bit位构成自增序列,可表示2^13 = 8192个数。 -![](http://img.dabin-coder.cn/image/20220509000004.png) +![](http://img.topjavaer.cn/img/20220509000004.png) 其中 workId (机器 id),最多可支持约420w次机器启动。内置实现为在启动时由数据库分配(表名为 WORKER_NODE),默认分配策略为用后即弃,后续可提供复用策略。 @@ -323,7 +340,7 @@ PRIMARY KEY(ID) COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB; ``` -## DefaultUidGenerator 实现 +### DefaultUidGenerator 实现 DefaultUidGenerator 就是正常的根据时间戳和机器位还有序列号的生成方式,和雪花算法很相似,对于时钟回拨也只是抛异常处理。仅有一些不同,如以秒为为单位而不再是毫秒和支持Docker等虚拟化环境。 @@ -371,7 +388,7 @@ protected synchronized long nextId() { ``` -## CachedUidGenerator 实现 +### CachedUidGenerator 实现 而官方建议的性能较高的 CachedUidGenerator 生成方式,是使用 RingBuffer 缓存生成的id。数组每个元素成为一个slot。RingBuffer容量,默认为Snowflake算法中sequence最大值(2^13 = 8192)。可通过 boostPower 配置进行扩容,以提高 RingBuffer 读写吞吐量。 @@ -382,13 +399,13 @@ Tail指针、Cursor指针用于环形数组上读写slot: - Cursor指针 表示Consumer消费到的最小序号(序号序列与Producer序列相同)。Cursor不能超过Tail,即不能消费未生产的slot。当Cursor已赶上tail,此时可通过rejectedTakeBufferHandler指定TakeRejectPolicy -[![img](https://img2018.cnblogs.com/blog/1162587/201907/1162587-20190707142503899-1530677221.png)](https://img2018.cnblogs.com/blog/1162587/201907/1162587-20190707142503899-1530677221.png) +![](http://img.topjavaer.cn/img/20220706225625.png) CachedUidGenerator采用了双RingBuffer,Uid-RingBuffer用于存储Uid、Flag-RingBuffer用于存储Uid状态(是否可填充、是否可消费)。 由于数组元素在内存中是连续分配的,可最大程度利用CPU cache以提升性能。但同时会带来「伪共享」FalseSharing问题,为此在Tail、Cursor指针、Flag-RingBuffer中采用了CacheLine 补齐方式。 -[![img](https://img2018.cnblogs.com/blog/1162587/201907/1162587-20190707142450492-1894906450.png)](https://img2018.cnblogs.com/blog/1162587/201907/1162587-20190707142450492-1894906450.png) +![](http://img.topjavaer.cn/img/20220706225530.png) RingBuffer填充时机 @@ -399,13 +416,13 @@ RingBuffer填充时机 - 周期填充 通过Schedule线程,定时补全空闲slots。可通过scheduleInterval配置,以应用定时填充功能,并指定Schedule时间间隔。 -# 美团Leaf +## 美团Leaf Leaf是美团基础研发平台推出的一个分布式ID生成服务,名字取自德国哲学家、数学家莱布尼茨的著名的一句话:“There are no two identical leaves in the world”,世间不可能存在两片相同的叶子。 Leaf 也提供了两种ID生成的方式,分别是 Leaf-segment 数据库方案和 Leaf-snowflake 方案。 -## Leaf-segment 数据库方案 +### Leaf-segment 数据库方案 Leaf-segment 数据库方案,是在上文描述的在使用数据库的方案上,做了如下改变: @@ -427,7 +444,7 @@ CREATE TABLE `leaf_alloc` ( 原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示: -![](http://img.dabin-coder.cn/image/20220509000142.png) +![](http://img.topjavaer.cn/img/20220509000142.png) 同时Leaf-segment 为了解决 TP999(满足千分之九百九十九的网络请求所需要的最低耗时)数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,TP999 数据会出现偶尔的尖刺的问题,提供了双buffer优化。 @@ -435,7 +452,7 @@ CREATE TABLE `leaf_alloc` ( 为了DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中,而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的 TP999 指标。详细实现如下图所示: -![](http://img.dabin-coder.cn/image/20220509000258.png) +![](http://img.topjavaer.cn/img/20220509000258.png) 采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。 @@ -444,7 +461,7 @@ CREATE TABLE `leaf_alloc` ( 对于这种方案依然存在一些问题,它仍然依赖 DB的稳定性,需要采用主从备份的方式提高 DB的可用性,还有 Leaf-segment方案生成的ID是趋势递增的,这样ID号是可被计算的,例如订单ID生成场景,通过订单id号相减就能大致计算出公司一天的订单量,这个是不能忍受的。 -## Leaf-snowflake方案 +### Leaf-snowflake方案 Leaf-snowflake方案完全沿用 snowflake 方案的bit位设计,对于workerID的分配引入了Zookeeper持久顺序节点的特性自动对snowflake节点配置 wokerID。避免了服务规模较大时,动手配置成本太高的问题。 @@ -454,19 +471,19 @@ Leaf-snowflake是按照下面几个步骤启动的: - 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。 - 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。 -![](http://img.dabin-coder.cn/image/20220509000333.png) +![](http://img.topjavaer.cn/img/20220509000333.png) 为了减少对 Zookeeper的依赖性,会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。 上文阐述过在类 snowflake算法上都存在时钟回拨的问题,Leaf-snowflake在解决时钟回拨的问题上是通过校验自身系统时间与 `leaf_forever/${self}`节点记录时间做比较然后启动报警的措施。 -![](http://img.dabin-coder.cn/image/20220509000708.png) +![](http://img.topjavaer.cn/img/20220509000708.png) 美团官方建议是由于强依赖时钟,对时间的要求比较敏感,在机器工作时NTP同步也会造成秒级别的回退,建议可以直接关闭NTP同步。要么在时钟回拨的时候直接不提供服务直接返回ERROR_CODE,等时钟追上即可。或者做一层重试,然后上报报警系统,更或者是发现有时钟回拨之后自动摘除本身节点并报警。 在性能上官方提供的数据目前 Leaf 的性能在4C8G 的机器上QPS能压测到近5w/s,TP999 1ms。 -# 总结 +## 总结 以上基本列出了所有常用的分布式ID生成方式,其实大致分类的话可以分为两类: @@ -482,4 +499,4 @@ Leaf-snowflake是按照下面几个步骤启动的: -[参考链接](https://www.cnblogs.com/jajian/p/11101213.html) \ No newline at end of file +[参考链接](https://www.cnblogs.com/jajian/p/11101213.html) diff --git "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\351\224\201.md" b/docs/advance/distributed/2-distributed-lock.md similarity index 95% rename from "\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\351\224\201.md" rename to docs/advance/distributed/2-distributed-lock.md index 8a65dd1..1016022 100644 --- "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\351\224\201.md" +++ b/docs/advance/distributed/2-distributed-lock.md @@ -1,3 +1,20 @@ +--- +sidebar: heading +title: 分布式锁 +category: 分布式 +tag: + - 分布式锁 +head: + - - meta + - name: keywords + content: 分布式锁,分布式 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# 分布式锁 + ## 为什么要使用分布式锁 在单机环境下,当存在多个线程可以同时改变某个变量(可变共享变量)时,就会出现线程安全问题。这个问题可以通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等来避免。 @@ -164,6 +181,8 @@ public class RedisTest { 前面的方案是基于**Redis单机版**的分布式锁讨论,还不是很完美。因为Redis一般都是集群部署的。 +![](http://img.topjavaer.cn/img/202308202338736.png) + 如果线程一在`Redis`的`master`节点上拿到了锁,但是加锁的`key`还没同步到`slave`节点。恰好这时,`master`节点发生故障,一个`slave`节点就会升级为`master`节点。线程二就可以顺理成章获取同个`key`的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。 为了解决这个问题,Redis作者antirez提出一种高级的分布式锁算法:**Redlock**。它的核心思想是这样的: @@ -172,6 +191,8 @@ public class RedisTest { 我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。 +![](http://img.topjavaer.cn/img/202308202339712.png) + RedLock的实现步骤: 1. 获取当前时间,以毫秒为单位。 @@ -187,6 +208,8 @@ RedLock的实现步骤: - 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。 - 如果获取锁失败,解锁! +Redisson 实现了 redLock 版本的锁,有兴趣的小伙伴,可以去了解一下。 + ### 基于ZooKeeper的实现方式 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: @@ -252,4 +275,4 @@ ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它 ## 参考链接 -https://mp.weixin.qq.com/s/xQknd6xsVDPBr4TbETTk2A \ No newline at end of file +https://mp.weixin.qq.com/s/xQknd6xsVDPBr4TbETTk2A diff --git "a/\345\210\206\345\270\203\345\274\217/\350\277\234\347\250\213\350\260\203\347\224\250.md" b/docs/advance/distributed/3-rpc.md similarity index 96% rename from "\345\210\206\345\270\203\345\274\217/\350\277\234\347\250\213\350\260\203\347\224\250.md" rename to docs/advance/distributed/3-rpc.md index beade88..a9d5008 100644 --- "a/\345\210\206\345\270\203\345\274\217/\350\277\234\347\250\213\350\260\203\347\224\250.md" +++ b/docs/advance/distributed/3-rpc.md @@ -1,3 +1,20 @@ +--- +sidebar: heading +title: RPC +category: 分布式 +tag: + - RPC +head: + - - meta + - name: keywords + content: RPC,分布式,RPC框架,RPC和HTTP,序列化技术 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# RPC + ## RPC简介 RPC,英文全名remote procedure call,即远程过程调用。就是说一个应用部署在A服务器上,想要调用B服务器上应用提供的方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 @@ -22,7 +39,7 @@ RPC是一个完整的远程调用方案,它通常包括通信协议和序列 > stub说的都是“一小块代码”,通常是有个caller要调用callee的时候,中间需要一些特殊处理的逻辑,就会用这种“小块代码”去做。 -![](http://img.dabin-coder.cn/image/20220508160414.png) +![](http://img.topjavaer.cn/img/20220508160414.png) 1. 服务消费端(client)以本地调用的方式调用远程服务; 2. 客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):`RpcRequest`; @@ -116,4 +133,4 @@ Protobuf有个缺点就是要传输每一个类的结构都要生成对应的pro 1. 对性能要求不高的场景,可以采用基于XML的SOAP协议 2. 性能和间接性有比较高要求的场景,那么Hessian、Protobuf、Thrift、Avro都可以。 3. 基于前后端分离,或者独立的对外API服务,选用JSON是比较好的,对于调试、可读性都很不错。 -4. Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的。 \ No newline at end of file +4. Avro设计理念偏于动态类型语言,那么这类的场景使用Avro是可以的。 diff --git "a/\345\210\206\345\270\203\345\274\217/\345\276\256\346\234\215\345\212\241.md" b/docs/advance/distributed/4-micro-service.md similarity index 89% rename from "\345\210\206\345\270\203\345\274\217/\345\276\256\346\234\215\345\212\241.md" rename to docs/advance/distributed/4-micro-service.md index c48afc0..aec218d 100644 --- "a/\345\210\206\345\270\203\345\274\217/\345\276\256\346\234\215\345\212\241.md" +++ b/docs/advance/distributed/4-micro-service.md @@ -1,3 +1,20 @@ +--- +sidebar: heading +title: 微服务 +category: 分布式 +tag: + - 微服务 +head: + - - meta + - name: keywords + content: 微服务,分布式,微服务设计原则,服务网关,微服务通讯方式,微服务框架,微服务链路追踪 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# 微服务 + ## 什么是微服务? 微服务是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务和服务之间采用轻量级的通信机制进行协作。每个服务可以被独立的部署到生产环境。 @@ -30,9 +47,15 @@ ## 分布式和微服务的区别 -从概念理解,分布式服务架构强调的是服务化以及服务的**分散化**,微服务则更强调服务的**专业化和精细分工**; +微服务解决的是系统复杂度问题,一般来说是业务问题,即在一个系统中承担职责太多了,需要打散,便于理解和维护,进而提升系统的开发效率和运行效率,微服务一般来说是针对应用层面的。 + +分布式解决的是系统性能问题,即解决系统部署上单点的问题,尽量让组成系统的子系统分散在不同的机器上进而提高系统的吞吐能力。 + +两者概念层面也是不一样的,微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分; -从实践的角度来看,**微服务架构通常是分布式服务架构**,反之则未必成立。 +而分布式是部署层面的东西,即强调物理层面的组成,即系统的各子系统部署在不同计算机上。 + +微服务可以是分布式的,即可以将不同服务部署在不同计算机上,当然如果量小也可以部署在单机上。 一句话概括:分布式:分散部署;微服务:分散能力。 @@ -71,6 +94,8 @@ **1、RPC** +远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。 + 优点:简单,常见。因为没有中间件代理,系统更简单 缺点: @@ -137,7 +162,7 @@ 更好的方式是采用API网关(也叫做服务网关),实现一个API网关**接管所有的入口流量**,类似Nginx的作用,将所有用户的请求转发给后端的服务器,但网关做的不仅仅只是简单的转发,也会针对流量做一些扩展,比如鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等,这样将通用的逻辑抽出来,由网关统一去做,业务方也能够更专注于业务逻辑,提升迭代的效率。 -![](http://img.dabin-coder.cn/image/20220508185101.jpg) +![](http://img.topjavaer.cn/img/20220508185101.jpg) 通过引入API网关,客户端只需要与API网关交互,而不用与各个业务方的接口分别通讯,但多引入一个组件就多引入了一个潜在的故障点,因此要实现一个高性能、稳定的网关,也会涉及到很多点。 @@ -145,7 +170,7 @@ **服务网关基本功能**: -![](http://img.dabin-coder.cn/image/20220508120340.png) +![](http://img.topjavaer.cn/img/20220508120340.png) - 智能路由:接收**外部**一切请求,并转发到后端的对外服务。注意:我们只转发外部请求,服务之间的请求不走网关,这就表示全链路追踪、内部服务API监控、内部服务之间调用的容错、智能路由不能在网关完成;当然,也可以将所有的服务调用都走网关,那么几乎所有的功能都可以集成到网关中,但是这样的话,网关的压力会很大,不堪重负。 - 权限校验:网关可以做一些用户身份认证,权限认证,防止非法请求操作API 接口,对内部服务起到保护作用 @@ -179,15 +204,3 @@ 所以Dubbo专注于服务治理;Spring Cloud关注于微服务架构生态。 -## 其他 - -### Spring Cloud基础知识 - -[Spring Cloud基础知识](../框架/SpringCloud微服务实战.md) - -### 什么是Service Mesh? - -Service Mesh 是微服务时代的 TCP/IP 协议。 - -参考:https://zhuanlan.zhihu.com/p/61901608 - diff --git a/docs/advance/distributed/5-distibuted-arch.md b/docs/advance/distributed/5-distibuted-arch.md new file mode 100644 index 0000000..372bd50 --- /dev/null +++ b/docs/advance/distributed/5-distibuted-arch.md @@ -0,0 +1,18 @@ +--- +sidebar: heading +title: 分布式架构 +category: 分布式 +tag: + - 分布式架构 +head: + - - meta + - name: keywords + content: 分布式架构,限流,熔断 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# 分布式架构,微服务、限流、熔断.... + +[分布式架构,微服务、限流、熔断....](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490543&idx=1&sn=ee34bee96511d5e548381e0576f8b484&chksm=ce98e6a9f9ef6fbf7db9c2b6d2fed26853a3bc13a50c3228ab57bea55afe0772008cdb1f957b&token=1594696656&lang=zh_CN#rd) diff --git a/docs/advance/distributed/6-distributed-transaction.md b/docs/advance/distributed/6-distributed-transaction.md new file mode 100644 index 0000000..a952e8a --- /dev/null +++ b/docs/advance/distributed/6-distributed-transaction.md @@ -0,0 +1,212 @@ +--- +sidebar: heading +title: 分布式事务 +category: 分布式 +tag: + - 分布式事务 +head: + - - meta + - name: keywords + content: 分布式事务,强一致性,弱一致性,最终一致性,CAP理论,BASE理论,2PC方案,TCC,本地消息表,saga事务,最大努力通知方案 + - - meta + - name: description + content: 分布式常见面试题总结,让天下没有难背的八股文! +--- + +# 分布式事务 + +## 简介 + +### 事务 + +事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。 + +### 分布式事务 + +分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务的特性。 + +### 强一致性、弱一致性、最终一致性 + +**强一致性** + +任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。 + +**弱一致性** + +数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。 + +**最终一致性** + +不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。 + +由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。 + +## 分布式事务的基础 + +### CAP理论 + +**Consistency**(一致性):数据一致更新,所有数据变动都是同步的(强一致性)。 + +**Availability**(可用性):好的响应性能。 + +**Partition tolerance**(分区容错性) :可靠性。 + +定理:任何分布式系统**只可同时满足二点**,没法三者兼顾。 + +CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。 + +CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。 + +AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。 + +### BASE理论 + +BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。 + +1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。 +2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。 +3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。 + +BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。 + +## 分布式事务解决方案 + +分布式事务的实现主要有以下 6 种方案: + +- 2PC 方案 +- TCC 方案 +- 本地消息表 +- MQ事务 +- Saga事务 +- 最大努力通知方案 + +### 2PC方案 + +2PC方案分为两阶段: + +第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交. + +第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。 + +优点: 尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。 + +缺点: + +- 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。 +- 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。 +- 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。 + +总的来说,2PC方案比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。 + +### TCC + +TCC 的全称是:`Try`、`Confirm`、`Cancel`。 + +- **Try 阶段**:这个阶段说的是对各个服务的资源做检测以及对资源进行 **锁定或者预留**。 +- **Confirm 阶段**:这个阶段说的是在各个服务中执行实际的操作。 +- **Cancel 阶段**:如果任何一个服务的业务方法执行出错,那么这里就需要 **进行补偿**,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚) +- + +举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。 + +如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。 + +如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)。 + +这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个**事务回滚实际上是严重依赖于你自己写代码来回滚和补偿**了,会造成补偿代码巨大。 + +### 本地消息表 + +本地消息表的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 + +![](http://img.topjavaer.cn/img/本地消息表.png) + +对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。 + +1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。 + +2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。 + +3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。 + +4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。 + +本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。 + +### MQ事务 + +基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。 + +MQ事务方案整体流程和本地消息表的流程很相似,如下图: + +![](http://img.topjavaer.cn/img/MQ事务方案.png) + +从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。 + +那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。 + +在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下: + +**正常情况:事务主动方发消息** + +![](http://img.topjavaer.cn/img/事务主动方发消息.png) + +这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下: + +- 发送方向 MQ 服务端(MQ Server)发送 half 消息。 +- MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。 +- 发送方开始执行本地事务逻辑。 +- 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。 +- MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。 + +**异常情况:事务主动方消息恢复** + +![](http://img.topjavaer.cn/img/事务主动方消息恢复.png) + +在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下: + +- MQ Server 对该消息发起消息回查。 +- 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 +- 发送方根据检查得到的本地事务的最终状态再次提交二次确认。 +- MQ Server基于 commit/rollback 对消息进行投递或者删除。 + +**优点** + +相比本地消息表方案,MQ 事务方案优点是: + +- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。 +- 吞吐量大于使用本地消息表方案。 + +**缺点** + +- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。 +- 业务处理服务需要实现消息状态回查接口。 + +### Saga事务 + +Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。 + +Saga的实现有很多种方式,其中最流行的两种方式是: + +- **基于事件的方式**。这种方式没有协调中心,整个模式的工作方式就像舞蹈一样,各个舞蹈演员按照预先编排的动作和走位各自表演,最终形成一只舞蹈。处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。 +- **基于命令的方式**。这种方式的工作形式就像一只乐队,由一个指挥家(协调中心)来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。 + +### 最大努力通知方案 + +最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。 + +最大努力通知的整体流程如下图: + +![](http://img.topjavaer.cn/img/最大努力通知方案.png) + +在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的; + +但是最大努力通知,事务主动方尽最大努力(重试,轮询....)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。 + +最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。 + +## 参考文章 + +https://www.pdai.tech/md/arch/arch-z-transection.html + +https://juejin.cn/post/6844903647197806605#heading-15 diff --git a/docs/advance/distributed/README.md b/docs/advance/distributed/README.md new file mode 100644 index 0000000..b2e29b3 --- /dev/null +++ b/docs/advance/distributed/README.md @@ -0,0 +1,8 @@ +## 目录 + +- [全局唯一ID生成方案](./1-global-unique-id.md) +- [分布式锁](./2-distributed-lock.md) +- [RPC](./3-rpc.md) +- [微服务](./4-micro-service.md) +- [分布式架构,微服务、限流、熔断....](./6-distibuted-arch.md) +- [分布式事务](./7-distributed-transaction.md) diff --git a/docs/advance/distributed/article/distributed-lock.md b/docs/advance/distributed/article/distributed-lock.md new file mode 100644 index 0000000..2e1c0fd --- /dev/null +++ b/docs/advance/distributed/article/distributed-lock.md @@ -0,0 +1,351 @@ +--- +sidebar: heading +title: 分布式锁 +category: 分布式 +tag: + - 分布式ID +head: + - - meta + - name: keywords + content: 分布式锁,Redis分布式锁,zookeeper分布式锁,redlock + - - meta + - name: description + content: 分布式锁常见面试题总结,让天下没有难背的八股文! +--- + +## 分布式锁怎么实现? + +一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高? + +## Redis 分布式锁 + +Redis 分布式锁有 3 个重要的考量点: + +- 互斥(只能有一个客户端获取锁) +- 不能死锁 +- 容错(只要大部分 Redis 节点创建了这把锁就可以) + +### Redis 最普通的分布式锁 + +第一个最普通的实现方式,就是在 Redis 里使用 `SET key value [EX seconds] [PX milliseconds] NX` 创建一个 key,这样就算加锁。其中: + +- `NX`:表示只有 `key` 不存在的时候才会设置成功,如果此时 redis 中存在这个 `key`,那么设置失败,返回 `nil`。 +- `EX seconds`:设置 `key` 的过期时间,精确到秒级。意思是 `seconds` 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。 +- `PX milliseconds`:同样是设置 `key` 的过期时间,精确到毫秒级。 + +比如执行以下命令: + +```bash +SET resource_name my_random_value PX 30000 NX +``` + +释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除: + +```lua +-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。 +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +为啥要用 `random_value` 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 `lua` 脚本来释放锁。 + +但是这样是肯定不行的。因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。 + +### RedLock 算法 + +这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁: + +1. 获取当前时间戳,单位是毫秒; +2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在 `5~50` 毫秒范围内); +3. 尝试在**大多数节点**上建立一个锁,比如 5 个节点就要求是 3 个节点 `n / 2 + 1` ; +4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了; +5. 要是锁建立失败了,那么就依次之前建立过的锁删除; +6. 只要别人建立了一把分布式锁,你就得**不断轮询去尝试获取锁**。 + +![](http://img.topjavaer.cn/img/redis-redlock.png) + +[Redis 官方](https://redis.io/)给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:https://redis.io/topics/distlock 。 + +## zk 分布式锁 + +zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能**注册个监听器**监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。 + +```java +/** + * ZooKeeperSession + */ +public class ZooKeeperSession { + + private static CountDownLatch connectedSemaphore = new CountDownLatch(1); + + private ZooKeeper zookeeper; + private CountDownLatch latch; + + public ZooKeeperSession() { + try { + this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher()); + try { + connectedSemaphore.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("ZooKeeper session established......"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取分布式锁 + * + * @param productId + */ + public Boolean acquireDistributedLock(Long productId) { + String path = "/product-lock-" + productId; + + try { + zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + return true; + } catch (Exception e) { + while (true) { + try { + // 相当于是给node注册一个监听器,去看看这个监听器是否存在 + Stat stat = zk.exists(path, true); + + if (stat != null) { + this.latch = new CountDownLatch(1); + this.latch.await(waitTime, TimeUnit.MILLISECONDS); + this.latch = null; + } + zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); + return true; + } catch (Exception ee) { + continue; + } + } + + } + return true; + } + + /** + * 释放掉一个分布式锁 + * + * @param productId + */ + public void releaseDistributedLock(Long productId) { + String path = "/product-lock-" + productId; + try { + zookeeper.delete(path, -1); + System.out.println("release the lock for product[id=" + productId + "]......"); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 建立 zk session 的 watcher + */ + private class ZooKeeperWatcher implements Watcher { + + public void process(WatchedEvent event) { + System.out.println("Receive watched event: " + event.getState()); + + if (KeeperState.SyncConnected == event.getState()) { + connectedSemaphore.countDown(); + } + + if (this.latch != null) { + this.latch.countDown(); + } + } + + } + + /** + * 封装单例的静态内部类 + */ + private static class Singleton { + + private static ZooKeeperSession instance; + + static { + instance = new ZooKeeperSession(); + } + + public static ZooKeeperSession getInstance() { + return instance; + } + + } + + /** + * 获取单例 + * + * @return + */ + public static ZooKeeperSession getInstance() { + return Singleton.getInstance(); + } + + /** + * 初始化单例的便捷方法 + */ + public static void init() { + getInstance(); + } + +} +``` + +也可以采用另一种方式,创建临时顺序节点: + +如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听**排在自己前面**的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,自己就可以获取到了锁,就可以执行代码了。 + +```java +public class ZooKeeperDistributedLock implements Watcher { + + private ZooKeeper zk; + private String locksRoot = "/locks"; + private String productId; + private String waitNode; + private String lockNode; + private CountDownLatch latch; + private CountDownLatch connectedLatch = new CountDownLatch(1); + private int sessionTimeout = 30000; + + public ZooKeeperDistributedLock(String productId) { + this.productId = productId; + try { + String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181"; + zk = new ZooKeeper(address, sessionTimeout, this); + connectedLatch.await(); + } catch (IOException e) { + throw new LockException(e); + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + } + + public void process(WatchedEvent event) { + if (event.getState() == KeeperState.SyncConnected) { + connectedLatch.countDown(); + return; + } + + if (this.latch != null) { + this.latch.countDown(); + } + } + + public void acquireDistributedLock() { + try { + if (this.tryLock()) { + return; + } else { + waitForLock(waitNode, sessionTimeout); + } + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + } + + public boolean tryLock() { + try { + // 传入进去的locksRoot + “/” + productId + // 假设productId代表了一个商品id,比如说1 + // locksRoot = locks + // /locks/10000000000,/locks/10000000001,/locks/10000000002 + lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); + + // 看看刚创建的节点是不是最小的节点 + // locks:10000000000,10000000001,10000000002 + List locks = zk.getChildren(locksRoot, false); + Collections.sort(locks); + + if (lockNode.equals(locksRoot + "/" + locks.get(0))) { + // 如果是最小的节点,则表示取得锁 + return true; + } + + // 如果不是最小的节点,找到比自己小1的节点 + int previousLockIndex = -1; + for (int i = 0; i < locks.size(); i++) { + if (lockNode.equals(locksRoot + "/" +locks.get(i))){ + previousLockIndex = i - 1; + break; + } + } + + this.waitNode = locks.get(previousLockIndex); + } catch (KeeperException e) { + throw new LockException(e); + } catch (InterruptedException e) { + throw new LockException(e); + } + return false; + } + + private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException { + Stat stat = zk.exists(locksRoot + "/" + waitNode, true); + if (stat != null) { + this.latch = new CountDownLatch(1); + this.latch.await(waitTime, TimeUnit.MILLISECONDS); + this.latch = null; + } + return true; + } + + public void unlock() { + try { + // 删除/locks/10000000000节点 + // 删除/locks/10000000001节点 + System.out.println("unlock " + lockNode); + zk.delete(lockNode, -1); + lockNode = null; + zk.close(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (KeeperException e) { + e.printStackTrace(); + } + } + + public class LockException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public LockException(String e) { + super(e); + } + + public LockException(Exception e) { + super(e); + } + } +} +``` + +但是,使用 zk 临时节点会存在另一个问题:由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。 + +针对这种情况,可以通过 JVM 调优,尽量避免长时间 GC 的情况发生。 + +## redis 分布式锁和 zk 分布式锁的对比 + +- redis 分布式锁,其实**需要自己不断去尝试获取锁**,比较消耗性能。 +- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。 + +另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。 + +总体上来说,zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。 + + + +参考链接:https://doocs.github.io/advanced-java diff --git a/docs/advance/excellent-article/1-redis-stock-minus.md b/docs/advance/excellent-article/1-redis-stock-minus.md new file mode 100644 index 0000000..63215c8 --- /dev/null +++ b/docs/advance/excellent-article/1-redis-stock-minus.md @@ -0,0 +1,332 @@ +--- +sidebar: heading +title: Redis 如何实现库存扣减操作和防止被超卖? +category: 优质文章 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis,库存扣减,超卖问题 + - - meta + - name: description + content: 优质文章汇总 +--- + +# Redis 如何实现库存扣减操作和防止被超卖? + +电商当项目经验已经非常普遍了,不管你是包装的还是真实的,起码要能讲清楚电商中常见的问题,比如库存的操作怎么防止商品被超卖 + +解决方案: +- 基于数据库单库存 +- 基于数据库多库存 +- 基于redis + +基于redis实现扣减库存的具体实现 +- 初始化库存回调函数(IStockCallback) +- 扣减库存服务(StockService) +- 调用 + +![](http://img.topjavaer.cn/img/库存扣减1.png) + +------ + +在日常开发中有很多地方都有类似扣减库存的操作,比如电商系统中的商品库存,抽奖系统中的奖品库存等。 + +## **解决方案** + +1. 使用mysql数据库,使用一个字段来存储库存,每次扣减库存去更新这个字段。 +2. 还是使用数据库,但是将库存分层多份存到多条记录里面,扣减库存的时候路由一下,这样子增大了并发量,但是还是避免不了大量的去访问数据库来更新库存。 +3. 将库存放到redis使用redis的incrby特性来扣减库存。 + +## **分析** + +在上面的第一种和第二种方式都是基于数据来扣减库存。 + +### 基于数据库单库存 + +第一种方式在所有请求都会在这里等待锁,获取锁有去扣减库存。在并发量不高的情况下可以使用,但是一旦并发量大了就会有大量请求阻塞在这里,导致请求超时,进而整个系统雪崩;而且会频繁的去访问数据库,大量占用数据库资源,所以在并发高的情况下这种方式不适用。 + +### 基于数据库多库存 + +第二种方式其实是第一种方式的优化版本,在一定程度上提高了并发量,但是在还是会大量的对数据库做更新操作大量占用数据库资源。 + +**基于数据库来实现扣减库存还存在的一些问题:** + +- 用数据库扣减库存的方式,扣减库存的操作必须在一条语句中执行,不能先selec在update,这样在并发下会出现超扣的情况。如: + +``` +update number set x=x-1 where x > 0 +``` + +- MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。 +- 当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。 + +### 基于redis + +针对上述问题的问题我们就有了第三种方案,将库存放到缓存,利用redis的incrby特性来扣减库存,解决了超扣和性能问题。但是一旦缓存丢失需要考虑恢复方案。比如抽奖系统扣奖品库存的时候,初始库存=总的库存数-已经发放的奖励数,但是如果是异步发奖,需要等到MQ消息消费完了才能重启redis初始化库存,否则也存在库存不一致的问题。 + +## **基于redis实现扣减库存的具体实现** + +- 我们使用redis的lua脚本来实现扣减库存 +- 由于是分布式环境下所以还需要一个分布式锁来控制只能有一个服务去初始化库存 +- 需要提供一个回调函数,在初始化库存的时候去调用这个函数获取初始化库存 + +### 初始化库存回调函数(IStockCallback ) + +``` +/** + * 获取库存回调 + */ +public interface IStockCallback { + + /** + * 获取库存 + * @return + */ + int getStock(); +} +``` + +### 扣减库存服务(StockService) + +``` +/** + * 扣库存 + * + */ +@Service +public class StockService { + Logger logger = LoggerFactory.getLogger(StockService.class); + + /** + * 不限库存 + */ + public static final long UNINITIALIZED_STOCK = -3L; + + /** + * Redis 客户端 + */ + @Autowired + private RedisTemplate redisTemplate; + + /** + * 执行扣库存的脚本 + */ + public static final String STOCK_LUA; + + static { + /** + * + * @desc 扣减库存Lua脚本 + * 库存(stock)-1:表示不限库存 + * 库存(stock)0:表示没有库存 + * 库存(stock)大于0:表示剩余库存 + * + * @params 库存key + * @return + * -3:库存未初始化 + * -2:库存不足 + * -1:不限库存 + * 大于等于0:剩余库存(扣减之后剩余的库存) + * redis缓存的库存(value)是-1表示不限库存,直接返回1 + */ + StringBuilder sb = new StringBuilder(); + sb.append("if (redis.call('exists', KEYS[1]) == 1) then"); + sb.append(" local stock = tonumber(redis.call('get', KEYS[1]));"); + sb.append(" local num = tonumber(ARGV[1]);"); + sb.append(" if (stock == -1) then"); + sb.append(" return -1;"); + sb.append(" end;"); + sb.append(" if (stock >= num) then"); + sb.append(" return redis.call('incrby', KEYS[1], 0 - num);"); + sb.append(" end;"); + sb.append(" return -2;"); + sb.append("end;"); + sb.append("return -3;"); + STOCK_LUA = sb.toString(); + } + + /** + * @param key 库存key + * @param expire 库存有效时间,单位秒 + * @param num 扣减数量 + * @param stockCallback 初始化库存回调函数 + * @return -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存 + */ + public long stock(String key, long expire, int num, IStockCallback stockCallback) { + long stock = stock(key, num); + // 初始化库存 + if (stock == UNINITIALIZED_STOCK) { + RedisLock redisLock = new RedisLock(redisTemplate, key); + try { + // 获取锁 + if (redisLock.tryLock()) { + // 双重验证,避免并发时重复回源到数据库 + stock = stock(key, num); + if (stock == UNINITIALIZED_STOCK) { + // 获取初始化库存 + final int initStock = stockCallback.getStock(); + // 将库存设置到redis + redisTemplate.opsForValue().set(key, initStock, expire, TimeUnit.SECONDS); + // 调一次扣库存的操作 + stock = stock(key, num); + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + redisLock.unlock(); + } + + } + return stock; + } + + /** + * 加库存(还原库存) + * + * @param key 库存key + * @param num 库存数量 + * @return + */ + public long addStock(String key, int num) { + + return addStock(key, null, num); + } + + /** + * 加库存 + * + * @param key 库存key + * @param expire 过期时间(秒) + * @param num 库存数量 + * @return + */ + public long addStock(String key, Long expire, int num) { + boolean hasKey = redisTemplate.hasKey(key); + // 判断key是否存在,存在就直接更新 + if (hasKey) { + return redisTemplate.opsForValue().increment(key, num); + } + + Assert.notNull(expire,"初始化库存失败,库存过期时间不能为null"); + RedisLock redisLock = new RedisLock(redisTemplate, key); + try { + if (redisLock.tryLock()) { + // 获取到锁后再次判断一下是否有key + hasKey = redisTemplate.hasKey(key); + if (!hasKey) { + // 初始化库存 + redisTemplate.opsForValue().set(key, num, expire, TimeUnit.SECONDS); + } + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } finally { + redisLock.unlock(); + } + + return num; + } + + /** + * 获取库存 + * + * @param key 库存key + * @return -1:不限库存; 大于等于0:剩余库存 + */ + public int getStock(String key) { + Integer stock = (Integer) redisTemplate.opsForValue().get(key); + return stock == null ? -1 : stock; + } + + /** + * 扣库存 + * + * @param key 库存key + * @param num 扣减库存数量 + * @return 扣减之后剩余的库存【-3:库存未初始化; -2:库存不足; -1:不限库存; 大于等于0:扣减库存之后的剩余库存】 + */ + private Long stock(String key, int num) { + // 脚本里的KEYS参数 + List keys = new ArrayList<>(); + keys.add(key); + // 脚本里的ARGV参数 + List args = new ArrayList<>(); + args.add(Integer.toString(num)); + + long result = redisTemplate.execute(new RedisCallback() { + @Override + public Long doInRedis(RedisConnection connection) throws DataAccessException { + Object nativeConnection = connection.getNativeConnection(); + // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 + // 集群模式 + if (nativeConnection instanceof JedisCluster) { + return (Long) ((JedisCluster) nativeConnection).eval(STOCK_LUA, keys, args); + } + + // 单机模式 + else if (nativeConnection instanceof Jedis) { + return (Long) ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args); + } + return UNINITIALIZED_STOCK; + } + }); + return result; + } + +} +``` + +### 调用 + +``` +@RestController +public class StockController { + + @Autowired + private StockService stockService; + + @RequestMapping(value = "stock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Object stock() { + // 商品ID + long commodityId = 1; + // 库存ID + String redisKey = "redis_key:stock:" + commodityId; + long stock = stockService.stock(redisKey, 60 * 60, 2, () -> initStock(commodityId)); + return stock >= 0; + } + + /** + * 获取初始的库存 + * + * @return + */ + private int initStock(long commodityId) { + // TODO 这里做一些初始化库存的操作 + return 1000; + } + + @RequestMapping(value = "getStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Object getStock() { + // 商品ID + long commodityId = 1; + // 库存ID + String redisKey = "redis_key:stock:" + commodityId; + + return stockService.getStock(redisKey); + } + + @RequestMapping(value = "addStock", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public Object addStock() { + // 商品ID + long commodityId = 2; + // 库存ID + String redisKey = "redis_key:stock:" + commodityId; + + return stockService.addStock(redisKey, 2); + } +} +``` + diff --git a/docs/advance/excellent-article/10-file-upload.md b/docs/advance/excellent-article/10-file-upload.md new file mode 100644 index 0000000..0064765 --- /dev/null +++ b/docs/advance/excellent-article/10-file-upload.md @@ -0,0 +1,354 @@ +--- +sidebar: heading +title: 大文件上传时如何做到秒传? +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 大文件上传优化,文件上传 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 大文件上传时如何做到秒传? + +大家好,我是大彬~ + +文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。 + +那有没有比较好的上传体验呢,答案有的,就是下边要介绍的几种上传方式 + +## 秒传 + +### 1、什么是秒传 + +通俗的说,你把要上传的东西上传,服务器会先做**MD5**校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让**MD5**改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了. + +### 2、本文实现的秒传核心逻辑 + +**a**、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位, + +**b**、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径 + +## 分片上传 + +### 1、什么是分片上传 + +分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。 + +### 2、分片上传的场景 + +1.大文件上传 + +2.网络环境环境不好,存在需要重传风险的场景 + +## 断点续传 + +### 1、什么是断点续传 + +断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。 + +#### 2、应用场景 + +断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。 + +#### 3、实现断点续传的核心逻辑 + +在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。 + +为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。 + +#### 4、实现流程步骤 + +a、方案一,常规步骤 + +- 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块; +- 初始化一个分片上传任务,返回本次分片上传唯一标识; +- 按照一定的策略(串行或并行)发送各个分片数据块; +- 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。 + +b、方案二、本文实现的步骤 + +- 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小 +- 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤) +- 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。 + +#### 5、分片上传/断点上传代码实现 + +a、前端采用百度提供的webuploader的插件,进行分片。因本文主要介绍服务端代码实现,webuploader如何进行分片,具体实现可以查看如下链接: + +> http://fex.baidu.com/webuploader/getting-started.html + +b、后端用两种方式实现文件写入,一种是用**RandomAccessFile**,如果对**RandomAccessFile**不熟悉的朋友,可以查看如下链接: + +> https://blog.csdn.net/dimudan2015/article/details/81910690 + +另一种是使用**MappedByteBuffer**,对**MappedByteBuffer**不熟悉的朋友,可以查看如下链接进行了解: + +> https://www.jianshu.com/p/f90866dcbffc + +## 后端进行写入操作的核心代码 + +### 1、RandomAccessFile实现方式 + +``` +@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS) +@Slf4j +public class RandomAccessUploadStrategy extends SliceUploadTemplate { + + @Autowired + private FilePathUtil filePathUtil; + + @Value("${upload.chunkSize}") + private long defaultChunkSize; + + @Override + public boolean upload(FileUploadRequestDTO param) { + RandomAccessFile accessTmpFile = null; + try { + String uploadDirPath = filePathUtil.getPath(param); + File tmpFile = super.createTmpFile(param); + accessTmpFile = new RandomAccessFile(tmpFile, "rw"); + //这个必须与前端设定的值一致 + long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 + : param.getChunkSize(); + long offset = chunkSize * param.getChunk(); + //定位到该分片的偏移量 + accessTmpFile.seek(offset); + //写入该分片数据 + accessTmpFile.write(param.getFile().getBytes()); + boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); + return isOk; + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + FileUtil.close(accessTmpFile); + } + return false; + } + +} +``` + +### 2、MappedByteBuffer实现方式 + +``` +@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER) +@Slf4j +public class MappedByteBufferUploadStrategy extends SliceUploadTemplate { + + @Autowired + private FilePathUtil filePathUtil; + + @Value("${upload.chunkSize}") + private long defaultChunkSize; + + @Override + public boolean upload(FileUploadRequestDTO param) { + + RandomAccessFile tempRaf = null; + FileChannel fileChannel = null; + MappedByteBuffer mappedByteBuffer = null; + try { + String uploadDirPath = filePathUtil.getPath(param); + File tmpFile = super.createTmpFile(param); + tempRaf = new RandomAccessFile(tmpFile, "rw"); + fileChannel = tempRaf.getChannel(); + + long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 + : param.getChunkSize(); + //写入该分片数据 + long offset = chunkSize * param.getChunk(); + byte[] fileData = param.getFile().getBytes(); + mappedByteBuffer = fileChannel +.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length); + mappedByteBuffer.put(fileData); + boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath); + return isOk; + + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + + FileUtil.freedMappedByteBuffer(mappedByteBuffer); + FileUtil.close(fileChannel); + FileUtil.close(tempRaf); + + } + + return false; + } + +} +``` + +### 3、文件操作核心模板类代码 + +``` +@Slf4j +public abstract class SliceUploadTemplate implements SliceUploadStrategy { + + public abstract boolean upload(FileUploadRequestDTO param); + + protected File createTmpFile(FileUploadRequestDTO param) { + + FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class); + param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath())); + String fileName = param.getFile().getOriginalFilename(); + String uploadDirPath = filePathUtil.getPath(param); + String tempFileName = fileName + "_tmp"; + File tmpDir = new File(uploadDirPath); + File tmpFile = new File(uploadDirPath, tempFileName); + if (!tmpDir.exists()) { + tmpDir.mkdirs(); + } + return tmpFile; + } + + @Override + public FileUploadDTO sliceUpload(FileUploadRequestDTO param) { + + boolean isOk = this.upload(param); + if (isOk) { + File tmpFile = this.createTmpFile(param); + FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile); + return fileUploadDTO; + } + String md5 = FileMD5Util.getFileMD5(param.getFile()); + + Map map = new HashMap<>(); + map.put(param.getChunk(), md5); + return FileUploadDTO.builder().chunkMd5Info(map).build(); + } + + /** + * 检查并修改文件上传进度 + */ + public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) { + + String fileName = param.getFile().getOriginalFilename(); + File confFile = new File(uploadDirPath, fileName + ".conf"); + byte isComplete = 0; + RandomAccessFile accessConfFile = null; + try { + accessConfFile = new RandomAccessFile(confFile, "rw"); + //把该分段标记为 true 表示完成 + System.out.println("set part " + param.getChunk() + " complete"); + //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127 + accessConfFile.setLength(param.getChunks()); + accessConfFile.seek(param.getChunk()); + accessConfFile.write(Byte.MAX_VALUE); + + //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传) + byte[] completeList = FileUtils.readFileToByteArray(confFile); + isComplete = Byte.MAX_VALUE; + for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) { + //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE + isComplete = (byte) (isComplete & completeList[i]); + System.out.println("check part " + i + " complete?:" + completeList[i]); + } + + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + FileUtil.close(accessConfFile); + } + boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete); + return isOk; + } + + /** + * 把上传进度信息存进redis + */ + private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath, + String fileName, File confFile, byte isComplete) { + + RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class); + if (isComplete == Byte.MAX_VALUE) { + redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true"); + redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5()); + confFile.delete(); + return true; + } else { + if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) { + redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false"); + redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(), + uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf"); + } + + return false; + } + } +/** + * 保存文件操作 + */ + public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) { + + FileUploadDTO fileUploadDTO = null; + + try { + + fileUploadDTO = renameFile(tmpFile, fileName); + if (fileUploadDTO.isUploadComplete()) { + System.out + .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName); + //TODO 保存文件信息到数据库 + + } + + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + + } + return fileUploadDTO; + } +/** + * 文件重命名 + * + * @param toBeRenamed 将要修改名字的文件 + * @param toFileNewName 新的名字 + */ + private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) { + //检查要重命名的文件是否存在,是否是文件 + FileUploadDTO fileUploadDTO = new FileUploadDTO(); + if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) { + log.info("File does not exist: {}", toBeRenamed.getName()); + fileUploadDTO.setUploadComplete(false); + return fileUploadDTO; + } + String ext = FileUtil.getExtension(toFileNewName); + String p = toBeRenamed.getParent(); + String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName; + File newFile = new File(filePath); + //修改文件名 + boolean uploadFlag = toBeRenamed.renameTo(newFile); + + fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp()); + fileUploadDTO.setUploadComplete(uploadFlag); + fileUploadDTO.setPath(filePath); + fileUploadDTO.setSize(newFile.length()); + fileUploadDTO.setFileExt(ext); + fileUploadDTO.setFileId(toFileNewName); + + return fileUploadDTO; + } +} +``` + +## 总结 + +在实现分片上传的过程,需要前端和后端配合,比如前后端的上传块号的文件大小,前后端必须得要一致,否则上传就会有问题。其次文件相关操作正常都是要搭建一个文件服务器的,比如使用**fastdfs**、**hdfs**等。 + +本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的**md5**值计算,后端写入的速度还是比较快。 + +如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器。 + +阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。 + +文末提供一个oss表单上传的链接demo,通过oss表单上传,可以直接从前端把文件上传到oss服务器,把上传的压力都推给oss服务器: + +> https://www.cnblogs.com/ossteam/p/4942227.html diff --git a/docs/advance/excellent-article/11-8-architect-pattern.md b/docs/advance/excellent-article/11-8-architect-pattern.md new file mode 100644 index 0000000..51d03d8 --- /dev/null +++ b/docs/advance/excellent-article/11-8-architect-pattern.md @@ -0,0 +1,18 @@ +--- +sidebar: heading +title: 8种架构模式 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构模式 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 8种架构模式 + +[8种架构模式](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247490779&idx=2&sn=eff9e8cf9b15c29630514a137f102701&chksm=ce98e19df9ef688bd9c7b775658c704a51b7961347a7aabf70e6c555cb57560aa5e8b1e497a1&token=1170645384&lang=zh_CN#rd) diff --git a/docs/advance/excellent-article/12-mysql-table-max-rows.md b/docs/advance/excellent-article/12-mysql-table-max-rows.md new file mode 100644 index 0000000..0fa64c0 --- /dev/null +++ b/docs/advance/excellent-article/12-mysql-table-max-rows.md @@ -0,0 +1,227 @@ +--- +sidebar: heading +title: MySQL最大建议行数 2000w,靠谱吗? +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: MySQL最大行数 + - - meta + - name: description + content: 优质文章汇总 +--- + +# MySQL最大建议行数 2000w,靠谱吗? + +## **1 背景** + +作为在后端圈开车的多年老司机,是不是经常听到过,“mysql 单表最好不要超过 2000w”,“单表超过 2000w 就要考虑数据迁移了”,“你这个表数据都马上要到 2000w 了,难怪查询速度慢” + +这些名言民语就和 “群里只讨论技术,不开车,开车速度不要超过 120 码,否则自动踢群”,只听过,没试过,哈哈。 + +下面我们就把车速踩到底,干到 180 码试试……. + +## **2 实验** + +实验一把看看… + +建一张表 + +``` +CREATE TABLE person( +id int NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键', +person_id tinyint not null comment '用户id', +person_name VARCHAR(200) comment '用户名称', +gmt_create datetime comment '创建时间', +gmt_modified datetime comment '修改时间' +) comment '人员信息表'; +``` + +插入一条数据 + +``` +insert into person values(1,1,'user_1', NOW(), now()); +``` + +利用 mysql 伪列 rownum 设置伪列起始点为 1 + +``` +select (@i:=@i+1) as rownum, person_name from person, (select @i:=100) as init; +set @i=1; +``` + +运行下面的 sql,连续执行 20 次,就是 2 的 20 次方约等于 100w 的数据;执行 23 次就是 2 的 23 次方约等于 800w , 如此下去即可实现千万测试数据的插入,如果不想翻倍翻倍的增加数据,而是想少量,少量的增加,有个技巧,就是在 SQL 的后面增加 where 条件,如 id > 某一个值去控制增加的数据量即可。 + +``` +insert into person(id, person_id, person_name, gmt_create, gmt_modified) + +select @i:=@i+1, + +left(rand()*10,10) as person_id, + +concat('user_',@i%2048), + +date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND), + +date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND) + +from person; +``` + +此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:`The total number of locks exceeds the lock table size`,这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。 + +``` +SET GLOBAL tmp_table_size =512*1024*1024; (512M) +SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G); +``` + +先来看一组测试数据,这组数据是在 mysql8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。 + +![](http://img.topjavaer.cn/img/max-row1.png) + +![](http://img.topjavaer.cn/img/max-row2.png) + +看到这组数据似乎好像真的和标题对应,当数据达到 2000w 以后,查询时长急剧上升;难道这就是铁律吗? + +那下面我们就来看看这个建议值 2kw 是怎么来的? + +## **3 单表数量限制** + +首先我们先想想数据库单表行数最大多大? + +``` +CREATE TABLE person( +id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键', +person_id tinyint not null comment '用户id', +person_name VARCHAR(200) comment '用户名称', +gmt_create datetime comment '创建时间', +gmt_modified datetime comment '修改时间' +) comment '人员信息表'; +``` + +看看上面的建表 sql,id 是主键,本身就是唯一的,也就是说主键的大小可以限制表的上限,如果主键声明 int 大小,也就是 32 位,那么支持 `2^32-1 ~~21` 亿;如果是 bigint,那就是 `2^62-1` ?(`36893488147419103232`),难以想象这个的多大了,一般还没有到这个限制之前,可能数据库已经爆满了!! + +有人统计过,如果建表的时候,自增字段选择无符号的 bigint , 那么自增长最大值是 `18446744073709551615`,按照一秒新增一条记录的速度,大约什么时候能用完? + +![](http://img.topjavaer.cn/img/max-row3.png) + +## **4 表空间** + +下面我们再来看看索引的结构,对了,我们下面讲内容都是基于 Innodb 引擎的,大家都知道 Innodb 的索引内部用的是 B+ 树 + +![](http://img.topjavaer.cn/img/max-row4.png) + +这张表数据,在硬盘上存储也是类似如此的,它实际是放在一个叫 `person.ibd` (innodb data)的文件中,也叫做表空间;虽然数据表中,他们看起来是一条连着一条,但是实际上在文件中它被分成很多小份的数据页,而且每一份都是 16K。大概就像下面这样,当然这只是我们抽象出来的,在表空间中还有段、区、组等很多概念,但是我们需要跳出来看。 + +![](http://img.topjavaer.cn/img/max-row5.png) + +## **5 页的数据结构** + +因为每个页只有 16K 的大小,但是如果数据很多,那一页肯定就放不下这些数据,那数据肯定就会被分到其他的页中,所以为了把这些页关联起来,肯定就会有记录前后页地址,方便找到对应页;同时每页都是唯一的,那就会需要有一个唯一标志来标记页,就是页号; + +页中会记录数据所以会存在读写操作,读写操作会存在中断或者其他异常导致数据不全等,那就会需要有校验机制,所以里面还有会校验码,而读操作最重要的就是效率问题,如果按照记录一个个进行遍历,那肯定是很费劲的,所以这里面还会为数据生成对应的页目录(`Page Directory`); 所以实际页的内部结构像是下面这样的。 + +![](http://img.topjavaer.cn/img/max-row6.png) + +从图中可以看出,一个 InnoDB 数据页的存储空间大致被划分成了 7 个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。 + +在页的 7 个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 `User Records` 部分。 + +但是在一开始生成页的时候,其实并没有 `User Records` 这个部分,每当我们插入一条记录,都会从 `Free Space` 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 `User Records` 部分,当 `Free Space` 部分的空间全部被 `User Records` 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。 + +这个过程的图示如下: + +![](http://img.topjavaer.cn/img/max-row7.png) + +刚刚上面说到了数据的新增的过程。 + +那下面就来说说,数据的查找过程,假如我们需要查找一条记录,我们可以把表空间中的每一页都加载到内存中,然后对记录挨个判断是不是我们想要的,在数据量小的时候,没啥问题,内存也可以撑;但是现实就是这么残酷,不会给你这个局面;为了解决这问题,mysql 中就有了索引的概念;大家都知道索引能够加快数据的查询,那到底是怎么个回事呢?下面我就来看看。 + +## **6 索引的数据结构** + +在 mysql 中索引的数据结构和刚刚描述的页几乎是一模一样的,而且大小也是 16K, 但是在索引页中记录的是页 (数据页,索引页) 的最小主键 id 和页号,以及在索引页中增加了层级的信息,从 0 开始往上算,所以页与页之间就有了上下层级的概念。 + +![](http://img.topjavaer.cn/img/max-row8.png)看到这个图之后,是不是有点似曾相似的感觉,是不是像一棵二叉树啊,对,没错!它就是一棵树,只不过我们在这里只是简单画了三个节点,2 层结构的而已,如果数据多了,可能就会扩展到 3 层的树,这个就是我们常说的 B+ 树,最下面那一层的 page level =0, 也就是叶子节点,其余都是非叶子节点。 + +![](http://img.topjavaer.cn/img/max-row9.png) + +看上图中,我们是单拿一个节点来看,首先它是一个非叶子节点(索引页),在它的内容区中有 id 和 页号地址两部分,这个 id 是对应页中记录的最小记录 id 值,页号地址是指向对应页的指针;而数据页与此几乎大同小异,区别在于数据页记录的是真实的行数据而不是页地址,而且 id 的也是顺序的。 + +## **7 单表建议值** + +下面我们就以 3 层,2 分叉(实际中是 M 分叉)的图例来说明一下查找一个行数据的过程。 + +比如说我们需要查找一个 id=6 的行数据,因为在非叶子节点中存放的是页号和该页最小的 id,所以我们从顶层开始对比,首先看页号 10 中的目录,有 `[id=1, 页号 = 20]`,`[id=5, 页号 = 30]`, 说明左侧节点最小 id 为 1,右侧节点最小 id 是 5;`6>5`, 那按照二分法查找的规则,肯定就往右侧节点继续查找,找到页号 30 的节点后,发现这个节点还有子节点(非叶子节点),那就继续比对,同理,`6>5&&6<7`, 所以找到了页号 60,找到页号 60 之后,发现此节点为叶子节点(数据节点),于是将此页数据加载至内存进行一一对比,结果找到了 `id=6 `的数据行。 + +从上述的过程中发现,我们为了查找 id=6 的数据,总共查询了三个页,如果三个页都在磁盘中(未提前加载至内存),那么最多需要经历三次的磁盘 IO。 + +需要注意的是,图中的页号只是个示例,实际情况下并不是连续的,在磁盘中存储也不一定是顺序的。 + +![](http://img.topjavaer.cn/img/max-row10.png)至此, + +我们大概已经了解了表的数据是怎么个结构了,也大概知道查询数据是个怎么的过程了,这样我们也就能大概估算这样的结构能存放多少数据了。 + +从上面的图解我们知道 B+ 数的叶子节点才是存在数据的,而非叶子节点是用来存放索引数据的。 + +所以,同样一个 16K 的页,非叶子节点里的每条数据都指向新的页,而新的页有两种可能 + +- 如果是叶子节点,那么里面就是一行行的数据 +- 如果是非叶子节点的话,那么就会继续指向新的页 + +假设 + +- 非叶子节点内指向其他页的数量为 x +- 叶子节点内能容纳的数据行数为 y +- B+ 数的层数为 z + +如下图中所示 + +`Total =x^(z-1) *y `也就是说总数会等于 x 的 `z-1` 次方 与 Y 的乘积。 + +![](http://img.topjavaer.cn/img/max-row11.png) + +> X =? + +在文章的开头已经介绍了页的结构,索引也也不例外,都会有 `File Header (38 byte)`、`Page Header (56 Byte)`、`Infimum + Supermum(26 byte)`、`File Trailer(8byte)`, 再加上页目录,大概 1k 左右,我们就当做它就是 1K, 那整个页的大小是 16K, 剩下 15k 用于存数据,在索引页中主要记录的是主键与页号,主键我们假设是 Bigint (8 byte), 而页号也是固定的(4Byte), 那么索引页中的一条数据也就是 12byte; 所以 `x=15*1024/12≈1280` 行。 + +> Y=? + +叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是 15k;但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量;每行数据占用空间越大,页中所放的行数量就会越少;这边我们暂时按一条行数据 1k 来算,那一页就能存下 15 条,`Y≈15`。 + +算到这边了,是不是心里已经有谱了啊 + +- 根据上述的公式,`Total =x^(z-1) y`,已知 `x=1280,y=15` +- 假设 B+ 树是两层,那就是 Z =2, `Total = (1280 ^1 )15 = 19200` +- 假设 B+ 树是三层,那就是 Z =3, `Total = (1280 ^2) *15 = 24576000 (约 2.45kw)` + +哎呀,妈呀!这不是正好就是文章开头说的最大行数建议值 2000w 嘛!对的,一般 B+ 数的层级最多也就是 3 层,你试想一下,如果是 4 层,除了查询的时候磁盘 IO 次数会增加,而且这个 Total 值会是多少,大概应该是 3 百多亿吧,也不太合理,所以,3 层应该是比较合理的一个值。 + +到这里难道就完了? + +no + +我们刚刚在说 Y 的值时候假设的是 1K ,那比如我实际当行的数据占用空间不是 1K , 而是 5K, 那么单个数据页最多只能放下 3 条数据 + +同样,还是按照 Z=3 的值来计算,那 `Total = (1280 ^2) *3 = 4915200 (近 500w)` + +所以,在保持相同的层级(相似查询性能)的情况下,在行数据大小不同的情况下,其实这个最大建议值也是不同的,而且影响查询性能的还有很多其他因素,比如,数据库版本,服务器配置,sql 的编写等等,MySQL 为了提高性能,会将表的索引装载到内存中。在 `InnoDB buffer size` 足够的情况下,其能完成全加载进内存,查询不会有问题。 + +但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降,所以增加硬件配置(比如把内存当磁盘使),可能会带来立竿见影的性能提升哈。 + +## **8 总结** + +Mysql 的表数据是以页的形式存放的,页在磁盘中不一定是连续的。 + +页的空间是 16K, 并不是所有的空间都是用来存放数据的,会有一些固定的信息,如,页头,页尾,页码,校验码等等。在 B+ 树中,叶子节点和非叶子节点的数据结构是一样的,区别在于,叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号。 + +索引结构不会影响单表最大行数,2kw 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。 + +## **9 参考** + +- *https://www.jianshu.com/p/cf5d381ef637* +- *https://www.modb.pro/db/139052* +- *《MYSQL 内核:INNODB 存储引擎 卷 1》* + +*来源:my.oschina.net/u/4090830/blog/5559454* diff --git a/docs/advance/excellent-article/13-order-by-work.md b/docs/advance/excellent-article/13-order-by-work.md new file mode 100644 index 0000000..7b73773 --- /dev/null +++ b/docs/advance/excellent-article/13-order-by-work.md @@ -0,0 +1,278 @@ +--- +sidebar: heading +title: order by是怎么工作的? +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: order by,MySQL,数据库排序 + - - meta + - name: description + content: 优质文章汇总 +--- + +# order by是怎么工作的? + +在你开发应用的时候,一定会经常碰到需要根据指定的字段排序来显示结果的需求。还是以我们前面举例用过的市民表为例,假设你要查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄。 + +假设这个表的部分定义是这样的: + +```r +CREATE TABLE `t` ( + `id` int(11) NOT NULL, + `city` varchar(16) NOT NULL, + `name` varchar(16) NOT NULL, + `age` int(11) NOT NULL, + `addr` varchar(128) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `city` (`city`) +) ENGINE=InnoDB; +``` + +这时,你的 SQL 语句可以这么写: + +```csharp +select city,name,age from t where city='杭州' order by name limit 1000 ; +``` + +这个语句看上去逻辑很清晰,但是你了解它的执行流程吗?今天,我就和你聊聊这个语句是怎么执行的,以及有什么参数会影响执行的行为。 + +# 全字段排序 + +前面我们介绍过索引,所以你现在就很清楚了,为避免全表扫描,我们需要在 city 字段加上索引。 + +在 city 字段上创建索引之后,我们用 explain 命令来看看这个语句的执行情况。 + +![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/MySQL%E5%AE%9E%E6%88%9845%E8%AE%B2/assets/826579b63225def812330ef6c344a303.png) + +图 1 使用 explain 命令查看语句的执行情况 + +Extra 这个字段中的“Using filesort”表示的就是需要排序,MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。 + +为了说明这个 SQL 查询语句的执行过程,我们先来看一下 city 这个索引的示意图。 + +![](http://img.topjavaer.cn/img/order-by-1.png) + +图 2 city 字段的索引示意图 + +从图中可以看到,满足 city='杭州’条件的行,是从 ID_X 到 ID_(X+N) 的这些记录。 + +通常情况下,这个语句执行流程如下所示 : + +1. 初始化 sort_buffer,确定放入 name、city、age 这三个字段; +2. 从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X; +3. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中; +4. 从索引 city 取下一个记录的主键 id; +5. 重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y; +6. 对 sort_buffer 中的数据按照字段 name 做快速排序; +7. 按照排序结果取前 1000 行返回给客户端。 + +我们暂且把这个排序过程,称为全字段排序,执行流程的示意图如下所示,下一篇文章中我们还会用到这个排序。 + +![](http://img.topjavaer.cn/img/order-by-10.png) + +图 3 全字段排序 + +图中“按 name 排序”这个动作,可能在内存中完成,也可能需要使用外部排序,这取决于排序所需的内存和参数 sort_buffer_size。 + +sort_buffer_size,就是 MySQL 为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于 sort_buffer_size,排序就在内存中完成。但如果排序数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。 + +你可以用下面介绍的方法,来确定一个排序语句是否使用了临时文件。 + +```sql +/* 打开 optimizer_trace,只对本线程有效 */ +SET optimizer_trace='enabled=on'; + +/* @a 保存 Innodb_rows_read 的初始值 */ +select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read'; + +/* 执行语句 */ +select city, name,age from t where city='杭州' order by name limit 1000; + +/* 查看 OPTIMIZER_TRACE 输出 */ +SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G + +/* @b 保存 Innodb_rows_read 的当前值 */ +select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read'; + +/* 计算 Innodb_rows_read 差值 */ +select @b-@a; +``` + +这个方法是通过查看 OPTIMIZER_TRACE 的结果来确认的,你可以从 number_of_tmp_files 中看到是否使用了临时文件。 + +![](http://img.topjavaer.cn/img/order-by-2.png) + +图 4 全排序的 OPTIMIZER_TRACE 部分结果 + +number_of_tmp_files 表示的是,排序过程中使用的临时文件数。你一定奇怪,为什么需要 12 个文件?内存放不下时,就需要使用外部排序,外部排序一般使用归并排序算法。可以这么简单理解,**MySQL 将需要排序的数据分成 12 份,每一份单独排序后存在这些临时文件中。然后把这 12 个有序文件再合并成一个有序的大文件。** + +如果 sort_buffer_size 超过了需要排序的数据量的大小,number_of_tmp_files 就是 0,表示排序可以直接在内存中完成。 + +否则就需要放在临时文件中排序。sort_buffer_size 越小,需要分成的份数越多,number_of_tmp_files 的值就越大。 + +接下来,我再和你解释一下图 4 中其他两个值的意思。 + +我们的示例表中有 4000 条满足 city='杭州’的记录,所以你可以看到 examined_rows=4000,表示参与排序的行数是 4000 行。 + +sort_mode 里面的 packed_additional_fields 的意思是,排序过程对字符串做了“紧凑”处理。即使 name 字段的定义是 varchar(16),在排序过程中还是要按照实际长度来分配空间的。 + +同时,最后一个查询语句 select @b-@a 的返回结果是 4000,表示整个执行过程只扫描了 4000 行。 + +这里需要注意的是,为了避免对结论造成干扰,我把 internal_tmp_disk_storage_engine 设置成 MyISAM。否则,select @b-@a 的结果会显示为 4001。 + +这是因为查询 OPTIMIZER_TRACE 这个表时,需要用到临时表,而 internal_tmp_disk_storage_engine 的默认值是 InnoDB。如果使用的是 InnoDB 引擎的话,把数据从临时表取出来的时候,会让 Innodb_rows_read 的值加 1。 + +# rowid 排序 + +在上面这个算法过程里面,只对原表的数据读了一遍,剩下的操作都是在 sort_buffer 和临时文件中执行的。但这个算法有一个问题,就是如果查询要返回的字段很多的话,那么 sort_buffer 里面要放的字段数太多,这样内存里能够同时放下的行数很少,要分成很多个临时文件,排序的性能会很差。 + +所以如果单行很大,这个方法效率不够好。 + +那么,**如果 MySQL 认为排序的单行长度太大会怎么做呢?** + +接下来,我来修改一个参数,让 MySQL 采用另外一种算法。 + +```java +SET max_length_for_sort_data = 16; +``` + +max_length_for_sort_data,是 MySQL 中专门控制用于排序的行数据的长度的一个参数。它的意思是,如果单行的长度超过这个值,MySQL 就认为单行太大,要换一个算法。 + +city、name、age 这三个字段的定义总长度是 36,我把 max_length_for_sort_data 设置为 16,我们再来看看计算过程有什么改变。 + +新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)和主键 id。 + +但这时,排序的结果就因为少了 city 和 age 字段的值,不能直接返回了,整个执行流程就变成如下所示的样子: + +1. 初始化 sort_buffer,确定放入两个字段,即 name 和 id; +2. 从索引 city 找到第一个满足 city='杭州’条件的主键 id,也就是图中的 ID_X; +3. 到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中; +4. 从索引 city 取下一个记录的主键 id; +5. 重复步骤 3、4 直到不满足 city='杭州’条件为止,也就是图中的 ID_Y; +6. 对 sort_buffer 中的数据按照字段 name 进行排序; +7. 遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。 + +这个执行流程的示意图如下,我把它称为 rowid 排序。 + +![](http://img.topjavaer.cn/img/order-by-23.png) + +图 5 rowid 排序 + +对比图 3 的全字段排序流程图你会发现,rowid 排序多访问了一次表 t 的主键索引,就是步骤 7。 + +需要说明的是,最后的“结果集”是一个逻辑概念,实际上 MySQL 服务端从排序后的 sort_buffer 中依次取出 id,然后到原表查到 city、name 和 age 这三个字段的结果,不需要在服务端再耗费内存存储结果,是直接返回给客户端的。 + +根据这个说明过程和图示,你可以想一下,这个时候执行 select @b-@a,结果会是多少呢? + +现在,我们就来看看结果有什么不同。 + +首先,图中的 examined_rows 的值还是 4000,表示用于排序的数据是 4000 行。但是 select @b-@a 这个语句的值变成 5000 了。 + +因为这时候除了排序过程外,在排序完成后,还要根据 id 去原表取值。由于语句是 limit 1000,因此会多读 1000 行。 + +![](http://img.topjavaer.cn/img/order-by-4.png) + +图 6 rowid 排序的 OPTIMIZER_TRACE 部分输出 + +从 OPTIMIZER_TRACE 的结果中,你还能看到另外两个信息也变了。 + +- sort_mode 变成了 ,表示参与排序的只有 name 和 id 这两个字段。 +- number_of_tmp_files 变成 10 了,是因为这时候参与排序的行数虽然仍然是 4000 行,但是每一行都变小了,因此需要排序的总数据量就变小了,需要的临时文件也相应地变少了。 + +# 全字段排序 VS rowid 排序 + +我们来分析一下,从这两个执行流程里,还能得出什么结论。 + +如果 MySQL 实在是担心排序内存太小,会影响排序效率,才会采用 rowid 排序算法,这样排序过程中一次可以排序更多行,但是需要再回到原表去取数据。 + +如果 MySQL 认为内存足够大,会优先选择全字段排序,把需要的字段都放到 sort_buffer 中,这样排序后就会直接从内存里面返回查询结果了,不用再回到原表去取数据。 + +这也就体现了 MySQL 的一个设计思想:**如果内存够,就要多利用内存,尽量减少磁盘访问。** + +对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择。 + +这个结论看上去有点废话的感觉,但是你要记住它,下一篇文章我们就会用到。 + +看到这里,你就了解了,MySQL 做排序是一个成本比较高的操作。那么你会问,是不是所有的 order by 都需要排序操作呢?如果不排序就能得到正确的结果,那对系统的消耗会小很多,语句的执行时间也会变得更短。 + +其实,并不是所有的 order by 语句,都需要排序操作的。从上面分析的执行过程,我们可以看到,MySQL 之所以需要生成临时表,并且在临时表上做排序操作,**其原因是原来的数据都是无序的。** + +你可以设想下,如果能够保证从 city 这个索引上取出来的行,天然就是按照 name 递增排序的话,是不是就可以不用再排序了呢? + +确实是这样的。 + +所以,我们可以在这个市民表上创建一个 city 和 name 的联合索引,对应的 SQL 语句是: + +```sql +alter table t add index city_user(city, name); +``` + +作为与 city 索引的对比,我们来看看这个索引的示意图。 + +![](http://img.topjavaer.cn/img/order-by-5.png) + +图 7 city 和 name 联合索引示意图 + +在这个索引里面,我们依然可以用树搜索的方式定位到第一个满足 city='杭州’的记录,并且额外确保了,接下来按顺序取“下一条记录”的遍历过程中,只要 city 的值是杭州,name 的值就一定是有序的。 + +这样整个查询过程的流程就变成了: + +1. 从索引 (city,name) 找到第一个满足 city='杭州’条件的主键 id; +2. 到主键 id 索引取出整行,取 name、city、age 三个字段的值,作为结果集的一部分直接返回; +3. 从索引 (city,name) 取下一个记录主键 id; +4. 重复步骤 2、3,直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。 + +![](http://img.topjavaer.cn/img/order-by-9.png) + +图 8 引入 (city,name) 联合索引后,查询语句的执行计划 + +可以看到,这个查询过程不需要临时表,也不需要排序。接下来,我们用 explain 的结果来印证一下。 + +![](http://img.topjavaer.cn/img/order-by-6.png) + +图 9 引入 (city,name) 联合索引后,查询语句的执行计划 + +从图中可以看到,Extra 字段中没有 Using filesort 了,也就是不需要排序了。而且由于 (city,name) 这个联合索引本身有序,所以这个查询也不用把 4000 行全都读一遍,只要找到满足条件的前 1000 条记录就可以退出了。也就是说,在我们这个例子里,只需要扫描 1000 次。 + +既然说到这里了,我们再往前讨论,**这个语句的执行流程有没有可能进一步简化呢?**不知道你还记不记得,我在第 5 篇文章[《 深入浅出索引(下)》]中,和你介绍的覆盖索引。 + +这里我们可以再稍微复习一下。**覆盖索引是指,索引上的信息足够满足查询请求,不需要再回到主键索引上去取数据。** + +按照覆盖索引的概念,我们可以再优化一下这个查询语句的执行流程。 + +针对这个查询,我们可以创建一个 city、name 和 age 的联合索引,对应的 SQL 语句就是: + +```sql +alter table t add index city_user_age(city, name, age); +``` + +这时,对于 city 字段的值相同的行来说,还是按照 name 字段的值递增排序的,此时的查询语句也就不再需要排序了。这样整个查询语句的执行流程就变成了: + +1. 从索引 (city,name,age) 找到第一个满足 city='杭州’条件的记录,取出其中的 city、name 和 age 这三个字段的值,作为结果集的一部分直接返回; +2. 从索引 (city,name,age) 取下一个记录,同样取出这三个字段的值,作为结果集的一部分直接返回; +3. 重复执行步骤 2,直到查到第 1000 条记录,或者是不满足 city='杭州’条件时循环结束。 + +![](http://img.topjavaer.cn/img/order-by-7.png) + +图 10 引入 (city,name,age) 联合索引后,查询语句的执行流程 + +然后,我们再来看看 explain 的结果。 + +![](http://img.topjavaer.cn/img/order-by-8.png) + +图 11 引入 (city,name,age) 联合索引后,查询语句的执行计划 + +可以看到,Extra 字段里面多了“Using index”,表示的就是使用了覆盖索引,性能上会快很多。 + +当然,这里并不是说要为了每个查询能用上覆盖索引,就要把语句中涉及的字段都建上联合索引,毕竟索引还是有维护代价的。这是一个需要权衡的决定。 + +# 小结 + +今天这篇文章,我和你介绍了 MySQL 里面 order by 语句的几种算法流程。 + +在开发系统的时候,你总是不可避免地会使用到 order by 语句。你心里要清楚每个语句的排序逻辑是怎么实现的,还要能够分析出在最坏情况下,每个语句的执行对系统资源的消耗,这样才能做到下笔如有神,不犯低级错误。 + +> 内容摘录自丁奇的《MySQL45讲》 diff --git a/docs/advance/excellent-article/14-architect-forward.md b/docs/advance/excellent-article/14-architect-forward.md new file mode 100644 index 0000000..be04cd4 --- /dev/null +++ b/docs/advance/excellent-article/14-architect-forward.md @@ -0,0 +1,66 @@ +--- +sidebar: heading +title: 架构的演进 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构,架构演进 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 架构的演进 + +### 传统单体应用架构 + +十多年前主流的应用架构都是单体应用,部署形式就是一台服务器加一个数据库,在这种架构下,运维人员会小心翼翼地维护这台服务器,以保证服务的可用性。 + +![](http://img.topjavaer.cn/img/20230329074204.png) + +#### 单体应用架构面临的问题 + +随着业务的增长,这种最简单的单体应用架构很快就面临两个问题。首先,这里只有一台服务器,如果这台服务器出现故障,例如硬件损坏,那么整个服务就会不可用;其次,业务量变大之后,一台服务器的资源很快会无法承载所有流量。 + +解决这两个问题最直接的方法就是在流量入口加一个负载均衡器,使单体应用同时部署到多台服务器上,这样服务器的单点问题就解决了,与此同时,这个单体应用也具备了水平伸缩的能力。 + +![](http://img.topjavaer.cn/img/20230329074220.png) + +### 微服务架构 + +#### 1. 微服务架构演进出通用服务 + +随着业务的进一步增长,更多的研发人员加入到团队中,共同在单体应用上开发特性。由于单体应用内的代码没有明确的物理边界,大家很快就会遇到各种冲突,需要人工协调,以及大量的 conflict merge 操作,研发效率直线下降。 + +因此大家开始把单体应用拆分成一个个可以独立开发、独立测试、独立部署的微服务应用,服务和服务之间通过 API 通讯,如 HTTP、GRPC 或者 DUBBO。基于领域驱动设计中 Bounded Context 拆分的微服务架构能够大幅提升中大型团队的研发效率。 + +#### 2. 微服务架构给运维带来挑战 + +应用从单体架构演进到微服务架构,从物理的角度看,分布式就成了默认选项,这时应用架构师就不得不面对分布式带来的新挑战。在这个过程中,大家都会开始使用一些分布式服务和框架,例如缓存服务 Redis,配置服务 ACM,状态协调服务 ZooKeeper,消息服务 Kafka,还有通讯框架如 GRPC 或者 DUBBO,以及分布式追踪系统等。 + +除分布式环境带来的挑战之外,微服务架构给运维也带来新挑战。研发人员原来只需要运维一个应用,现在可能需要运维十个甚至更多的应用,这意味着安全 patch 升级、容量评估、故障诊断等事务的工作量呈现成倍增长,这时,应用分发标准、生命周期标准、观测标准、自动化弹性等能力的重要性也更加凸显。 + +![](http://img.topjavaer.cn/img/20230329074233.png) + +### 云原生 + +#### 1. 基于云产品架构 + +一个架构是否是云原生,就看这个架构是否是长在云上的,这是对“云原生”的简单理解。这个“长在云上”不是简单地说用云的 IaaS 层服务,比如简单的 ECS、OSS 这些基本的计算存储;而是应该理解成有没有使用云上的分布式服务,比如 Redis、Kafka 等,这些才是直接影响到业务架构的服务。微服务架构下,分布式服务是必要的,原来大家都是自己研发这样的服务,或者基于开源版本自己运维这样的服务。而到了云原生时代,业务则可以直接使用云服务。 + +另外两个不得不提的技术就是 Docker 和 Kubenetes,其中,前者标准化了应用分发的标准,不论是 Spring Boot 写的应用,还是 NodeJS 写的应用,都以镜像的方式分发;而后者在前者的技术上又定义了应用生命周期的标准,一个应用从启动到上线,到健康检查,再到下线,都有了统一的标准。 + +#### 2. 应用生命周期托管 + +有了应用分发的标准和生命周期的标准,云就能提供标准化的应用托管服务。包括应用的版本管理、发布、上线后的观测、自愈等。例如对于无状态的应用来说,一个底层物理节点的故障根本不会影响到研发,因为应用托管服务基于标准化应用生命周期可以自动完成腾挪工作,在故障物理节点上将应用的容器下线,在新的物理节点上启动同等数量的应用容器。可以看出,云原生进一步释放了价值红利。 + +在此基础上,由于应用托管服务能够感知到应用运行期的数据,例如业务流量的并发、cpu load、内存占用等,业务就可以配置基于这些指标的伸缩规则,再由平台执行这些规则,根据业务流量的实际情况增加或者减少容器数量,这就是最基本的 auto scaling——自动伸缩。这能够帮助用户避免在业务低峰期限制资源,节省成本,提升运维效率。 + +### 本文总结 + +在架构的演进过程中,研发运维人员逐渐把关注点从机器上移走,希望更多地由平台系统管理机器,而不是由人去管理,这就是一个对 Serverless 的朴素理解。 + +> 本文部分内容摘抄自网络 diff --git a/docs/advance/excellent-article/15-http-vs-rpc.md b/docs/advance/excellent-article/15-http-vs-rpc.md new file mode 100644 index 0000000..a1e3f92 --- /dev/null +++ b/docs/advance/excellent-article/15-http-vs-rpc.md @@ -0,0 +1,282 @@ +--- +sidebar: heading +title: 有了HTTP,为啥还要用RPC +category: 优质文章 +tag: + - 网络 +head: + - - meta + - name: keywords + content: HTTP和RPC,HTTP,RPC + - - meta + - name: description + content: 优质文章汇总 +--- + +# 有了HTTP,为啥还要用RPC + +> 原文链接:https://www.jianshu.com/p/9d42b926d40d + +## 既然有 HTTP 请求,为什么还要用 RPC 调用? + +一直以来都没有深究过RPC和HTTP的区别,不都是写一个服务然后在客户端调用么? + +HTTP和RPC最本质的区别,就是 **RPC 主要是基于 TCP/IP 协议的**,而 **HTTP 服务主要是基于 HTTP 协议的**。 + +我们都知道 HTTP 协议是在传输层协议 TCP 之上的,所以效率来看的话,RPC 当然是要更胜一筹啦! + +HTTP和RPC的相同点是,底层通讯都是基于socket,都可以实现远程调用,都可以实现服务调用服务 + +### HTTP 的本质 + +首先你要明确 HTTP 是一个协议,是一个超文本传输协议。 + +HTTP 它是协议,不是运输通道。 + +它基于 TCP/IP 来传输文本、图片、视频、音频等。 + +重点来了。 + +HTTP 不提供数据包的传输功能,也就是数据包从浏览器到服务端再来回的传输和它没关系。 + +这是 TCP/IP 干的。 + +那 HTTP 有啥用?我们来分析一波。 + +我们上网要么就是获取一些信息来看,要么就是修改一些信息。 + +比如你用浏览器刷微博就是获取信息,发微博就是修改信息。 + +所以说浏览器需要告知服务器它需要什么,这次的请求是要获取哪些信息?发怎么样的微博。 + +这就涉及到浏览器和服务器之间的通信交互。 + +而交互就需要一种格式。 + +像你我之间的谈话就用中文,你要突然换成俄语我听不懂那不就 GG 了。 + +所以说 HTTP 它规定了一种格式,一种通信格式,大家都用这个格式来交谈。 + +这样不论你是什么服务器、什么浏览器都能顺利的交流,减少交互的成本。 + +就像全世界如果都讲中文,那我们不就不需要学英文了,那不就较少交互的成本了。 + +不像现在我们还得学英文,不然就看不懂文档等等。 + +万一之后俄语又起来了,咱还得对接俄文,这交互成本是不是就上来了。 + +而网络世界还好,咱们现在的 Web 交互基本上就是 HTTP 了。 + +其实 HTTP 协议的格式很像我们信封,有个固定的格式。 + +![](http://img.topjavaer.cn/img/http-rpc1.png) + +左上角写邮编,右上角贴邮票,然后地址姓名啥的依次来。 + +因为计算机是很死板的,不像我们人一样有一种立体扫描感,所以要规定先写头、再写尾。 + +你要是先写尾,再写头计算机就认不出来了。 + +所以 HTTP 就规定了请求先搞请求行、再搞请求报头、再搞请求体。 + +响应就状态行、响应报头、响应体。 + +![](http://img.topjavaer.cn/img/http-rpc2.png) + +所以 HTTP 的本质是什么? + +**就是客户端和服务端约定好的一种通信格式。** + +### HTTP 和 RPC 的关系 + +HTTP 和 RPC 其实是两个维度的东西, HTTP 指的是通信协议。 + +而 RPC 则是远程调用,其对应的是本地调用。 + +RPC 的通信可以用 HTTP 协议,也可以自定义协议,是不做约束的。 + +像之前的单体时代,我们的 service 调用就是自己实现的方法,是本地进程内的调用。 + +``` +public User getUserById(Long id) { + return userDao.getUserById(id); // 这叫本地调用 + } +``` + +现在都是微服务了,根据业务模块做了不同的拆分,像用户的服务不用我这个小组负责,我这小组只要写订单服务就行了。 + +但是我们服务需要用到用户的信息,于是我们需要调用用户小组的服务,于是代码变成了以下这种 + +``` +public User getUserById(Long id) { + return userConsumer.getUserById(id); // 这是远程调用,逻辑是用户小组的服务实现的。 + } +``` + +可能还有些小伙伴不太清楚,再来看个图。 + +![](http://img.topjavaer.cn/img/http-rpc3.png) + +把之前的用户实现拆分出来弄了一个用户服务,订单相关的也拆成了订单服务,都单独部署。 + +这样订单相关的服务要获取用户的信息就需要远程调用了。 + +可以看到 RPC 就是通过网络进行远程调用,订单服务其实就是客户端,而用户服务是服务端。 + +这又涉及到交互了,所以也需要约定一个格式,至于要不要用 HTTP 这个格式,就是大家自己看着办。 + +至此相信你对 HTTP 是啥也清楚了。 + +RPC 和 HTTP 的之间的关系也清楚了。 + +### 那为什么要有 RPC? + +可能你常听到什么什么之间是 RPC 调用的,那你有没有想过为什么要 RPC, 我们直接 WebClient HTTP 调用不行么? + +其实 RPC 调用是因为服务的拆分,或者本身公司内部的多个服务之间的通信。 + +服务的拆分独立部署,那服务间的调用就必然需要网络通信,用 WebClient 调用当然可行,但是比较麻烦。 + +我们想即使服务被拆分了但是使用起来还是和之前本地调用一样方便。 + +所以就出现了 RPC 框架,来屏蔽这些底层调用细节,使得我们编码上还是和之前本地调用相差不多。 + +并且 HTTP 协议比较的冗余,RPC 都是内部调用所以不需要太考虑通用性,只要公司内部保持格式统一即可。 + +所以可以做各种定制化的协议来使得通信更高效。 + +比如规定 yes 代表 yes的练级攻略,你看是不是更高效了,少传输的 5 个字。 + +就像特殊行动的暗号,高效简洁! + +所以公司内部服务的调用一般都用 RPC,而 HTTP 的优势在于通用,大家都认可这个协议。 + +所以三方平台提供的接口都是通过 HTTP 协议调用的。 + +所以现在知道为什么我们调用第三方都是 HTTP ,公司内部用 RPC 了吧? + +上面这段话看起来仿佛 HTTP 和 RPC 是对等关系,不过相信大家看了之前的解析心里应该都有数了。 + +下面来具体说一说 RPC 服务和 HTTP 服务的区别。 + +#### OSI 网络七层模型 + +在说 RPC 和 HTTP 的区别之前,我觉的有必要了解一下 OSI 的七层网络结构模型( + +![](http://img.topjavaer.cn/img/http-rpc4.png) + +它可以分为以下几层:(从上到下) + +- **第一层:应用层。**定义了用于在网络中进行通信和传输数据的接口。 +- **第二层:表示层。**定义不同的系统中数据的传输格式,编码和解码规范等。 +- **第三层:会话层。**管理用户的会话,控制用户间逻辑连接的建立和中断。 +- **第四层:传输层。**管理着网络中的端到端的数据传输。 +- **第五层:网络层。**定义网络设备间如何传输数据。 +- **第六层:链路层。**将上面的网络层的数据包封装成数据帧,便于物理层传输。 +- **第七层:物理层。**这一层主要就是传输这些二进制数据。 + +![](http://img.topjavaer.cn/img/http-rpc5.png) + +> **实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。** + +我们应该将重点放在应用层和传输层这两个层面。因为 HTTP 是应用层协议,而 TCP 是传输层协议。 + +好,知道了网络的分层模型以后我们可以更好地理解为什么 RPC 服务相比 HTTP 服务要 Nice 一些! + +### RPC 服务 + +从三个角度来介绍 RPC 服务,分别是: + +- **RPC 架构** +- **同步异步调用** +- **流行的 RPC 框架** + +**RPC 架构** + +先说说 RPC 服务的基本架构吧。我们可以很清楚地看到,一个完整的 RPC 架构里面包含了四个核心的组件。 + +分别是: + +- **Client** +- **Server** +- **Client Stub** +- **Server Stub(这个Stub大家可以理解为存根)** + +![](http://img.topjavaer.cn/img/http-rpc6.png) + +分别说说这几个组件: + +- **客户端(Client),**服务的调用方。 +- **服务端(Server),**真正的服务提供者。 +- **客户端存根,**存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。微信搜索公众号:Linux技术迷,回复:linux 领取资料 。 +- 服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。 + +RPC 主要是用在大型企业里面,因为大型企业里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候 RPC 的优势就比较明显了。 + +![](http://img.topjavaer.cn/img/http-rpc7.png) + +比如我们有一个处理订单的系统服务,先声明它的所有的接口,然后将整个项目打包,服务端这边引入,然后实现相应的功能,客户端这边也只需要引入就可以调用了。 + +为什么这么做? + +主要是为了减少客户端这边的包大小,因为每一次打包发布的时候,包太多总是会影响效率。 + +另外也是将客户端和服务端解耦,提高代码的可移植性。 + +**同步调用与异步调用** + +什么是同步调用?什么是异步调用? + +同步调用就是客户端等待调用执行完成并返回结果。 + +异步调用就是客户端不等待调用执行完成返回结果,不过依然可以通过回调函数等接收到返回结果的通知。如果客户端并不关心结果,则可以变成一个单向的调用。 + +#### 流行的 RPC 框架 + +目前流行的开源 RPC 框架还是比较多的。下面重点介绍三种: + +**①gRPC** 是 Google 最近公布的开源软件,基于最新的 HTTP2.0 协议,并支持常见的众多编程语言。 + +我们知道 HTTP2.0 是基于二进制的 HTTP 协议升级版本,目前各大浏览器都在快马加鞭的加以支持。 + +这个 RPC 框架是基于 HTTP 协议实现的,底层使用到了 Netty 框架的支持。 + +**②Thrift** 是 Facebook 的一个开源项目,主要是一个跨语言的服务开发框架。它有一个代码生成器来对它所定义的 IDL 定义文件自动生成服务代码框架。 + +用户只要在其之前进行二次开发就行,对于底层的 RPC 通讯等都是透明的。不过这个对于用户来说的话需要学习特定领域语言这个特性,还是有一定成本的。 + +**③Dubbo** 是阿里集团开源的一个极为出名的 RPC 框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。 + +### HTTP 服务 + +通常,我们的开发模式一直定性为 HTTP 接口开发,也就是我们常说的 RESTful 风格的服务接口。 + +的确,对于在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。 + +利用现成的 HTTP 协议进行传输。 + +平时的工作主要就是进行接口的开发,还要写一大份接口文档,严格地标明输入输出是什么?说清楚每一个接口的请求方法,以及请求参数需要注意的事项等。 + +![](http://img.topjavaer.cn/img/http-rpc8.png) + +比如下面这个例子: + +``` +POST http://www.httpexample.com/restful/buyer/info/shar +``` + +接口可能返回一个 JSON 字符串或者是 XML 文档。然后客户端再去处理这个返回的信息,从而可以比较快速地进行开发。 + +但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC 框架的好处就显示出来了,首先就是长链接,不必每次通信都要像 HTTP 一样去 3 次握手什么的,减少了网络开销。 + +其次就是 RPC 框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。 + +### 小结 + +RPC 服务和 HTTP 服务还是存在很多的不同点的,一般来说,RPC 服务主要是针对大型企业的,而 HTTP 服务主要是针对小企业的,因为 RPC 效率更高,而 HTTP 服务开发迭代会更快。 + +很多RPC框架包含了重试机制,路由策略,负载均衡策略,高可用策略,流量控制策略等等。如果应用进程之间只使用HTTP协议通信,显然是无法完成上述功能的。 + +总之,选用什么样的框架不是按照市场上流行什么而决定的,而是要对整个项目进行完整地评估,从而在仔细比较两种开发框架对于整个项目的影响,最后再决定什么才是最适合这个项目的。 + +一定不要为了使用 RPC 而每个项目都用 RPC,而是要因地制宜,具体情况具体分析。 diff --git a/docs/advance/excellent-article/16-what-is-jwt.md b/docs/advance/excellent-article/16-what-is-jwt.md new file mode 100644 index 0000000..2fc9f62 --- /dev/null +++ b/docs/advance/excellent-article/16-what-is-jwt.md @@ -0,0 +1,184 @@ +--- +sidebar: heading +title: 什么是JWT +category: 优质文章 +tag: + - web +head: + - - meta + - name: keywords + content: jwt,web开发 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 什么是JWT + +JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。 + +## 传统的session认证 + +http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。 + +这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来. + +#### 基于session认证所显露的问题 + +**Session**: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。 + +**扩展性**: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。 + +**CSRF**: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。 + +## 基于token的鉴权机制 + +基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。 + +流程上是这样的: + +- 用户使用用户名密码来请求服务器 +- 服务器进行验证用户的信息 +- 服务器通过验证发送给用户一个token +- 客户端存储token,并在每次请求时附送上这个token值 +- 服务端验证token值,并返回数据 + +这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持`CORS(跨来源资源共享)`策略,一般我们在服务端这么做就可以了`Access-Control-Allow-Origin: *`。 + +## JWT长什么样? + +JWT是由三段信息构成的,将这三段信息文本用`.`链接一起就构成了Jwt字符串。就像这样: + +```css +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW +``` + +## JWT的构成 + +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature). + +### header + +jwt的头部承载两部分信息: + +- 声明类型,这里是jwt +- 声明加密的算法 通常直接使用 HMAC SHA256 + +完整的头部就像下面这样的JSON: + +```bash +{ + 'typ': 'JWT', + 'alg': 'HS256' +} +``` + +然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分. + +```undefined +eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 +``` + +### playload + +载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分 + +- 标准中注册的声明 +- 公共的声明 +- 私有的声明 + +**标准中注册的声明** (建议但不强制使用) : + +- **iss**: jwt签发者 +- **sub**: jwt所面向的用户 +- **aud**: 接收jwt的一方 +- **exp**: jwt的过期时间,这个过期时间必须要大于签发时间 +- **nbf**: 定义在什么时间之前,该jwt都是不可用的. +- **iat**: jwt的签发时间 +- **jti**: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。 + +**公共的声明** : + 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密. + +**私有的声明** : + 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 + +定义一个payload: + +```json +{ + "sub": "1234567890", + "name": "John Doe", + "admin": true +} +``` + +然后将其进行base64加密,得到Jwt的第二部分。 + +```undefined +eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 +``` + +### signature + +jwt的第三部分是一个签证信息,这个签证信息由三部分组成: + +- header (base64后的) +- payload (base64后的) +- secret + +这个部分需要base64加密后的header和base64加密后的payload使用`.`连接组成的字符串,然后通过header中声明的加密方式进行加盐`secret`组合加密,然后就构成了jwt的第三部分。 + +```csharp +// javascript +var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); + +var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ +``` + +将这三部分用`.`连接成一个完整的字符串,构成了最终的jwt: + +```css + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ +``` + +**注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。** + +### 如何应用 + +一般是在请求头里加入`Authorization`,并加上`Bearer`标注: + +```bash +fetch('api/user/1', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +``` + +服务端会验证token,如果验证通过就会返回相应的资源。整个流程就是这样的: + +![](http://img.topjavaer.cn/img/jwt.png) + +## 总结 + +### 优点 + +- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。 +- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。 +- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。 +- 它不需要在服务端保存会话信息, 所以它易于应用的扩展 + +### 安全相关 + +- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。 +- 保护好secret私钥,该私钥非常重要。 +- 如果可以,请使用https协议 + + + +## 参考链接 + +- [什么是 JWT](https://www.jianshu.com/p/576dbf44b2ae) + +- [JSON Web Token 入门教程](https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) diff --git a/docs/advance/excellent-article/17-limit-scheme.md b/docs/advance/excellent-article/17-limit-scheme.md new file mode 100644 index 0000000..d49d6e0 --- /dev/null +++ b/docs/advance/excellent-article/17-limit-scheme.md @@ -0,0 +1,227 @@ +--- +sidebar: heading +title: 限流的几种方案 +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 限流方案,令牌桶算法,漏桶算法,滑动窗口,Guava限流,网关层限流 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 限流的几种方案 + +### 文章目录 + +- 限流基本概念 + +- - QPS和连接数控制 + - 传输速率 + - 黑白名单 + - 分布式环境 + +- 限流方案常用算法 + +- - 令牌桶算法 + - 漏桶算法 + - 滑动窗口 + +- 常用的限流方案 + +- - Nginx限流 + - 中间件限流 + - 限流组件 + - 合法性验证限流 + - Guava限流 + - 网关层限流 + +- 从架构维度考虑限流设计 + +- 具体的实现限流的手段: + +- - Tomcat限流 + +### 限流基本概念 + +对一般的限流场景来说它具有两个维度的信息: + +- 时间 限流基于某段时间范围或者某个时间点,也就是我们常说的“时间窗口”,比如对每分钟、每秒钟的时间窗口做限定 +- 资源 基于可用资源的限制,比如设定最大访问次数,或最高可用连接数 + +上面两个维度结合起来看,限流就是在某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求。但在真正的场景里,我们不止设置一种限流规则,而是会设置多个限流规则共同作用,主要的几种限流规则如下: + +##### QPS和连接数控制 + +对于连接数和QPS)限流来说,我们可设定IP维度的限流,也可以设置基于单个服务器的限流。 + +在真实环境中通常会设置多个维度的限流规则,比如设定同一个IP每秒访问频率小于10,连接数小于5,再设定每台机器QPS最高1000,连接数最大保持200。更进一步,我们可以把某个服务器组或整个机房的服务器当做一个整体,设置更high-level的限流规则,这些所有限流规则都会共同作用于流量控制。 + +##### 传输速率 + +对于“传输速率”大家都不会陌生,比如资源的下载速度。有的网站在这方面的限流逻辑做的更细致,比如普通注册用户下载速度为100k/s,购买会员后是10M/s,这背后就是基于用户组或者用户标签的限流逻辑。 + +##### 黑白名单 + +黑白名单是各个大型企业应用里很常见的限流和放行手段,而且黑白名单往往是动态变化的。举个例子,如果某个IP在一段时间的访问次数过于频繁,被系统识别为机器人用户或流量攻击,那么这个IP就会被加入到黑名单,从而限制其对系统资源的访问,这就是我们俗称的“封IP”。 + +我们平时见到的爬虫程序,比如说爬知乎上的美女图片,或者爬券商系统的股票分时信息,这类爬虫程序都必须实现更换IP的功能,以防被加入黑名单。 + +有时我们还会发现公司的网络无法访问12306这类大型公共网站,这也是因为某些公司的出网IP是同一个地址,因此在访问量过高的情况下,这个IP地址就被对方系统识别,进而被添加到了黑名单。使用家庭宽带的同学们应该知道,大部分网络运营商都会将用户分配到不同出网IP段,或者时不时动态更换用户的IP地址。 + +白名单就更好理解了,相当于御赐金牌在身,可以自由穿梭在各种限流规则里,畅行无阻。比如某些电商公司会将超大卖家的账号加入白名单,因为这类卖家往往有自己的一套运维系统,需要对接公司的IT系统做大量的商品发布、补货等等操作。 + +##### 分布式环境 + +分布式区别于单机限流的场景,它把整个分布式环境中所有服务器当做一个整体来考量。比如说针对IP的限流,我们限制了1个IP每秒最多10个访问,不管来自这个IP的请求落在了哪台机器上,只要是访问了集群中的服务节点,那么都会受到限流规则的制约。 + +我们最好将限流信息保存在一个“中心化”的组件上,这样它就可以获取到集群中所有机器的访问状态,目前有两个比较主流的限流方案: + +- 网关层限流 将限流规则应用在所有流量的入口处 +- 中间件限流 将限流信息存储在分布式环境中某个中间件里(比如Redis缓存),每个组件都可以从这里获取到当前时刻的流量统计,从而决定是拒绝服务还是放行流量 +- sentinel,springcloud生态圈为微服务量身打造的一款用于分布式限流、熔断降级等组件 + +### 限流方案常用算法 + +##### 令牌桶算法 + +Token Bucket令牌桶算法是目前应用最为广泛的限流算法,顾名思义,它有以下两个关键角色: + +- 令牌 获取到令牌的Request才会被处理,其他Requests要么排队要么被直接丢弃 +- 桶 用来装令牌的地方,所有Request都从这个桶里面获取令牌 主要涉及到2个过程: +- **令牌生成** + +这个流程涉及到令牌生成器和令牌桶,前面我们提到过令牌桶是一个装令牌的地方,既然是个桶那么必然有一个容量,也就是说令牌桶所能容纳的令牌数量是一个固定的数值。 + +对于令牌生成器来说,它会根据一个预定的速率向桶中添加令牌,比如我们可以配置让它以每秒100个请求的速率发放令牌,或者每分钟50个。注意这里的发放速度是匀速,也就是说这50个令牌并非是在每个时间窗口刚开始的时候一次性发放,而是会在这个时间窗口内匀速发放。 + +在令牌发放器就是一个水龙头,假如在下面接水的桶子满了,那么自然这个水(令牌)就流到了外面。在令牌发放过程中也一样,令牌桶的容量是有限的,如果当前已经放满了额定容量的令牌,那么新来的令牌就会被丢弃掉。 + +- **令牌获取** + +每个访问请求到来后,必须获取到一个令牌才能执行后面的逻辑。假如令牌的数量少,而访问请求较多的情况下,一部分请求自然无法获取到令牌,那么这个时候我们可以设置一个“缓冲队列”来暂存这些多余的令牌。 + +缓冲队列其实是一个可选的选项,并不是所有应用了令牌桶算法的程序都会实现队列。当有缓存队列存在的情况下,那些暂时没有获取到令牌的请求将被放到这个队列中排队,直到新的令牌产生后,再从队列头部拿出一个请求来匹配令牌。 + +当队列已满的情况下,这部分访问请求将被丢弃。在实际应用中我们还可以给这个队列加一系列的特效,比如设置队列中请求的存活时间,或者将队列改造为PriorityQueue,根据某种优先级排序,而不是先进先出。 + +##### 漏桶算法 + +Leaky Bucket,又是个桶,限流算法是跟桶杠上了,那么漏桶和令牌桶有什么不同呢, + +漏桶算法的前半段和令牌桶类似,但是操作的对象不同,令牌桶是将令牌放入桶里,而漏桶是将访问请求的数据包放到桶里。同样的是,如果桶满了,那么后面新来的数据包将被丢弃。 + +漏桶算法的后半程是有鲜明特色的,它永远只会以一个恒定的速率将数据包从桶内流出。打个比方,如果我设置了漏桶可以存放100个数据包,然后流出速度是1s一个,那么不管数据包以什么速率流入桶里,也不管桶里有多少数据包,漏桶能保证这些数据包永远以1s一个的恒定速度被处理。 + +- **漏桶 vs 令牌桶的区别** + +根据它们各自的特点不难看出来,这两种算法都有一个“恒定”的速率和“不定”的速率。令牌桶是以恒定速率创建令牌,但是访问请求获取令牌的速率“不定”,反正有多少令牌发多少,令牌没了就干等。而漏桶是以“恒定”的速率处理请求,但是这些请求流入桶的速率是“不定”的。 + +从这两个特点来说,漏桶的天然特性决定了它不会发生突发流量,就算每秒1000个请求到来,那么它对后台服务输出的访问速率永远恒定。而令牌桶则不同,其特性可以“预存”一定量的令牌,因此在应对突发流量的时候可以在短时间消耗所有令牌,其突发流量处理效率会比漏桶高,但是导向后台系统的压力也会相应增多。 + +##### 滑动窗口 + +比如说,我们在每一秒内有5个用户访问,第5秒内有10个用户访问,那么在0到5秒这个时间窗口内访问量就是15。如果我们的接口设置了时间窗口内访问上限是20,那么当时间到第六秒的时候,这个时间窗口内的计数总和就变成了10,因为1秒的格子已经退出了时间窗口,因此在第六秒内可以接收的访问量就是20-10=10个。 + +滑动窗口其实也是一种计算器算法,它有一个显著特点,当时间窗口的跨度越长时,限流效果就越平滑。打个比方,如果当前时间窗口只有两秒,而访问请求全部集中在第一秒的时候,当时间向后滑动一秒后,当前窗口的计数量将发生较大的变化,拉长时间窗口可以降低这种情况的发生概率 + +### 常用的限流方案 + +##### 合法性验证限流 + +比如验证码、IP 黑名单等,这些手段可以有效的防止恶意攻击和爬虫采集; + +##### Guawa限流 + +在限流领域中,Guava在其多线程模块下提供了以`RateLimiter`为首的几个限流支持类,但是作用范围仅限于“当前”这台服务器,也就是说Guawa的限流是单机的限流,跨了机器或者jvm进程就无能为力了 比如说,目前我有2台服务器[`Server 1`,`Server 2`],这两台服务器都部署了一个登陆服务,假如我希望对这两台机器的流量进行控制,比如将两台机器的访问量总和控制在每秒20以内,如果用Guava来做,只能独立控制每台机器的访问量<=10。 + +尽管Guava不是面对分布式系统的解决方案,但是其作为一个简单轻量级的客户端限流组件,非常适合来讲解限流算法 + +##### 网关层限流 + +服务网关,作为整个分布式链路中的第一道关卡,承接了所有用户来访请求,因此在网关层面进行限流是一个很好的切入点 上到下的路径依次是: + +1. 用户流量从网关层转发到后台服务 +2. 后台服务承接流量,调用缓存获取数据 +3. 缓存中无数据,则访问数据库 + +流量自上而下是逐层递减的,在网关层聚集了最多最密集的用户访问请求,其次是后台服务。 + +然后经过后台服务的验证逻辑之后,刷掉了一部分错误请求,剩下的请求落在缓存上,如果缓存中没有数据才会请求漏斗最下方的数据库,因此数据库层面请求数量最小(相比较其他组件来说数据库往往是并发量能力最差的一环,阿里系的MySQL即便经过了大量改造,单机并发量也无法和Redis、Kafka之类的组件相比) + +目前主流的网关层有以软件为代表的Nginx,还有Spring Cloud中的Gateway和Zuul这类网关层组件 + +**Nginx限流** + +在系统架构中,Nginx的代理与路由转发是其作为网关层的一个很重要的功能,由于Nginx天生的轻量级和优秀的设计,让它成为众多公司的首选,Nginx从网关这一层面考虑,可以作为最前置的网关,抵挡大部分的网络流量,因此使用Nginx进行限流也是一个很好的选择,在Nginx中,也提供了常用的基于限流相关的策略配置. + +Nginx 提供了两种限流方法:一种是控制速率,另一种是控制并发连接数。 + +**控制速率** + +我们需要使用 `limit_req_zone` 用来限制单位时间内的请求数,即速率限制, + +因为Nginx的限流统计是基于毫秒的,我们设置的速度是 2r/s,转换一下就是500毫秒内单个IP只允许通过1个请求,从501ms开始才允许通过第2个请求。 + +- **控制速率优化版** + +上面的速率控制虽然很精准但是在生产环境未免太苛刻了,实际情况下我们应该控制一个IP单位总时间内的总访问次数,而不是像上面那样精确到毫秒,我们可以使用 burst 关键字开启此设置 + +`burst=4`意思是每个IP最多允许4个突发请求 + +**控制并发数** + +利用 `limit_conn_zone` 和 `limit_conn` 两个指令即可控制并发数 + +其中 `limit_conn perip 10` 表示限制单个 IP 同时最多能持有 10 个连接;`limit_conn perserver 100` 表示 server 同时能处理并发连接的总数为 100 个。 + +> 注意:只有当 request header 被后端处理后,这个连接才进行计数。 + +**中间件限流** + +对于分布式环境来说,无非是需要一个类似中心节点的地方存储限流数据。打个比方,如果我希望控制接口的访问速率为每秒100个请求,那么我就需要将当前1s内已经接收到的请求的数量保存在某个地方,并且可以让集群环境中所有节点都能访问。那我们可以用什么技术来存储这个临时数据呢? + +那么想必大家都能想到,必然是redis了,利用Redis过期时间特性,我们可以轻松设置限流的时间跨度(比如每秒10个请求,或者每10秒10个请求)。同时Redis还有一个特殊技能–脚本编程,我们可以将限流逻辑编写成一段脚本植入到Redis中,这样就将限流的重任从服务层完全剥离出来,同时Redis强大的并发量特性以及高可用集群架构也可以很好的支持庞大集群的限流访问。【reids + lua】 + +**限流组件** + +除了上面介绍的几种方式以外,目前也有一些开源组件提供了类似的功能,比如Sentinel就是一个不错的选择。Sentinel是阿里出品的开源组件,并且包含在了Spring Cloud Alibaba组件库中,Sentinel提供了相当丰富的用于限流的API以及可视化管控台,可以很方便的帮助我们对限流进行治理 + +### 从架构维度考虑限流设计 + +在真实的项目里,不会只使用一种限流手段,往往是几种方式互相搭配使用,让限流策略有一种层次感,达到资源的最大使用率。在这个过程中,限流策略的设计也可以参考前面提到的漏斗模型,上宽下紧,漏斗不同部位的限流方案设计要尽量关注当前组件的高可用。 + +以我参与的实际项目为例,比如说我们研发了一个商品详情页的接口,通过手机淘宝导流,app端的访问请求首先会经过阿里的mtop网关,在网关层我们的限流会做的比较宽松,等到请求通过网关抵达后台的商品详情页服务之后,再利用一系列的中间件+限流组件,对服务进行更加细致的限流控制 + +### 具体的实现限流的手段 + +1)Tomcat 使用 maxThreads来实现限流。 + +2)Nginx的`limit_req_zone`和 burst来实现速率限流。 + +3)Nginx的`limit_conn_zone`和 `limit_conn`两个指令控制并发连接的总数。 + +4)时间窗口算法借助 Redis的有序集合可以实现。 + +5)漏桶算法可以使用Redis-Cell来实现。 + +6)令牌算法可以解决Google的guava包来实现。 + +> 需要注意的是借助Redis实现的限流方案可用于分布式系统,而guava实现的限流只能应用于单机环境。如果你觉得服务器端限流麻烦,可以在不改任何代码的情况下直接使用容器限流(Nginx或Tomcat),但前提是能满足项目中的业务需求。 + +##### Tomcat限流 + +Tomcat 8.5 版本的最大线程数在 `conf/server.xml` 配置中,maxThreads 就是 Tomcat 的最大线程数,当请求的并发大于此值(maxThreads)时,请求就会排队执行,这样就完成了限流的目的。 + +注意: + +> maxThreads 的值可以适当的调大一些,Tomcat默认为 150(Tomcat 版本 8.5),但这个值也不是越大越好,要看具体的服务器配置,需要注意的是每开启一个线程需要耗用 1MB 的 JVM 内存空间用于作为线程栈之用,并且线程越多 GC 的负担也越重。 + +最后需要注意一下,操作系统对于进程中的线程数有一定的限制,Windows 每个进程中的线程数不允许超过 2000,Linux 每个进程中的线程数不允许超过 1000。 + + + +> 参考链接:blog.csdn.net/liuerchong/article/details/118882053 diff --git a/docs/advance/excellent-article/18-db-connect-resource.md b/docs/advance/excellent-article/18-db-connect-resource.md new file mode 100644 index 0000000..d016413 --- /dev/null +++ b/docs/advance/excellent-article/18-db-connect-resource.md @@ -0,0 +1,111 @@ +--- +sidebar: heading +title: 为什么说数据库连接很消耗资源 +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 数据库连接 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 为什么说数据库连接很消耗资源 + +相信有过工作经验的同学都知道数据库连接是一个比较耗资源的操作。那么资源到底是耗费在哪里呢? + +本文主要想探究一下连接数据库的细节,尤其是在`Web`应用中要使用数据库来连接池,以免每次发送一次请求就重新建立一次连接。对于这个问题,答案都是一致的,建立数据库连接很耗时,但是这个耗时是都多少呢,又是分别在哪些方面产生的耗时呢? + +本文以连接`MySQL`数据库为例,因为`MySQL`数据库是开源的,其通信协议是公开的,所以我们能够详细分析建立连接的整个过程。 + +> 在本文中,消耗资源的分析主要集中在网络上,当然,资源也包括内存、`CPU`等计算资源,使用的编程语言是`Java`,但是不排除编程语言也会有一定的影响。 + +首先先看一下连接数据库的`Java`代码,如下: + +```java +Class.forName("com.mysql.jdbc.Driver"); + +String name = "xttblog2"; +String password = "123456"; +String url = "jdbc:mysql://xxx:3306/xttblog2"; +Connection conn = DriverManager.getConnection(url, name, password); +// 之后程序终止,连接被强制关闭 +``` + +然后通过`Wireshark`,分析整个连接的建立过程,如下: + +![](http://img.topjavaer.cn/img/db-connect-1.png) + +## **Wireshark抓包** + +在上图中显示的连接过程中,可以看出`MySQL`的通信协议是基于`TCP`传输协议的,而且该协议是二进制协议,不是类似于`HTTP`的文本协议,其中建立连接的过程具体如下: + +- 第1步:建立`TCP`连接,通过三次握手实现; +- 第2步:服务器发送给客户端「握手信息」 ,客户端响应该握手消息; +- 第3步:客户端「发送认证包」 ,用于用户验证,验证成功后,服务器返回`OK`响应,之后开始执行命令; + +用户验证成功之后,会进行一些连接变量的设置,**比如字符集、是否自动提交事务等,其间会有多次数据的交互**。完成了这些步骤后,才会执行真正的数据查询和更新等操作。 + +在本文的测试中,只用了`5`行代码来建立连接,但是并没有通过该连接去执行任何操作,所以在程序执行完毕之后,连接不是通过`Connection.close()`关闭的,而是由于程序执行完毕,导致进程终止,造成与数据库的连接异常关闭,所以最后会出现`TCP`的`RST`报文。 + +在这个最简单的代码中,没有设置任何额外的连接属性,所以在设置属性上占用的时间可以认为是最少的(其实,虽然我们没有设置任何属性,但是驱动仍然设置了字符集、事务自动提交等,这取决于具体的驱动实现),所以整个连接所使用的时间可以认为是最少的。 + +但从统计信息中可以看出,在不包括最后`TCP`的`RST`报文时(因为该报文不需要服务器返回任何响应),但是其中仍需在客户端和服务器之间进行往返「`7`」 次,「也就是说完成一次连接,可以认为,数据在客户端和服务器之间需要至少往返`7`次」 ,从时间上来看,从开始`TCP`的三次握手,到最终连接强制断开为止(不包括最后的`RST`报文),总共花费了: + +```java +10.416042 - 10.190799 = 0.225243s = 225.243ms!!! +``` + +这意味着,建立一次数据库连接需要`225ms`,而这还是还可以认为是最少的,当然「花费的时间可能受到网络状况、数据库服务器性能以及应用代码是否高效的影响」 ,但是这里只是一个最简单的例子,已经足够说明问题了! + +由于上面是程序异常终止了,但是在正常的应用程序中,连接的关闭一般都是通过`Connection.close()`完成的,代码如下: + +```java +Class.forName("com.mysql.jdbc.Driver"); + +String name = "shine_user"; +String password = "123"; +String url = "jdbc:mysql://xxx:3306/clever_mg_test"; +Connection conn = DriverManager.getConnection(url, name, password); +conn.close(); +``` + +这样的话,情况发生了变化,主要体现在与数据库连接的断开,如下图: + +![](http://img.topjavaer.cn/img/db-connect-2.png) + +## **网络抓包** + +- 第1步:此时处于`MySQL`通信协议阶段,客户端发送关闭连接请求,而且不用等待服务端的响应; +- 第2步:`TCP`断开连接,`4`次挥手完成连接断开; + +这里是完整地完成了从数据库连接的建立到关闭,整个过程花费了: + +``` +747.284311 - 747.100954 = 0.183357s = 183.357ms +``` + +这里可能也有网络状况的影响,比上述的`225ms`少了,但是也几乎达到了`200ms`的级别。 + +那么问题来了,想象一下这个场景,对于一个日活`2万`的网站来说,假设每个用户只会发送`5`个请求,那么一天就是`10万`个请求,对于建立数据库连接,我们保守一点计算为`150ms`好了,那么一天当中花费在建立数据库连接的时间有(还不包括执行查询和更新操作): + +``` +100000 * 150ms = 15000000ms = 15000s = 250min = 4.17h +``` + +也就说每天花费在建立数据库连接上的时间已经达到「`4个小时`」 ,所以说数据库连接池是必须的,而且当日活增加时,单单使用数据库连接池也不能完全保证你的服务能够正常运行,还需要考虑其他的解决方案: + +- 缓存 +- SQL 的预编译 +- 负载均衡 +- …… + + + +总之,数据库连接真的很耗时,所以不要频繁的**建立连接**。 + + + diff --git a/docs/advance/excellent-article/19-java19.md b/docs/advance/excellent-article/19-java19.md new file mode 100644 index 0000000..fdf8a95 --- /dev/null +++ b/docs/advance/excellent-article/19-java19.md @@ -0,0 +1,71 @@ +--- +sidebar: heading +title: Java19新特性 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: Java19新特性 + - - meta + - name: description + content: 优质文章汇总 +--- + +# Java19新特性 + +JDK 19 / Java 19 已正式发布。 + +![](http://img.topjavaer.cn/img/java19.png) + +新版本总共包含 7 个新的 JEP: + +| 405: | Record Patterns (Preview) | +| ---- | ------------------------------------------- | +| 422: | Linux/RISC-V Port | +| 424: | Foreign Function & Memory API (Preview) | +| 425: | Virtual Threads (Preview) | +| 426: | Vector API (Fourth Incubator) | +| 427: | Pattern Matching for switch (Third Preview) | +| 428: | Structured Concurrency (Incubator) | + +- 405:记录模式 (Record Patterns) 进入预览阶段 + +Record Patterns 可对 record 的值进行解构,Record patterns 和 Type patterns 通过嵌套能够实现强大的、声明性的、可组合的数据导航和处理形式。 + +该特性目前处于预览阶段。 + +- 422:将 JDK 移植到 Linux/RISC-V 平台 + +目前只支持 RISC-V 的 RV64GV 配置,它是一个通用的 64 位 ISA。将来会考虑支持其他的 RISC-V 配置,例如通用的 32 位配置 (RV32G)。 + +- 424:外部函数和内存 API (Foreign Function & Memory API) 进入预览阶段 + +Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。 + +一句话总结:该特性让 Java 调用普通 native 代码更加方便和高效。 + +- 425:虚拟线程 (Virtual Threads) 进入预览阶段 + +为 Java 引入虚拟线程,虚拟线程是 JDK 实现的轻量级线程,它在其他多线程语言中已经被证实是十分有用的,比如 Go 中的 Goroutine、Erlang 中的进程。另外,最新 Java 面试题整理好了,大家可以在Java面试库小程序在线刷题。 + +虚拟线程避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂,可以有效减少编写、维护和观察高吞吐量并发应用程序的工作量。 + +- 426:向量 API (Vector API) 进入第 4 孵化阶段 + +向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。 + +- 427:switch 模式匹配 (Pattern Matching for switch) 进入第 3 预览阶段 + +用 `switch` 表达式和语句的模式匹配,以及对模式语言的扩展来增强 Java 编程语言。将模式匹配扩展到 `switch` 中,允许针对一些模式测试表达式,这样就可以简明而安全地表达复杂的面向数据的查询。 + +- 428:结构化并发 (Structured Concurrency) 进入孵化阶段 + +JDK 19 引入了结构化并发,这是一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代 java.util.concurrent,目前处于孵化阶段。 + +结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。 + +下载地址:https://jdk.java.net/19/ + +Release Note:https://jdk.java.net/19/release-notes diff --git a/docs/advance/excellent-article/2-spring-transaction.md b/docs/advance/excellent-article/2-spring-transaction.md new file mode 100644 index 0000000..6567d64 --- /dev/null +++ b/docs/advance/excellent-article/2-spring-transaction.md @@ -0,0 +1,126 @@ +--- +sidebar: heading +title: Transactional事务注解详解 +category: 优质文章 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring事务,事务传播行为,事务注解 + - - meta + - name: description + content: 优质文章汇总 +--- + +# @Transactional 事务注解详解 + +## Spring事务的传播行为 + +**先简单介绍一下Spring事务的传播行为:** + +所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在`TransactionDefinition`定义中括了如下几个表示传播行为的常量: + +- `TransactionDefinition.PROPAGATION_REQUIRED`:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。 +- `TransactionDefinition.PROPAGATION_REQUIRES_NEW`:创建一个新的事务,如果当前存在事务,则把当前事务挂起。 +- `TransactionDefinition.PROPAGATION_SUPPORTS`:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 +- `TransactionDefinition.PROPAGATION_NOT_SUPPORTED`:以非事务方式运行,如果当前存在事务,则把当前事务挂起。 +- `TransactionDefinition.PROPAGATION_NEVER`:以非事务方式运行,如果当前存在事务,则抛出异常。 +- `TransactionDefinition.PROPAGATION_MANDATORY`:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 +- `TransactionDefinition.PROPAGATION_NESTED`:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于`TransactionDefinition.PROPAGATION_REQUIRED`。‍ + +## Spring事务的回滚机制 + +**然后说一下Spring事务的回滚机制:** + +Spring的AOP即声明式事务管理默认是针对`unchecked exception`回滚。Spring的事务边界是在调用业务方法之前开始的,业务方法执行完毕之后来执行`commit or rollback`(Spring默认取决于是否抛出`runtimeException`)。 + +如果你在方法中有`try{}catch(Exception e){}`处理,那么try里面的代码块就脱离了事务的管理,若要事务生效需要在catch中`throw new RuntimeException ("xxxxxx");`这一点也是面试中会问到的事务失效的场景。 + +## @Transactional注解实现原理 + +再简单介绍一下`@Transactional`注解底层实现方式吧,毫无疑问,是通过动态代理,那么动态代理又分为JDK自身和CGLIB,这个也不多赘述了,毕竟今天的主题是如何将`@Transactional`对于事物的控制应用到炉火纯青。哈哈~ + +第一点要注意的就是在`@Transactional`注解的方法中,再调用本类中的其他方法method2时,那么method2方法上的`@Transactional`注解是不!会!生!效!的!但是加上也并不会报错,拿图片简单帮助理解一下吧。这一点也是面试中会问到的事务失效的场景。 + +![](http://img.topjavaer.cn/img/spring事务1.png) + +通过代理对象在目标对象前后进行方法增强,也就是事务的开启提交和回滚。那么继续调用本类中其他方法是怎样呢,如下图: + +![](http://img.topjavaer.cn/img/spring事务2.png) + +可见目标对象内部的自我调用,也就是通过this.指向的目标对象将不会执行方法的增强。 + +先说第二点需要注意的地方,等下说如何解决上面第一点的问题。第二点就是`@Transactional`注解的方法必须是公共方法,就是必须是public修饰符!!! + +至于这个的原因,发表下个人的理解吧,因为JVM的动态代理是基于接口实现的,通过代理类将目标方法进行增强,想一下也是啦,没有权限访问那么你让我怎么进行,,,好吧,这个我也没有深入研究底层,个人理解个人理解。 + +在这里我也放个问题吧,希望有高手可以回复指点指点我,因为JVM动态代理是基于接口实现的,那么是不是service层都要按照接口和实现类的开发模式,注解才会生效呢,就是说`controller`层直接调用没有接口的service层,加了注解也一样不起作用吧,这个懒了,没有测试,其一是因为没有人会这么开发吧,其二是我就认为是不起作用的,哈哈。 + +**下面来解决一下第一点的问题,如何在方法中调用本类中其他方法呢。** + +通过`AopContext.currentProxy ()`获取到本类的代理对象,再去调用就好啦。因为这个是CGLIB实现,所以要开启AOP,当然也很简单,在springboot启动类上加上注解`@EnableAspectJAutoProxy(exposeProxy = true)`就可以啦,这个依赖大家自行搜一下就好啦。要注意,注意,代理对象调用的方法也要是public修饰符,否则方法中获取不到注入的bean,会报空指针错误。 + +emmmm,我先把调用的方式和结果说下吧。自己简单写了代码,有点粗糙,就不要介意啦,嘿嘿。。。 + +Controller中调用Service + +``` +@RestController +public class TransactionalController { + + @Autowired + private TransactionalService transactionalService; + + @PostMapping("transactionalTest") + public void transacionalTest(){ + transactionalService.transactionalMethod(); + } +} +``` + +Service中实现对事务的控制:接口 + +``` +public interface TransactionalService { + void transactionalMethod(); +} +``` + +Service中实现对事务的控制:实现类(各种情况的说明都写在图片里了,这样方便阅读,有助于快速理解吧) + +![](http://img.topjavaer.cn/img/spring事务3.png) + +![](http://img.topjavaer.cn/img/spring事务4.png) + +上面两种情况不管使不使用代理调用方法1和方法2,方法`transactionalMethod`都处在一个事务中,四条更新操作全部失败。 + +那么有人可能会有疑问了,在方法1和方法2上都加`@Transactional`注解呢?答案是结果和上面是一致的。 + +小结只要方法`transactionalMethod`上有注解,并且方法1和方法2都处于当前事务中(不使用代理调用,方法1和方法2上的`@Transactional`注解是不生效的;使用代理,需要方法1和方法2都处在`transactionalMethod`方法的事务中,默认或者嵌套事务均可,当然也可以不加`@Transactional`注解),那么整体保持事务一致性。 + +如果想要方法1和方法2均单独保持事务一致性怎么办呢,刚说过了,如果不是用代理调用`@Transactional`注解是不生效的,所以一定要使用代理调用实现,然后让方法1和方法2分别单独开启新的事务,便OK啦。下面摆上图片。 + +![](http://img.topjavaer.cn/img/spring事务5.png) + +![](http://img.topjavaer.cn/img/spring事务6.png) + +这两种情况都是方法1和方法2均处在单独的事务中,各自保持事务的一致性。 + +接下来进行进一步的优化,可以在`transactionalMethod`方法中分别对方法1和方法2进行控制。要将代码的艺术发挥到极致嘛,下面装逼开始。 + +![](http://img.topjavaer.cn/img/spring事务7.png) + +代码太长了,超过屏幕了,粘贴出来截的图,红框注释需要仔细看,希望不要影响你的阅读体验,至此,本篇关于`@Transactioinal`注解的使用就到此为止啦, + +简单总结一下吧: + +1、就是`@Transactional`注解保证的是每个方法处在一个事务,如果有try一定在catch中抛出运行时异常。 + +2、方法必须是public修饰符。否则注解不会生效,但是加了注解也没啥毛病,不会报错,只是没卵用而已。 + +3、this.本方法的调用,被调用方法上注解是不生效的,因为无法再次进行切面增强。 + + + +> 原文:blog.csdn.net/fanxb92/article/details/81296005 diff --git a/docs/advance/excellent-article/20-architect-pattern.md b/docs/advance/excellent-article/20-architect-pattern.md new file mode 100644 index 0000000..82d68ab --- /dev/null +++ b/docs/advance/excellent-article/20-architect-pattern.md @@ -0,0 +1,249 @@ +--- +sidebar: heading +title: 几种常见的架构模式 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 架构模式 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 几种常见的架构模式 + +分享一些工作中会用到的一些架构方面的设计模式。总体而言,共有八种,分别是: + +1. **单库单应用模式**:最简单的,可能大家都见过 +2. **内容分发模式**:目前用的比较多 +3. **查询分离模式**:对于大并发的查询、业务 +4. **微服务模式**:适用于复杂的业务模式的拆解 +5. **多级缓存模式**:可以把缓存玩的很好 +6. **分库分表模式**:解决单机数据库瓶颈 +7. **弹性伸缩模式**:解决波峰波谷业务流量不均匀的方法之一 +8. **多机房模式**:解决高可用、高性能的一种方法 + +### **三、单库单应用模式** + +这是最简单的一种设计模式,我们的大部分本科毕业设计、一些小的应用,基本上都是这种模式,这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a5f8e57770~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示,这种模式一般只有一个数据库,一个业务应用层,一个后台管理系统,所有的业务都是用过业务层完成的,所有的数据也都是存储在一个数据库中的,好一点会有数据库的同步。虽然简单,但是**也并不是一无是处**。 + +- **优点**:结构简单、开发速度快、实现简单,可用于产品的第一版等有原型验证需求、用户少的设计。 +- **缺点**:性能差、基本没有高可用、扩展性差,不适用于大规模部署、应用等生产环境。 + +### **四、内容分发模式** + +基本上所有的大型的网站都有或多或少的采用这一种设计模式,常见的应用场景是使用CDN技术把网页、图片、CSS、JS等这些静态资源分发到离用户最近的服务器。这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a5f8ff4662~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + +如上图所示,这种模式较单库单应用模式多了一个CDN、一个云存储OSS(七牛、又拍等雷同)。一个典型的应用流程(以用户上传、查看图片需求为例)如下: + +1. 上传的时候,用户选择本地机器上的一个图片进行上传 +2. 程序会把这个图片上传到云存储OSS上,并返回该图片的一个URL +3. 程序把这个URL字符串存储在业务数据库中,上传完成。 +4. 查看的时候,程序从业务数据库得到该图片的URL +5. 程序通过DNS查询这个URL的图片服务器 +6. **智能DNS会解析这个URL,得到与用户最近的服务器(或集群)的地址A** +7. 然后把服务器A上的图片返回给程序 +8. 程序显示该图片,查看完成。 + +由上可知,这个模式的关键是智能DNS,它能够解析出离用户最近的服务器。运行原理大致是:根据请求者的IP得到请求地点B,然后通过计算或者配置得到与B最近或通讯时间最短的服务器C,然后把C的IP地址返回给请求者。这种模式的优缺点如下: + +- **优点**:资源下载快、无需过多的开发与配置,同时也减轻了后端服务器对资源的存储压力,减少带宽的使用。 +- **缺点**:目前来说OSS,CDN的价格还是稍微有些贵(虽然已经降价好几次了),只适用于中小规模的应用,另外由于网络传输的延迟、CDN的同步策略等,会有一些一致性、更新慢方面的问题。 + +### **五、查询分离模式** + +这种模式主要解决单机数据库压力过大,从而导致业务缓慢甚至超时,查询响应时间变长的问题,也包括需要大量数据库服务器计算资源的查询请求。这个可以说是单库单应用模式的升级版本,也是技术架构迭代演进过程中的必经之路。 这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a5fcca9706~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + +如上图所示,这种模式较单库单应用模式与内容分发模式多了几个部分,一个是**业务数据库的主从分离**,一个是**引入了ES**,为什么要这样?都解决了哪些痛点,下面具体结合业务需求场景进行叙述。 + +**场景一:全文关键词检索** + +我想这个需求,绝大多数应用都会有,如果使用传统的数据库技术,大部分可能都会使用like这种SQL语句,高级一点可能是先分词,然后通过分词index相关的记录。SQL语句的性能问题与全表扫描机制导致了非常严重的性能问题,现在基本上很少见到。 这里的ES是ElasticSearch的缩写,是一种查询引擎,类似的还有Solr等,都差不多的技术,ES较Solr配置简单、使用方便,所以这里选用了它。另外,ES支持横向扩展,理论上没有性能的瓶颈。同时,还支持各种插件、自定义分词器等,可扩展性较强。在这里,使用ES不仅可以替代数据库完成全文检索功能,还可以实现诸如分页、排序、分组、分面等功能。具体的,请同学们自行学习之。那怎么使用呢?一个一般的流程是这样的: + +1. 服务端把一条业务数据落库 +2. 服务端异步把该条数据发送到ES +3. ES把该条记录按照规则、配置**放入自己的索引库** +4. 客户端查询的时候,由服务端把这个**请求发送到ES**,得到数据后,根据需求拼装、组合数据,返回给客户端 + +实际中怎么用,还请同学们根据实际情况做组合、取舍。 + +**场景二:大量的普通查询** + +这个场景是指我们的业务中的大部分辅助性的查询,如:取钱的时候先查询一下余额,根据用户的ID查询用户的记录,取得该用户最新的一条取钱记录等。我们肯定是要天天要用的,而且用的还非常多。同时呢,我们的写入请求也是非常多的,导致**大量的写入、查询操作压向同一数据库**,然后,数据库挂了,系统挂了,领导生气了,被开除了,还不起房贷了,露宿街头了,老婆跟别人跑了,...... + +不敢想,所以要求我们必须分散数据库的压力,一个业界较成熟的方案就是数据库的读写分离,**写的时候入主库,读的时候读从库**。这样就把压力分散到不同的数据库了,如果一个读库性能不行,扛不住的话,可以一主多从,横向扩展。可谓是一剂良药啊!那怎么使用呢?一个一般的流程是这样的: + +1. 服务端把一条业务数据落库 +2. 数据库同步或异步或半同步把该条数据**复制**到从库 +3. 服务端读数据的时候直接去**从库读**相应的数据 + +比较简单吧,一些聪明的、爱思考的、上进的同学可能发现问题了,也包括上面介绍的场景一,就是延迟问题,如:数据还没有到从库,我就马上读,那么是读不到的,会发生问题的。 对于这个问题,各家公司解决的思路不一样,方法不尽相同。一个普遍的解决方案是:**读不到就读主库**,当然这么说也是有前提条件的,但具体的方案这里就不一一展开了,我可能会在接下来的分享中详解各种方案。 另外,关于数据库的复制模式,还请同学们自行学习,太多了,这里说不清。该总结一下这种模式的优缺点的了,如下: + +- **优点**:减少数据库的压力,理论上提供无限高的读性能,间接提高业务(写)的性能,专用的查询、索引、全文(分词)解决方案。 +- **缺点**:数据延迟,数据一致性的保证。 + +### **六、微服务模式** + +上面的模式看似不错,解决了性能问题,我可以不用露宿街头了、老婆还是我的,哈哈。但是 + +软件系统天生的复杂性决定了,除了性能,还有其他诸如高可用、健壮性等大量问题等待我们解决,再加上各个部门间的撕逼、扯皮,更让我们码农雪上加霜,所以 + +继续吧...... + +微服务模式可以说是最近的热点,花花绿绿、大大小小、国内国外的公司都在鼓吹,实践这个模式,可是大部分都没有弄清楚**为什么**要这么做,也并不知道这么做有什么**好处、坏处**,在这里,我将以我自己的亲身实践说一下我对这个模式的看法,不喜勿喷!随着业务与人员的增加,遇到了如下的问题: + +1. 单机数据库写请求量大量增加,导致数据库压力变大 +2. 数据库一旦挂了,那么整个业务都挂了 +3. 业务代码越来越多,都在一个GIT里,越来越难以维护 +4. 代码腐化严重、臭味越来越浓 +5. 上线越来越频繁,经常是一个小功能的修改,就要整个大项目要重新编译 +6. 部门越来越多,该哪个部门改动大项目中的哪个东西,撕逼的厉害 +7. 其他一些外围系统直接连接数据库,导致一旦数据库结构发生变化,所有的相关系统都要通知,甚至对修改不敏感的系统也要通知 +8. 每个应用服务器需要开通所有的权限、网络、FTP、各种各样的,因为每个服务器部署的应用都是一样的 +9. 作为架构师,我已经失去了对这个系统的把控...... + +为了解决上述问题,我司使用了微服务模式,这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a5fc90d54b~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示,我把**业务分块,做了垂直切分,切成一个个独立的系统,每个系统各自衍化,有自己的库、缓存、ES等辅助系统,系统之间的实时交互通过RPC,异步交互通过MQ,通过这种组合,共同完成整个系统功能。** 那么,这么做是否真的解决上述问题了呢?不玩虚的,一个个来说。对于问题一,由于拆分成了多个子系统,系统的压力被分散了,而各个子系统都有自己的数据库实例,所以数据库的压力变小。 + +对于问题二,一个子系统A的数据库挂了,只是影响到系统A和使用系统A的那些功能,不会所有的功能不可用,从而解决一个数据库挂了,导致所有功能不可用的问题。 + +问题三、四,也因为拆分得到了解决,各个子系统有自己独立的GIT代码库,不会相互影响。通用的模块可通过库、服务、平台的形式解决。 + +问题五,子系统A发生改变,需要上线,那么我只需要编译A,然后上线就可以了,不需要其他系统做同样的事情。 + +问题六,顺应了**康威定律**,我部门该干什么事、输出什么,也通过服务的形式暴露出来,我部只管把我部的职责、软件功能做好就可以。 + +问题七,所有需要我部数据的需求,都通过接口的形式发布出去,客户通过接口获取数据,从而屏蔽了底层数据库结构,甚至数据来源,我部只需保证我部的接口契约没有发生变化即可,新的需求增加新的接口,不会影响老的接口。 + +问题八,不同的子系统需要不同的权限,这个问题也优雅的解决了。 + +问题九,暂时控制住了复杂性,我只需控制好大的方面,定义好系统边界、接口、大的流程,然后再分而治之、逐个击破、合纵连横。 + +目前来说,所有问题得到解决!bingo! 但是,还有许多其他的副作用会随之产生,如RPC、MQ的超高稳定性、超高性能,网络延迟,数据一致性等问题,这里就不展开来讲了,太多了,一本书都讲不完。 + +另外,对于这个模式来说,最难把握的是**度**,切记**不要切分过细**,我见过一个功能一个子系统,上百个方法分成上百个子系统的,真的是太过度了。实践中,一个较为可行的方法是:**能不分就不分,除非有非常必要的理由**!。 + +- **优点**:相对高性能,可扩展性强,高可用,适合于中等以上规模公司架构。 +- **缺点**:复杂、度不好把握。指不仅需要一个能在高层把控大方向、大流程、总体技术的人,还需要能够针对各个子系统有针对性的开发。把握不好度或者滥用的话,这个模式适得其反! + +### **七、多级缓存模式** + +这个模式可以说是应对超高查询压力的一种普遍采用的策略,基本的思想就是在所有链路的地方,能加缓存就加缓存,如下图所示: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a5fe721312~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示,一般在三个地方加入缓存,一个是客户端处,一个是API网关处,一个是具体的后端业务处,下面分别介绍。 + +**客户端处缓存**:这个地方加缓存可以说是效果最好的---无延迟。因为不用经过长长的网络链条去后端业务处获取数据,从而导致加载时间过长,客户流失等损失。虽然有CDN的支持,但是从客户端到CDN还是有网络延迟的,虽然不大。具体的技术依据不同的客户端而定,对于WEB来讲,有浏览器本地缓存、Cookie、Storage、缓存策略等技术;对于APP来讲,有本地数据库、本地文件、本地内存、进程内缓存支持。以上提到的各种技术有兴趣的同学可以继续展开来学习。如果客户端缓存没有命中,那么就会去后端业务拿数据,一般来讲,都会有个API网关,在这里加缓存也是非常有必要的。 + +**API网关处缓存**:这个地方加缓存的好处是不用把请求发送到后方,直接在这里就处理了,然后返回给请求者。常见的技术,如http请求,API网关用的基本都是nginx,可以使用nginx本身的缓存模块,也可以使用Lua+Redis技术定制化。其他的也都大同小异。 + +**后端业务处**:这个我想就不用多说了,大家应该差不多都知道,什么Redis,Memcache,Jvm内等等,不熬述了。 + +实践中,要结合具体的实际情况,综合利用各级缓存技术,使得各种请求最大程度的在到达后端业务之前就被解决掉,从而减少后端服务压力、减少占用带宽、增强用户体验。至于是否只有这三个地方加缓存,我觉得要活学活用,**心法比剑法重要!**总结一下这个模式的优缺点: + +- **优点**:抗住大量读请求,减少后端压力。 +- **缺点**:数据一致性问题较突出,容易发生雪崩,即:如果客户端缓存失效、API网关缓存失效,那么所有的大量请求瞬间压向后端业务系统,后果可想而知。 + +### **八、分库分表模式** + +这种模式主要解决单表写入、读取、存储压力过大,从而导致业务缓慢甚至超时,交易失败,容量不够的问题。一般有水平切分和垂直切分两种,这里主要介绍水平切分。这个模式也是技术架构迭代演进过程中的必经之路。 这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a717cfbdbf~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示红色部分,把一张表分到了几个不同的库中,从而分担压力。是不是很笼统?哈哈,那我们接下来就详细的讲解一下。首先澄清几个概念,如下: **主机**:硬件,指一台物理机,或者虚拟机,有自己的CPU,内存,硬盘等。 **实例**:数据库实例,如一个MySQL服务进程。一个主机可以有多个实例,不同的实例有不同的进程,监听不同的端口。 **库**:指表的集合,如学校库,可能包含教师表、学生表、食堂表等等,这些表在一个库中。一个实例中可以有多个库。库与库之间用库名来区分。 **表**:库中的表,不必多说,不懂的就不用往下看了,不解释。 + +那么怎么把单表分散呢?到底怎么个分发呢?分发到哪里呢?以下是几个工作中的实践,分享一下: **主机**:这是最主要的也是最重要的点,本质上分库分表是因为计算与存储资源不够导致的,而这种资源主要是由物理机,主机提供的,所以在这里分是最基本的,毕竟没有可用的计算资源,怎么分效果都不是太好的。 实例:实例控制着连接数,同时受OS限制,CPU、内存、硬盘、网络IO也会受间接影响。会出现热实例的现象,即:有些实例特别忙,有些实例非常的空闲。一个典型的现象是:由于单表反应慢,导致连接池被打满,所有其他的业务都受影响了。这时候,把表分到不同的实例是有一些效果的。 库:一般是由于单库中最大单表数量的限制,才采取分库。 表:单表压力过大,索引量大,容量大,单表的锁。据以上,把单表水平切分成不同的表。 + +大型应用中,都是一台主机上只有一个实例,一个实例中只有一个库,**库==实例==主机,所以才有了分库分表**这个简称。 + +既然知道了基本理论,那么具体是怎么做的呢?逻辑是怎么跑的呢?接下来以一个例子来讲解一下。 这个需求很简单,用户表(user),单表数据量1亿,查询、插入、存储都出现了问题,怎么办呢? + +首先,分析问题,这个明显是由于数据量太大了而导致的问题。 其次,设计方案,可以分为10个库,这样每个库的数据量就降到了1KW,单表1KW数据量还是有些大,而且不利于以后量的增长,所以每个库再分100个表,这个每个单表数据量就为10W了,对于查询、索引更新、单表文件大小、打开速度,都有一些益处。接下来,给IT部门打电话,要10台物理机,扩展数据库...... 最后,逻辑实现,这里应该是最有学问的地方。首先是写入数据,需要知道写到哪个分库分表中,读也是一样的,所以,需要有个请求**路由层**,负责把请求分发、转换到不同的库表中,一般有路由规则的概念。 + +怎么样,简单吧?哈哈,too 那义务。说说这个模式的问题,主要是带来了事务上的问题,因为分库分表,事务完成不了,而**分布式事务**又太笨重,所以这里需要有一定的策略,保证在这种情况下事务能够完成。采取的策略如:最终一致性、复制、特殊设计等。再有就是业务代码的改造,一些关联查询要改造,一些单表orderBy的问题需要特殊处理,也包括groupBy语句,如何解决这些副作用不是一句两句能说清楚的,以后有时间,我单独讲讲这些。 + +该总结一下这种模式的优缺点的了,如下: + +- **优点**:减少数据库单表的压力。 +- **缺点**:事务保证困难、业务逻辑需要做大量改造。 + +### **九、弹性伸缩模式** + +这种模式主要解决突发流量的到来,导致无法横向扩展或者横向扩展太慢,进而影响业务,全站崩溃的问题。这个模式是一种相对来说比较高级的技术,也是各个大公司目前都在研究、试用的技术。截至今日,有这种思想的架构师就已经是很不错了,能够拿到较高薪资,更别提那些已经实践过的,甚至实现了底层系统的那些,所以,你懂得...... 这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a719a46586~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示,多了一个弹性伸缩服务,用来动态的增加、减少实例。原理上非常简单,但是这个模式到底解决什么问题呢?先说说由来和意义。 + +每年的双11、六一八或者一些大促到来之前,我们都会为大流量的到来做以下几个方面的工作: 提前准备10倍甚至更多的机器,即使用不上也要放在那里备着,以防万一。这样浪费了大量的资源。 每台机器配置、调试、引流,以便让所有的机器都可用。这样浪费了大量的人力、物力,更容易出错。 如果机器准备不充分,那么还要加班加点的重复上面的工作。这样做特别容易出错,引来领导的不满,没时间回家陪老婆,然后你的老婆就......(自己想) + +在双十一之后,我们还要人工做缩容,非常的辛苦。一般一年中会有多次促销,那么我们就会一直这样,实在是烦! + +最严重的,**突然间的大流量爆发,会让我们触不及防**,半夜起来扩容是在正常不过的事情,为此,我们偷懒起来,要更多的机器备着,也就出现了大量的cpu利用率为1%的机器。 + +我相信,如果你是老板一定很**震惊**吧!!! 哈哈,那么如何改变这种情况呢?请接着看 + +为此,首先把所有的计算资源整合成资源池的概念,然后通过一些策略、监控、服务,动态的从资源池中获取资源,用完后在放回到池子中,供其他系统使用。 具体实现上比较成熟的两种资源池方案是VM、docker,每个都有着自己强大的生态。监控的点有CPU、内存、硬盘、网络IO、服务质量等,根据这些,在配合一些预留、扩张、收缩策略,就可以简单的实现自动伸缩。怎么样?是不是很神奇?深入的内容我们会在的码农原创的公众号文章中详细介绍。 + +该总结一下这种模式的优缺点的了,如下: + +- **优点**:弹性、随需计算,充分优化企业计算资源。 +- **缺点**:应用要从架构层做到可横向扩展化改造、依赖的底层配套比较多,对技术水平、实力、应用规模要求较高。 + +### **十、多机房模式** + +这种模式主要解决**不同地区高性能、高可用**的问题。 + +随着应用用户不断的增加,用户群体分布在全球各地,如果把服务器部署在一个地方,一个机房,比如北京,那么美国的用户使用应用的时候就会特别慢,因为每一个请求都需要通过海底光缆走上个那么一秒钟(预估)左右,这样对用户体验及其不好。怎么办?使用多机房部署。 + +这种模式的一般设计见下图: + +![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/11/29/16eb61a7198c976d~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image) + + + +如上图所示,一个典型的用户请求流程如下: + +用户请求一个链接A 通过DNS智能解析到离用户最近的机房B 使用B机房服务链接A + +是不是觉得很简单,没啥?其实这里面的问题没有表面这么简单,下面一一道来。 首先是**数据同步**问题,在中国产生的数据要同步到美国,美国的也一样,数据同步就会涉及数据版本、一致性、更新丢弃、删除等问题。 其次是一地多机房的请求**路由**问题,典型的是如上图,中国的北京机房和杭州机房,如果北京机房挂了,那么要能够通过路由把所有发往北京机房的请求转发到杭州机房。异地也存在这个问题。 + +所以,多机房模式,也就是异地多活并不是那么的简单,这里只是起了个头,具体的有哪些坑,会在另一篇文章中介绍。 + +该总结一下这种模式的优缺点的了,如下: + +- **优点**:高可用、高性能、异地多活。 +- **缺点**:数据同步、数据一致性、请求路由。 + +至此,整个关于**八种架构设计模式及其优缺点概述**就介绍完了,大约1W字左右。最后,我想说的是没有银弹、灵活运用,共勉! + + + + + +> 参考链接:https://juejin.cn/post/6844904007438172167 diff --git a/docs/advance/excellent-article/22-distributed-scheduled-task.md b/docs/advance/excellent-article/22-distributed-scheduled-task.md new file mode 100644 index 0000000..2d643a5 --- /dev/null +++ b/docs/advance/excellent-article/22-distributed-scheduled-task.md @@ -0,0 +1,150 @@ +--- +sidebar: heading +title: 新一代分布式任务调度框架 +category: 优质文章 +tag: + - 分布式 +head: + - - meta + - name: keywords + content: 分布式任务调度框架,任务调度 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 新一代分布式任务调度框架 + +我们先思考下面几个业务场景的解决方案: + +- 支付系统每天凌晨1点跑批,进行一天清算,每月1号进行上个月清算 +- 电商整点抢购,商品价格8点整开始优惠 +- 12306购票系统,超过30分钟没有成功支付订单的,进行回收处理 +- 商品成功发货后,需要向客户发送短信提醒 + +> 类似的业务场景非常多,我们怎么解决? + +## 为什么我们需要定时任务 + +很多业务场景需要我们某一特定的时刻去做某件任务,定时任务解决的就是这种业务场景。一般来说,系统可以使用消息传递代替部分定时任务,两者有很多相似之处,可以相互替换场景。 + +如,上面发货成功发短信通知客户的业务场景,我们可以在发货成功后发送MQ消息到队列,然后去消费mq消息,发送短信。但在某些场景下不能互换: + +- **时间驱动/事件驱动:**内部系统一般可以通过时间来驱动,但涉及到外部系统,则只能使用时间驱动。如怕取外部网站价格,每小时爬一次 +- **批量处理/逐条处理:**批量处理堆积的数据更加高效,在不需要实时性的情况下比消息中间件更有优势。而且有的业务逻辑只能批量处理。如移动每个月结算我们的话费 +- **实时性/非实时性:**消息中间件能够做到实时处理数据,但是有些情况下并不需要实时,比如:vip升级 +- **系统内部/系统解耦:**定时任务调度一般是在系统内部,而消息中间件可用于两个系统间 + +## java有哪些定时任务的框架 + +### **单机** + +- **timer:**是一个定时器类,通过该类可以为指定的定时任务进行配置。TimerTask类是一个定时任务类,该类实现了Runnable接口,缺点异常未检查会中止线程 +- **ScheduledExecutorService:**相对延迟或者周期作为定时任务调度,缺点没有绝对的日期或者时间 +- **spring定时框架:**配置简单功能较多,如果系统使用单机的话可以优先考虑spring定时器 + +### **分布** + +- **Quartz:**Java事实上的定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能 +- **TBSchedule:**阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重 +- **elastic-job:**当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,目前是版本2.15,并且可以支持云开发 +- **Saturn:**是唯品会自主研发的分布式的定时任务的调度平台,基于当当的elastic-job 版本1开发,并且可以很好的部署到docker容器上。 +- **xxl-job:** 是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。 + +## 分布式任务调度系统对比 + +> 参与对比的可选系统方案:elastic——job (以下简称E-Job)与 xxx-job(以下简称X-Job) + +### **支持集群部署** + +**X-Job**:集群部署唯一要求为:保证每个集群节点配置(db和登陆账号等)保持一致。调度中心通过db配置区分不同集群。 + +> 执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。集群部署唯一要求为:保证集群中每个执行器的配置项 “`xxl.job.admin.addresses/调度中心地址`” 保持一致,执行器根据该配置进行执行器自动注册等操作。 + +**E-Job**:重写Quartz基于数据库的分布式功能,改用Zookeeper实现注册中心 + +> 作业注册中心:基于Zookeeper和其客户端Curator实现的全局作业注册控制中心。用于注册,控制和协调分布式作业执行。 + +### **多节点部署时任务不能重复执行** + +**X-Job**:使用Quartz基于数据库的分布式功能 + +**E-Job**:将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,elastic-job将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片。 + +### **日志可追溯** + +**X-Job**:支持,有日志查询界面 + +**E-Job**:可通过事件订阅的方式处理调度过程的重要事件,用于查询、统计和监控。Elastic-Job目前提供了基于关系型数据库两种事件订阅方式记录事件。 + +### **监控告警** + +**X-Job**:调度失败时,将会触发失败报警,如发送报警邮件。 + +> 任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔 + +**E-Job**:通过事件订阅方式可自行实现 + +> 作业运行状态监控、监听作业服务器存活、监听近期数据处理成功、数据流类型作业(可通过监听近期数据处理成功数判断作业流量是否正常,如果小于作业正常处理的阀值,可选择报警。)、监听近期数据处理失败(可通过监听近期数据处理失败数判断作业处理结果,如果大于0,可选择报警。) + +### **弹性扩容缩容** + +**X-Job**:使用Quartz基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力 + +**E-Job**:通过zk实现各服务的注册、控制及协调 + +### **支持并行调度** + +**X-Job**:调度系统多线程(默认10个线程)触发调度运行,确保调度精确执行,不被堵塞。 + +**E-Job**:采用任务分片方式实现。将一个任务拆分为n个独立的任务项,由分布式的服务器并行执行各自分配到的分片项。 + +### **高可用策略** + +**X-Job**:“调度中心”通过DB锁保证集群分布式调度的一致性, 一次任务调度只会触发一次执行; + +**E-Job**:调度器的高可用是通过运行几个指向同一个ZooKeeper集群的`Elastic-Job-Cloud-Scheduler`实例来实现的。ZooKeeper用于在当前主`Elastic-Job-Cloud-Scheduler`实例失败的情况下执行领导者选举。通过至少两个调度器实例来构成集群,集群中只有一个调度器实例提供服务,其他实例处于”待命”状态。当该实例失败时,集群会选举剩余实例中的一个来继续提供服务。 + +### **失败处理策略** + +**X-Job**:调度失败时的处理策略,策略包括:失败告警(默认)、失败重试; + +**E-Job**:弹性扩容缩容在下次作业运行前重分片,但本次作业执行的过程中,下线的服务器所分配的作业将不会重新被分配。失效转移功能可以在本次作业运行中用空闲服务器抓取孤儿作业分片执行。同样失效转移功能也会牺牲部分性能。 + +### **动态分片策略** + +**X-Job:**分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。 + +执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时传递分片参数;可根据分片参数开发分片任务; + +**E-Job:**支持多种分片策略,可自定义分片策略 + +默认包含三种分片策略:基于平均分配算法的分片策略、 作业名的哈希值奇偶数决定IP升降序算法的分片策略、根据作业名的哈希值对Job实例列表进行轮转的分片策略,支持自定义分片策略 + +elastic-job的分片是通过zookeeper来实现的。分片的分片由主节点分配,如下三种情况都会触发主节点上的分片算法执行:a、新的Job实例加入集群 b、现有的Job实例下线(如果下线的是leader节点,那么先选举然后触发分片算法的执行) c、主节点选举” + +### **和quartz框架对比** + +- 调用API的的方式操作任务,不人性化; +- 需要持久化业务`QuartzJobBean`到底层数据表中,系统侵入性相当严重。 +- 调度逻辑和`QuartzJobBean`耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况加,此时调度系统的性能将大大受限于业务; +- Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能。 + +## 综合对比 + +![](http://img.topjavaer.cn/img/定时任务框架选型.png) + +## 总结和结论 + +**共同点:** + +E-Job和X-job都有广泛的用户基础和完整的技术文档,都能满足定时任务的基本功能需求。 + +**不同点:** + +- X-Job 侧重的业务实现的简单和管理的方便,学习成本简单,失败策略和路由策略丰富。推荐使用在“用户基数相对少,服务器数量在一定范围内”的情景下使 +- E-Job 关注的是数据,增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。但是学习成本相对高些,推荐在“数据量庞大,且部署服务器数量较多”时使用 + + + +> 摘录自网络 diff --git a/docs/advance/excellent-article/23-arthas-intro.md b/docs/advance/excellent-article/23-arthas-intro.md new file mode 100644 index 0000000..24a19bc --- /dev/null +++ b/docs/advance/excellent-article/23-arthas-intro.md @@ -0,0 +1,496 @@ +--- +sidebar: heading +title: Arthas 常用命令 +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: Arthas常用命令,Arthas + - - meta + - name: description + content: 优质文章汇总 +--- + +# Arthas 常用命令 + +### 简介 + +Arthas 是Alibaba开源的Java诊断工具,动态跟踪Java代码;实时监控JVM状态,可以在不中断程序执行的情况下轻松完成JVM相关问题排查工作 。支持JDK 6+,支持Linux/Mac/Windows。这个工具真的很好用,而且入门超简单,十分推荐。 + +### 使用场景 + +1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? +2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? +3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? +4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! +5. 是否有一个全局视角来查看系统的运行状况? +6. 有什么办法可以监控到JVM的实时运行状态?接下来,围绕这6个问题,学习下Arthas的基本用法。 + +### 安装 + +执行下面命令下载 + +``` +wget https://alibaba.github.io/arthas/arthas-boot.jar +``` + +用java -jar的方式启动 + +``` +java -jar arthas-boot.jar + +[INFO] Found existing java process, please choose one and hit RETURN. +* [1]: 79952 cn.test.MobileApplication + [2]: 93872 org.jetbrains.jps.cmdline.Launcher +``` + +然后输入数字,选择你想要监听的应用,回车即可 + +### 常用命令 + +查询arthas版本 + +``` +[arthas@79952]$ version +3.1.4 +``` + +#### 1、stack + +输出当前方法被调用的调用路径 + +很多时候我们都知道一个方法被执行,但是有很多地方调用了它,你并不知道是谁调用了它,此时你需要的是 stack 命令。 + +| 参数名称 | 参数说明 | +| :--------------- | :--------------- | +| *class-pattern* | 类名表达式匹配 | +| *method-pattern* | 方法名表达式匹配 | + +```bash +[arthas@79952]$ stack com.baomidou.mybatisplus.extension.service.IService getOne +Press Q or Ctrl+C to abort. +Affect(class-cnt:202 , method-cnt:209) cost in 10761 ms. +ts=2019-11-13 11:49:13;thread_name=http-nio-8801-exec-6;id=2d;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@a6c54c3 + @com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.getOne() + at com.baomidou.mybatisplus.extension.service.IService.getOne(IService.java:230) + ...... ...... + at cn.test.mobile.controller.order.OrderController.getOrderInfo(OrderController.java:500) +``` + +可以看到OrderController.java的第500行调用了这个getOne接口。 + +*注意这个命令需要调用后才会触发日志,相似的还有watch、trace等* + +#### 2、jad + +反编译指定已加载类的源码 + +有时候,版本发布后,代码竟然没有执行,代码是最新的吗,这时可以使用jad反编译相应的class。 + +```bash +jad cn.test.mobile.controller.order.OrderController +``` + +仅编译指定的方法 + +```bash +jad cn.test.mobile.controller.order.OrderController getOrderInfo + +ClassLoader: +@RequestMapping(value={"getOrderInfo"}, method={RequestMethod.POST}) +public Object getOrderInfo(HttpServletRequest request, @RequestBody Map map) { + ResponseVo responseVo = new ResponseVo(); + ... ... ... ... +``` + +#### 3、sc + +“Search-Class” 的简写 ,查看JVM已加载的类信息 有的时候,你只记得类的部分关键词,你可以用sc获取完整名称 当你碰到这个错的时候“ClassNotFoundException”或者“ClassDefNotFoundException”,你可以用这个命令验证下 + +| 参数名称 | 参数说明 | +| :--------------- | :----------------------------------------------------------- | +| *class-pattern* | 类名表达式匹配 | +| *method-pattern* | 方法名表达式匹配 | +| [d] | 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次 | + +模糊搜索 + +```bash +sc *OrderController* +cn.test.mobile.controller.order.OrderController +``` + +打印类的详细信息 sc -d + +```bash +sc -d cn.test.mobile.controller.order.OrderController + + class-info cn.test.mobile.controller.order.OrderController + code-source /F:/IDEA-WORKSPACE-TEST-qyb/trunk/BE/mobile/target/classes/ + name cn.test.mobile.controller.order.OrderController + isInterface false + isAnnotation false + isEnum false + isAnonymousClass false + isArray false + isLocalClass false + isMemberClass false + isPrimitive false + isSynthetic false + simple-name OrderController + modifier public + annotation org.springframework.web.bind.annotation.RestController,org.springframework.web.bind.annotation.Requ + estMapping + interfaces + super-class +-cn.test.mobile.controller.BaseController + +-java.lang.Object + class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2 + +-sun.misc.Launcher$ExtClassLoader@480bdb19 + classLoaderHash 18b4aac2 +``` + +与之相应的还有sm( “Search-Method” ),查看已加载类的方法信息 + +查看String里的方法 + +```bash +sm java.lang.String +java.lang.String ([BII)V +java.lang.String ([BLjava/nio/charset/Charset;)V +java.lang.String ([BLjava/lang/String;)V +java.lang.String ([BIILjava/nio/charset/Charset;)V +java.lang.String ([BIILjava/lang/String;)V +... ... ... ... +``` + +查看String中toString的详细信息 + +```bash +sm -d java.lang.String toString +declaring-class java.lang.String + method-name toString + modifier public + annotation + parameters + return java.lang.String + exceptions + classLoaderHash null +``` + +#### 4、watch + +可以监测一个方法的入参和返回值 + +有些问题线上会出现,本地重现不了,这时这个命令就有用了 + +| 参数名称 | 参数说明 | +| :------------------ | :------------------------------------------------------- | +| *class-pattern* | 类名表达式匹配 | +| *method-pattern* | 方法名表达式匹配 | +| *express* | 观察表达式 | +| *condition-express* | 条件表达式 | +| [b] | 在**方法调用之前**观察 | +| [e] | 在**方法异常之后**观察 | +| [s] | 在**方法返回之后**观察 | +| [f] | 在**方法结束之后**(正常返回和异常返回)观察,**默认选项** | +| [E] | 开启正则表达式匹配,默认为通配符匹配 | +| [x:] | 指定输出结果的属性遍历深度,默认为 1 | + +观察getOrderInfo的出参和返回值,出参就是方法结束后的入参 + +``` +watch cn.test.mobile.controller.order.OrderController getOrderInfo "{params,returnObj}" -x 2 + +Press Q or Ctrl+C to abort. +Affect(class-cnt:1 , method-cnt:1) cost in 456 ms. +ts=2019-11-13 15:30:18; [cost=18.48307ms] result=@ArrayList[ + @Object[][ # 这个就是出参,params + @RequestFacade[org.apache.catalina.connector.RequestFacade@1d81dbd7], + @LinkedHashMap[isEmpty=false;size=2], # 把遍历深度x改为3就可以查看map里的值了 + ], + @ResponseVo[ # 这个就是返回值 returnObj + log=@Logger[Logger[cn.test.db.common.vo.ResponseVo]], + success=@Boolean[true], + message=@String[Ok], + count=@Integer[0], + code=@Integer[1000], + data=@HashMap[isEmpty=false;size=1], + ], +] +``` + +观察getOrderInfo的入参和返回值 + +``` +watch cn.test.mobile.controller.order.OrderController getOrderInfo "{params,returnObj}" -x 3 -b + +Press Q or Ctrl+C to abort. +Affect(class-cnt:1 , method-cnt:1) cost in 93 ms. +ts=2019-11-13 15:37:38; [cost=0.012479ms] result=@ArrayList[ + @Object[][ + @RequestFacade[ + request=@Request[org.apache.catalina.connector.Request@d04e652], + sm=@StringManager[org.apache.tomcat.util.res.StringManager@7ae7a97b], + ], + @LinkedHashMap[ + @String[payNo]:@String[190911173713755288], + @String[catalogId]:@String[6], + ], + ], + null,# -b是方法调用之前观察,所以还没有返回值 +] +``` + +如果需要捕捉异常的话,使用throwExp,如{params,returnObj,throwExp} + +#### 5、trace + +输出方法内部调用路径,和路径上每个节点的耗时 + +可以通过这个命令,查看哪些方法耗性能,从而找出导致性能缺陷的代码,这个耗时还包含了arthas执行的时间哦。 + +| 参数名称 | 参数说明 | +| :------------------ | :----------------------------------- | +| *class-pattern* | 类名表达式匹配 | +| *method-pattern* | 方法名表达式匹配 | +| *condition-express* | 条件表达式 | +| [E] | 开启正则表达式匹配,默认为通配符匹配 | +| `[n:]` | 命令执行次数 | +| `#cost` | 方法执行耗时 | + +输出getOrderInfo的调用路径 + +```bash +trace -j cn.test.mobile.controller.order.OrderController getOrderInfo + +Press Q or Ctrl+C to abort. +Affect(class-cnt:1 , method-cnt:1) cost in 92 ms. +---ts=2019-11-13 15:46:59;thread_name=http-nio-8801-exec-4;id=2b;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@a6c54c3 + ---[15.509011ms] cn.test.mobile.controller.order.OrderController:getOrderInfo() + +---[0.03584ms] cn.test.db.common.vo.ResponseVo:() #472 + +---[0.00992ms] java.util.HashMap:() #473 + +---[0.02176ms] cn.test.mobile.controller.order.OrderController:getUserInfo() #478 + +---[0.024ms] java.util.Map:get() #483 + +---[0.00896ms] java.lang.Object:toString() #483 + +---[0.00864ms] java.lang.Integer:parseInt() #483 + +---[0.019199ms] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:() #500 + +---[0.135679ms] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:allEq() #500 + +---[12.476072ms] cn.test.db.service.IOrderMediaService:getOne() #500 + +---[0.0128ms] java.util.HashMap:put() #501 + +---[0.443517ms] cn.test.db.common.vo.ResponseVo:setSuccess() #503 + `---[0.03488ms] java.util.Map:put() #504 +``` + +输出getOrderInfo的调用路径,且cost大于10ms,-j是指过滤掉jdk中的方法,可以看到输出少了很多 + +```bash +trace -j cn.test.mobile.controller.order.OrderController getOrderInfo '#cost > 10' + +Press Q or Ctrl+C to abort. +Affect(class-cnt:1 , method-cnt:1) cost in 96 ms. +---ts=2019-11-13 15:53:42;thread_name=http-nio-8801-exec-2;id=29;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@a6c54c3 + ---[13.803743ms] cn.test.mobile.controller.order.OrderController:getOrderInfo() + +---[0.01312ms] cn.test.db.common.vo.ResponseVo:() #472 + +---[0.01408ms] cn.test.mobile.controller.order.OrderController:getUserInfo() #478 + +---[0.0128ms] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:() #500 + +---[0.303998ms] com.baomidou.mybatisplus.core.conditions.query.QueryWrapper:allEq() #500 + +---[12.675431ms] cn.test.db.service.IOrderMediaService:getOne() #500 + `---[0.409917ms] cn.test.db.common.vo.ResponseVo:setSuccess() #503 +``` + +#### 6、jobs + +执行后台异步任务 + +线上有些问题是偶然发生的,这时就需要使用异步任务,把信息写入文件。 + +使用 & 指定命令去后台运行,使用 > 将结果重写到日志文件,以trace为例 + +```bash +trace -j cn.test.mobile.controller.order.OrderController getOrderInfo > test.out & +``` + +jobs——列出所有job + +```bash + jobs +[76]* + Running trace -j cn.test.mobile.controller.order.OrderController getOrderInfo >> test.out & + execution count : 0 + start time : Wed Nov 13 16:13:23 CST 2019 + timeout date : Thu Nov 14 16:13:23 CST 2019 + session : f4fba846-e90b-4234-959e-e78ad0a5db8c (current) +``` + +job id是76, * 表示此job是当前session创建,状态是Running,execution count是执行次数,timeout date是超时时间 + +异步执行时间,默认为1天,如果要修改,使用options命令, + +``` +options job-timeout 2d +``` + +options可选参数 1d, 2h, 3m, 25s,分别代表天、小时、分、秒 + +kill——强制终止任务 + +``` +kill 76 +kill job 76 success +``` + +最多同时支持8个命令使用重定向将结果写日志 + +请勿同时开启过多的后台异步命令,以免对目标JVM性能造成影响 + +#### 7、logger + +查看logger信息,更新logger level + +查看 + +```bash +logger + name ROOT + class ch.qos.logback.classic.Logger + classLoader sun.misc.Launcher$AppClassLoader@18b4aac2 + classLoaderHash 18b4aac2 #改日志级别时要用到它 + level INFO + effectiveLevel INFO + ... ... ... ... +``` + +更新日志级别 + +``` +logger --name ROOT --level debug +update logger level success. +``` + +如果执行这个命令时出错:update logger level fail. + +指定classLoaderHash重试一下试试 + +``` +logger -c 18b4aac2 --name ROOT --level debug +update logger level success. +``` + +#### 8、dashboard + +查看当前系统的实时数据面板 这个命令可以全局的查看jvm运行状态,比如内存和cpu占用情况 + +```bash +dashboard +ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPT DAEMON +17 Abandoned connection cleanup main 5 TIMED_WAI 0 0:0 false true +1009 AsyncAppender-Worker-arthas-c system 5 WAITING 0 0:0 false true +5 Attach Listener system 5 RUNNABLE 0 0:0 false true +23 ContainerBackgroundProcessor[ main 5 TIMED_WAI 0 0:0 false true +55 DestroyJavaVM main 5 RUNNABLE 0 0:11 false false +3 Finalizer system 8 WAITING 0 0:0 false true +18 HikariPool-1 housekeeper main 5 TIMED_WAI 0 0:0 false true +39 NioBlockingSelector.BlockPoll main 5 RUNNABLE 0 0:0 false true +2 Reference Handler system 10 WAITING 0 0:0 false true +4 Signal Dispatcher system 9 RUNNABLE 0 0:0 false true +69 System Clock main 5 TIMED_WAI 0 0:34 false true +25 Thread-2 main 5 TIMED_WAI 0 0:0 false false +37 Timer-0 main 5 TIMED_WAI 0 0:0 false true +Memory used total max usage GC +heap 216M 415M 3614M 5.99% gc.ps_scavenge.count 96 +ps_eden_space 36M 78M 1276M 2.90% gc.ps_scavenge.time(ms) 3054 +ps_survivor_space 17M 38M 38M 46.53% gc.ps_marksweep.count 4 +ps_old_gen 161M 298M 2711M 5.97% gc.ps_marksweep.time(ms) 804 +nonheap 175M 180M -1 97.09% +code_cache 35M 35M 240M 14.85% +``` + +ID: Java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应 我们可以通过 thread id 查看线程的堆栈 信息 + +```bash +thread 2 +"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@66ad4272 + at java.lang.Object.wait(Native Method) + - waiting on java.lang.ref.Reference$Lock@66ad4272 + at java.lang.Object.wait(Object.java:502) + at java.lang.ref.Reference.tryHandlePending(Reference.java:191) + at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) +``` + +NAME: 线程名 + +GROUP: 线程组名 + +PRIORITY: 线程优先级, 1~10之间的数字,越大表示优先级越高 + +STATE: 线程的状态 + +CPU%: 线程消耗的cpu占比,采样100ms,将所有线程在这100ms内的cpu使用量求和,再算出每个线程的cpu使用占比。 + +TIME: 线程运行总时间,数据格式为分:秒 + +INTERRUPTED: 线程当前的中断位状态 + +DAEMON: 是否是daemon线程 + +#### 9、redefine + +redefine jvm已加载的类 ,可以在不重启项目的情况下,热更新类。 + +这个功能真的很强大,但是命令不一定会成功 + +下面我们来模拟:假设我想修改OrderController里的某几行代码,然后热更新至jvm: + +a. 反编译OrderController,默认情况下,反编译结果里会带有ClassLoader信息,通过--source-only选项,可以只打印源代码。方便和mc/redefine命令结合使用 + +``` +jad --source-only cn.test.mobile.controller.order.OrderController > OrderController.java +``` + +生成的OrderController.java在哪呢,执行pwd就知道在哪个目录了 + +b. 查找加载OrderController的ClassLoader + +``` +sc -d cn.test.mobile.controller.order.OrderController | grep classLoaderHash +classLoaderHash 18b4aac2 +``` + +c. 修改保存好OrderController.java之后,使用mc(Memory Compiler)命令来编译成字节码,并且通过-c参数指定ClassLoader + +``` +mc -c 18b4aac2 OrderController.java -d ./ +``` + +d. 热更新刚才修改后的代码 + +``` +redefine -c 18b4aac2 OrderController.class +redefine success, size: 1 +``` + +然后代码就更新成功了。 + +### 其他 + +如果java -jar选择启动某个应用的时候,报下面的错 + +```bash +java -jar arthas-boot.jar +[INFO] arthas-boot version: 3.1.4 +[INFO] Process 11544 already using port 3658 +[INFO] Process 11544 already using port 8563 +[INFO] Found existing java process, please choose one and hit RETURN. +* [1]: 11544 + [2]: 119504 cn.test.MobileApplication + [3]: 136340 org.jetbrains.jps.cmdline.Launcher + [4]: 3068 +2 #选择第2个启动 +[ERROR] Target process 119504 is not the process using port 3658, you will connect to an unexpected process. +[ERROR] 1. Try to restart arthas-boot, select process 11544, shutdown it first with running the 'shutdown' command. +[ERROR] 2. Or try to use different telnet port, for example: java -jar arthas-boot.jar --telnet-port 9998 --http-port -1 +``` + +注意提示[ERROR] 1,只需要进入11544这个应用,然后执行shutdown关闭这个应用就可以启动了 diff --git a/docs/advance/excellent-article/24-generic.md b/docs/advance/excellent-article/24-generic.md new file mode 100644 index 0000000..46aa386 --- /dev/null +++ b/docs/advance/excellent-article/24-generic.md @@ -0,0 +1,292 @@ +--- +sidebar: heading +title: 泛型详解 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: 泛型 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 泛型详解 + +Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。 + +泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +## 泛型带来的好处 + +在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。 + +那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。 + +``` +public class GlmapperGeneric { + private T t; + public void set(T t) { this.t = t; } + public T get() { return t; } + + public static void main(String[] args) { + // do nothing + } + + /** + * 不指定类型 + */ + public void noSpecifyType(){ + GlmapperGeneric glmapperGeneric = new GlmapperGeneric(); + glmapperGeneric.set("test"); + // 需要强制类型转换 + String test = (String) glmapperGeneric.get(); + System.out.println(test); + } + + /** + * 指定类型 + */ + public void specifyType(){ + GlmapperGeneric glmapperGeneric = new GlmapperGeneric(); + glmapperGeneric.set("test"); + // 不需要强制类型转换 + String test = glmapperGeneric.get(); + System.out.println(test); + } +} +``` + +上面这段代码中的 specifyType 方法中 省去了强制转换,可以在编译时候检查类型安全,可以用在类,方法,接口上。 + +## 泛型中通配符 + +我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢? + +### 常用的 T,E,K,V,? + +本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。**通常情况下,T,E,K,V,?是这样约定的:** + +- ?表示不确定的 java 类型 +- T (type) 表示具体的一个java类型 +- K V (key value) 分别代表java键值中的Key Value +- E (element) 代表Element + +### ?无界通配符 + +先从一个小例子看起,原文在 这里 。 + +我有一个父类 Animal 和几个子类,如狗、猫等,现在我需要一个动物的列表,我的第一个想法是像这样的: + +``` +List listAnimals +``` + +但是老板的想法确实这样的: + +``` +List listAnimals +``` + +为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。 + +``` +static int countLegs (List animals ) { + int retVal = 0; + for ( Animal animal : animals ) + { + retVal += animal.countLegs(); + } + return retVal; +} + +static int countLegs1 (List< Animal > animals ){ + int retVal = 0; + for ( Animal animal : animals ) + { + retVal += animal.countLegs(); + } + return retVal; +} + +public static void main(String[] args) { + List dogs = new ArrayList<>(); + // 不会报错 + countLegs( dogs ); + // 报错 + countLegs1(dogs); +} +``` + +当调用 countLegs1 时,就会飘红,提示的错误信息如下: + +![](http://img.topjavaer.cn/img/泛型1.png) + +所以,对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。像 countLegs 方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。 + +### 上界通配符 < ? extends E> + +> 上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。 + +在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处: + +- 如果传入的类型不是 E 或者 E 的子类,编译不成功 +- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用 + +``` +private E test(K arg1, E arg2){ + E result = arg2; + arg2.compareTo(arg1); + //..... + return result; +} +``` + +> 类型参数列表中如果有多个类型参数上限,用逗号分开 + +### 下界通配符 < ? super E> + +> 下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object + +在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。 + +``` +private void test(List dst, List src){ + for (T t : src) { + dst.add(t); + } +} + +public static void main(String[] args) { + List dogs = new ArrayList<>(); + List animals = new ArrayList<>(); + new Test3().test(animals,dogs); +} +// Dog 是 Animal 的子类 +class Dog extends Animal { + +} +``` + +dst 类型 “大于等于” src 的类型,这里的“大于等于”是指 dst 表示的范围比 src 要大,因此装得下 dst 的容器也就能装 src 。 + +### ?和 T 的区别 + +![](http://img.topjavaer.cn/img/泛型2.png) + +?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 : + +``` +// 可以 +T t = operate(); + +// 不可以 +?car = operate(); +``` + +简单总结下: + +T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。 + +#### 区别1:通过 T 来 确保 泛型参数的一致性 + +``` +// 通过 T 来 确保 泛型参数的一致性 +public void +test(List dest, List src) + +//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型 +public void +test(List dest, List src) +``` + +像下面的代码中,约定的 T 是 Number 的子类才可以,但是申明时是用的 String ,所以就会飘红报错。 + +![](http://img.topjavaer.cn/img/泛型3.png) + +不能保证两个 List 具有相同的元素类型的情况 + +``` +GlmapperGeneric glmapperGeneric = new GlmapperGeneric<>(); +List dest = new ArrayList<>(); +List src = new ArrayList<>(); +glmapperGeneric.testNon(dest,src); +``` + +上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换。 + +#### 区别2:类型参数可以多重限定而通配符不行 + +![](http://img.topjavaer.cn/img/泛型4.png) + +使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。 + +#### 区别3:通配符可以使用超类限定而类型参数不行 + +类型参数 T 只具有 一种 类型限定方式: + +``` +T extends A +``` + +但是通配符 ? 可以进行 两种限定: + +``` +? extends A +? super A +``` + +## `Class`和 `Class`区别 + +前面介绍了 ?和 T 的区别,那么对于,`Class`和 ``又有什么区别呢?`Class`和 `Class` + +最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。 + +``` +// 通过反射的方式生成 multiLimit +// 对象,这里比较明显的是,我们需要使用强制类型转换 +MultiLimit multiLimit = (MultiLimit) +Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance(); +``` + +对于上述代码,在运行期,如果反射的类型不是 MultiLimit 类,那么一定会报 java.lang.ClassCastException 错误。 + +对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题: + +![](http://img.topjavaer.cn/img/泛型5.png) + +`Class`在实例化的时候,T 要替换成具体类。`Class`它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做申明: + +``` +// 可以 +public Class clazz; +// 不可以,因为 T 需要指定类型 +public Class clazzT; +``` + +所以当不知道定声明什么类型的 Class 的时候可以定义一 个Class。 + +```java +public class Test3 { + public Class clazz; + public Class clazzT; +} +``` + +那如果也想 `public Class clazzT;`这样的话,就必须让当前的类也指定 T , + +```java +public class Test3 { + public Class clazz; + // 不会报错 + public Class clazzT; +} +``` + +## 小结 + +本文零碎整理了下 JAVA 泛型中的一些点,不是很全,仅供参考。如果文中有不当的地方,欢迎指正。 + +> 转自:juejin.im/post/5d5789d26fb9a06ad0056bd9 diff --git a/docs/advance/excellent-article/25-select-count-slow-query.md b/docs/advance/excellent-article/25-select-count-slow-query.md new file mode 100644 index 0000000..0300ea2 --- /dev/null +++ b/docs/advance/excellent-article/25-select-count-slow-query.md @@ -0,0 +1,222 @@ +--- +sidebar: heading +title: SELECT COUNT(*) 会造成全表扫描?回去等通知吧 +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: count(*),全表扫描,执行计划 + - - meta + - name: description + content: 优质文章汇总 +--- + + +# SELECT COUNT(*) 会造成全表扫描?回去等通知吧 + +## 前言 + +SELECT COUNT(*)会不会导致全表扫描引起慢查询呢? + +``` +SELECT COUNT(*) FROM SomeTable +``` + +网上有一种说法,针对无 where_clause 的 **COUNT(\*)**,MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这种说法对不对呢 + +针对这个疑问,我首先去生产上找了一个千万级别的表使用 EXPLAIN 来查询了一下执行计划 + +``` +EXPLAIN SELECT COUNT(*) FROM SomeTable +``` + +结果如下 + +![](http://img.topjavaer.cn/img/select-count1.png) + +如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是 COUNT(1),还是 COUNT(*),MySQL 都会用**成本最小**的辅助索引查询方式来计数,也就是使用 COUNT(*) 由于 MySQL 的优化已经保证了它的查询性能是最好的!随带提一句,COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用COUNT(*)查询表的行数! + +所以这种说法确实是对的。但有个前提,在 MySQL 5.6 之后的版本中才有这种优化。 + +那么这个成本最小该怎么定义呢,有时候在 WHERE 中指定了多个条件,为啥最终 MySQL 执行的时候却选择了另一个索引,甚至不选索引? + +本文将会给你答案,本文将会从以下两方面来分析 + +- SQL 选用索引的执行成本如何计算 +- 实例说明 + +## SQL 选用索引的执行成本如何计算 + +就如前文所述,在有多个索引的情况下, 在查询数据前,MySQL 会选择成本最小原则来选择使用对应的索引,这里的成本主要包含两个方面。 + +- IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关 +- CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。 + +## 实例说明 + +为了根据以上两个成本来算出使用索引的最终成本,我们先准备一个表(以下操作基于 MySQL 5.7.18) + +``` +CREATE TABLE `person` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `score` int(11) NOT NULL, + `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `name_score` (`name`(191),`score`), + KEY `create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +这个表除了主键索引之外,还有另外两个索引, name_score 及 create_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下: + +``` +CREATE PROCEDURE insert_person() +begin + declare c_id integer default 1; + while c_id<=100000 do + insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second)); + set c_id=c_id+1; + end while; +end +``` + +插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引 + +``` +EXPLAIN SELECT COUNT(*) FROM person +``` + +![图片](https://mmbiz.qpic.cn/mmbiz_png/OyweysCSeLXtag3Q38nMHUxszkvkXfFaXejKpeg0pWWQcs16SwlOR1o1Fo5nw1B2RCAME61LN3hH3E5uDQuicXQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +从结果上看它选择了 create_time 辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的! + +我们再来看以下 SQL 会使用哪个索引 + +``` +SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18' +``` + +![图片](https://mmbiz.qpic.cn/mmbiz_png/OyweysCSeLXtag3Q38nMHUxszkvkXfFa4XR8ibNMfganIREu5JQF2bJYt0r7tZpYWdCcIaIW4emVC9SVWiajULYw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +用了全表扫描!理论上应该用 name_score 或者 create_time 索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用 **SELECT \*** 造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下 + +``` +SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18' +``` + +结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧 + +``` +-- 全表扫描执行时间: 4.0 ms +SELECT create_time FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18' + +-- 使用覆盖索引执行时间: 2.0 ms +SELECT create_time FROM person force index(create_time) WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18' +``` + +从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。 + +前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令 + +``` +SHOW TABLE STATUS LIKE 'person' +``` + +![图片](https://mmbiz.qpic.cn/mmbiz_png/OyweysCSeLXtag3Q38nMHUxszkvkXfFaBxFRZE5oFBbjrVryW8vMSG0GLuznuVA6vd69ZdEw09rmQYKaSy9S1Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +可以发现 + +1. 行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是**估算**,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是 100264 * 0.2 = 20052.8。 +2. 数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。 + +也就是说全表扫描的成本是 20052.8 + 353 = 20406。 + +这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用 optimizer trace 功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。 + +optimizer_trace 功能使用如下 + +``` +SET optimizer_trace="enabled=on"; +SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18'; +SELECT * FROM information_schema.OPTIMIZER_TRACE; +SET optimizer_trace="enabled=off"; +``` + +执行之后我们主要观察使用 name_score,create_time 索引及全表扫描的成本。 + +先来看下使用 name_score 索引执行的的预估执行成本: + +``` +{ + "index": "name_score", + "ranges": [ + "name84059 <= name" + ], + "index_dives_for_eq_ranges": true, + "rows": 25372, + "cost": 30447 +} +``` + +可以看到执行成本为 30447,高于我们之前算出来的全表扫描成本:20406。所以没选择此索引执行 + +注意:这里的 30447 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和。 + +再来看下使用 create_time 索引执行的的预估执行成本: + +``` +{ + "index": "create_time", + "ranges": [ + "0x5ec8c516 < create_time" + ], + "index_dives_for_eq_ranges": true, + "rows": 50132, + "cost": 60159, + "cause": "cost" +} +``` + +可以看到成本是 60159,远大于全表扫描成本 20406,自然也没选择此索引。 + +再来看计算出的全表扫描成本: + +``` +{ + "considered_execution_plans": [ + { + "plan_prefix": [ + ], + "table": "`person`", + "best_access_path": { + "considered_access_paths": [ + { + "rows_to_scan": 100264, + "access_type": "scan", + "resulting_rows": 100264, + "cost": 20406, + "chosen": true + } + ] + }, + "condition_filtering_pct": 100, + "rows_for_plan": 100264, + "cost_for_plan": 20406, + "chosen": true + } + ] +} +``` + +注意看 cost:20406,与我们之前算出来的完全一样!这个值在以上三者算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。 + +实际上 optimizer trace 详细列出了覆盖索引,回表的成本统计情况,有兴趣的可以去研究一下。 + +从以上分析可以看出, MySQL 选择的执行计划未必是最佳的,原因有挺多,就比如上文说的行数统计信息不准,再比如 MySQL 认为的最优跟我们认为不一样,我们可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。 + +## 总结 + +本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用 EXPLAIN, optimizer trace 来优化我们的查询语句。 diff --git a/docs/advance/excellent-article/26-java-stream.md b/docs/advance/excellent-article/26-java-stream.md new file mode 100644 index 0000000..7330950 --- /dev/null +++ b/docs/advance/excellent-article/26-java-stream.md @@ -0,0 +1,199 @@ +--- +sidebar: heading +title: Java Stream常见用法汇总,开发效率大幅提升 +category: 优质文章 +tag: + - java +head: + - - meta + - name: keywords + content: stream流操作,java stream,java8 + - - meta + - name: description + content: 优质文章汇总 +--- + +# Java Stream常见用法汇总,开发效率大幅提升 + +Java8 新增的 Stream 流大大减轻了我们代码的工作量,但是 Stream 流的用法较多,实际使用的时候容易遗忘,整理一下供大家参考。 + +## 1. 概述 + +Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来对 Java 集合运算和表达的高阶抽象。 + +Stream API 可以极大提高 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。 + +这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。 + +![](http://img.topjavaer.cn/img/stream1.png) + +## 2. 创建 + +### 2.1 集合自带 Stream 流方法 + +``` +List list = new ArrayList<>(); +// 创建一个顺序流 +Stream stream = list.stream(); +// 创建一个并行流 +Stream parallelStream = list.parallelStream(); +``` + +### 2.1 通过 Array 数组创建 + +``` +int[] array = {1,2,3,4,5}; +IntStream stream = Arrays.stream(array); +``` + +### 2.3 使用 Stream 的静态方法创建 + +``` +Stream stream = Stream.of(1, 2, 3, 4, 5); +Stream stream = Stream.iterate(0, (x) -> x + 3).limit(3); // 输出 0,3,6 + +Stream stream = Stream.generate(() -> "Hello").limit(3); // 输出 Hello,Hello,Hello +Stream stream = Stream.generate(Math::random).limit(3); // 输出3个随机数 +``` + +### 2.3 数值流 + +``` +// 生成有限的常量流 +IntStream intStream = IntStream.range(1, 3); // 输出 1,2 +IntStream intStream = IntStream.rangeClosed(1, 3); // 输出 1,2,3 +// 生成一个等差数列 +IntStream.iterate(1, i -> i + 3).limit(5).forEach(System.out::println); // 输出 1,4,7,10,13 +// 生成无限常量数据流 +IntStream generate = IntStream.generate(() -> 10).limit(3); // 输出 10,10,10 +``` + +另外还有 LongStream、DoubleStream 都有这几个方法。 + +## 3. 使用 + +初始化一些数据,示例中使用。 + +``` +public class Demo { + class User{ + // 姓名 + private String name; + + // 年龄 + private Integer age; + } + + public static void main(String[] args) { + List users = new ArrayList<>(); + users.add(new User("Tom", 1)); + users.add(new User("Jerry", 2)); + } +} +``` + +### 3.1 遍历 forEach + +``` +// 循环输出user对象 +users.stream().forEach(user -> System.out.println(user)); +``` + +### 3.2 查找 find + +``` +// 取出第一个对象 +User user = users.stream().findFirst().orElse(null); // 输出 {"age":1,"name":"Tom"} +// 随机取出任意一个对象 +User user = users.stream().findAny().orElse(null); +``` + +### 3.3 匹配 match + +``` +// 判断是否存在name是Tom的用户 +boolean existTom = users.stream().anyMatch(user -> "Tom".equals(user.getName())); +// 判断所有用户的年龄是否都小于5 +boolean checkAge = users.stream().allMatch(user -> user.getAge() < 5); +``` + +### 3.4 筛选 filter + +``` +// 筛选name是Tom的用户 +users.stream() + .filter(user -> "Tom".equals(user.name)) + .forEach(System.out::println); // 输出 {"age":1,"name":"Tom"} +``` + +### 3.5 映射 map/flatMap + +``` +// 打印users里的name +users.stream().map(User::getName).forEach(System.out::println); // 输出 Tom Jerry +// List> 转 List +List> userList = new ArrayList<>(); +List users = userList.stream().flatMap(Collection::stream).collect(Collectors.toList()); +``` + +### 3.6 归约 reduce + +``` +// 求用户年龄之和 +Integer sum = users.stream().map(User::getAge).reduce(Integer::sum).orElse(0); +// 求用户年龄的乘积 +Integer product = users.stream().map(User::getAge).reduce((x, y) -> x * y).orElse(0); +``` + +### 3.7 排序 sorted + +``` +// 按年龄倒序排 +List collect = users.stream() + .sorted(Comparator.comparing(User::getAge).reversed()) + .collect(Collectors.toList()); + +//多属性排序 +List result = persons.stream() + .sorted(Comparator.comparing((Person p) -> p.getNamePinyin()) + .thenComparing(Person::getAge)).collect(Collectors.toList()); +``` + +### 3.8 收集 collect + +``` +// list转换成map +Map map = users.stream() + .collect(Collectors.toMap(User::getAge, Function.identity())); + +// 按年龄分组 +Map> userMap = users.stream().collect(Collectors.groupingBy(User::getAge)); + +// 求平均年龄 +Double ageAvg = users.stream().collect(Collectors.averagingInt(User::getAge)); // 输出 1.5 + +// 求年龄之和 +Integer ageSum = users.stream().collect(Collectors.summingInt(User::getAge)); + +// 求年龄最大的用户 +User user = users.stream().collect(Collectors.maxBy(Comparator.comparing(User::getAge))).orElse(null); + +// 把用户姓名拼接成逗号分隔的字符串输出 +String names = users.stream().map(User::getName).collect(Collectors.joining(",")); // 输出 Tom,Jerry +``` + +### 3.9 List 转换成 Map 时遇到重复主键 + +![](http://img.topjavaer.cn/img/stream2.png) + +这样转换会报错,因为 ID 重复。 + +![](http://img.topjavaer.cn/img/stream3.png) + +可以这样做 + +![](http://img.topjavaer.cn/img/stream4.png) + + + +--end-- diff --git a/docs/advance/excellent-article/27-mq-usage.md b/docs/advance/excellent-article/27-mq-usage.md new file mode 100644 index 0000000..5ccf9b9 --- /dev/null +++ b/docs/advance/excellent-article/27-mq-usage.md @@ -0,0 +1,137 @@ +--- +sidebar: heading +title: 消息队列常见的使用场景 +category: 优质文章 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列使用场景,异步 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 消息队列常见的使用场景 + +消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题 + +实现高性能,高可用,可伸缩和最终一致性架构。 + +使用较多的消息队列有 RocketMQ,RabbitMQ,Kafka,ZeroMQ,MetaMQ + +以下介绍消息队列在实际应用中常用的使用场景。 + +异步处理,应用解耦,流量削锋、日志处理和消息通讯五个场景。 + +#### 场景 1:异步处理 + +场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式 + +(1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端 + +![](http://img.topjavaer.cn/img/mq使用场景1.png) + +(2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间 + +![](http://img.topjavaer.cn/img/mq使用场景2.png) + +假设三个业务节点每个使用 50 毫秒钟,不考虑网络等其他开销,则串行方式的时间是 150 毫秒,并行的时间可能是 100 毫秒。 + +因为 CPU 在单位时间内处理的请求数是一定的,假设 CPU1 秒内吞吐量是 100 次。则串行方式 1 秒内 CPU 可处理的请求量是 7 次(1000/150)。并行方式处理的请求量是 10 次(1000/100) + +> 小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢? + +引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下: + +![](http://img.topjavaer.cn/img/mq使用场景3.png) + +按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是 50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 毫秒。因此架构改变后,系统的吞吐量提高到每秒 20 QPS。比串行提高了 3 倍,比并行提高了两倍 + +#### 场景 2:应用解耦 + +场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图 + +![](http://img.topjavaer.cn/img/mq使用场景4.png) + +传统模式的缺点: + +- 假如库存系统无法访问,则订单减库存将失败,从而导致订单失败 +- 订单系统与库存系统耦合 + +> 如何解决以上问题呢?引入应用消息队列后的方案,如下图: + +![](http://img.topjavaer.cn/img/mq使用场景5.png) + +- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功 +- 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作 +- 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦 + +#### 场景 3:流量削锋 + +流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛 + +应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。 + +- 可以控制活动的人数 +- 可以缓解短时间内高流量压垮应用 + +![](http://img.topjavaer.cn/img/mq使用场景6.png) + +- 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面 +- 秒杀业务根据消息队列中的请求信息,再做后续处理 + +#### 场景 4:日志处理 + +日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。架构简化如下 + +![](http://img.topjavaer.cn/img/mq使用场景7.png) + +- 日志采集客户端,负责日志数据采集,定时写入 Kafka 队列 +- Kafka 消息队列,负责日志数据的接收,存储和转发 +- 日志处理应用:订阅并消费 kafka 队列中的日志数据 + +以下是新浪 kafka 日志处理应用案例 + +![](http://img.topjavaer.cn/img/mq使用场景8.png) + +(1)、Kafka:接收用户日志的消息队列 + +(2)、Logstash:做日志解析,统一成 JSON 输出给 Elasticsearch + +(3)、Elasticsearch:实时日志分析服务的核心技术,一个 schemaless,实时的数据存储服务,通过 index 组织数据,兼具强大的搜索和统计功能 + +(4)、Kibana:基于 Elasticsearch 的数据可视化组件,超强的数据可视化能力是众多公司选择 ELK stack 的重要原因 + +#### 场景 5:消息通讯 + +消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等 + +点对点通讯: + +![](http://img.topjavaer.cn/img/mq使用场景9.png) + +客户端 A 和客户端 B 使用同一队列,进行消息通讯。 + +聊天室通讯: + +![](http://img.topjavaer.cn/img/mq使用场景10.png) + +客户端 A,客户端 B,客户端 N 订阅同一主题,进行消息发布和接收。实现类似聊天室效果。 + +以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。 + + + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) \ No newline at end of file diff --git a/docs/advance/excellent-article/28-springboot-forbid-tomcat.md b/docs/advance/excellent-article/28-springboot-forbid-tomcat.md new file mode 100644 index 0000000..3d55a1a --- /dev/null +++ b/docs/advance/excellent-article/28-springboot-forbid-tomcat.md @@ -0,0 +1,72 @@ +--- +sidebar: heading +title: 大公司为什么禁止SpringBoot项目使用Tomcat? +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: Spring Boot,Tomcat + - - meta + - name: description + content: 优质文章汇总 +--- + +# 大公司为什么禁止SpringBoot项目使用Tomcat? + +## 前言 + +在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。同时,SpringBoot也支持Undertow容器,我们可以很方便的用Undertow替换Tomcat,而Undertow的性能和内存使用方面都优于Tomcat,那我们如何使用Undertow技术呢?本文将为大家细细讲解。 + +## SpringBoot中的Tomcat容器 + +SpringBoot可以说是目前最火的Java Web框架了。它将开发者从繁重的xml解救了出来,让开发者在几分钟内就可以创建一个完整的Web服务,极大的提高了开发者的工作效率。Web容器技术是Web项目必不可少的组成部分,因为任Web项目都要借助容器技术来运行起来。在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。推荐:[最全面的Java面试网站](https://topjavaer.cn) + +## SpringBoot设置Undertow + +对于Tomcat技术,Java程序员应该都非常熟悉,它是Web应用最常用的容器技术。我们最早的开发的项目基本都是部署在Tomcat下运行,那除了Tomcat容器,SpringBoot中我们还可以使用什么容器技术呢?没错,就是题目中的Undertow容器技术。SrpingBoot已经完全继承了Undertow技术,我们只需要引入Undertow的依赖即可,如下图所示。 + +![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/89d3d0c3442f49be8eefc65eca316c49~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) + +![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed1025c2627f426a858507ad19ae8e31~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) + +配置好以后,我们启动应用程序,发现容器已经替换为Undertow。那我们为什么需要替换Tomcat为Undertow技术呢? + +## Tomcat与Undertow的优劣对比 + +Tomcat是Apache基金下的一个轻量级的Servlet容器,支持Servlet和JSP。Tomcat具有Web服务器特有的功能,包括 Tomcat管理和控制平台、安全局管理和Tomcat阀等。Tomcat本身包含了HTTP服务器,因此也可以视作单独的Web服务器。但是,Tomcat和ApacheHTTP服务器不是一个东西,ApacheHTTP服务器是用C语言实现的HTTP Web服务器。Tomcat是完全免费的,深受开发者的喜爱。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d06f4449d9e4d2983b24e09b4fda910~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image) + +Undertow是Red Hat公司的开源产品, 它完全采用Java语言开发,是一款灵活的高性能Web服务器,支持阻塞IO和非阻塞IO。由于Undertow采用Java语言开发,可以直接嵌入到Java项目中使用。同时, Undertow完全支持Servlet和Web Socket,在高并发情况下表现非常出色。 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36ee9da8d1404eefa1518f0f2796a76d~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image) + +我们在相同机器配置下压测Tomcat和Undertow,得到的测试结果如下所示:**QPS测试结果对比:** Tomcat + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/785bf4e5e3174556b158bb8e484a14b9~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image) + +Undertow + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0ec493f88954686b9f6d3554684c9b4~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image) + +**内存使用对比:** + +Tomcat + +![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d593ce0dad884e3c8b07e0d6602fdac6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) + +Undertow + +![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88a059beec2e4779ba971891fbfc8638~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) + +通过测试发现,在高并发系统中,Tomcat相对来说比较弱。在相同的机器配置下,模拟相等的请求数,Undertow在性能和内存使用方面都是最优的。并且Undertow新版本默认使用持久连接,这将会进一步提高它的并发吞吐能力。所以,如果是高并发的业务系统,Undertow是最佳选择。 + +## 最后 + +SpingBoot中我们既可以使用Tomcat作为Http服务,也可以用Undertow来代替。Undertow在高并发业务场景中,性能优于Tomcat。所以,如果我们的系统是高并发请求,不妨使用一下Undertow,你会发现你的系统性能会得到很大的提升。 + + + +> 参考链接:原文地址:toutiao.com/a677547665941699021 diff --git a/docs/advance/excellent-article/29-idempotent-design.md b/docs/advance/excellent-article/29-idempotent-design.md new file mode 100644 index 0000000..6cf7d5b --- /dev/null +++ b/docs/advance/excellent-article/29-idempotent-design.md @@ -0,0 +1,34 @@ +--- +sidebar: heading +title: 接口的幂等性如何设计? +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: 接口幂等,幂等性 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +## 接口的幂等性如何设计? + +分布式系统中的某个接口,该如何保证幂等性? + +假如有个服务提供一个付款接口供外部调用,这个服务部署在了 5 台机器上。然后用户在前端上操作的时候,不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,结果一个订单扣款扣两次。 + +这就是典型的接口幂等性问题。 + +所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。这就是幂等性。 + +其实保证幂等性主要是三点: + +对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次。每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在数据库中记录一个状态,比如支付之前记录一条这个订单的支付流水。 + +每次接收请求需要进行判断,判断之前是否处理过。如果订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。 + +实际运作过程中,你要结合自己的业务来,比如说利用 Redis,用 orderId 作为唯一键。只有成功插入这个支付流水,才可以执行实际的支付扣款。 + +要求是支付一个订单,必须插入一条支付流水,order_id 建一个唯一键 unique key 。你在支付一个订单之前,先插入一条支付流水,order_id 就已经进去了。你就可以写一个标识到 Redis 里面去, set order_id payed ,下一次重复请求过来了,先查 Redis 的 order_id 对应的 value,如果是 payed 就说明已经支付过了,就别重复支付了。 diff --git a/docs/advance/excellent-article/3-springboot-auto-assembly.md b/docs/advance/excellent-article/3-springboot-auto-assembly.md new file mode 100644 index 0000000..2e1dcfa --- /dev/null +++ b/docs/advance/excellent-article/3-springboot-auto-assembly.md @@ -0,0 +1,559 @@ +--- +sidebar: heading +title: Spring Boot 自动装配原理 +category: 优质文章 +tag: + - Spring Boot +head: + - - meta + - name: keywords + content: SpringBoot自动装配原理,自动装配 + - - meta + - name: description + content: 优质文章汇总 +--- + +# Spring Boot 自动装配原理 + +首先,先看SpringBoot的主配置类: + +```java +@SpringBootApplication +public class StartEurekaApplication +{ + public static void main(String[] args) + { + SpringApplication.run(StartEurekaApplication.class, args); + } +} +``` + +点进@SpringBootApplication来看,发现@SpringBootApplication是一个组合注解。 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan(excludeFilters = { + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { + +} +``` + +首先我们先来看 @SpringBootConfiguration: + +```java +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Configuration +public @interface SpringBootConfiguration { +} +``` + +可以看到这个注解除了元注解以外,就只有一个@Configuration,那也就是说这个注解相当于@Configuration,所以这两个注解作用是一样的,它让我们能够去注册一些额外的Bean,并且导入一些额外的配置。 + +那@Configuration还有一个作用就是把该类变成一个配置类,不需要额外的XML进行配置。所以@SpringBootConfiguration就相当于@Configuration。进入@Configuration,发现@Configuration核心是@Component,说明Spring的配置类也是Spring的一个组件。 + +```java +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Configuration { + @AliasFor( + annotation = Component.class + ) + String value() default ""; +} +``` + +继续来看下一个@EnableAutoConfiguration,这个注解是开启自动配置的功能。 + +```java +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import({AutoConfigurationImportSelector.class}) +public @interface EnableAutoConfiguration { + String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; + + Class[] exclude() default {}; + + String[] excludeName() default {}; +} +``` + +可以看到它是由 @AutoConfigurationPackage,@Import(EnableAutoConfigurationImportSelector.class)这两个而组成的,我们先说@AutoConfigurationPackage,他是说:让包中的类以及子包中的类能够被自动扫描到spring容器中。 + +```java +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import({Registrar.class}) +public @interface AutoConfigurationPackage { +} +``` + +使用@Import来给Spring容器中导入一个组件 ,这里导入的是Registrar.class。来看下这个Registrar: + +```java +static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { + Registrar() { + } + + public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { + AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()); + } + + public Set determineImports(AnnotationMetadata metadata) { + return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata)); + } + } +``` + +就是通过以上这个方法获取扫描的包路径,可以debug查看具体的值: + +![](http://img.topjavaer.cn/img/springboot自动配置1.png)那metadata是什么呢,可以看到是标注在@SpringBootApplication注解上的DemosbApplication,也就是我们的主配置类Application: + +![](http://img.topjavaer.cn/img/springboot自动配置2.png)其实就是将主配置类(即@SpringBootApplication标注的类)的所在包及子包里面所有组件扫描加载到Spring容器。因此我们要把DemoApplication放在项目的最高级中(最外层目录)。 + +看看注解@Import(AutoConfigurationImportSelector.class),@Import注解就是给Spring容器中导入一些组件,这里传入了一个组件的选择器:AutoConfigurationImportSelector。 + +![](http://img.topjavaer.cn/img/springboot自动配置3.png)可以从图中看出AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector,ImportSelector有一个方法为:selectImports。将所有需要导入的组件以全类名的方式返回,这些组件就会被添加到容器中。 + +```java +public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!this.isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } else { + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); + AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = + this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata); + return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); + } +} +``` + +会给容器中导入非常多的自动配置类(xxxAutoConfiguration);就是给容器中导入这个场景需要的所有组件,并配置好这些组件。 + +![](http://img.topjavaer.cn/img/springboot自动配置4.png)有了自动配置类,免去了我们手动编写配置注入功能组件等的工作。那是如何获取到这些配置类的呢,看看下面这个方法: + +```java +protected AutoConfigurationImportSelector.AutoConfigurationEntry + getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { + if (!this.isEnabled(annotationMetadata)) { + return EMPTY_ENTRY; + } else { + AnnotationAttributes attributes = this.getAttributes(annotationMetadata); + List configurations = this.getCandidateConfigurations(annotationMetadata, attributes); + configurations = this.removeDuplicates(configurations); + Set exclusions = this.getExclusions(annotationMetadata, attributes); + this.checkExcludedClasses(configurations, exclusions); + configurations.removeAll(exclusions); + configurations = this.filter(configurations, autoConfigurationMetadata); + this.fireAutoConfigurationImportEvents(configurations, exclusions); + return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); + } +} +``` + +我们可以看到getCandidateConfigurations()这个方法,他的作用就是引入系统已经加载好的一些类,到底是那些类呢: + +```java +protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { + List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); + Assert.notEmpty(configurations, + "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); + return configurations; +} +public static List loadFactoryNames(Class factoryClass, @Nullable ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); +} +``` + +会从META-INF/spring.factories中获取资源,然后通过Properties加载资源: + +```java +private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { + MultiValueMap result = (MultiValueMap)cache.get(classLoader); + if (result != null) { + return result; + } else { + try { + Enumeration urls = classLoader != + null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); + LinkedMultiValueMap result = new LinkedMultiValueMap(); + + while(urls.hasMoreElements()) { + URL url = (URL)urls.nextElement(); + UrlResource resource = new UrlResource(url); + Properties properties = PropertiesLoaderUtils.loadProperties(resource); + Iterator var6 = properties.entrySet().iterator(); + + while(var6.hasNext()) { + Map.Entry entry = (Map.Entry)var6.next(); + String factoryClassName = ((String)entry.getKey()).trim(); + String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); + int var10 = var9.length; + + for(int var11 = 0; var11 < var10; ++var11) { + String factoryName = var9[var11]; + result.add(factoryClassName, factoryName.trim()); + } + } + } + + cache.put(classLoader, result); + return result; + } catch (IOException var13) { + throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); + } + } +} +``` + +可以知道SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作。以前我们需要自己配置的东西,自动配置类都帮我们完成了。如下图可以发现Spring常见的一些类已经自动导入。 + +![](http://img.topjavaer.cn/img/springboot自动配置5.png)接下来看@ComponentScan注解,@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }),这个注解就是扫描包,然后放入spring容器。 + +```java +@ComponentScan(excludeFilters = { + @Filter(type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}), + @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})}) +public @interface SpringBootApplication {} +``` + +总结下@SpringbootApplication:就是说,他已经把很多东西准备好,具体是否使用取决于我们的程序或者说配置。 + +接下来继续看run方法: + +```java +public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +``` + +来看下在执行run方法到底有没有用到哪些自动配置的东西,我们点进run: + +```java +public ConfigurableApplicationContext run(String... args) { + //计时器 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ConfigurableApplicationContext context = null; + Collection exceptionReporters = new ArrayList(); + this.configureHeadlessProperty(); + //监听器 + SpringApplicationRunListeners listeners = this.getRunListeners(args); + listeners.starting(); + + Collection exceptionReporters; + try { + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); + this.configureIgnoreBeanInfo(environment); + Banner printedBanner = this.printBanner(environment); + //准备上下文 + context = this.createApplicationContext(); + exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context); + //预刷新context + this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); + //刷新context + this.refreshContext(context); + //刷新之后的context + this.afterRefresh(context, applicationArguments); + stopWatch.stop(); + if (this.logStartupInfo) { + (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); + } + + listeners.started(context); + this.callRunners(context, applicationArguments); + } catch (Throwable var10) { + this.handleRunFailure(context, var10, exceptionReporters, listeners); + throw new IllegalStateException(var10); + } + + try { + listeners.running(context); + return context; + } catch (Throwable var9) { + this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null); + throw new IllegalStateException(var9); + } +} +``` + +那我们关注的就是 refreshContext(context); 刷新context,我们点进来看。 + +```java +private void refreshContext(ConfigurableApplicationContext context) { + refresh(context); + if (this.registerShutdownHook) { + try { + context.registerShutdownHook(); + } + catch (AccessControlException ex) { + // Not allowed in some environments. + } + } +} +``` + +我们继续点进refresh(context); + +```java +protected void refresh(ApplicationContext applicationContext) { + Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext); + ((AbstractApplicationContext) applicationContext).refresh(); +} +``` + +他会调用 ((AbstractApplicationContext) applicationContext).refresh();方法,我们点进来看: + +```java +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + // Prepare this context for refreshing. + prepareRefresh(); + // Tell the subclass to refresh the internal bean factory. + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + // Prepare the bean factory for use in this context. + prepareBeanFactory(beanFactory); + + try { + // Allows post-processing of the bean factory in context subclasses. + postProcessBeanFactory(beanFactory); + // Invoke factory processors registered as beans in the context. + invokeBeanFactoryPostProcessors(beanFactory); + // Register bean processors that intercept bean creation. + registerBeanPostProcessors(beanFactory); + // Initialize message source for this context. + initMessageSource(); + // Initialize event multicaster for this context. + initApplicationEventMulticaster(); + // Initialize other special beans in specific context subclasses. + onRefresh(); + // Check for listener beans and register them. + registerListeners(); + // Instantiate all remaining (non-lazy-init) singletons. + finishBeanFactoryInitialization(beanFactory); + // Last step: publish corresponding event. + finishRefresh(); + }catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + // Destroy already created singletons to avoid dangling resources. + destroyBeans(); + // Reset 'active' flag. + cancelRefresh(ex); + // Propagate exception to caller. + throw ex; + }finally { + // Reset common introspection caches in Spring's core, since we + // might not ever need metadata for singleton beans anymore... + resetCommonCaches(); + } + } +} +``` + +由此可知,就是一个spring的bean的加载过程。继续来看一个方法叫做 onRefresh(): + +```java +protected void onRefresh() throws BeansException { + // For subclasses: do nothing by default. +} +``` + +他在这里并没有直接实现,但是我们找他的具体实现: + +![](http://img.topjavaer.cn/img/springboot自动配置6.png)比如Tomcat跟web有关,我们可以看到有个ServletWebServerApplicationContext: + +```java +@Override +protected void onRefresh() { + super.onRefresh(); + try { + createWebServer(); + } + catch (Throwable ex) { + throw new ApplicationContextException("Unable to start web server", ex); + } +} +``` + +可以看到有一个createWebServer();方法他是创建web容器的,而Tomcat不就是web容器,那是如何创建的呢,我们继续看: + +```java +private void createWebServer() { + WebServer webServer = this.webServer; + ServletContext servletContext = getServletContext(); + if (webServer == null && servletContext == null) { + ServletWebServerFactory factory = getWebServerFactory(); + this.webServer = factory.getWebServer(getSelfInitializer()); + } + else if (servletContext != null) { + try { + getSelfInitializer().onStartup(servletContext); + } + catch (ServletException ex) { + throw new ApplicationContextException("Cannot initialize servlet context", + ex); + } + } + initPropertySources(); +} +``` + +factory.getWebServer(getSelfInitializer());他是通过工厂的方式创建的。 + +``` +public interface ServletWebServerFactory { + WebServer getWebServer(ServletContextInitializer... initializers); +} +``` + +可以看到 它是一个接口,为什么会是接口。因为我们不止是Tomcat一种web容器。 + +![](http://img.topjavaer.cn/img/springboot自动配置7.png) + +我们看到还有Jetty,那我们来看TomcatServletWebServerFactory: + +```java +@Override +public WebServer getWebServer(ServletContextInitializer... initializers) { + Tomcat tomcat = new Tomcat(); + File baseDir = (this.baseDirectory != null) ? this.baseDirectory + : createTempDir("tomcat"); + tomcat.setBaseDir(baseDir.getAbsolutePath()); + Connector connector = new Connector(this.protocol); + tomcat.getService().addConnector(connector); + customizeConnector(connector); + tomcat.setConnector(connector); + tomcat.getHost().setAutoDeploy(false); + configureEngine(tomcat.getEngine()); + for (Connector additionalConnector : this.additionalTomcatConnectors) { + tomcat.getService().addConnector(additionalConnector); + } + prepareContext(tomcat.getHost(), initializers); + return getTomcatWebServer(tomcat); +} +``` + +那这块代码,就是我们要寻找的内置Tomcat,在这个过程当中,我们可以看到创建Tomcat的一个流程。 + +如果不明白的话, 我们在用另一种方式来理解下,大家要应该都知道stater举点例子。 + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-freemarker + +``` + +首先自定义一个stater。 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 2.1.4.RELEASE + + +com.zgw +gw-spring-boot-starter +1.0-SNAPSHOT + + + + org.springframework.boot + spring-boot-autoconfigure + + +``` + +我们先来看maven配置写入版本号,如果自定义一个stater的话必须依赖spring-boot-autoconfigure这个包,我们先看下项目目录。 + +![](http://img.topjavaer.cn/img/springboot自动配置8.png)img + +```java +public class GwServiceImpl implements GwService{ + @Autowired + GwProperties properties; + + @Override + public void Hello() + { + String name=properties.getName(); + System.out.println(name+"说:你们好啊"); + } +} +``` + +我们做的就是通过配置文件来定制name这个是具体实现。 + +```java +@Component +@ConfigurationProperties(prefix = "spring.gwname") +public class GwProperties { + + String name="zgw"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +这个类可以通过@ConfigurationProperties读取配置文件。 + +```java +@Configuration +@ConditionalOnClass(GwService.class) //扫描类 +@EnableConfigurationProperties(GwProperties.class) //让配置类生效 +public class GwAutoConfiguration { + + /** + * 功能描述 托管给spring + * @author zgw + * @return + */ + @Bean + @ConditionalOnMissingBean + public GwService gwService() + { + return new GwServiceImpl(); + } +} +``` + +这个为配置类,为什么这么写因为,spring-boot的stater都是这么写的,我们可以参照他仿写stater,以达到自动配置的目的,然后我们在通过spring.factories也来进行配置。 + +```properties +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gw.GwAutoConfiguration +``` + +然后这样一个简单的stater就完成了,然后可以进行maven的打包,在其他项目引入就可以使用。 + +> 来源:cnblogs.com/jing99/p/11504113.html diff --git a/docs/advance/excellent-article/30-yi-di-duo-huo.md b/docs/advance/excellent-article/30-yi-di-duo-huo.md new file mode 100644 index 0000000..75657f1 --- /dev/null +++ b/docs/advance/excellent-article/30-yi-di-duo-huo.md @@ -0,0 +1,563 @@ +--- +sidebar: heading +title: 异地多活 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 异地多活,同城灾备,同城双活,两地三中心,异地双活,异地多活 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +在软件开发领域,「异地多活」是分布式系统架构设计的一座高峰,很多人经常听过它,但很少人理解其中的原理。 + +**异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的?** + +这些疑问,想必是每个程序看到异地多活这个名词时,都想要搞明白的问题。 + +有幸,我曾经深度参与过一个中等互联网公司,建设异地多活系统的设计与实施过程。所以今天,我就来和你聊一聊异地多活背后的的实现原理。 + +认真读完这篇文章,我相信你会对异地多活架构,有更加深刻的理解。 + +**这篇文章干货很多,希望你可以耐心读完。** + +![](http://img.topjavaer.cn/img/202306290845298.png) + + + +# 01 系统可用性 + +要想理解异地多活,我们需要从架构设计的原则说起。 + +现如今,我们开发一个软件系统,对其要求越来越高,如果你了解一些「架构设计」的要求,就知道一个好的软件架构应该遵循以下 3 个原则: + +1. 高性能 +2. 高可用 +3. 易扩展 + +其中,高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。例如 1 秒可处理 10W 并发请求,接口响应时间 5 ms 等等。 + +易扩展表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统。 + +而「高可用」这个概念,看起来很抽象,怎么理解它呢?通常用 2 个指标来衡量: + +- **平均故障间隔 MTBF**(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高 +- **故障恢复时间 MTTR**(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小 + +可用性与这两者的关系: + +> 可用性(Availability)= MTBF / (MTBF + MTTR) * 100% + +这个公式得出的结果是一个「比例」,通常我们会用「N 个 9」来描述一个系统的可用性。 + +![](http://img.topjavaer.cn/img/202306290845411.png) + +从这张图你可以看到,要想达到 4 个 9 以上的可用性,平均每天故障时间必须控制在 10 秒以内。 + +也就是说,只有故障的时间「越短」,整个系统的可用性才会越高,每提升 1 个 9,都会对系统提出更高的要求。 + +我们都知道,系统发生故障其实是不可避免的,尤其是规模越大的系统,发生问题的概率也越大。这些故障一般体现在 3 个方面: + +1. **硬件故障**:CPU、内存、磁盘、网卡、交换机、路由器 +2. **软件问题**:代码 Bug、版本迭代 +3. **不可抗力**:地震、水灾、火灾、战争 + +这些风险随时都有可能发生。所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键。 + +可如何做到快速恢复呢? + +这篇文章要讲的「异地多活」架构,就是为了解决这个问题,而提出的高效解决方案。 + +下面,我会从一个最简单的系统出发,带你一步步演化出一个支持「异地多活」的系统架构。 + +在这个过程中,你会看到一个系统会遇到哪些可用性问题,以及为什么架构要这样演进,从而理解异地多活架构的意义。 + +# 02 单机架构 + +我们从最简单的开始讲起。 + +假设你的业务处于起步阶段,体量非常小,那你的架构是这样的: + +![](http://img.topjavaer.cn/img/202306290846808.png) + +这个架构模型非常简单,客户端请求进来,业务应用读写数据库,返回结果,非常好理解。 + +但需要注意的是,这里的数据库是「单机」部署的,所以它有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,那这意味着所有数据就全部「丢失」了,这个损失是巨大的。 + +如何避免这个问题呢?我们很容易想到一个方案:**备份**。 + +![](http://img.topjavaer.cn/img/202306290846288.png) + +你可以对数据做备份,把数据库文件「定期」cp 到另一台机器上,这样,即使原机器丢失数据,你依旧可以通过备份把数据「恢复」回来,以此保证数据安全。 + +这个方案实施起来虽然比较简单,但存在 2 个问题: + +1. **恢复需要时间**:业务需先停机,再恢复数据,停机时间取决于恢复的速度,恢复期间服务「不可用」 +2. **数据不完整**:因为是定期备份,数据肯定不是「最新」的,数据完整程度取决于备份的周期 + +很明显,你的数据库越大,意味故障恢复时间越久。那按照前面我们提到的「高可用」标准,这个方案可能连 1 个 9 都达不到,远远无法满足我们对可用性的要求。 + +那有什么更好的方案,既可以快速恢复业务?还能尽可能保证数据完整性呢? + +这时你可以采用这个方案:**主从副本**。 + +# 03 主从副本 + +你可以在另一台机器上,再部署一个数据库实例,让这个新实例成为原实例的「副本」,让两者保持「实时同步」,就像这样: + +![](http://img.topjavaer.cn/img/202306290846494.png) + +我们一般把原实例叫作主库(master),新实例叫作从库(slave)。这个方案的优点在于: + +- **数据完整性高**:主从副本实时同步,数据「差异」很小 +- **抗故障能力提升**:主库有任何异常,从库可随时「切换」为主库,继续提供服务 +- **读性能提升**:业务应用可直接读从库,分担主库「压力」读压力 + +这个方案不错,不仅大大提高了数据库的可用性,还提升了系统的读性能。 + +同样的思路,你的「业务应用」也可以在其它机器部署一份,避免单点。因为业务应用通常是「无状态」的(不像数据库那样存储数据),所以直接部署即可,非常简单。 + +![](http://img.topjavaer.cn/img/202306290846710.png) + +因为业务应用部署了多个,所以你现在还需要部署一个「接入层」,来做请求的「负载均衡」(一般会使用 nginx 或 LVS),这样当一台机器宕机后,另一台机器也可以「接管」所有流量,持续提供服务。 + +![](http://img.topjavaer.cn/img/202306290848119.png) + +从这个方案你可以看出,提升可用性的关键思路就是:**冗余**。 + +没错,担心一个实例故障,那就部署多个实例,担心一个机器宕机,那就部署多台机器。 + +到这里,你的架构基本已演变成主流方案了,之后开发新的业务应用,都可以按照这种模式去部署。 + +![](http://img.topjavaer.cn/img/202306290846366.png) + +但这种方案还有什么风险吗? + +# 04 风险不可控 + +现在让我们把视角下放,把焦点放到具体的「部署细节」上来。 + +按照前面的分析,为了避免单点故障,你的应用虽然部署了多台机器,但这些机器的分布情况,我们并没有去深究。 + +而一个机房有很多服务器,这些服务器通常会分布在一个个「机柜」上,如果你使用的这些机器,刚好在一个机柜,还是存在风险。 + +如果恰好连接这个机柜的交换机 / 路由器发生故障,那么你的应用依旧有「不可用」的风险。 + +> 虽然交换机 / 路由器也做了路线冗余,但不能保证一定不出问题。 + +部署在一个机柜有风险,那把这些机器打散,分散到不同机柜上,是不是就没问题了? + +这样确实会大大降低出问题的概率。但我们依旧不能掉以轻心,因为无论怎么分散,它们总归还是在一个相同的环境下:**机房**。 + +那继续追问,机房会不会发生故障呢? + +一般来讲,建设一个机房的要求其实是很高的,地理位置、温湿度控制、备用电源等等,机房厂商会在各方面做好防护。但即使这样,我们每隔一段时间还会看到这样的新闻: + +- 2015 年 5 月 27 日,杭州市某地光纤被挖断,近 3 亿用户长达 5 小时无法访问支付宝 +- 2021 年 7 月 13 日,B 站部分服务器机房发生故障,造成整站持续 3 个小时无法访问 +- 2021 年 10 月 9 日,富途证券服务器机房发生电力闪断故障,造成用户 2 个小时无法登陆、交易 +- … + +可见,即使机房级别的防护已经做得足够好,但只要有「概率」出问题,那现实情况就有可能发生。虽然概率很小,但一旦真的发生,影响之大可见一斑。 + +看到这里你可能会想,机房出现问题的概率也太小了吧,工作了这么多年,也没让我碰上一次,有必要考虑得这么复杂吗? + +但你有没有思考这样一个问题:**不同体量的系统,它们各自关注的重点是什么?** + +体量很小的系统,它会重点关注「用户」规模、增长,这个阶段获取用户是一切。等用户体量上来了,这个阶段会重点关注「性能」,优化接口响应时间、页面打开速度等等,这个阶段更多是关注用户体验。 + +等体量再大到一定规模后你会发现,「可用性」就变得尤为重要。像微信、支付宝这种全民级的应用,如果机房发生一次故障,那整个影响范围可以说是非常巨大的。 + +所以,再小概率的风险,我们在提高系统可用性时,也不能忽视。 + +分析了风险,再说回我们的架构。那到底该怎么应对机房级别的故障呢? + +没错,还是**冗余**。 + +# 05 同城灾备 + +想要抵御「机房」级别的风险,那应对方案就不能局限在一个机房内了。 + +现在,你需要做机房级别的冗余方案,也就是说,你需要再搭建一个机房,来部署你的服务。 + +简单起见,你可以在「同一个城市」再搭建一个机房,原机房我们叫作 A 机房,新机房叫 B 机房,这两个机房的网络用一条「专线」连通。 + +![](http://img.topjavaer.cn/img/202306290848349.png) + +有了新机房,怎么把它用起来呢?这里还是要优先考虑「数据」风险。 + +为了避免 A 机房故障导致数据丢失,所以我们需要把数据在 B 机房也存一份。最简单的方案还是和前面提到的一样:**备份**。 + +A 机房的数据,定时在 B 机房做备份(拷贝数据文件),这样即使整个 A 机房遭到严重的损坏,B 机房的数据不会丢,通过备份可以把数据「恢复」回来,重启服务。 + +![](http://img.topjavaer.cn/img/202306290848730.png) + +这种方案,我们称之为「**冷备**」。为什么叫冷备呢?因为 B 机房只做备份,不提供实时服务,它是冷的,只会在 A 机房故障时才会启用。 + +但备份的问题依旧和之前描述的一样:数据不完整、恢复数据期间业务不可用,整个系统的可用性还是无法得到保证。 + +所以,我们还是需要用「主从副本」的方式,在 B 机房部署 A 机房的数据副本,架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849386.png) + +这样,就算整个 A 机房挂掉,我们在 B 机房也有比较「完整」的数据。 + +数据是保住了,但这时你需要考虑另外一个问题:**如果 A 机房真挂掉了,要想保证服务不中断,你还需要在 B 机房「紧急」做这些事情**: + +1. B 机房所有从库提升为主库 +2. 在 B 机房部署应用,启动服务 +3. 部署接入层,配置转发规则 +4. DNS 指向 B 机房接入层,接入流量,业务恢复 + +看到了么?A 机房故障后,B 机房需要做这么多工作,你的业务才能完全「恢复」过来。 + +你看,整个过程需要人为介入,且需花费大量时间来操作,恢复之前整个服务还是不可用的,这个方案还是不太爽,如果能做到故障后立即「切换」,那就好了。 + +因此,要想缩短业务恢复的时间,你必须把这些工作在 B 机房「提前」做好,也就是说,你需要在 B 机房提前部署好接入层、业务应用,等待随时切换。架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849453.png) + +这样的话,A 机房整个挂掉,我们只需要做 2 件事即可: + +1. B 机房所有从库提升为主库 +2. DNS 指向 B 机房接入层,接入流量,业务恢复 + +这样一来,恢复速度快了很多。 + +到这里你会发现,B 机房从最开始的「空空如也」,演变到现在,几乎是「镜像」了一份 A 机房的所有东西,从最上层的接入层,到中间的业务应用,到最下层的存储。两个机房唯一的区别是,**A 机房的存储都是主库,而 B 机房都是从库**。 + +这种方案,我们把它叫做「**热备**」。 + +热的意思是指,B 机房处于「待命」状态,A 故障后 B 可以随时「接管」流量,继续提供服务。热备相比于冷备最大的优点是:**随时可切换**。 + +无论是冷备还是热备,因为它们都处于「备用」状态,所以我们把这两个方案统称为:**同城灾备**。 + +同城灾备的最大优势在于,我们再也不用担心「机房」级别的故障了,一个机房发生风险,我们只需把流量切换到另一个机房即可,可用性再次提高,是不是很爽?(后面还有更爽的) + +# 06 同城双活 + +我们继续来看这个架构。 + +虽然我们有了应对机房故障的解决方案,但这里有个问题是我们不能忽视的:**A 机房挂掉,全部流量切到 B 机房,B 机房能否真的如我们所愿,正常提供服务?** + +这是个值得思考的问题。 + +这就好比有两支军队 A 和 B,A 军队历经沙场,作战经验丰富,而 B 军队只是后备军,除了有军人的基本素养之外,并没有实战经验,战斗经验基本为 0。 + +如果 A 军队丧失战斗能力,需要 B 军队立即顶上时,作为指挥官的你,肯定也会担心 B 军队能否真的担此重任吧? + +我们的架构也是如此,此时的 B 机房虽然是随时「待命」状态,但 A 机房真的发生故障,我们要把全部流量切到 B 机房,其实是不敢百分百保证它可以「如期」工作的。 + +你想,我们在一个机房内部署服务,还总是发生各种各样的问题,例如:发布应用的版本不一致、系统资源不足、操作系统参数不一样等等。现在多部署一个机房,这些问题只会增多,不会减少。 + +另外,从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了! + +因此,我们需要让 B 机房也接入流量,实时提供服务,这样做的好处,**一是可以实时训练这支后备军,让它达到与 A 机房相同的作战水平,随时可切换,二是 B 机房接入流量后,可以分担 A 机房的流量压力**。这才是把 B 机房资源优势,发挥最大化的最好方案! + +那怎么让 B 机房也接入流量呢?很简单,就是把 B 机房的接入层 IP 地址,加入到 DNS 中,这样,B 机房从上层就可以有流量进来了。 + +![](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342320382121.jpg) + +但这里有一个问题:别忘了,B 机房的存储,现在可都是 A 机房的「从库」,从库默认可都是「不可写」的,B 机房的写请求打到本机房存储上,肯定会报错,这还是不符合我们预期。怎么办? + +这时,你就需要在「业务应用」层做改造了。 + +你的业务应用在操作数据库时,需要区分「读写分离」(一般用中间件实现),即两个机房的「读」流量,可以读任意机房的存储,但「写」流量,只允许写 A 机房,因为主库在 A 机房。 + +![img](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342338343503.jpg) + +这会涉及到你用的所有存储,例如项目中用到了 MySQL、Redis、MongoDB 等等,操作这些数据库,都需要区分读写请求,所以这块需要一定的业务「改造」成本。 + +因为 A 机房的存储都是主库,所以我们把 A 机房叫做「主机房」,B 机房叫「从机房」。 + +两个机房部署在「同城」,物理距离比较近,而且两个机房用「专线」网络连接,虽然跨机房访问的延迟,比单个机房内要大一些,但整体的延迟还是可以接受的。 + +业务改造完成后,B 机房可以慢慢接入流量,从 10%、30%、50% 逐渐覆盖到 100%,你可以持续观察 B 机房的业务是否存在问题,有问题及时修复,逐渐让 B 机房的工作能力,达到和 A 机房相同水平。 + +现在,因为 B 机房实时接入了流量,此时如果 A 机房挂了,那我们就可以「大胆」地把 A 的流量,全部切换到 B 机房,完成快速切换! + +到这里你可以看到,我们部署的 B 机房,在物理上虽然与 A 有一定距离,但整个系统从「逻辑」上来看,我们是把这两个机房看做一个「整体」来规划的,也就是说,相当于把 2 个机房当作 1 个机房来用。 + +这种架构方案,比前面的同城灾备更「进了一步」,B 机房实时接入了流量,还能应对随时的故障切换,这种方案我们把它叫做「**同城双活**」。 + +因为两个机房都能处理业务请求,这对我们系统的内部维护、改造、升级提供了更多的可实施空间(流量随时切换),现在,整个系统的弹性也变大了,是不是更爽了? + +那这种架构有什么问题呢? + +# 07 两地三中心 + +还是回到风险上来说。 + +虽然我们把 2 个机房当做一个整体来规划,但这 2 个机房在物理层面上,还是处于「一个城市」内,如果是整个城市发生自然灾害,例如地震、水灾(河南水灾刚过去不久),那 2 个机房依旧存在「全局覆没」的风险。 + +真是防不胜防啊?怎么办?没办法,继续冗余。 + +但这次冗余机房,就不能部署在同一个城市了,你需要把它放到距离更远的地方,部署在「异地」。 + +> 通常建议两个机房的距离要在 1000 公里以上,这样才能应对城市级别的灾难。 + +假设之前的 A、B 机房在北京,那这次新部署的 C 机房可以放在上海。 + +按照前面的思路,把 C 机房用起来,最简单粗暴的方案还就是做「冷备」,即定时把 A、B 机房的数据,在 C 机房做备份,防止数据丢失。 + +![](http://img.topjavaer.cn/img/202306290850748.png) + +这种方案,就是我们经常听到的「**两地三中心**」。 + +**两地是指 2 个城市,三中心是指有 3 个机房,其中 2 个机房在同一个城市,并且同时提供服务,第 3 个机房部署在异地,只做数据灾备。** + +这种架构方案,通常用在银行、金融、政企相关的项目中。它的问题还是前面所说的,启用灾备机房需要时间,而且启用后的服务,不确定能否如期工作。 + +所以,要想真正的抵御城市级别的故障,越来越多的互联网公司,开始实施「**异地双活**」。 + +# 08 伪异地双活 + +这里,我们还是分析 2 个机房的架构情况。我们不再把 A、B 机房部署在同一个城市,而是分开部署,例如 A 机房放在北京,B 机房放在上海。 + +前面我们讲了同城双活,那异地双活是不是直接「照搬」同城双活的模式去部署就可以了呢? + +事情没你想的那么简单。 + +如果还是按照同城双活的架构来部署,那异地双活的架构就是这样的: + +![](http://img.topjavaer.cn/img/202306290850564.png) + +注意看,两个机房的网络是通过「跨城专线」连通的。 + +此时两个机房都接入流量,那上海机房的请求,可能要去读写北京机房的存储,这里存在一个很大的问题:**网络延迟**。 + +因为两个机房距离较远,受到物理距离的限制,现在,两地之间的网络延迟就变成了「**不可忽视**」的因素了。 + +北京到上海的距离大约 1300 公里,即使架设一条高速的「网络专线」,光纤以光速传输,一个来回也需要近 10ms 的延迟。 + +况且,网络线路之间还会经历各种路由器、交换机等网络设备,实际延迟可能会达到 30ms ~ 100ms,如果网络发生抖动,延迟甚至会达到 1 秒。 + +> 不止是延迟,远距离的网络专线质量,是远远达不到机房内网络质量的,专线网络经常会发生延迟、丢包、甚至中断的情况。总之,不能过度信任和依赖「跨城专线」。 + +你可能会问,这点延迟对业务影响很大吗?影响非常大! + +试想,一个客户端请求打到上海机房,上海机房要去读写北京机房的存储,一次跨机房访问延迟就达到了 30ms,这大致是机房内网网络(0.5 ms)访问速度的 60 倍(30ms / 0.5ms),一次请求慢 60 倍,来回往返就要慢 100 倍以上。 + +而我们在 App 打开一个页面,可能会访问后端几十个 API,每次都跨机房访问,整个页面的响应延迟有可能就达到了**秒级**,这个性能简直惨不忍睹,难以接受。 + +看到了么,虽然我们只是简单的把机房部署在了「异地」,但「同城双活」的架构模型,在这里就不适用了,还是按照这种方式部署,这是「伪异地双活」! + +那如何做到真正的异地双活呢? + +# 09 真正的异地双活 + +既然「跨机房」调用延迟是不容忽视的因素,那我们只能尽量避免跨机房「调用」,规避这个延迟问题。 + +也就是说,上海机房的应用,不能再「跨机房」去读写北京机房的存储,只允许读写上海本地的存储,实现「就近访问」,这样才能避免延迟问题。 + +还是之前提到的问题:上海机房存储都是从库,不允许写入啊,除非我们只允许上海机房接入「读流量」,不接收「写流量」,否则无法满足不再跨机房的要求。 + +很显然,只让上海机房接收读流量的方案不现实,因为很少有项目是只有读流量,没有写流量的。所以这种方案还是不行,这怎么办? + +此时,你就必须在「**存储层**」做改造了。 + +要想上海机房读写本机房的存储,那上海机房的存储不能再是北京机房的从库,而是也要变为「主库」。 + +你没看错,两个机房的存储必须都是「**主库**」,而且两个机房的数据还要「**互相同步**」数据,即客户端无论写哪一个机房,都能把这条数据同步到另一个机房。 + +因为只有两个机房都拥有「全量数据」,才能支持任意切换机房,持续提供服务。 + +怎么实现这种「双主」架构呢?它们之间如何互相同步数据? + +如果你对 MySQL 有所了解,MySQL 本身就提供了双主架构,它支持双向复制数据,但平时用的并不多。而且 Redis、MongoDB 等数据库并没有提供这个功能,所以,你必须开发对应的「数据同步中间件」来实现双向同步的功能。 + +此外,除了数据库这种有状态的软件之外,你的项目通常还会使用到消息队列,例如 RabbitMQ、Kafka,这些也是有状态的服务,所以它们也需要开发双向同步的中间件,支持任意机房写入数据,同步至另一个机房。 + +看到了么,这一下子复杂度就上来了,单单针对每个数据库、队列开发同步中间件,就需要投入很大精力了。 + +> 业界也开源出了很多数据同步中间件,例如阿里的 Canal、RedisShake、MongoShake,可分别在两个机房同步 MySQL、Redis、MongoDB 数据。 +> +> 很多有能力的公司,也会采用自研同步中间件的方式来做,例如饿了么、携程、美团都开发了自己的同步中间件。 +> +> 我也有幸参与设计开发了 MySQL、Redis/Codis、MongoDB 的同步中间件,有时间写一篇文章详细聊聊实现细节,欢迎持续关注。:) + +现在,整个架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290850052.png) + +注意看,两个机房的存储层都互相同步数据的。有了数据同步中间件,就可以达到这样的效果: + +- 北京机房写入 X = 1 +- 上海机房写入 Y = 2 +- 数据通过中间件双向同步 +- 北京、上海机房都有 X = 1、Y = 2 的数据 + +这里我们用中间件双向同步数据,就不用再担心专线问题,专线出问题,我们的中间件可以自动重试,直到成功,达到数据最终一致。 + +但这里还会遇到一个问题,两个机房都可以写,操作的不是同一条数据那还好,如果修改的是同一条的数据,发生冲突怎么办? + +- 用户短时间内发了 2 个修改请求,都是修改同一条数据 +- 一个请求落在北京机房,修改 X = 1(还未同步到上海机房) +- 另一个请求落在上海机房,修改 X = 2(还未同步到北京机房) +- 两个机房以哪个为准? + +也就是说,在很短的时间内,同一个用户修改同一条数据,两个机房无法确认谁先谁后,数据发生「冲突」。 + +这是一个很严重的问题,系统发生故障并不可怕,可怕的是数据发生「错误」,因为修正数据的成本太高了。我们一定要避免这种情况的发生。解决这个问题,有 2 个方案。 + +**第一个方案**,数据同步中间件要有自动「合并」数据、解决「冲突」的能力。 + +这个方案实现起来比较复杂,要想合并数据,就必须要区分出「先后」顺序。我们很容易想到的方案,就是以「时间」为标尺,以「后到达」的请求为准。 + +但这种方案需要两个机房的「时钟」严格保持一致才行,否则很容易出现问题。例如: + +- 第 1 个请求落到北京机房,北京机房时钟是 10:01,修改 X = 1 +- 第 2 个请求落到上海机房,上海机房时钟是 10:00,修改 X = 2 + +因为北京机房的时间「更晚」,那最终结果就会是 X = 1。但这里其实应该以第 2 个请求为准,X = 2 才对。 + +可见,完全「依赖」时钟的冲突解决方案,不太严谨。 + +所以,通常会采用第二种方案,从「源头」就避免数据冲突的发生。 + +# 10 如何实施异地双活 + +既然自动合并数据的方案实现成本高,那我们就要想,能否从源头就「避免」数据冲突呢? + +这个思路非常棒! + +从源头避免数据冲突的思路是:**在最上层接入流量时,就不要让冲突的情况发生**。 + +具体来讲就是,要在最上层就把用户「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海 机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。 + +所以这时,你需要在接入层之上,再部署一个「路由层」(通常部署在云服务器上),自己可以配置路由规则,把用户「分流」到不同的机房内。 + +![](http://img.topjavaer.cn/img/202306290851907.png) + +但这个路由规则,具体怎么定呢?有很多种实现方式,最常见的我总结了 3 类: + +1. 按业务类型分片 +2. 直接哈希分片 +3. 按地理位置分片 + +**1、按业务类型分片** + +这种方案是指,按应用的「业务类型」来划分。 + +举例:假设我们一共有 4 个应用,北京和上海机房都部署这些应用。但应用 1、2 只在北京机房接入流量,在上海机房只是热备。应用 3、4 只在上海机房接入流量,在北京机房是热备。 + +这样一来,应用 1、2 的所有业务请求,只读写北京机房存储,应用 3、4 的所有请求,只会读写上海机房存储。 + +![](http://img.topjavaer.cn/img/202306290851532.png) + +这样按业务类型分片,也可以避免同一个用户修改同一条数据。 + +> 这里按业务类型在不同机房接入流量,还需要考虑多个应用之间的依赖关系,要尽可能的把完成「相关」业务的应用部署在同一个机房,避免跨机房调用。 +> +> 例如,订单、支付服务有依赖关系,会产生互相调用,那这 2 个服务在 A 机房接入流量。社区、发帖服务有依赖关系,那这 2 个服务在 B 机房接入流量。 + +**2、直接哈希分片** + +这种方案就是,最上层的路由层,会根据用户 ID 计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内。 + +举例:一共 200 个用户,根据用户 ID 计算哈希值,然后根据路由规则,把用户 1 - 100 路由到北京机房,101 - 200 用户路由到上海机房,这样,就避免了同一个用户修改同一条数据的情况发生。 + +![](http://img.topjavaer.cn/img/202306290851605.png) + +**3、按地理位置分片** + +这种方案,非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案。 + +拿外卖服务举例,你要点外卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的。 + +针对这种特征,就可以在最上层,按用户的「地理位置」来做分片,分散到不同的机房。 + +举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房。这样的分片规则,也能避免数据冲突。 + +![](http://img.topjavaer.cn/img/202306290851996.png) + +> 提醒:这 3 种常见的分片规则,第一次看不太好理解,建议配合图多理解几遍。搞懂这 3 个分片规则,你才能真正明白怎么做异地多活。 + +总之,分片的核心思路在于,**让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问**。 + +阿里在实施这种方案时,给它起了个名字,叫做「**单元化**」。 + +> 当然,最上层的路由层把用户分片后,理论来说同一个用户只会落在同一个机房内,但不排除程序 Bug 导致用户会在两个机房「漂移」。 +> +> 安全起见,每个机房在写存储时,还需要有一套机制,能够检测「数据归属」,应用层操作存储时,需要通过中间件来做「兜底」,避免不该写本机房的情况发生。(篇幅限制,这里不展开讲,理解思路即可) + +现在,两个机房就可以都接收「读写」流量(做好分片的请求),底层存储保持「双向」同步,两个机房都拥有全量数据,当任意机房故障时,另一个机房就可以「接管」全部流量,实现快速切换,简直不要太爽。 + +不仅如此,因为机房部署在异地,我们还可以更细化地「优化」路由规则,让用户访问就近的机房,这样整个系统的性能也会大大提升。 + +> 这里还有一种情况,是无法做数据分片的:**全局数据**。例如系统配置、商品库存这类需要强一致的数据,这类服务依旧只能采用写主机房,读从机房的方案,不做双活。 +> +> 双活的重点,是要优先保证「核心」业务先实现双活,并不是「全部」业务实现双活。 + +至此,我们才算实现了真正的「**异地双活**」! + +> 到这里你可以看出,完成这样一套架构,需要投入的成本是巨大的。 +> +> 路由规则、路由转发、数据同步中间件、数据校验兜底策略,不仅需要开发强大的中间件,同时还要业务配合改造(业务边界划分、依赖拆分)等一些列工作,没有足够的人力物力,这套架构很难实施。 + +# 11 异地多活 + +理解了异地双活,那「异地多活」顾名思义,就是在异地双活的基础上,部署多个机房即可。架构变成了这样: + +![](http://img.topjavaer.cn/img/202306290852204.png) + +这些服务按照「单元化」的部署方式,可以让每个机房部署在任意地区,随时扩展新机房,你只需要在最上层定义好分片规则就好了。 + +但这里还有一个小问题,随着扩展的机房越来越多,当一个机房写入数据后,需要同步的机房也越来越多,这个实现复杂度会比较高。 + +所以业界又把这一架构又做了进一步优化,把「网状」架构升级为「星状」: + +![](http://img.topjavaer.cn/img/202306290852633.png) + +这种方案必须设立一个「中心机房」,任意机房写入数据后,都只同步到中心机房,再由中心机房同步至其它机房。 + +这样做的好处是,一个机房写入数据,只需要同步数据到中心机房即可,不需要再关心一共部署了多少个机房,实现复杂度大大「简化」。 + +但与此同时,这个中心机房的「稳定性」要求会比较高。不过也还好,即使中心机房发生故障,我们也可以把任意一个机房,提升为中心机房,继续按照之前的架构提供服务。 + +至此,我们的系统彻底实现了「**异地多活**」! + +多活的优势在于,**可以任意扩展机房「就近」部署。任意机房发生故障,可以完成快速「切换」**,大大提高了系统的可用性。 + +同时,我们也再也不用担心系统规模的增长,因为这套架构具有极强的「**扩展能力**」。 + +怎么样?我们从一个最简单的应用,一路优化下来,到最终的架构方案,有没有帮你彻底理解异地多活呢? + +# 总结 + +好了,总结一下这篇文章的重点。 + +1、一个好的软件架构,应该遵循高性能、高可用、易扩展 3 大原则,其中「高可用」在系统规模变得越来越大时,变得尤为重要 + +2、系统发生故障并不可怕,能以「最快」的速度恢复,才是高可用追求的目标,异地多活是实现高可用的有效手段 + +3、提升高可用的核心是「冗余」,备份、主从副本、同城灾备、同城双活、两地三中心、异地双活,异地多活都是在做冗余 + +4、同城灾备分为「冷备」和「热备」,冷备只备份数据,不提供服务,热备实时同步数据,并做好随时切换的准备 + +5、同城双活比灾备的优势在于,两个机房都可以接入「读写」流量,提高可用性的同时,还提升了系统性能。虽然物理上是两个机房,但「逻辑」上还是当做一个机房来用 + +6、两地三中心是在同城双活的基础上,额外部署一个异地机房做「灾备」,用来抵御「城市」级别的灾害,但启用灾备机房需要时间 + +7、异地双活才是抵御「城市」级别灾害的更好方案,两个机房同时提供服务,故障随时可切换,可用性高。但实现也最复杂,理解了异地双活,才能彻底理解异地多活 + +8、异地多活是在异地双活的基础上,任意扩展多个机房,不仅又提高了可用性,还能应对更大规模的流量的压力,扩展性最强,是实现高可用的最终方案 + +# 后记 + +这篇文章我从「宏观」层面,向你介绍了异地多活架构的「核心」思路,整篇文章的信息量还是很大的,如果不太好理解,我建议你多读几遍。 + +因为篇幅限制,很多细节我并没有展开来讲。这篇文章更像是讲异地多活的架构之「道」,而真正实施的「术」,要考虑的点其实也非常繁多,因为它需要开发强大的「基础设施」才可以完成实施。 + +不仅如此,要想真正实现异地多活,还需要遵循一些原则,例如业务梳理、业务分级、数据分类、数据最终一致性保障、机房切换一致性保障、异常处理等等。同时,相关的运维设施、监控体系也要能跟得上才行。 + +宏观上需要考虑业务(微服务部署、依赖、拆分、SDK、Web 框架)、基础设施(服务发现、流量调度、持续集成、同步中间件、自研存储),微观上要开发各种中间件,还要关注中间件的高性能、高可用、容错能力,其复杂度之高,只有亲身参与过之后才知道。 + +我曾经有幸参与过,存储层同步中间件的设计与开发,实现过「跨机房」同步 MySQL、Redis、MongoDB 的中间件,踩过的坑也非常多。当然,这些中间件的设计思路也非常有意思,有时间单独分享一下这些中间件的设计思路。 + +值得提醒你的是,只有真正理解了「异地双活」,才能彻底理解「异地多活」。在我看来,从同城双活演变为异地双活的过程,是最为复杂的,最核心的东西包括,**业务单元化划分、存储层数据双向同步、最上层的分片逻辑**,这些是实现异地多活的重中之重。 + diff --git a/docs/advance/excellent-article/31-mysql-data-sync-es.md b/docs/advance/excellent-article/31-mysql-data-sync-es.md new file mode 100644 index 0000000..8d43d94 --- /dev/null +++ b/docs/advance/excellent-article/31-mysql-data-sync-es.md @@ -0,0 +1,503 @@ +--- +sidebar: heading +title: MySQL数据如何实时同步到ES +category: 优质文章 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL,ES,elasticsearch,数据同步 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + + +## 前言 + +我们一般会使用MySQL用来存储数据,用Es来做全文检索和特殊查询,那么如何将数据优雅的从MySQL同步到Es呢?我们一般有以下几种方式: + +1.**双写**。在代码中先向MySQL中写入数据,然后紧接着向Es中写入数据。这个方法的缺点是代码严重耦合,需要手动维护MySQL和Es数据关系,非常不便于维护。 + +2.**发MQ,异步执行**。在执行完向Mysql中写入数据的逻辑后,发送MQ,告诉消费端这个数据需要写入Es,消费端收到消息后执行向Es写入数据的逻辑。这个方式的优点是Mysql和Es数据维护分离,开发Mysql和Es的人员只需要关心各自的业务。缺点是依然需要维护发送、接收MQ的逻辑,并且引入了MQ组件,增加了系统的复杂度。 + +3.**使用Datax进行全量数据同步**。这个方式优点是可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。缺点是Datax是一种全量同步数据的方式,不使用实时同步。如果系统对数据时效性不强,可以考虑此方式。 + +4.**使用Canal进行实时数据同步**。这个方式具有跟Datax一样的优点,可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。与Datax不同的是Canal是一种实时同步数据的方式,对数据时效性较强的系统,我们会采用Canal来进行实时数据同步。 + +那么就让我们来看看Canal是如何使用的。 + +## 官网 + +https://github.com/alibaba/canal + +## 1.Canal简介 + +![](http://img.topjavaer.cn/img/202306262359509.png) + +**canal [kə'næl]** ,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费 + +早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。 + +基于日志增量订阅和消费的业务包括 + +- 数据库镜像 +- 数据库实时备份 +- 索引构建和实时维护(拆分异构索引、倒排索引等) +- 业务 cache 刷新 +- 带业务逻辑的增量数据处理 + +当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x + +### **MySQL主备复制原理** + +![](http://img.topjavaer.cn/img/202306262359485.png) + +- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看) +- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log) +- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据 + +### **canal工作原理** + +- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议 +- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) +- canal 解析 binary log 对象(原始为 byte 流) + +## 2.开启MySQL Binlog + +- 对于自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下 + +```ini +[mysqld] +log-bin=mysql-bin # 开启 binlog +binlog-format=ROW # 选择 ROW 模式 +server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 +lua +## 复制代码注意:针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步 +``` + +- 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant + +```sql +CREATE USER canal IDENTIFIED BY 'canal'; +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; +FLUSH PRIVILEGES; +``` + +注意:Mysql版本为8.x时启动canal可能会出现“caching_sha2_password Auth failed”错误,这是因为8.x创建用户时默认的密码加密方式为**caching_sha2_password**,与canal的方式不一致,所以需要将canal用户的密码加密方式修改为**mysql_native_password** + +```sql +ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal'; #更新一下用户密码 +FLUSH PRIVILEGES; #刷新权限 +``` + +## 3.安装Canal + +### 3.1 下载Canal + +**点击下载地址,选择版本后点击canal.deployer文件下载** + +![](http://img.topjavaer.cn/img/202306262359063.png) + +### 3.2 修改配置文件 + +打开目录下conf/example/instance.properties文件,主要修改以下内容 + +```ini +## mysql serverId,不要和 mysql 的 server_id 重复 +canal.instance.mysql.slaveId = 10 +#position info,需要改成自己的数据库信息 +canal.instance.master.address = 127.0.0.1:3306 +#username/password,需要改成自己的数据库信息,与刚才添加的用户保持一致 +canal.instance.dbUsername = canal +canal.instance.dbPassword = canal +``` + +### 3.3 启动和关闭 + +```bash +#进入文件目录下的bin文件夹 +#启动 +sh startup.sh +#关闭 +sh stop.sh +``` + +## 4.Springboot集成Canal + +### 4.1 Canal数据结构 + +![](http://img.topjavaer.cn/img/202306270000776.png) + +### 4.2 引入依赖 + +```xml + + + com.alibaba.otter + canal.client + 1.1.6 + + + + + + com.alibaba.otter + canal.protocol + 1.1.6 + + + + + co.elastic.clients + elasticsearch-java + 8.4.3 + + + + + jakarta.json + jakarta.json-api + 2.0.1 + +``` + +### 4.3 application.yaml + +```yaml +custom: + elasticsearch: + host: localhost #主机 + port: 9200 #端口 + username: elastic #用户名 + password: 3bf24a76 #密码 +``` + +### 4.4 EsClient + +```java +@Setter +@ConfigurationProperties(prefix = "custom.elasticsearch") +@Configuration +public class EsClient { + + /** + * 主机 + */ + private String host; + + /** + * 端口 + */ + private Integer port; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + + @Bean + public ElasticsearchClient elasticsearchClient() { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + // Create the low-level client + RestClient restClient = RestClient.builder(new HttpHost(host, port)) + .setHttpClientConfigCallback(httpAsyncClientBuilder -> + httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) + .build(); + // Create the transport with a Jackson mapper + RestClientTransport transport = new RestClientTransport( + restClient, new JacksonJsonpMapper()); + // Create the transport with a Jackson mapper + return new ElasticsearchClient(transport); + } +} +``` + +### 4.5 Music实体类 + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Music { + + /** + * id + */ + private String id; + + /** + * 歌名 + */ + private String name; + + /** + * 歌手名 + */ + private String singer; + + /** + * 封面图地址 + */ + private String imageUrl; + + /** + * 歌曲地址 + */ + private String musicUrl; + + /** + * 歌词地址 + */ + private String lrcUrl; + + /** + * 歌曲类型id + */ + private String typeId; + + /** + * 是否被逻辑删除,1 是,0 否 + */ + private Integer isDeleted; + + /** + * 创建时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + +} +``` + +### 4.6 CanalClient + +```java +@Slf4j +@Component +public class CanalClient { + + @Resource + private ElasticsearchClient client; + + + /** + * 实时数据同步程序 + * + * @throws InterruptedException + * @throws InvalidProtocolBufferException + */ + public void run() throws InterruptedException, IOException { + CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress( + "localhost", 11111), "example", "", ""); + + while (true) { + //连接 + connector.connect(); + //订阅数据库 + connector.subscribe("cloudmusic_music.music"); + //获取数据 + Message message = connector.get(100); + + List entryList = message.getEntries(); + if (CollectionUtils.isEmpty(entryList)) { + //没有数据,休息一会 + TimeUnit.SECONDS.sleep(2); + } else { + for (CanalEntry.Entry entry : entryList) { + //获取类型 + CanalEntry.EntryType entryType = entry.getEntryType(); + + //判断类型是否为ROWDATA + if (CanalEntry.EntryType.ROWDATA.equals(entryType)) { + //获取序列化后的数据 + ByteString storeValue = entry.getStoreValue(); + //反序列化数据 + CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue); + //获取当前事件操作类型 + CanalEntry.EventType eventType = rowChange.getEventType(); + //获取数据集 + List rowDataList = rowChange.getRowDatasList(); + + if (eventType == CanalEntry.EventType.INSERT) { + log.info("------新增操作------"); + + List musicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + musicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量新增文档 + index(musicList); + //打印新增集合 + log.info(Arrays.toString(musicList.toArray())); + } else if (eventType == CanalEntry.EventType.UPDATE) { + log.info("------更新操作------"); + + List beforeMusicList = new ArrayList<>(); + List afterMusicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + //更新前 + beforeMusicList.add(createMusic(rowData.getBeforeColumnsList())); + //更新后 + afterMusicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量更新文档 + index(afterMusicList); + //打印更新前集合 + log.info("更新前:{}", Arrays.toString(beforeMusicList.toArray())); + //打印更新后集合 + log.info("更新后:{}", Arrays.toString(afterMusicList.toArray())); + } else if (eventType == CanalEntry.EventType.DELETE) { + //删除操作 + log.info("------删除操作------"); + + List idList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + for (CanalEntry.Column column : rowData.getBeforeColumnsList()) { + if("id".equals(column.getName())) { + idList.add(column.getValue()); + break; + } + } + } + //es批量删除文档 + delete(idList); + //打印删除id集合 + log.info(Arrays.toString(idList.toArray())); + } + } + } + } + } + } + + /** + * 根据canal获取的数据创建Music对象 + * + * @param columnList + * @return + */ + private Music createMusic(List columnList) { + Music music = new Music(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + for (CanalEntry.Column column : columnList) { + switch (column.getName()) { + case "id" -> music.setId(column.getValue()); + case "name" -> music.setName(column.getValue()); + case "singer" -> music.setSinger(column.getValue()); + case "image_url" -> music.setImageUrl(column.getValue()); + case "music_url" -> music.setMusicUrl(column.getValue()); + case "lrc_url" -> music.setLrcUrl(column.getValue()); + case "type_id" -> music.setTypeId(column.getValue()); + case "is_deleted" -> music.setIsDeleted(Integer.valueOf(column.getValue())); + case "create_time" -> + music.setCreateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + case "update_time" -> + music.setUpdateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + default -> { + } + } + } + + return music; + } + + /** + * es批量新增、更新文档(不存在:新增, 存在:更新) + * + * @param musicList 音乐集合 + * @throws IOException + */ + private void index(List musicList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + musicList.forEach(music -> br + .operations(op -> op + .index(idx -> idx + .index("music") + .id(music.getId()) + .document(music)))); + + client.bulk(br.build()); + } + + /** + * es批量删除文档 + * + * @param idList 音乐id集合 + * @throws IOException + */ + private void delete(List idList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + idList.forEach(id -> br + .operations(op -> op + .delete(idx -> idx + .index("music") + .id(id)))); + + client.bulk(br.build()); + } + +} +``` + +### 4.7 ApplicationContextAware + +```java +@Component +public class ApplicationContextUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextUtil.applicationContext = applicationContext; + } + + public static T getBean (Class classType) { + return applicationContext.getBean(classType); + } + +} +``` + +### 4.8 main + +```java +@Slf4j +@SpringBootApplication +public class CanalApplication { + public static void main(String[] args) throws InterruptedException, IOException { + SpringApplication.run(CanalApplication.class, args); + log.info("数据同步程序启动"); + + CanalClient client = ApplicationContextUtil.getBean(CanalClient.class); + client.run(); + } +} +``` + +## 5.总结 + +那么以上就是Canal组件的介绍啦,希望大家都能有所收获~ + diff --git a/docs/advance/excellent-article/4-remove-duplicate-code.md b/docs/advance/excellent-article/4-remove-duplicate-code.md new file mode 100644 index 0000000..0532fdc --- /dev/null +++ b/docs/advance/excellent-article/4-remove-duplicate-code.md @@ -0,0 +1,610 @@ +--- +sidebar: heading +title: 干掉 “重复代码” 的技巧有哪些 +category: 优质文章 +tag: + - 开发技巧 +head: + - - meta + - name: keywords + content: 重复代码,开发技巧 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 干掉 “重复代码” 的技巧有哪些 + +软件工程师和码农最大的区别就是平时写代码时习惯问题,码农很喜欢写重复代码而软件工程师会利用各种技巧去干掉重复的冗余代码。 + +业务同学抱怨业务开发没有技术含量,用不到**设计模式**、**Java 高级特性**、**OOP**,平时写代码都在堆 **CRUD**,个人成长无从谈起。 + +其实,我认为不是这样的。设计模式、OOP 是前辈们在大型项目中积累下来的经验,通过这些方法论来改善大型项目的可维护性。反射、注解、泛型等高级特性在框架中大量使用的原因是,框架往往需要以同一套算法来应对不同的数据结构,而这些特性可以帮助减少重复代码,提升项目可维护性。 + +在我看来,可维护性是大型项目成熟度的一个重要指标,而提升可维护性非常重要的一个手段就是减少代码重复。那为什么这样说呢? + +- 如果多处重复代码实现完全相同的功能,很容易修改一处忘记修改另一处,造成 Bug +- 有一些代码并不是完全重复,而是相似度很高,修改这些类似的代码容易改(复制粘贴)错,把原本有区别的地方改为了一样。 + +今天,我就从业务代码中最常见的三个需求展开,聊聊如何使用 Java 中的一些高级特性、设计模式,以及一些工具消除重复代码,才能既优雅又高端。通过今天的学习,也希望改变你对业务代码没有技术含量的看法。 + +## 1. 利用工厂模式 + 模板方法模式,消除 if…else 和重复代码 + +假设要开发一个购物车下单的功能,针对不同用户进行不同处理: + +- 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣; +- VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣; +- 内部用户可以免运费,无商品折扣。 + +我们的目标是实现三种类型的购物车业务逻辑,把入参 Map 对象(Key 是商品 ID,Value 是商品数量),转换为出参购物车类型 Cart。 + +先实现针对普通用户的购物车处理逻辑: + +``` +//购物车 +@Data +public class Cart { + //商品清单 + private List items = new ArrayList<>(); + //总优惠 + private BigDecimal totalDiscount; + //商品总价 + private BigDecimal totalItemPrice; + //总运费 + private BigDecimal totalDeliveryPrice; + //应付总价 + private BigDecimal payPrice; +} +//购物车中的商品 +@Data +public class Item { + //商品ID + private long id; + //商品数量 + private int quantity; + //商品单价 + private BigDecimal price; + //商品优惠 + private BigDecimal couponPrice; + //商品运费 + private BigDecimal deliveryPrice; +} +//普通用户购物车处理 +public class NormalUserCart { + public Cart process(long userId, Map items) { + Cart cart = new Cart(); + + //把Map的购物车转换为Item列表 + List itemList = new ArrayList<>(); + items.entrySet().stream().forEach(entry -> { + Item item = new Item(); + item.setId(entry.getKey()); + item.setPrice(Db.getItemPrice(entry.getKey())); + item.setQuantity(entry.getValue()); + itemList.add(item); + }); + cart.setItems(itemList); + + //处理运费和商品优惠 + itemList.stream().forEach(item -> { + //运费为商品总价的10% + item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1"))); + //无优惠 + item.setCouponPrice(BigDecimal.ZERO); + }); + + //计算商品总价 + cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add)); + //计算运费总价 + cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); + //计算总优惠 + cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); + //应付总价=商品总价+运费总价-总优惠 + cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); + return cart; + } +} +``` + +然后实现针对 VIP 用户的购物车逻辑。与普通用户购物车逻辑的不同在于,VIP 用户能享受同类商品多买的折扣。所以,这部分代码只需要额外处理多买折扣部分: + +``` +public class VipUserCart { + + + public Cart process(long userId, Map items) { + ... + + + itemList.stream().forEach(item -> { + //运费为商品总价的10% + item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1"))); + //购买两件以上相同商品,第三件开始享受一定折扣 + if (item.getQuantity() > 2) { + item.setCouponPrice(item.getPrice() + .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))) + .multiply(BigDecimal.valueOf(item.getQuantity() - 2))); + } else { + item.setCouponPrice(BigDecimal.ZERO); + } + }); + + + ... + return cart; + } +} +``` + +最后是免运费、无折扣的内部用户,同样只是处理商品折扣和运费时的逻辑差异: + +``` +public class InternalUserCart { + + + public Cart process(long userId, Map items) { + ... + + itemList.stream().forEach(item -> { + //免运费 + item.setDeliveryPrice(BigDecimal.ZERO); + //无优惠 + item.setCouponPrice(BigDecimal.ZERO); + }); + + ... + return cart; + } +} +``` + +对比一下代码量可以发现,三种购物车 **70%** 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑都是一样的。 + +正如我们开始时提到的,代码重复本身不可怕,可怕的是漏改或改错。比如,写 VIP 用户购物车的同学发现商品总价计算有 Bug,不应该是把所有 Item 的 price 加在一起,而是应该把所有 Item 的 **price\*quantity** 加在一起。 + +这时,他可能会只修改 VIP 用户购物车的代码,而忽略了普通用户、内部用户的购物车中,重复的逻辑实现也有相同的 Bug。 + +有了三个购物车后,我们就需要根据不同的用户类型使用不同的购物车了。如下代码所示,使用三个 if 实现不同类型用户调用不同购物车的 process 方法: + +``` +@GetMapping("wrong") +public Cart wrong(@RequestParam("userId") int userId) { + //根据用户ID获得用户类型 + String userCategory = Db.getUserCategory(userId); + //普通用户处理逻辑 + if (userCategory.equals("Normal")) { + NormalUserCart normalUserCart = new NormalUserCart(); + return normalUserCart.process(userId, items); + } + //VIP用户处理逻辑 + if (userCategory.equals("Vip")) { + VipUserCart vipUserCart = new VipUserCart(); + return vipUserCart.process(userId, items); + } + //内部用户处理逻辑 + if (userCategory.equals("Internal")) { + InternalUserCart internalUserCart = new InternalUserCart(); + return internalUserCart.process(userId, items); + } + + return null; +} +``` + +电商的营销玩法是多样的,以后势必还会有更多用户类型,需要更多的购物车。我们就只能不断增加更多的购物车类,一遍一遍地写重复的购物车逻辑、写更多的 if 逻辑吗? + +当然不是,相同的代码应该只在一处出现! + +如果我们熟记抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢? + +其实,这个模式就是**模板方法模式**。我们在父类中实现了购物车处理的流程模板,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。由于父类的逻辑不完整无法单独工作,因此需要定义为抽象类。 + +如下代码所示,AbstractCart 抽象类实现了购物车通用的逻辑,额外定义了两个抽象方法让子类去实现。其中,processCouponPrice 方法用于计算商品折扣,processDeliveryPrice 方法用于计算运费。 + +``` +public abstract class AbstractCart { + //处理购物车的大量重复逻辑在父类实现 + public Cart process(long userId, Map items) { + + Cart cart = new Cart(); + + List itemList = new ArrayList<>(); + items.entrySet().stream().forEach(entry -> { + Item item = new Item(); + item.setId(entry.getKey()); + item.setPrice(Db.getItemPrice(entry.getKey())); + item.setQuantity(entry.getValue()); + itemList.add(item); + }); + cart.setItems(itemList); + //让子类处理每一个商品的优惠 + itemList.stream().forEach(item -> { + processCouponPrice(userId, item); + processDeliveryPrice(userId, item); + }); + //计算商品总价 + cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add)); + //计算总运费 + cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); + //计算总折扣 + cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add)); + //计算应付价格 + cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount())); + return cart; + } + + //处理商品优惠的逻辑留给子类实现 + protected abstract void processCouponPrice(long userId, Item item); + //处理配送费的逻辑留给子类实现 + protected abstract void processDeliveryPrice(long userId, Item item); +} +``` + +有了这个抽象类,三个子类的实现就非常简单了。普通用户的购物车 **NormalUserCart**,实现的是 0 优惠和 10% 运费的逻辑: + +``` +@Service(value = "NormalUserCart") +public class NormalUserCart extends AbstractCart { + + @Override + protected void processCouponPrice(long userId, Item item) { + item.setCouponPrice(BigDecimal.ZERO); + } + + @Override + protected void processDeliveryPrice(long userId, Item item) { + item.setDeliveryPrice(item.getPrice() + .multiply(BigDecimal.valueOf(item.getQuantity())) + .multiply(new BigDecimal("0.1"))); + } +} +``` + +VIP 用户的购物车 VipUserCart,直接继承了 NormalUserCart,只需要修改多买优惠策略: + +``` +@Service(value = "VipUserCart") +public class VipUserCart extends NormalUserCart { + + @Override + protected void processCouponPrice(long userId, Item item) { + if (item.getQuantity() > 2) { + item.setCouponPrice(item.getPrice() + .multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))) + .multiply(BigDecimal.valueOf(item.getQuantity() - 2))); + } else { + item.setCouponPrice(BigDecimal.ZERO); + } + } +} +``` + +内部用户购物车 InternalUserCart 是最简单的,直接设置 0 运费和 0 折扣即可: + +``` +@Service(value = "InternalUserCart") +public class InternalUserCart extends AbstractCart { + @Override + protected void processCouponPrice(long userId, Item item) { + item.setCouponPrice(BigDecimal.ZERO); + } + + @Override + protected void processDeliveryPrice(long userId, Item item) { + item.setDeliveryPrice(BigDecimal.ZERO); + } +} +``` + +抽象类和三个子类的实现关系图,如下所示: + +![](http://img.topjavaer.cn/img/重复代码1.png) + +是不是比三个独立的购物车程序简单了很多呢?接下来,我们再看看如何能避免三个 if 逻辑。 + +或许你已经注意到了,定义三个购物车子类时,我们在 **@Service** 注解中对 Bean 进行了命名。既然三个购物车都叫 XXXUserCart,那我们就可以把用户类型字符串拼接 UserCart 构成购物车 Bean 的名称,然后利用 Spring 的 IoC 容器,通过 Bean 的名称直接获取到 **AbstractCart**,调用其 process 方法即可实现通用。 + +其实,这就是工厂模式,只不过是借助 Spring 容器实现罢了: + +``` +@GetMapping("right") +public Cart right(@RequestParam("userId") int userId) { + String userCategory = Db.getUserCategory(userId); + AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart"); + return cart.process(userId, items); +} +``` + +试想, 之后如果有了新的用户类型、新的用户逻辑,是不是完全不用对代码做任何修改,只要新增一个 XXXUserCart 类继承 AbstractCart,实现特殊的优惠和运费处理逻辑就可以了? + +**这样一来,我们就利用工厂模式 + 模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险**。这就是设计模式中的**开闭原则**:对修改关闭,对扩展开放。 + +## 2. 利用注解 + 反射消除重复代码 + +是不是有点兴奋了,业务代码居然也能 OOP 了。我们再看一个三方接口的调用案例,同样也是一个普通的业务逻辑。 + +假设银行提供了一些 API 接口,对参数的序列化有点特殊,不使用 JSON,而是需要我们把参数依次拼在一起构成一个大字符串。 + +- 按照银行提供的 API 文档的顺序,把所有参数构成定长的数据,然后拼接在一起作为整个字符串。 + +- 因为每一种参数都有固定长度,未达到长度时需要做填充处理: + +- - 字符串类型的参数不满长度部分需要以下划线右填充,也就是字符串内容靠左; + - 数字类型的参数不满长度部分以 0 左填充,也就是实际数字靠右; + - 货币类型的表示需要把金额向下舍入 2 位到分,以分为单位,作为数字类型同样进行左填充。 + +- 对所有参数做 MD5 操作作为签名(为了方便理解,Demo 中不涉及加盐处理)。 + +比如,创建用户方法和支付方法的定义是这样的: + +![](http://img.topjavaer.cn/img/重复代码2.png) + +代码很容易实现,直接根据接口定义实现填充操作、加签名、请求调用操作即可: + +``` +public class BankService { + + //创建用户方法 + public static String createUser(String name, String identity, String mobile, int age) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + //字符串靠左,多余的地方填充_ + stringBuilder.append(String.format("%-10s", name).replace(' ', '_')); + //字符串靠左,多余的地方填充_ + stringBuilder.append(String.format("%-18s", identity).replace(' ', '_')); + //数字靠右,多余的地方用0填充 + stringBuilder.append(String.format("%05d", age)); + //字符串靠左,多余的地方用_填充 + stringBuilder.append(String.format("%-11s", mobile).replace(' ', '_')); + //最后加上MD5作为签名 + stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); + return Request.Post("http://localhost:45678/reflection/bank/createUser") + .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON) + .execute().returnContent().asString(); + } + + //支付方法 + public static String pay(long userId, BigDecimal amount) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + //数字靠右,多余的地方用0填充 + stringBuilder.append(String.format("%020d", userId)); + //金额向下舍入2位到分,以分为单位,作为数字靠右,多余的地方用0填充 + stringBuilder.append(String.format("%010d", amount.setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue())); + //最后加上MD5作为签名 + stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); + return Request.Post("http://localhost:45678/reflection/bank/pay") + .bodyString(stringBuilder.toString(), ContentType.APPLICATION_JSON) + .execute().returnContent().asString(); + } +} +``` + +可以看到,这段代码的重复粒度更细: + +- 三种标准数据类型的处理逻辑有重复,稍有不慎就会出现 Bug; +- 处理流程中字符串拼接、加签和发请求的逻辑,在所有方法重复; +- 实际方法的入参的参数类型和顺序,不一定和接口要求一致,容易出错; +- 代码层面针对每一个参数硬编码,无法清晰地进行核对,如果参数达到几十个、上百个,出错的概率极大。 + +那应该如何改造这段代码呢?没错,就是要用注解和反射! + +使用注解和反射这两个武器,就可以针对银行请求的所有逻辑均使用一套代码实现,不会出现任何重复。 + +要实现接口逻辑和逻辑实现的剥离,首先需要以 POJO 类(只有属性没有任何业务逻辑的数据类)的方式定义所有的接口参数。比如,下面这个创建用户 API 的参数: + +``` +@Data +public class CreateUserAPI { + private String name; + private String identity; + private String mobile; + private int age; +} +``` + +有了接口参数定义,我们就能通过自定义注解为接口和所有参数增加一些元数据。如下所示,我们定义一个接口 API 的注解 BankAPI,包含接口 URL 地址和接口说明: + +``` +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Documented +@Inherited +public @interface BankAPI { + String desc() default ""; + String url() default ""; +} +``` + +然后,我们再定义一个自定义注解 @BankAPIField,用于描述接口的每一个字段规范,包含参数的次序、类型和长度三个属性: + +``` +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +@Inherited +public @interface BankAPIField { + int order() default -1; + int length() default -1; + String type() default ""; +} +``` + +接下来,注解就可以发挥威力了。 + +如下所示,我们定义了 CreateUserAPI 类描述创建用户接口的信息,通过为接口增加 @BankAPI 注解,来补充接口的 URL 和描述等元数据;通过为每一个字段增加 @BankAPIField 注解,来补充参数的顺序、类型和长度等元数据: + +``` +@BankAPI(url = "/bank/createUser", desc = "创建用户接口") +@Data +public class CreateUserAPI extends AbstractAPI { + @BankAPIField(order = 1, type = "S", length = 10) + private String name; + @BankAPIField(order = 2, type = "S", length = 18) + private String identity; + @BankAPIField(order = 4, type = "S", length = 11) //注意这里的order需要按照API表格中的顺序 + private String mobile; + @BankAPIField(order = 3, type = "N", length = 5) + private int age; +} +``` + +另一个 PayAPI 类也是类似的实现: + +``` +@BankAPI(url = "/bank/pay", desc = "支付接口") +@Data +public class PayAPI extends AbstractAPI { + @BankAPIField(order = 1, type = "N", length = 20) + private long userId; + @BankAPIField(order = 2, type = "M", length = 10) + private BigDecimal amount; +} +``` + +这 2 个类继承的 AbstractAPI 类是一个空实现,因为这个案例中的接口并没有公共数据可以抽象放到基类。 + +通过这 2 个类,我们可以在几秒钟内完成和 API 清单表格的核对。理论上,如果我们的核心翻译过程(也就是把注解和接口 API 序列化为请求需要的字符串的过程)没问题,只要注解和表格一致,API 请求的翻译就不会有任何问题。 + +以上,我们通过注解实现了对 API 参数的描述。接下来,我们再看看反射如何配合注解实现动态的接口参数组装: + +- 第 3 行代码中,我们从类上获得了 BankAPI 注解,然后拿到其 URL 属性,后续进行远程调用。 +- 第 6~9 行代码,使用 stream 快速实现了获取类中所有带 BankAPIField 注解的字段,并把字段按 order 属性排序,然后设置私有字段反射可访问。 +- 第 12~38 行代码,实现了反射获取注解的值,然后根据 BankAPIField 拿到的参数类型,按照三种标准进行格式化,将所有参数的格式化逻辑集中在了这一处。 +- 第 41~48 行代码,实现了参数加签和请求调用。 + +``` +private static String remoteCall(AbstractAPI api) throws IOException { + //从BankAPI注解获取请求地址 + BankAPI bankAPI = api.getClass().getAnnotation(BankAPI.class); + bankAPI.url(); + StringBuilder stringBuilder = new StringBuilder(); + Arrays.stream(api.getClass().getDeclaredFields()) //获得所有字段 + .filter(field -> field.isAnnotationPresent(BankAPIField.class)) //查找标记了注解的字段 + .sorted(Comparator.comparingInt(a -> a.getAnnotation(BankAPIField.class).order())) //根据注解中的order对字段排序 + .peek(field -> field.setAccessible(true)) //设置可以访问私有字段 + .forEach(field -> { + //获得注解 + BankAPIField bankAPIField = field.getAnnotation(BankAPIField.class); + Object value = ""; + try { + //反射获取字段值 + value = field.get(api); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + //根据字段类型以正确的填充方式格式化字符串 + switch (bankAPIField.type()) { + case "S": { + stringBuilder.append(String.format("%-" + bankAPIField.length() + "s", value.toString()).replace(' ', '_')); + break; + } + case "N": { + stringBuilder.append(String.format("%" + bankAPIField.length() + "s", value.toString()).replace(' ', '0')); + break; + } + case "M": { + if (!(value instanceof BigDecimal)) + throw new RuntimeException(String.format("{} 的 {} 必须是BigDecimal", api, field)); + stringBuilder.append(String.format("%0" + bankAPIField.length() + "d", ((BigDecimal) value).setScale(2, RoundingMode.DOWN).multiply(new BigDecimal("100")).longValue())); + break; + } + default: + break; + } + }); + //签名逻辑 + stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString())); + String param = stringBuilder.toString(); + long begin = System.currentTimeMillis(); + //发请求 + String result = Request.Post("http://localhost:45678/reflection" + bankAPI.url()) + .bodyString(param, ContentType.APPLICATION_JSON) + .execute().returnContent().asString(); + log.info("调用银行API {} url:{} 参数:{} 耗时:{}ms", bankAPI.desc(), bankAPI.url(), param, System.currentTimeMillis() - begin); + return result; +} +``` + +可以看到,所有处理参数排序、填充、加签、请求调用的核心逻辑,都汇聚在了 remoteCall 方法中。有了这个核心方法,BankService 中每一个接口的实现就非常简单了,只是参数的组装,然后调用 remoteCall 即可。 + +``` +//创建用户方法 +public static String createUser(String name, String identity, String mobile, int age) throws IOException { + CreateUserAPI createUserAPI = new CreateUserAPI(); + createUserAPI.setName(name); + createUserAPI.setIdentity(identity); + createUserAPI.setAge(age); + createUserAPI.setMobile(mobile); + return remoteCall(createUserAPI); +} +//支付方法 +public static String pay(long userId, BigDecimal amount) throws IOException { + PayAPI payAPI = new PayAPI(); + payAPI.setUserId(userId); + payAPI.setAmount(amount); + return remoteCall(payAPI); +} +``` + +其实,**许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码**。 + +反射给予了我们在不知晓类结构的时候,按照固定的逻辑处理类的成员;而注解给了我们为这些成员补充元数据的能力,使得我们利用反射实现通用逻辑的时候,可以从外部获得更多我们关心的数据。 + +## 3. 利用属性拷贝工具消除重复代码 + +最后,我们再来看一种业务代码中经常出现的代码逻辑,实体之间的转换复制。 + +对于三层架构的系统,考虑到层之间的解耦隔离以及每一层对数据的不同需求,通常每一层都会有自己的 POJO 作为数据实体。比如,数据访问层的实体一般叫作 DataObject 或 DO,业务逻辑层的实体一般叫作 Domain,表现层的实体一般叫作 Data Transfer Object 或 DTO。 + +这里我们需要注意的是,如果手动写这些实体之间的赋值代码,同样容易出错。 + +对于复杂的业务系统,实体有几十甚至几百个属性也很正常。就比如 ComplicatedOrderDTO 这个数据传输对象,描述的是一个订单中的几十个属性。如果我们要把这个 DTO 转换为一个类似的 DO,复制其中大部分的字段,然后把数据入库,势必需要进行很多属性映射赋值操作。就像这样,密密麻麻的代码是不是已经让你头晕了? + +``` +ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO(); +ComplicatedOrderDO orderDO = new ComplicatedOrderDO(); +orderDO.setAcceptDate(orderDTO.getAcceptDate()); +orderDO.setAddress(orderDTO.getAddress()); +orderDO.setAddressId(orderDTO.getAddressId()); +orderDO.setCancelable(orderDTO.isCancelable()); +orderDO.setCommentable(orderDTO.isComplainable()); //属性错误 +orderDO.setComplainable(orderDTO.isCommentable()); //属性错误 +orderDO.setCancelable(orderDTO.isCancelable()); +orderDO.setCouponAmount(orderDTO.getCouponAmount()); +orderDO.setCouponId(orderDTO.getCouponId()); +orderDO.setCreateDate(orderDTO.getCreateDate()); +orderDO.setDirectCancelable(orderDTO.isDirectCancelable()); +orderDO.setDeliverDate(orderDTO.getDeliverDate()); +orderDO.setDeliverGroup(orderDTO.getDeliverGroup()); +orderDO.setDeliverGroupOrderStatus(orderDTO.getDeliverGroupOrderStatus()); +orderDO.setDeliverMethod(orderDTO.getDeliverMethod()); +orderDO.setDeliverPrice(orderDTO.getDeliverPrice()); +orderDO.setDeliveryManId(orderDTO.getDeliveryManId()); +orderDO.setDeliveryManMobile(orderDO.getDeliveryManMobile()); //对象错误 +``` + +**如果不是代码中有注释,你能看出其中的诸多问题吗?** + +如果原始的 DTO 有 100 个字段,我们需要复制 90 个字段到 DO 中,保留 10 个不赋值,最后应该如何校验正确性呢?数数吗?即使数出有 90 行代码,也不一定正确,因为属性可能重复赋值。 + +有的时候字段命名相近,比如 complainable 和 commentable,容易搞反(第 7 和第 8 行),或者对两个目标字段重复赋值相同的来源字段(比如第 28 行) + +明明要把 DTO 的值赋值到 DO 中,却在 set 的时候从 DO 自己取值(比如第 20 行),导致赋值无效。 + +这段代码并不是我随手写出来的,而是一个真实案例。有位同学就像代码中那样把经纬度赋值反了,因为落库的字段实在太多了。这个 Bug 很久都没发现,直到真正用到数据库中的经纬度做计算时,才发现一直以来都存错了。 + +修改方法很简单,可以使用类似 BeanUtils 这种 Mapping 工具来做 Bean 的转换,copyProperties 方法还允许我们提供需要忽略的属性: + +``` +ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO(); +ComplicatedOrderDO orderDO = new ComplicatedOrderDO(); +BeanUtils.copyProperties(orderDTO, orderDO, "id"); +return orderDO; +``` + +## 总结 + +第一种代码重复是,有多个并行的类实现相似的代码逻辑。我们可以考虑提取相同逻辑在父类中实现,差异逻辑通过抽象方法留给子类实现。使用类似的模板方法把相同的流程和逻辑固定成模板,保留差异的同时尽可能避免代码重复。同时,可以使用 Spring 的 IoC 特性注入相应的子类,来避免实例化子类时的大量 if…else 代码。 + +第二种代码重复是,使用硬编码的方式重复实现相同的数据处理算法。我们可以考虑把规则转换为自定义注解,作为元数据对类或对字段、方法进行描述,然后通过反射动态读取这些元数据、字段或调用方法,实现规则参数和规则定义的分离。也就是说,把变化的部分也就是规则的参数放入注解,规则的定义统一处理。 + +第三种代码重复是,业务代码中常见的 DO、DTO、VO 转换时大量字段的手动赋值,遇到有上百个属性的复杂类型,非常非常容易出错。我的建议是,不要手动进行赋值,考虑使用 Bean 映射工具进行。此外,还可以考虑采用单元测试对所有字段进行赋值正确性校验。 diff --git a/docs/advance/excellent-article/5-jvm-optimize.md b/docs/advance/excellent-article/5-jvm-optimize.md new file mode 100644 index 0000000..dc7f399 --- /dev/null +++ b/docs/advance/excellent-article/5-jvm-optimize.md @@ -0,0 +1,109 @@ +--- +sidebar: heading +title: 一次简单的 JVM 调优,拿去写到简历里 +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: JVM调优 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 一次简单的 JVM 调优,拿去写到简历里 + +大家好,我是大彬。 + +JVM调优一直是面试官很喜欢问的问题。周末在网上看到一篇JVM调优的文章,给大家分享一下。 + +> 来源地址:https://zhenbianshu.github.io + +## **背景** + +最近对负责的项目进行了一次性能优化,其中包括对 JVM 参数的调整,算是进行了一次简单的 JVM 调优,JVM 参数调整之后,服务的整体性能有 5% 左右的提升,还算不错。 + +先介绍一下项目的基本情况: + +项目是一个高 QPS 压力的 web 服务,单机 QPS 一直维持在 1.5K 以上,由于旧机器的”拖累”,配置的堆大小是 8G,其中 young 区是 4G,垃圾回收器用的是 parNew + CMS。 + +## **旧状** + +首先是查看当前 GC 的情况,主要是使用 `jstat` 查看 GC 的概况,再查看 gc log,分析单次 gc 的详细状况。 + +使用 `jstat -gcutil pid 1000` 每隔一秒打印一次 gc 统计信息。 + +![](http://img.topjavaer.cn/img/jvm调优1.png) + +可以看到,单次 gc 平均耗时是 60ms 左右,还算可以接受,但 YGC 非常频繁,基本上每秒一次,有的时候还会一秒两次,在一秒两次的时候,服务对业务响应时长的压力就会变得很大。 + +接着查看 gc log,打印 gc log 需要在 JVM 启动参数里添加以下参数: + +- `-XX:+PrintGCDateStamps`:打印 gc 发生的时间戳。 +- `-XX:+PrintTenuringDistribution`:打印 gc 发生时的分代信息。 +- `-XX:+PrintGCApplicationStoppedTime`:打印 gc 停顿时长 +- `-XX:+PrintGCApplicationConcurrentTime`:打印 gc 间隔的服务运行时长 +- `-XX:+PrintGCDetails`:打印 gc 详情,包括 gc 前/内存等。 +- `-Xloggc:../gclogs/gc.log.date`:指定 gc log 的路径 + +看到的 gc log 形如: + +![](http://img.topjavaer.cn/img/jvm调优2.png) + +单次 GC 方面并不能直接看出问题,但可以看到 gc 前有很多次 18ms 左右的停顿。 + +## **分析和调整** + +### YGC 频繁 + +直接查看 gc log 并不直观,我们可以借用一些可视化工具来帮助我们分析, `[gceasy](https://gceasy.io/)` 是个挺不错的网站,我们把 gc log 上传上去后, gceasy 可以帮助我们生成各个维度的图表帮助分析。 + +查看 gceasy 生成的报告,发现我们服务的 gc 吞吐量是 95%,它指的是 JVM 运行业务代码的时长占 JVM 总运行时长的比例,这个比例确实有些低了,运行 100 分钟就有 5 分钟在执行 gc。幸好这些 GC 中绝大多数都是 YGC,单次时长可控且分布平均,这使得我们服务还能平稳运行。 + +解决这个问题要么是减少对象的创建,要么就增大 young 区。前者不是一时半会儿都解决的,需要查找代码里可能有问题的点,分步优化。 + +而后者虽然改一下配置就行,但以我们对 GC 最直观的印象来说,增大 young 区,YGC 的时长也会迅速增大。 + +其实这点不必太过担心,我们知道 YGC 的耗时是由 `GC 标记 + GC 复制` 组成的,相对于 GC 复制,GC 标记是非常快的。而 young 区内大多数对象的生命周期都非常短,如果将 young 区增大一倍,GC 标记的时长会提升一倍,但到 GC 发生时被标记的对象大部分已经死亡, GC 复制的时长肯定不会提升一倍,所以我们可以放心增大 young 区大小。 + +由于低内存旧机器都被换掉了,我把堆大小调整到了 12G,young 区保留为 8G。 + +### 分代调整 + +除了 GC 太频繁之外,GC 后各分代的平均大小也需要调整。 + +![](http://img.topjavaer.cn/img/jvm调优3.png) + +我们知道 GC 的提升机制,每次 GC 后,JVM 存活代数大于 `MaxTenuringThreshold` 的对象提升到老年代。当然,JVM 还有动态年龄计算的规则:按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值,但看各代总的内存大小,是达不到 survivor 区的一半的。 + +![](http://img.topjavaer.cn/img/jvm调优4.png) + +所以这十五个分代内的对象会一直在两个 survivor 区之间来回复制,再观察各分代的平均大小,可以看到,四代以上的对象已经有一半都会保留到老年区了,所以可以将这些对象直接提升到老年代,以减少对象在两个 survivor 区之间复制的性能开销。 + +所以我把 MaxTenuringThreshold 的值调整为 4,将存活超过四代的对象直接提升到老年代。 + +### 偏向锁停顿 + +还有一个问题是 gc log 里有很多 18ms 左右的停顿,有时候连续有十多条,虽然每次停顿时长不长,但连续多次累积的时间也非常可观。 + +是因为 1.8 之后 JVM 对锁进行了优化,添加了偏向锁的概念,避免了很多不必要的加锁操作,但偏向锁一旦遇到锁竞争,取消锁需要进入 `safe point`,导致 STW。 + +解决方式很简单,JVM 启动参数里添加 `-XX:-UseBiasedLocking` 即可。 + +## **结果** + +调整完 JVM 参数后先是对服务进行压测,发现性能确实有提升,也没有发生严重的 GC 问题,之后再把调整好的配置放到线上机器进行灰度,同时收集 gc log,再次进行分析。 + +由于 young 区大小翻倍了,所以 YGC 的频率减半了,GC 的吞量提升到了 97.75%。平均 GC 时长略有上升,从 60ms 左右提升到了 66ms,还是挺符合预期的。 + +由于 CMS 在进行 GC 时也会清理 young 区,CMS 的时长也受到了影响,CMS 的最终标记和并发清理阶段耗时增加了,也比较正常。 + +另外我还统计了对业务的影响,之前因为 GC 导致超时的请求大大减少了。 + +## **小结** + +总之,这是一次挺成功的 GC 调整,让我对 GC 有了更深的理解,但由于没有深入到 old 区,之前学习到的 CMS 相关的知识还没有复习到。 + +不过性能优化并不是一朝一夕的事,需要时刻关注问题,及时做出调整。 diff --git a/docs/advance/excellent-article/6-spring-three-cache.md b/docs/advance/excellent-article/6-spring-three-cache.md new file mode 100644 index 0000000..1b3ff9f --- /dev/null +++ b/docs/advance/excellent-article/6-spring-three-cache.md @@ -0,0 +1,134 @@ +--- +sidebar: heading +title: Spring 为何需要三级缓存解决循环依赖,而不是二级缓存? +category: 优质文章 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring,循环依赖问题,三级缓存 + - - meta + - name: description + content: 优质文章汇总 +--- + +# Spring 为何需要三级缓存解决循环依赖,而不是二级缓存? + +## **前言** + +在使用spring框架的日常开发中,bean之间的循环依赖太频繁了,spring已经帮我们去解决循环依赖问题,对我们开发者来说是无感知的,下面具体分析一下spring是如何解决bean之间循环依赖,为什么要使用到三级缓存,而不是二级缓存? + +## **bean生命周期** + +首先大家需要了解一下bean在spring中的生命周期,bean在spring的加载流程,才能够更加清晰知道spring是如何解决循环依赖的。 + +![](http://img.topjavaer.cn/img/三级依赖1.png) + +我们在spring的BeanFactory工厂列举了很多接口,代表着bean的生命周期,我们主要记住的是我圈红线圈出来的接口, 再结合spring的源码来看这些接口主要是在哪里调用的 + +![](http://img.topjavaer.cn/img/三级依赖2.png) + +AbstractAutowireCapableBeanFactory类的doCreateBean方法是创建bean的开始,我们可以看到首先需要实例化这个bean,也就是在堆中开辟一块内存空间给这个对象,createBeanInstance方法里面逻辑大概就是采用反射生成实例对象,进行到这里表示对象还并未进行属性的填充,也就是@Autowired注解的属性还未得到注入 + +![](http://img.topjavaer.cn/img/三级依赖3.png) + +我们可以看到第二步就是填充bean的成员属性,populateBean方法里面的逻辑大致就是对使用到了注入属性的注解就会进行注入,如果在注入的过程发现注入的对象还没生成,则会跑去生产要注入的对象,第三步就是调用initializeBean方法初始化bean,也就是调用我们上述所提到的接口 + +![](http://img.topjavaer.cn/img/三级缓存4.png) + +可以看到initializeBean方法中,首先调用的是使用的Aware接口的方法,我们具体看一下invokeAwareMethods方法中会调用Aware接口的那些方法 + +![](http://img.topjavaer.cn/img/三级缓存5.png) + +我们可以知道如果我们实现了BeanNameAware,BeanClassLoaderAware,BeanFactoryAware三个Aware接口的话,会依次调用setBeanName(), setBeanClassLoader(), setBeanFactory()方法,再看applyBeanPostProcessorsBeforeInitialization源码 + +![](http://img.topjavaer.cn/img/三级缓存6.png) + +发现会如果有类实现了BeanPostProcessor接口,就会执行postProcessBeforeInitialization方法,这里需要注意的是:如果多个类实现BeanPostProcessor接口,那么多个实现类都会执行postProcessBeforeInitialization方法,可以看到是for循环依次执行的,还有一个注意的点就是如果加载A类到spring容器中,A类也重写了BeanPostProcessor接口的postProcessBeforeInitialization方法,这时要注意A类的postProcessBeforeInitialization方法并不会得到执行,因为A类还未加载完成,还未完全放到spring的singletonObjects一级缓存中。 + +再看一个注意的点 + +![](http://img.topjavaer.cn/img/三级缓存7.png) + +![](http://img.topjavaer.cn/img/三级缓存8.png) + +可以看到ApplicationContextAwareProcessor也实现了BeanPostProcessor接口,重写了postProcessBeforeInitialization方法,方法里面并调用了invokeAwareInterfaces方法,而invokeAwareInterfaces方法也写着如果实现了众多的Aware接口,则会依次执行相应的方法,值得注意的是ApplicationContextAware接口的setApplicationContext方法,再看一下invokeInitMethods源码 + +![](http://img.topjavaer.cn/img/三级缓存9.png) + +发现如果实现了InitializingBean接口,重写了afterPropertiesSet方法,则会调用afterPropertiesSet方法,最后还会调用是否指定了init-method,可以通过标签,或者@Bean注解的initMethod指定,最后再看一张applyBeanPostProcessorsAfterInitialization源码图 + +![](http://img.topjavaer.cn/img/三级缓存10.png) + +发现跟之前的postProcessBeforeInitialization方法类似,也是循环遍历实现了BeanPostProcessor的接口实现类,执行postProcessAfterInitialization方法。整个bean的生命执行流程就如上面截图所示,哪个接口的方法在哪里被调用,方法的执行流程。 + +最后,对bean的生命流程进行一个流程图的总结 + +![](http://img.topjavaer.cn/img/三级缓存11.png) + +## **三级缓存解决循环依赖** + +上一小节对bean的生命周期做了一个整体的流程分析,对spring如何去解决循环依赖的很有帮助。前面我们分析到填充属性时,如果发现属性还未在spring中生成,则会跑去生成属性对象实例。 + +![](http://img.topjavaer.cn/img/三级缓存12.png) + +我们可以看到填充属性的时候,spring会提前将已经实例化的bean通过ObjectFactory半成品暴露出去,为什么称为半成品是因为这时候的bean对象实例化,但是未进行属性填充,是一个不完整的bean实例对象 + +![](http://img.topjavaer.cn/img/三级缓存13.png) + +spring利用singletonObjects, earlySingletonObjects, singletonFactories三级缓存去解决的,所说的缓存其实也就是三个Map + +![](http://img.topjavaer.cn/img/三级缓存14.png) + +可以看到三级缓存各自保存的对象,这里重点关注二级缓存earlySingletonObjects和三级缓存singletonFactory,一级缓存可以进行忽略。前面我们讲过先实例化的bean会通过ObjectFactory半成品提前暴露在三级缓存中 + +![](http://img.topjavaer.cn/img/三级缓存15.png) + +singletonFactory是传入的一个匿名内部类,调用ObjectFactory.getObject()最终会调用getEarlyBeanReference方法。再来看看循环依赖中是怎么拿其它半成品的实例对象的。 + +我们假设现在有这样的场景AService依赖BService,BService依赖AService + +\1. AService首先实例化,实例化通过ObjectFactory半成品暴露在三级缓存中 + +\2. 填充属性BService,发现BService还未进行过加载,就会先去加载BService + +\3. 再加载BService的过程中,实例化,也通过ObjectFactory半成品暴露在三级缓存 + +\4. 填充属性AService的时候,这时候能够从三级缓存中拿到半成品的ObjectFactory + +![](http://img.topjavaer.cn/img/三级缓存16.png) + +拿到ObjectFactory对象后,调用ObjectFactory.getObject()方法最终会调用getEarlyBeanReference()方法,getEarlyBeanReference这个方法主要逻辑大概描述下如果bean被AOP切面代理则返回的是beanProxy对象,如果未被代理则返回的是原bean实例。 + +这时我们会发现能够拿到bean实例(属性未填充),然后从三级缓存移除,放到二级缓存earlySingletonObjects中,而此时B注入的是一个半成品的实例A对象,不过随着B初始化完成后,A会继续进行后续的初始化操作,最终B会注入的是一个完整的A实例,因为在内存中它们是同一个对象。 + +下面是重点,我们发现这个二级缓存好像显得有点多余,好像可以去掉,只需要一级和三级缓存也可以做到解决循环依赖的问题??? + +只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个bean被AOP进行了切面代理,那么只使用两个缓存是无法解决问题,下面来看一下bean被AOP进行了切面代理的场景 + +![](http://img.topjavaer.cn/img/三级缓存17.png) + +我们发现AService的testAopProxy被AOP代理了,看看传入的匿名内部类的getEarlyBeanReference返回的是什么对象。 + +![](http://img.topjavaer.cn/img/三级缓存18.png) + +发现singletonFactory.getObject()返回的是一个AService的代理对象,还是被CGLIB代理的。再看一张再执行一遍singletonFactory.getObject()返回的是否是同一个AService的代理对象 + +![](http://img.topjavaer.cn/img/三级缓存19.png) + +我们会发现再执行一遍singleFactory.getObject()方法又是一个新的代理对象,这就会有问题了,因为AService是单例的,每次执行singleFactory.getObject()方法又会产生新的代理对象。 + +假设这里只有一级和三级缓存的话,我每次从三级缓存中拿到singleFactory对象,执行getObject()方法又会产生新的代理对象,这是不行的,因为AService是单例的,所有这里我们要借助二级缓存来解决这个问题,将执行了singleFactory.getObject()产生的对象放到二级缓存中去,后面去二级缓存中拿,没必要再执行一遍singletonFactory.getObject()方法再产生一个新的代理对象,保证始终只有一个代理对象。还有一个注意的点 + +![](http://img.topjavaer.cn/img/三级缓存20.png) + +既然singleFactory.getObject()返回的是代理对象,那么注入的也应该是代理对象,我们可以看到注入的确实是经过CGLIB代理的AService对象。所以如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象 + +## **总结** + +前面先讲到bean的加载流程,了解了bean加载流程对spring如何解决循环依赖的问题很有帮助,后面再分析到spring为什么需要利用到三级缓存解决循环依赖问题,而不是二级缓存。网上可以试试AOP的情形,实践一下就能明白二级缓存为什么解决不了AOP代理的场景了 + +在工作中,一直认为编程代码不是最重要的,重要的是在工作中所养成的编程思维。 + +原文:cnblogs.com/semi-sub/p/13548479.html diff --git a/docs/advance/excellent-article/7-sql-optimize.md b/docs/advance/excellent-article/7-sql-optimize.md new file mode 100644 index 0000000..a77731f --- /dev/null +++ b/docs/advance/excellent-article/7-sql-optimize.md @@ -0,0 +1,436 @@ +--- +sidebar: heading +title: 这几个SQL语法的坑,你踩过吗 +category: 优质文章 +tag: + - 数据库 +head: + - - meta + - name: keywords + content: SQL语法 + - - meta + - name: description + content: 优质文章汇总 +--- + +# 这几个SQL语法的坑,你踩过吗 + +> 本文已经收录到Github仓库,该仓库包含**计算机基础、Java基础、多线程、JVM、常见框架、分布式、微服务、设计模式、架构**等核心知识点,欢迎star~ +> +> 地址:https://github.com/Tyson0314/Java-learning + +大家好,我是大彬~ + +今天给大家分享几个SQL常见的“坏毛病”及优化技巧。 + +**SQL语句的执行顺序:** + +![](http://img.topjavaer.cn/img/sql优化1.png) + +## 1、LIMIT 语句 + +分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。 + +``` +SELECT * +FROM operation +WHERE type = 'SQLStats' + AND name = 'SlowLog' +ORDER BY create_time +LIMIT 1000, 10; +``` + +好吧,可能90%以上的 DBA 解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢? + +要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。 + +在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL 重新设计如下: + +``` +SELECT * +FROM operation +WHERE type = 'SQLStats' +AND name = 'SlowLog' +AND create_time > '2017-03-16 14:00:00' +ORDER BY create_time limit 10; +``` + +在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。 + +## 2、隐式转换 + +SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句: + +``` +mysql> explain extended SELECT * + > FROM my_balance b + > WHERE b.bpn = 14000000123 + > AND b.isverified IS NULL ; +mysql> show warnings; +| Warning | 1739 | Cannot use ref access on index 'bpn' due to type or collation conversion on field 'bpn' +``` + +其中字段 bpn 的定义为 varchar(20),MySQL 的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。 + +上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。 + +## 3、关联更新、删除 + +虽然 MySQL5.6 引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成 JOIN。 + +比如下面 UPDATE 语句,MySQL 实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。 + +``` +UPDATE operation o +SET status = 'applying' +WHERE o.id IN (SELECT id + FROM (SELECT o.id, + o.status + FROM operation o + WHERE o.group = 123 + AND o.status NOT IN ( 'done' ) + ORDER BY o.parent, + o.id + LIMIT 1) t); +``` + +执行计划: + +``` ++----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+ +| 1 | PRIMARY | o | index | | PRIMARY | 8 | | 24 | Using where; Using temporary | +| 2 | DEPENDENT SUBQUERY | | | | | | | | Impossible WHERE noticed after reading const tables | +| 3 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort | ++----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+ +``` + +重写为 JOIN 之后,子查询的选择模式从 DEPENDENT SUBQUERY 变成 DERIVED,执行速度大大加快,从7秒降低到2毫秒。 + +``` +UPDATE operation o + JOIN (SELECT o.id, + o.status + FROM operation o + WHERE o.group = 123 + AND o.status NOT IN ( 'done' ) + ORDER BY o.parent, + o.id + LIMIT 1) t + ON o.id = t.id +SET status = 'applying' +``` + +执行计划简化为: + +``` ++----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ +| 1 | PRIMARY | | | | | | | | Impossible WHERE noticed after reading const tables | +| 2 | DERIVED | o | ref | idx_2,idx_5 | idx_5 | 8 | const | 1 | Using where; Using filesort | ++----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+ +``` + +## 4、混合排序 + +MySQL 不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。 + +``` +SELECT * +FROM my_order o + INNER JOIN my_appraise a ON a.orderid = o.id +ORDER BY a.is_reply ASC, + a.appraise_time DESC +LIMIT 0, 20 +``` + +执行计划显示为全表扫描: + +``` ++----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra ++----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+ +| 1 | SIMPLE | a | ALL | idx_orderid | NULL | NULL | NULL | 1967647 | Using filesort | +| 1 | SIMPLE | o | eq_ref | PRIMARY | PRIMARY | 122 | a.orderid | 1 | NULL | ++----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+ +``` + +由于 is_reply 只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。 + +``` +SELECT * +FROM ((SELECT * + FROM my_order o + INNER JOIN my_appraise a + ON a.orderid = o.id + AND is_reply = 0 + ORDER BY appraise_time DESC + LIMIT 0, 20) + UNION ALL + (SELECT * + FROM my_order o + INNER JOIN my_appraise a + ON a.orderid = o.id + AND is_reply = 1 + ORDER BY appraise_time DESC + LIMIT 0, 20)) t +ORDER BY is_reply ASC, + appraisetime DESC +LIMIT 20; +``` + +## 5、EXISTS语句 + +MySQL 对待 EXISTS 子句时,仍然采用嵌套子查询的执行方式。如下面的 SQL 语句: + +``` +SELECT * +FROM my_neighbor n + LEFT JOIN my_neighbor_apply sra + ON n.id = sra.neighbor_id + AND sra.user_id = 'xxx' +WHERE n.topic_status < 4 + AND EXISTS(SELECT 1 + FROM message_info m + WHERE n.id = m.neighbor_id + AND m.inuser = 'xxx') + AND n.topic_type <> 5 +``` + +执行计划为: + +``` ++----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+ +| 1 | PRIMARY | n | ALL | | NULL | NULL | NULL | 1086041 | Using where | +| 1 | PRIMARY | sra | ref | | idx_user_id | 123 | const | 1 | Using where | +| 2 | DEPENDENT SUBQUERY | m | ref | | idx_message_info | 122 | const | 1 | Using index condition; Using where | ++----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+ +``` + +去掉 exists 更改为 join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。 + +``` +SELECT * +FROM my_neighbor n + INNER JOIN message_info m + ON n.id = m.neighbor_id + AND m.inuser = 'xxx' + LEFT JOIN my_neighbor_apply sra + ON n.id = sra.neighbor_id + AND sra.user_id = 'xxx' +WHERE n.topic_status < 4 + AND n.topic_type <> 5 +``` + +新的执行计划: + +``` ++----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ +| 1 | SIMPLE | m | ref | | idx_message_info | 122 | const | 1 | Using index condition | +| 1 | SIMPLE | n | eq_ref | | PRIMARY | 122 | ighbor_id | 1 | Using where | +| 1 | SIMPLE | sra | ref | | idx_user_id | 123 | const | 1 | Using where | ++----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+ +``` + +## 6、条件下推 + +外部查询条件不能够下推到复杂的视图或子查询的情况有: + +1、聚合子查询; + +2、含有 LIMIT 的子查询; + +3、UNION 或 UNION ALL 子查询; + +4、输出字段中的子查询; + +如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后: + +``` +SELECT * +FROM (SELECT target, + Count(*) + FROM operation + GROUP BY target) t +WHERE target = 'rm-xxxx' ++----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ +| 1 | PRIMARY | | ref | | | 514 | const | 2 | Using where | +| 2 | DERIVED | operation | index | idx_4 | idx_4 | 519 | NULL | 20 | Using index | ++----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+ +``` + +确定从语义上查询条件可以直接下推后,重写如下: + +``` +SELECT target, + Count(*) +FROM operation +WHERE target = 'rm-xxxx' +GROUP BY target +``` + +执行计划变为: + +``` ++----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ +| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index | ++----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+ +``` + +关于 MySQL 外部条件不能下推的详细解释说明请参考以前文章:MySQL · 性能优化 · 条件下推到物化表 http://mysql.taobao.org/monthly/2016/07/08 + +## 7、提前缩小范围 + +先上初始 SQL 语句: + +``` +SELECT * +FROM my_order o + LEFT JOIN my_userinfo u + ON o.uid = u.uid + LEFT JOIN my_productinfo p + ON o.pid = p.pid +WHERE ( o.display = 0 ) + AND ( o.ostaus = 1 ) +ORDER BY o.selltime DESC +LIMIT 0, 15 +``` + +该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。 + +``` ++----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+ +| 1 | SIMPLE | o | ALL | NULL | NULL | NULL | NULL | 909119 | Using where; Using temporary; Using filesort | +| 1 | SIMPLE | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL | +| 1 | SIMPLE | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) | ++----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+ +``` + +由于最后 WHERE 条件以及排序均针对最左主表,因此可以先对 my_order 排序提前缩小数据量再做左连接。SQL 重写后如下,执行时间缩小为1毫秒左右。 + +``` +SELECT * +FROM ( +SELECT * +FROM my_order o +WHERE ( o.display = 0 ) + AND ( o.ostaus = 1 ) +ORDER BY o.selltime DESC +LIMIT 0, 15 +) o + LEFT JOIN my_userinfo u + ON o.uid = u.uid + LEFT JOIN my_productinfo p + ON o.pid = p.pid +ORDER BY o.selltime DESC +limit 0, 15 +``` + +再检查执行计划:子查询物化后(select_type=DERIVED)参与 JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及 LIMIT 子句后,实际执行时间变得很小。 + +``` ++----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+ +| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | ++----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+ +| 1 | PRIMARY | | ALL | NULL | NULL | NULL | NULL | 15 | Using temporary; Using filesort | +| 1 | PRIMARY | u | eq_ref | PRIMARY | PRIMARY | 4 | o.uid | 1 | NULL | +| 1 | PRIMARY | p | ALL | PRIMARY | NULL | NULL | NULL | 6 | Using where; Using join buffer (Block Nested Loop) | +| 2 | DERIVED | o | index | NULL | idx_1 | 5 | NULL | 909112 | Using where | ++----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+ +``` + +## 8、中间结果集下推 + +再来看下面这个已经初步优化过的例子(左连接中的主表优先作用查询条件): + +``` +SELECT a.*, + c.allocated +FROM ( + SELECT resourceid + FROM my_distribute d + WHERE isdelete = 0 + AND cusmanagercode = '1234567' + ORDER BY salecode limit 20) a +LEFT JOIN + ( + SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated + FROM my_resources + GROUP BY resourcesid) c +ON a.resourceid = c.resourcesid +``` + +那么该语句还存在其它问题吗?不难看出子查询 c 是全表聚合查询,在表数量特别大的情况下会导致整个语句的性能下降。 + +其实对于子查询 c,左连接最后结果集只关心能和主表 resourceid 能匹配的数据。因此我们可以重写语句如下,执行时间从原来的2秒下降到2毫秒。 + +``` +SELECT a.*, + c.allocated +FROM ( + SELECT resourceid + FROM my_distribute d + WHERE isdelete = 0 + AND cusmanagercode = '1234567' + ORDER BY salecode limit 20) a +LEFT JOIN + ( + SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated + FROM my_resources r, + ( + SELECT resourceid + FROM my_distribute d + WHERE isdelete = 0 + AND cusmanagercode = '1234567' + ORDER BY salecode limit 20) a + WHERE r.resourcesid = a.resourcesid + GROUP BY resourcesid) c +ON a.resourceid = c.resourcesid +``` + +但是子查询 a 在我们的SQL语句中出现了多次。这种写法不仅存在额外的开销,还使得整个语句显的繁杂。使用 WITH 语句再次重写: + +``` +WITH a AS +( + SELECT resourceid + FROM my_distribute d + WHERE isdelete = 0 + AND cusmanagercode = '1234567' + ORDER BY salecode limit 20) +SELECT a.*, + c.allocated +FROM a +LEFT JOIN + ( + SELECT resourcesid, sum(ifnull(allocation, 0) * 12345) allocated + FROM my_resources r, + a + WHERE r.resourcesid = a.resourcesid + GROUP BY resourcesid) c +ON a.resourceid = c.resourcesid +``` + +## 总结 + +数据库编译器产生执行计划,决定着SQL的实际执行方式。但是编译器只是尽力服务,所有数据库的编译器都不是尽善尽美的。 + +上述提到的多数场景,在其它数据库中也存在性能问题。了解数据库编译器的特性,才能避规其短处,写出高性能的SQL语句。 + +程序员在设计数据模型以及编写SQL语句时,要把算法的思想或意识带进来。 + +编写复杂SQL语句要养成使用 WITH 语句的习惯。简洁且思路清晰的SQL语句也能减小数据库的负担 。 + +> 来源:yq.aliyun.com/articles/72501 diff --git a/docs/advance/excellent-article/8-interface-idempotent.md b/docs/advance/excellent-article/8-interface-idempotent.md new file mode 100644 index 0000000..26d6f46 --- /dev/null +++ b/docs/advance/excellent-article/8-interface-idempotent.md @@ -0,0 +1,98 @@ +--- +sidebar: heading +title: 如何保证接口幂等性? +category: 优质文章 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 接口幂等性 + - - meta + - name: description + content: 优质文章汇总 +--- + + +# 面试官:如何保证接口幂等性?一口气说了9种方法! + +大家好,我是大彬~ + +今天来聊聊接口幂等性。 + +什么是接口幂等性?如何保证接口幂等性? + +## 什么是接口幂等性? + +首先看看幂等性的概念: + +幂等性原本是数学上的概念,用在接口上就可以理解为:**同一个接口,多次发出同一个请求,必须保证操作只执行一次**。调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生。 + +比如下面这些情况,如果没有实现接口幂等性会有很严重的后果:支付接口,重复支付会导致多次扣钱 ;订单接口,同一个订单可能会多次创建。 + +## 为什么会产生接口幂等性问题? + +那么,什么情况下,会产生接口幂等性的问题呢? + +- 网络波动, 可能会引起重复请求 +- 用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用 +- 使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等) +- 页面重复刷新 +- 使用浏览器后退按钮重复之前的操作,导致重复提交表单 +- 使用浏览器历史记录重复提交表单 +- 浏览器重复的HTTP请求 +- 定时任务重复执行 +- 用户双击提交按钮 + +## 如何保证接口幂等性? + +那么最关键的来了,如何保证接口幂等性? + +解决办法分为两个方向,一个方向是客户端防止重复调用,一个是服务端进行校验。当然,客户端防止重复提交并不是绝对可靠的,优点是实现起来比较简单。 + +### **按钮只可操作一次** + +一般是提交后把按钮置灰或loding状态,消除用户因为重复点击而产生的重复记录,比如添加操作,由于点击两次而产生两条记录。 + +### **token机制** + +功能上允许重复提交,但要保证重复提交不产生副作用,比如点击n次只产生一条记录,具体实现就是进入页面时申请一个token,然后后面所有的请求都带上这个token,后端根据token来避免重复请求。 + +![](http://img.topjavaer.cn/img/接口幂等.png) + +### **使用唯一索引防止新增脏数据** + +利用数据库唯一索引机制,当数据重复时,插入数据库会抛出异常,保证不会出现脏数据。 + +### **乐观锁** + +如果更新已有数据,可以进行加锁更新,也可以设计表结构时使用乐观锁,通过version来做乐观锁,这样既能保证执行效率,又能保证幂等, 乐观锁的version版本在更新业务数据要自增。 + +```mysql +update table set version = version + 1 where id = #{id} and version = #{version} +``` + +示例: 当有重复请求的时候,第一个请求会获取当前商品的version版本号,得到的version为1,紧接着由于第一个请求还没更新商品的version,第二个请求获取的version依然也是1, 这时候第一个请求操作更新的时候带上version并作为条件并且自增更新,这时候商品的version就会变成2,当第二个请求去操作更新的时候明显version不一致导致更新失败。 + +### **select + insert or update or delete** + +该方案就是操作之前先查询一下,符合要求再插入,该方案在没有并发的系统中可以解决幂等问题,在单JVM有并发的时候可以用JVM加锁来保证幂等性,在分布式环境它是无法保证幂等性,可以使用分布式来保证。 + +### **分布式锁** + +如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。 + +### **状态机幂等** + +在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助 。 + +### **防重表** + +以支付为例: 使用唯一主键去做防重表的唯一索引,比如使用订单号作为防重表的唯一索引,每一次请求都根据订单号向防重表中插入一条数据,插入成功说明可以处理后面的业务,当处理完业务逻辑之后删除防重表中的订单号数据,后续如果有重复请求,则会因为防重表唯一索引原因导致插入失败,直接返回操作失败,直到第一次请求返回结果,可以看出防重表作用就是加锁的功能。 + +> 注: 最好结合状态机幂等先判断一下 + +### **缓冲队列** + +将请求都快速地接收下来后放入缓冲队列中,后续使用异步任务处理队列中的数据,过滤掉重复的请求,该解决方案优点是同步处理改成异步处理、高吞吐量,缺点则是不能及时地返回请求结果,需要后续轮询得处理结果。 + diff --git a/docs/advance/excellent-article/9-jvm-optimize-param.md b/docs/advance/excellent-article/9-jvm-optimize-param.md new file mode 100644 index 0000000..6feb1f6 --- /dev/null +++ b/docs/advance/excellent-article/9-jvm-optimize-param.md @@ -0,0 +1,258 @@ +--- +sidebar: heading +title: 美团面试:熟悉哪些JVM调优参数? +category: 优质文章 +tag: + - JVM +head: + - - meta + - name: keywords + content: JVM调优参数,JVM调优 + - - meta + - name: description + content: 优质文章汇总 +--- + + +# 美团面试:熟悉哪些JVM调优参数? + +今天来熟悉一下,关于`JVM`调优常用的一些参数。 + +X或者XX开头的都是非标准化参数 + +![](http://img.topjavaer.cn/img/20230327084105.png) + +意思就是说标准化参数不会变,非标准化参数可能在每个`JDK`版本中有所变化,但是就目前来看X开头的非标准化的参数改变的也是非常少。 + +``` +格式:-XX:[+-] 表示启用或者禁用name属性。 +例子:-XX:+UseG1GC(表示启用G1垃圾收集器) +``` + +`-XX:+PrintCommandLineFlags`查看当前`JVM`设置过的相关参数: + +![](http://img.topjavaer.cn/img/20230327084121.png) + +## JVM参数分类 + +根据`JVM`参数开头可以区分参数类型,共三类:“`-`”、“`-X`”、“`-XX`”, + +标准参数(-):所有的JVM实现都必须实现这些参数的功能,而且向后兼容; + +例子:`-verbose:class`,`-verbose:gc`,`-verbose:jni……` + +非标准参数(-X):默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容; + +例子:`Xms20m`,`-Xmx20m`,`-Xmn20m`,`-Xss128k……` + +非Stable参数(-XX):此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用; + +例子:`-XX:+PrintGCDetails`,`-XX:-UseParallelGC`,`-XX:+PrintGCTimeStamps……` + +## 堆参数设置 + +``` +-Xms` 初始堆大小,ms是memory start的简称 ,等价于`-XX:InitialHeapSize``-Xmx` 最大堆大小,mx是memory max的简称 ,等价于参数`-XX:MaxHeapSize +``` + +> 注意:在通常情况下,服务器项目在运行过程中,堆空间会不断的收缩与扩张,势必会造成不必要的系统压力。 +> +> 所以在生产环境中,`JVM`的`Xms`和`Xmx`要设置成大小一样的,能够避免`GC`在调整堆大小带来的不必要的压力。 + +`-XX:NewSize=n` 设置年轻代大小`-XX:NewRatio=n` 设置年轻代和年老代的比值。 + +如:`-XX:NewRatio=3`,表示年轻代与年老代比值为`1:3`,年轻代占整个年轻代年老代和的1/4,`默认新生代和老年代的比例=1:2`。`-XX:SurvivorRatio=n` 年轻代中Eden区与两个Survivor区的比值。 + +注意Survivor区有两个,默认是8,表示:`Eden:S0:S1=8:1:1` + +如:`-XX:SurvivorRatio=3`,表示`Eden:Survivor`=3:2,一个Survivor区占整个年轻代的1/5。 + +## 元空间参数 + +**-XX:MetaspaceSize**:`Metaspace` 空间初始大小,如果不设置的话,默认是20.79M,这个初始大小是触发首次 `Metaspace Full GC`的阈值。 + +例如:`-XX:MetaspaceSize=256M` + +**-XX:MaxMetaspaceSize**:`Metaspace` 最大值,默认不限制大小,但是线上环境建议设置。 + +例如:`-XX:MaxMetaspaceSize=256M` + +**-XX:MinMetaspaceFreeRatio**:最小空闲比,当 `Metaspace` 发生 GC 后,会计算 `Metaspace` 的空闲比,如果空闲比(空闲空间/当前 `Metaspace` 大小)小于此值,就会触发 `Metaspace` 扩容。默认值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40 + +**-XX:MaxMetaspaceFreeRatio**:最大空闲比,当 `Metaspace`发生 GC 后,会计算 `Metaspace` 的空闲比,如果空闲比(空闲空间/当前 Metaspace 大小)大于此值,就会触发 `Metaspace` 释放空间。默认值是 70 ,也就是 70%,例如 -`XX:MaxMetaspaceFreeRatio=70` + +> 建议将 `MetaspaceSize` 和 `MaxMetaspaceSize`设置为同样大小,避免频繁扩容。 + +## 栈参数设置 + +**-Xss**:栈空间大小,栈是线程独占的,所以是一个线程使用栈空间的大小。 + +例如:`-Xss256K`,如果不设置此参数,默认值是`1M`,一般来讲设置成 `256K` 就足够了。 + +## 收集器参数设置 + +Serial垃圾收集器(新生代) + +> 开启:-XX:+UseSerialGC 关闭:-XX:-UseSerialGC //新生代使用Serial 老年代则使用SerialOld + +ParNew垃圾收集器(新生代) + +> 开启 -XX:+UseParNewGC 关闭 -XX:-UseParNewGC //新生代使用功能ParNew 老年代则使用功能CMS + +Parallel Scavenge收集器(新生代) + +> 开启 -XX:+UseParallelOldGC 关闭 -XX:-UseParallelOldGC //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 + +ParallelOl垃圾收集器(老年代) + +> 开启 -XX:+UseParallelGC 关闭 -XX:-UseParallelGC //新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 + +CMS垃圾收集器(老年代) + +> 开启 -XX:+UseConcMarkSweepGC 关闭 -XX:-UseConcMarkSweepGC + +G1垃圾收集器 + +> 开启 -XX:+UseG1GC 关闭 -XX:-UseG1GC + +## GC策略参数配置 + +GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间,比如减小年轻代 + +> -XX:MaxGCPauseMillis + +堆占用了多少比例的时候触发GC,就即触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45% + +> -XX:InitiatingHeapOccupancyPercent=n + +新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 + +> -XX:PretenureSizeThreshold=1000000 // + +进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7 + +> -XX:InitialTenuringThreshol=7 + +升级老年代年龄,最大值15 + +> -XX:MaxTenuringThreshold + +GC并行执行线程数 + +> -XX:ParallelGCThreads=16 + +禁用 System.gc(),由于该方法默认会触发 FGC,并且忽略参数中的 UseG1GC 和 UseConcMarkSweepGC,因此必要时可以禁用该方法。 + +> -XX:-+DisableExplicitGC + +设置吞吐量大小,默认99 + +> XX:GCTimeRatio + +打开自适应策略,各个区域的比率,晋升老年代的年龄等参数会被自动调整。以达到吞吐量,停顿时间的平衡点。 + +> XX:UseAdaptiveSizePolicy + +设置GC时间占用程序运行时间的百分比 + +> GCTimeRatio + +## Dump异常快照 + +``` +-XX:+HeapDumpOnOutOfMemoryError +-XX:HeapDumpPath +``` + +堆内存出现`OOM`的概率是所有内存耗尽异常中最高的,出错时的堆内信息对解决问题非常有帮助。 + +所以给`JVM`设置这个参数(`-XX:+HeapDumpOnOutOfMemoryError`),让`JVM`遇到`OOM`异常时能输出堆内信息,并通过(`-XX:+HeapDumpPath`)参数设置堆内存溢出快照输出的文件地址。 + +这对于特别是对相隔数月才出现的`OOM`异常尤为重要。 + +``` +-Xms10M -Xmx10M -Xmn2M -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError +-XX:HeapDumpPath=D:\study\log_hprof\gc.hprof +-XX:OnOutOfMemoryError +``` + +表示发生`OOM后`,运行`jconsole.exe`程序。 + +这里可以不用加“”,因为`jconsole.exe`路径Program Files含有空格。利用这个参数,我们可以在系统`OOM`后,自定义一个脚本,可以用来发送邮件告警信息,可以用来重启系统等等。 + +``` +-XX:OnOutOfMemoryError="C:\Program Files\Java\jdk1.8.0_151\bin\jconsole.exe" +``` + +## 8G内存的服务器该如何设置 + +``` +java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0 +``` + +`-Xmx3500m` 设置`JVM`最大可用内存为3550M。 + +`-Xms3500m` 设置`JVM``初始`内存为`3550m`。此值可以设置与`-Xmx`相同,以避免每次垃圾回收完成后JVM重新分配内存。`-Xmn2g` 设置年轻代大小为`2G`。 + +> 整个堆大小=年轻代大小 + 年老代大小 + 方法区大小 + +`-Xss128k` 设置每个线程的堆栈大小。 + +`JDK1.5`以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 + +`-XX:NewRatio=4` 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 。 + +`-XX:SurvivorRatio=4` 设置年轻代中Eden区与Survivor区的大小比值。 + +设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 `-XX:MaxPermSize=16m` 设置持久代大小为16m。 + +`-XX:MaxTenuringThreshold=0` 设置垃圾最大年龄。 + +如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概论。 + +## 项目中,GC日志配置 + +比如,我们启动一个user-service项目: + +``` + java + -XX:+PrintGCDetails -XX:+PrintGCDateStamps + -XX:+UseGCLogFileRotation + -XX:+PrintHeapAtGC -XX:NumberOfGCLogFiles=5 + -XX:GCLogFileSize=20M + -Xloggc:/opt/user-service-gc-%t.log + -jar user-service-1.0-SNAPSHOT.jar +``` + +参数解释: + +``` + -Xloggc:/opt/app/ard-user/user-service-gc-%t.log 设置日志目录和日志名称 + -XX:+UseGCLogFileRotation 开启滚动生成日志 + -XX:NumberOfGCLogFiles=5 滚动GC日志文件数,默认0,不滚动 + -XX:GCLogFileSize=20M GC文件滚动大小,需开启UseGCLogFileRotation + -XX:+PrintGCDetails 开启记录GC日志详细信息(包括GC类型、各个操作使用的时间),并且在程序运行结束打印出JVM的内存占用情况 + -XX:+ PrintGCDateStamps 记录系统的GC时间 + -XX:+PrintGCCause 产生GC的原因(默认开启) +``` + +## 项目中没用过怎么办? + +对于很多没用过的人来说,面试官问项目中这些参数是怎么用?此时,很容易选择妥协,傻傻的回答**没用过**。 + +偷偷的告诉你,很多面试官也没有用过。 + +另外,你可以自己搞个小项目,把`JVM`参数设置小点,使用测试工具`JMeter`,多线程测试一下。 + +在代码里可以自己编造以下问题: + +> 内存溢出 +> +> 内存泄漏 +> +> 栈溢出 + +然后使用`JVM`参数进行调优,或者通过`JVM`工具和相关命令找到问题,然后解决问题。 + +> 自己一定要动手,别人永远是别人的,自己体会了自己经历了,那才是自己的。 + diff --git "a/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" new file mode 100644 index 0000000..9a2241a --- /dev/null +++ "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" @@ -0,0 +1,91 @@ +MySQL中编写SQL时,遵循良好的习惯能够提高查询性能、保障数据一致性、提升代码可读性和维护性。以下列举了多个编写SQL的好习惯 + +#### 1.使用EXPLAIN分析查询计划 + +在编写或优化复杂查询时,先使用EXPLAIN命令查看查询执行计划,理解MySQL如何执行查询、访问哪些表、使用哪种类型的联接以及索引的使用情况。 + +好处:有助于识别潜在的性能瓶颈,如全表扫描、错误的索引选择、过多的临时表或文件排序等,从而针对性地优化查询或调整索引结构。 + +#### 2.避免全表扫描 + +2. 习惯:尽可能利用索引来避免全表扫描,尤其是在处理大表时。确保在WHERE、JOIN条件和ORDER BY、GROUP BY子句中使用的列有适当的索引。 + +好处:极大地减少数据访问量,提高查询性能,减轻I/O压力。 + +#### 3. 为表和字段添加注释 + +3. 习惯:在创建表时,为表和每个字段添加有意义的注释,描述其用途、数据格式、业务规则等信息。 + +好处:提高代码可读性和可维护性,帮助其他开发人员快速理解表结构和字段含义,减少沟通成本和误解。 + +#### 4. 明确指定INSERT语句的列名 + +习惯:在INSERT语句中显式列出要插入数据的列名,即使插入所有列也应如此。 + +好处:避免因表结构变化导致的插入错误,增强代码的健壮性,同时也提高了语句的清晰度。 + +#### 5. 格式化SQL语句 + +习惯:保持SQL语句的格式整洁,使用一致的大小写(如关键词大写、表名和列名小写),合理缩进,避免过长的单行语句。 + +好处:提高代码可读性,便于审查、调试和团队协作。 + +#### 6. 使用LIMIT限制结果集大小 + +习惯:在执行SELECT、DELETE或UPDATE操作时,若不需要处理全部数据,务必使用LIMIT子句限制结果集大小,特别是在生产环境中。 + +好处:防止因误操作导致大量数据被修改或删除,降低风险,同时也能提高查询性能。 + +#### 7.使用JOIN语句代替子查询 + +习惯:在可能的情况下,优先使用JOIN操作代替嵌套的子查询,特别是在处理多表关联查询时。 + +好处:许多情况下JOIN的执行效率高于子查询,而且JOIN语句通常更易于理解和优化。 + +#### 8.避免在WHERE子句中对NULL进行比较 + +习惯:使用IS NULL和IS NOT NULL来检查字段是否为NULL,而不是直接与NULL进行等值或不等值比较。 + +好处:正确处理NULL值,避免逻辑错误和未预期的结果。 + +#### 9.避免在查询中使用SELECT + +习惯:明确列出需要的列名,而不是使用SELECT *从表中获取所有列。 + +好处:减少网络传输的数据量,降低I/O开销,提高查询性能,同时也有利于代码的清晰性和可维护性。 + +#### 10. 数据库对象命名规范 + +习惯:遵循一致且有意义的命名约定,如使用小写字母、下划线分隔单词,避免使用MySQL保留字,保持表名、列名、索引名等的简洁性和一致性。 + +好处:提高代码可读性,减少命名冲突,便于团队协作和维护。 + +#### 11. 事务管理 + +习惯:对一系列需要保持原子性的操作使用事务管理,确保数据的一致性。 + +好处:在发生异常时能够回滚未完成的操作,避免数据处于不一致状态。 + +#### 12.适时使用索引覆盖 + +习惯:对于只查询索引列且不需要访问数据行的查询(如计数、统计),创建覆盖索引以避免回表操作。 + +好处:极大提升查询性能,减少I/O开销。 + +#### 13.遵循第三范式或适当反范式 + +习惯:根据业务需求和查询模式,合理设计表结构,遵循第三范式以减少数据冗余和更新异常,或适当反范式以优化查询性能。 + +好处:保持数据一致性,减少数据维护成本,或提高查询效率。 + +#### 14.使用预编译语句(PreparedStatement) + +习惯:在应用程序中使用预编译语句(如Java中的PreparedStatement)执行SQL,特别是对于动态拼接SQL语句的情况。 + +好处:避免SQL注入攻击,提高查询性能,减少数据库服务器的解析开销。 + +#### 15.定期分析与优化表和索引 + +习惯:定期运行ANALYZE TABLE收集统计信息,以便MySQL优化器做出更准确的查询计划决策。根据查询性能监控结果,适时调整索引或重构表结构。 + +好处:确保数据库持续高效运行,适应不断变化的业务需求和数据分布。 \ No newline at end of file diff --git a/docs/advance/excellent-article/README.md b/docs/advance/excellent-article/README.md new file mode 100644 index 0000000..2f7a2ac --- /dev/null +++ b/docs/advance/excellent-article/README.md @@ -0,0 +1,25 @@ + +## 优质文章汇总 + +- [Redis如何实现库存扣减操作和防止被超卖?](./1-redis-stock-minus.md) +- [@Transactional事务注解详解](./2-spring-transaction.md) +- [SpringBoot自动装配原理](./3-springboot-auto-assembly.md) +- [干掉“重复代码”的技巧有哪些](./4-remove-duplicate-code.md) +- [一次简单的 *JVM* 调优,拿去写到简历里](./5-jvm-optimize.md) +- [Spring为何需要三级缓存解决循环依赖,而不是二级缓存?](./6-spring-three-cache.md) +- [8种最坑SQL语法](./7-sql-optimize.md) +- [面试官:如何保证接口幂等性?一口气说了12种方法!](./8-interface-idempotent.md) +- [美团面试:熟悉哪些JVM调优参数?](./9-jvm-optimize-param.md) +- [大文件上传时如何做到秒传?](./10-file-upload.md) +- [8种架构模式](./11-8-architect-pattern.md) +- [MySQL最大建议行数 2000w,靠谱吗?](./12-mysql-table-max-rows.md) +- [order by是怎么工作的?](./13-order-by-work.md) +- [架构的演进](./14-architect-forward.md) +- [有了HTTP,为啥还要用RPC](./15-http-vs-rpc.md) +- [什么是JWT](./16-what-is-jwt.md) +- [限流的几种方案](./17-limit-scheme.md) +- [为什么说数据库连接很消耗资源](./18-db-connect-resource.md) +- [Java19新特性](./19-java19.md) +- [几种常见的架构模式](./20-architect-pattern.md) +- [新一代分布式任务调度框架](./22-distributed-scheduled-task.md) +- [Arthas 常用命令](./23-arthas-intro.md) \ No newline at end of file diff --git "a/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" new file mode 100644 index 0000000..92ca41f --- /dev/null +++ "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" @@ -0,0 +1,430 @@ +# 实现异步编程,我有八种方式! + +## **一、前言** + +> 异步执行对于开发者来说并不陌生,在实际的开发过程中,很多场景多会使用到异步,相比同步执行,异步可以大大缩短请求链路耗时时间,比如:**发送短信、邮件、异步更新等**,这些都是典型的可以通过异步实现的场景。 + +## **二、异步的八种实现方式** + +1. 线程Thread +2. Future +3. 异步框架`CompletableFuture` +4. Spring注解@Async +5. Spring `ApplicationEvent`事件 +6. 消息队列 +7. 第三方异步框架,比如Hutool的`ThreadUtil` +8. Guava异步 + +## **三、什么是异步?** + +首先我们先看一个常见的用户下单的场景: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHah6HIeYkttOlF0y1ia4PMKjqdDWibtnJv1pBQIDP1vW4OpB8Fm5xEb5uw/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +在同步操作中,我们执行到 **发送短信** 的时候,我们必须等待这个方法彻底执行完才能执行 **赠送积分** 这个操作,如果 **赠送积分** 这个动作执行时间较长,发送短信需要等待,这就是典型的同步场景。 + +实际上,发送短信和赠送积分没有任何的依赖关系,通过异步,我们可以实现`赠送积分`和`发送短信`这两个操作能够同时进行,比如: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHa844UkQPwo59jkAYiaQ8RUYIxUpRGk99HehD2nAXvRx6aGZchRveICDA/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +这就是所谓的异步,是不是非常简单,下面就说说异步的几种实现方式吧。 + +## **四、异步编程** + +#### **4.1 线程异步** + +```java +public class AsyncThread extends Thread { + + @Override + public void run() { + System.out.println("Current thread name:" + Thread.currentThread().getName() + " Send email success!"); + } + + public static void main(String[] args) { + AsyncThread asyncThread = new AsyncThread(); + asyncThread.run(); + } +} +``` + +当然如果每次都创建一个`Thread`线程,频繁的创建、销毁,浪费系统资源,我们可以采用线程池: + +```java +private ExecutorService executorService = Executors.newCachedThreadPool(); + +public void fun() { + executorService.submit(new Runnable() { + @Override + public void run() { + log.info("执行业务逻辑..."); + } + }); +} +``` + +可以将业务逻辑封装到`Runnable`或`Callable`中,交由线程池来执行。 + +#### **4.2 Future异步** + +```java +@Slf4j +public class FutureManager { + + public String execute() throws Exception { + + ExecutorService executor = Executors.newFixedThreadPool(1); + Future future = executor.submit(new Callable() { + @Override + public String call() throws Exception { + + System.out.println(" --- task start --- "); + Thread.sleep(3000); + System.out.println(" --- task finish ---"); + return "this is future execute final result!!!"; + } + }); + + //这里需要返回值时会阻塞主线程 + String result = future.get(); + log.info("Future get result: {}", result); + return result; + } + + @SneakyThrows + public static void main(String[] args) { + FutureManager manager = new FutureManager(); + manager.execute(); + } +} +``` + +输出结果: + +``` + --- task start --- + --- task finish --- + Future get result: this is future execute final result!!! +``` + +##### **4.2.1 Future的不足之处** + +Future的不足之处的包括以下几点: + +1️⃣ 无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务执行结束之后,主线程无法得到任务完成与否的通知,它需要通过get方法主动获取任务执行的结果。 + +2️⃣ Future件彼此孤立:有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的`CompletableFuture`,`CompletableFuture`就可以将多个Future串联起来形成任务流。 + +3️⃣ Futrue没有很好的错误处理机制:截止目前,如果某个异步任务在执行发的过程中发生了异常,调用者无法被动感知,必须通过捕获get方法的异常才知晓异步任务执行是否出现了错误,从而在做进一步的判断处理。 + +#### **4.3 CompletableFuture实现异步** + +```java +public class CompletableFutureCompose { + + /** + * thenAccept子任务和父任务公用同一个线程 + */ + @SneakyThrows + public static void thenRunAsync() { + CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> { + System.out.println(Thread.currentThread() + " cf1 do something...."); + return 1; + }); + CompletableFuture cf2 = cf1.thenRunAsync(() -> { + System.out.println(Thread.currentThread() + " cf2 do something..."); + }); + //等待任务1执行完成 + System.out.println("cf1结果->" + cf1.get()); + //等待任务2执行完成 + System.out.println("cf2结果->" + cf2.get()); + } + + public static void main(String[] args) { + thenRunAsync(); + } +} +``` + +我们不需要显式使用`ExecutorService,CompletableFuture `内部使用了`ForkJoinPool`来处理异步任务,如果在某些业务场景我们想自定义自己的异步线程池也是可以的。 + +#### **4.4 Spring的@Async异步** + +##### **4.4.1 自定义异步线程池** + +``` +/** + * 线程池参数配置,多个线程池实现线程池隔离,@Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName") + * + * @author: jacklin + * @since: 2021/5/18 11:44 + **/ +@EnableAsync +@Configuration +public class TaskPoolConfig { + + /** + * 自定义线程池 + * + * @author: jacklin + * @since: 2021/11/16 17:41 + **/ + @Bean("taskExecutor") + public Executor taskExecutor() { + //返回可用处理器的Java虚拟机的数量 12 + int i = Runtime.getRuntime().availableProcessors(); + System.out.println("系统最大线程数 : " + i); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程池大小 + executor.setCorePoolSize(16); + //最大线程数 + executor.setMaxPoolSize(20); + //配置队列容量,默认值为Integer.MAX_VALUE + executor.setQueueCapacity(99999); + //活跃时间 + executor.setKeepAliveSeconds(60); + //线程名字前缀 + executor.setThreadNamePrefix("asyncServiceExecutor -"); + //设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行 + executor.setAwaitTerminationSeconds(60); + //等待所有的任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + return executor; + } +} +``` + +##### **4.4.2 AsyncService** + +```java +public interface AsyncService { + + MessageResult sendSms(String callPrefix, String mobile, String actionType, String content); + + MessageResult sendEmail(String email, String subject, String content); +} + +@Slf4j +@Service +public class AsyncServiceImpl implements AsyncService { + + @Autowired + private IMessageHandler mesageHandler; + + @Override + @Async("taskExecutor") + public MessageResult sendSms(String callPrefix, String mobile, String actionType, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendSms(callPrefix, mobile, actionType, content); + + } catch (Exception e) { + log.error("发送短信异常 -> ", e) + } + } + + + @Override + @Async("taskExecutor") + public sendEmail(String email, String subject, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendsendEmail(email, subject, content); + + } catch (Exception e) { + log.error("发送email异常 -> ", e) + } + } +} +``` + +在实际项目中, 使用`@Async`调用线程池,推荐等方式是是使用自定义线程池的模式,不推荐直接使用@Async直接实现异步。 + +#### **4.5 Spring ApplicationEvent事件实现异步** + +##### **4.5.1 定义事件** + +```java +public class AsyncSendEmailEvent extends ApplicationEvent { + + /** + * 邮箱 + **/ + private String email; + + /** + * 主题 + **/ + private String subject; + + /** + * 内容 + **/ + private String content; + + /** + * 接收者 + **/ + private String targetUserId; + +} +``` + +##### **4.5.2 定义事件处理器** + +```java +@Slf4j +@Component +public class AsyncSendEmailEventHandler implements ApplicationListener { + + @Autowired + private IMessageHandler mesageHandler; + + @Async("taskExecutor") + @Override + public void onApplicationEvent(AsyncSendEmailEvent event) { + if (event == null) { + return; + } + + String email = event.getEmail(); + String subject = event.getSubject(); + String content = event.getContent(); + String targetUserId = event.getTargetUserId(); + mesageHandler.sendsendEmailSms(email, subject, content, targerUserId); + } +} +``` + +另外,可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合Spring Retry重试来帮助我们避免这种异常造成数据不一致问题。 + +#### **4.6 消息队列** + +##### **4.6.1 回调事件消息生产者** + +``` +@Slf4j +@Component +public class CallbackProducer { + + @Autowired + AmqpTemplate amqpTemplate; + + public void sendCallbackMessage(CallbackDTO allbackDTO, final long delayTimes) { + + log.info("生产者发送消息,callbackDTO,{}", callbackDTO); + + amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(), CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(), JsonMapper.getInstance().toJson(genseeCallbackDTO), new MessagePostProcessor() { + @Override + public Message postProcessMessage(Message message) throws AmqpException { + //给消息设置延迟毫秒值,通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间 + message.getMessageProperties().setHeader("x-delay", delayTimes); + message.getMessageProperties().setCorrelationId(callbackDTO.getSdkId()); + return message; + } + }); + } +} +``` + +##### **4.6.2 回调事件消息消费者** + +``` +@Slf4j +@Component +@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory") +public class CallbackConsumer { + + @Autowired + private IGlobalUserService globalUserService; + + @RabbitHandler + public void handle(String json, Channel channel, @Headers Map map) throws Exception { + + if (map.get("error") != null) { + //否认消息 + channel.basicNack((Long) map.get(AmqpHeaders.DELIVERY_TAG), false, true); + return; + } + + try { + + CallbackDTO callbackDTO = JsonMapper.getInstance().fromJson(json, CallbackDTO.class); + //执行业务逻辑 + globalUserService.execute(callbackDTO); + //消息消息成功手动确认,对应消息确认模式acknowledge-mode: manual + channel.basicAck((Long) map.get(AmqpHeaders.DELIVERY_TAG), false); + + } catch (Exception e) { + log.error("回调失败 -> {}", e); + } + } +} +``` + +#### **4.7 ThreadUtil异步工具类** + +```java +@Slf4j +public class ThreadUtils { + + public static void main(String[] args) { + for (int i = 0; i < 3; i++) { + ThreadUtil.execAsync(() -> { + ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); + int number = threadLocalRandom.nextInt(20) + 1; + System.out.println(number); + }); + log.info("当前第:" + i + "个线程"); + } + + log.info("task finish!"); + } +} +``` + +#### **4.8 Guava异步** + +`Guava`的`ListenableFuture`顾名思义就是可以监听的`Future`,是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。 + +如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。 + +使用**Guava ListenableFuture**可以帮我们检测Future是否完成了,不需要再通过get()方法苦苦等待异步的计算结果,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。 + +`ListenableFuture`是一个接口,它从`jdk`的`Future`接口继承,添加了`void addListener(Runnable listener, Executor executor)`方法。 + +我们看下如何使用`ListenableFuture`。首先需要定义`ListenableFuture`的实例: + +```java +ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + final ListenableFuture listenableFuture = executorService.submit(new Callable() { + @Override + public Integer call() throws Exception { + log.info("callable execute...") + TimeUnit.SECONDS.sleep(1); + return 1; + } + }); +``` + +首先通过`MoreExecutors`类的静态方法`listeningDecorator`方法初始化一个`ListeningExecutorService`的方法,然后使用此实例的`submit`方法即可初始化`ListenableFuture`对象。 + +`ListenableFuture`要做的工作,在Callable接口的实现类中定义,这里只是休眠了1秒钟然后返回一个数字1,有了`ListenableFuture`实例,可以执行此Future并执行Future完成之后的回调函数。 + +```java + Futures.addCallback(listenableFuture, new FutureCallback() { + @Override + public void onSuccess(Integer result) { + //成功执行... + System.out.println("Get listenable future's result with callback " + result); + } + + @Override + public void onFailure(Throwable t) { + //异常情况处理... + t.printStackTrace(); + } +}); +``` \ No newline at end of file diff --git a/docs/advance/system-design/1-scan-code-login.md b/docs/advance/system-design/1-scan-code-login.md new file mode 100644 index 0000000..4c78f64 --- /dev/null +++ b/docs/advance/system-design/1-scan-code-login.md @@ -0,0 +1,90 @@ +--- +sidebar: heading +title: 扫码登录原理 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,扫码登录原理,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 扫码登录原理 + +今天给大家介绍下扫码登录功能是怎么设计的。 + +扫码登录功能主要分为三个阶段:**待扫描、已扫描待确认、已确认**。 + +整体流程图如图。 + +![](http://img.topjavaer.cn/img/整个流程.png) + +下面分阶段来看看设计原理。 + +**1、待扫描阶段** + +首先是待扫描阶段,这个阶段是 PC 端跟服务端的交互过程。 + +每次用户打开PC端登陆请求,系统返回一个**唯一的二维码ID**,并将二维码ID的信息绘制成二维码返回给用户。 + +这里的二维码ID一定是唯一的,后续流程会将二维码ID跟身份信息绑定,不唯一的话就会造成你登陆了其他用户的账号或者其他用户登陆你的账号。 + +此时在 PC 端会启动一个定时器,**轮询查询二维码是否被扫描**。 + +如果移动端未扫描的话,那么一段时间后二维码将会失效。 + +这个阶段的交互过程如下图所示。 + +![](http://img.topjavaer.cn/img/第一阶段.png) + +**2、已扫描待确认阶段** + +第二个阶段是已扫描待确认阶段,主要是移动端跟服务端交互的过程。 + +首先移动端扫描二维码,获取二维码 ID,然后**将手机端登录的凭证(token)和 二维码 ID 作为参数发送给服务端** + +此时的手机在之前已经是登录的,不存在没登录的情况。 + +服务端接受请求后,会将 token 与二维码 ID 关联,然后会生成一个临时token,这个 token 会返回给移动端,临时 token 用作确认登录的凭证。 + +PC 端的定时器,会轮询到二维码的状态已经发生变化,会将 PC 端的二维码更新为已扫描,请在手机端确认。 + +**这里为什么要有手机端确认的操作?** + +假设没有确认这个环节,很容易就会被坏人拦截token去冒充登录。所以二维码扫描一定要有这个确认的页面,让用户去确认是否进行登录。 + +另外,二维码扫描确认之后,再往用户app或手机等发送登录提醒的通知,告知如果不是本人登录的,则建议用户立即修改密码。 + +这个阶段是交互过程如下图所示。 + +![](http://img.topjavaer.cn/img/20220411002823.png) + +**3、已确认** + +扫码登录的最后阶段,用户点击确认登录,移动端携带上一步骤中获取的临时 token访问服务端。 + +服务端校对完成后,会更新二维码状态,并且给 PC 端生成一个正式的 token。 + +后续 PC 端就是持有这个 token 访问服务端。 + +这个阶段是交互过程如下图所示。 + +![](http://img.topjavaer.cn/img/20220411002832.png) + +以上就是整个扫码登录功能的详细设计! + + + + + diff --git a/docs/advance/system-design/10-pdd-visit-statistics.md b/docs/advance/system-design/10-pdd-visit-statistics.md new file mode 100644 index 0000000..04ae5d3 --- /dev/null +++ b/docs/advance/system-design/10-pdd-visit-statistics.md @@ -0,0 +1,70 @@ +--- +sidebar: heading +title: 如何用 Redis 统计用户访问量? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,用户访问量统计,Redis,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +拼多多有数亿的用户,那么对于某个网页,怎么使用Redis来统计一个网站的用户访问数呢? + +1、**Hash** + +哈希是Redis的一种基础数据结构,Redis底层维护的是一个开散列,会把不同的key映射到哈希表上,如果是遇到关键字冲突,那么就会拉出一个链表出来。 + +当一个用户访问的时候,如果用户登陆过,那么我们就使用用户的id,如果用户没有登陆过,那么我们也能够前端页面随机生成一个key用来标识用户 + +当用户访问的时候,我们可以使用HSET命令,key可以选择URI与对应的日期进行拼凑,field可以使用用户的id或者随机标识,value可以简单设置为1。 + +当我们要统计某一个网站某一天的访问量的时候,就可以直接使用HLEN来得到最终的结果了。 + +![](http://img.topjavaer.cn/img/image-20230103192155492.png) + +**优点**:简单,容易实现,查询也是非常方便,数据准确性非常高。 + +**缺点**:占用内存过大。随着key的增多,性能也会下降。网站访问量不高还行,拼多多这种数亿PV的网站肯定顶不住。 + +2、**Bitset** + +我们知道,对于一个32位的int,如果我们只用来记录id,那么只能够记录一个用户,但如果我们转成2进制,每位用来表示一个用户,那么我们就能够一口气表示32个用户,空间节省了32倍! + +对于有大量数据的场景,如果我们使用bitset,那么可以节省非常多的内存。 + +对于没有登陆的用户,我们也可以使用哈希算法,把对应的用户标识哈希成一个数字id。bitset非常的节省内存,假设有1亿个用户,也只需要100000000/8/1024/1024约等于12兆内存。 + +![](http://img.topjavaer.cn/img/image-20230103192209138.png) + +Redis已经为我们提供了SETBIT的方法,使用起来非常的方便,我们可以看看下面的例子。 + +我们在item页面可以不停地使用SETBIT命令,设置用户已经访问了该页面,也可以使用GETBIT的方法查询某个用户是否访问。最后我们通过BITCOUNT可以统计该网页每天的访问数量。 + +![](http://img.topjavaer.cn/img/image-20230103192638814.png) + +**优点**: 占用内存更小,查询方便,可以指定查询某个用户,数据可能略有瑕疵,对于非登陆的用户,可能不同的key映射到同一个id,否则需要维护一个非登陆用户的映射,有额外的开销。 + +**缺点**: 如果用户非常的稀疏,那么占用的内存可能比方法一更大。 + +3、**概率算法** + +对于拼多多这种多个页面都可能非常多访问量的网站,如果所需要的数量不用那么准确,可以使用概率算法。 + +事实上,我们对一个网站的UV的统计,1亿跟1亿零30万其实是差不多的。 + +在Redis中,已经封装了HyperLogLog算法,他是一种基数评估算法。这种算法的特征,一般都是数据不存具体的值,而是存用来计算概率的一些相关数据。 + +![](http://img.topjavaer.cn/img/image-20230103192416865.png) + +当用户访问网站的时候,我们可以使用PFADD命令,设置对应的命令,最后我们只要通过PFCOUNT就能顺利计算出最终的结果,因为这个只是一个概率算法,所以可能存在0.81%的误差。 + +**优点**: 占用内存极小,对于一个key,只需要12kb。对于拼多多这种超多用户的特别适用。 + +**缺点**: 查询指定用户的时候,可能会出错,毕竟存的不是具体的数据。总数也存在一定的误差。 + +上面就是常见的3种适用Redis统计网站用户访问数的方法了。 diff --git a/docs/advance/system-design/11-realtime-subscribe-push.md b/docs/advance/system-design/11-realtime-subscribe-push.md new file mode 100644 index 0000000..a38120e --- /dev/null +++ b/docs/advance/system-design/11-realtime-subscribe-push.md @@ -0,0 +1,127 @@ +--- +sidebar: heading +title: 实时订阅推送设计与实现 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,实时订阅推送设计,消息推送,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 实时订阅推送设计与实现 + +什么是订阅推送?就是用户订阅了优惠劵的推送,在可领取前的一分钟就要把提醒信息推送到用户的app中。具体方案就是到具体的推送时间点了,coupon系统调用消息中心的推送接口,把信息推送出去。 + +下面我们分析一下这个功能的业务情景。假设公司目前注册用户6000W+,比如有一张无门槛的优惠劵下单立减20元,那么抢这张劵的人就会比较多,我们保守估计10W+,百万级别不好说。我们初定为20W万人,那么这20W条推送信息要在一分钟推送完成!并且一个用户是可以订阅多张劵的。所以我们知道了这个订阅功能的有两个突出的难点: + +1、**推送的实效性**:推送慢了,用户会抱怨没有及时通知他们错过了开抢时机。 + +2、**推送的体量大**:爆款的神劵,人人都想抢! + +然而推送体量又会影响到推送的实效性。这真是一个让人头疼的问题! + +那就让我们把问题一个个解决掉吧! + +推送的实效性的问题:当用户在领劵中心订阅了某个劵的领取提醒后,在后台就会生成一条用户的订阅提醒记录,里面记录了在哪个时间点给用户发送推送信息。所以问题就变成了系统如何快速实时选出哪些要推送的记录! + +- 方案1:**MQ的延迟投递**。MQ虽然支持消息的延迟投递,但是不适合用来做精确时间点投递!并且用户执行订阅之后又取消订阅的话,要把发出去的MQ消息delete掉这个操作有点头大,短时间内难以落地!并且用户可以取消之后再订阅,这又涉及到去重的问题。所以MQ的方案否掉。 + +- 方案2:**传统定时任务**。这个相对来说就简单一点,用定时任务是去db里面load用户的订阅提醒记录,从中选出当前可以推送的记录。但有句话说得好任何脱离实际业务的设计都是耍流氓。下面我们就分析一下传统的定时任务到底适不适合我们的这个业务! + +| 能否支持多机同时跑 | 一般不能,同一时刻只能单机跑。 | +| ------------------ | --------------------------------------------- | +| 存储数据源 | 一般是mysql或者其它传统数据库,并且是单表存储 | +| 频率 | 支持秒、分、时、天,一般不能太快 | + + 如上表格所示,可以看到一般传统的定时任务存在以下缺点: + + 1、**性能瓶颈**。只有一台机在处理,在大体量数据面前力不从心! + + 2、**实效性差**。定时任务的频率不能太高,太高会业务数据库造成很大的压力! + + 3、**单点故障**。万一跑的那台机挂了,那整个业务不可用了。这是一个很可怕的事情! + + 所以传统定时任务也不太适合这个业务。 + +那有其他解决方案吗?其实可以对传统的定时任务做一个简单的改造即可!把它变成可以同时多机跑,并且实效性可以精确到秒级,并且拒绝单点故障的定时任务集群!这其中就要借助我们的强大的redis了。 + +- 方案3:**定时任务集群** + +首先我们要定义定时任务集群要解决的三个问题! + +1、**实效性要高** + +2、**吞吐量要大** + +3、**服务要稳定**,不能有单点故障 + +下面是整个定时任务集群的架构图。 + +![](http://img.topjavaer.cn/img/定时任务集群.png) + +架构很简单:我们把用户的订阅推送记录存储到redis集群的sortedSet队列里面,且以提醒用户提醒时间戳作为score值,然后在我们个每业务server里面起一个定时器频率是秒级,我的设定就是1s,然后经过负载均衡之后从某个队列里面获取要推送的用户记录进行推送。下面我们分析以下这个架构。 + +1、性能:除去带宽等其它因素,基本与机器数成线性相关。机器数量越多吞吐量越大,机器数量少时相对的吞吐量就减少。 + +2、实效性:提高到了秒级,效果还可以接受。 + +3、单点故障?不存在的!除非redis集群或者所有server全挂了。 + + + +这里解释一下为什么用redis? + +1. redis 可以作为一个高性能的存储db,性能要比MySQL好很多,并且支持持久化,稳定性好。 + +2. redis SortedSet队列天然支持以时间作为条件排序,完美满足我们选出要推送的记录。 + +既然方案已经有了,怎么实现呢? + +首先我们以user_id作为key,然后mod队列数hash到redis SortedSet队列里面。为什么要这样呢,因为如果用户同时订阅了两张劵并且推送时间很近,这样的两条推送就可以合并成一条,并且这样hash也相对均匀。下面是部分代码的截图: + +![](http://img.topjavaer.cn/img/消息订阅推送1.png) + +然后要决定队列的数量,一般正常来说我们有多少台处理的服务器就定义多少条队列。因为队列太少,会造成队列竞争,太多可能会导致记录得不到及时处理。 + +然而最佳实践是队列数量应该是可动态配置化的,因为线上的集群机器数是会经常变的。大促的时候我们会加机器是不是,并且业务量增长了,机器数也是会增加是不是~。所以我是借用了淘宝的diamond进行队列数的动态配置。 + + ![](http://img.topjavaer.cn/img/消息订阅推送2.png) + + 我们每次从队列里面取多少条记录也是可以动态配置的 + +![](http://img.topjavaer.cn/img/消息订阅推送3.png) + +这样就可以随时根据实际的生产情况调整整个集群的吞吐量。 所以我们的定时任务集群还是具有一个特性就是支持动态调整。 + +最后一个关键组件就是负载均衡了。这个是非常重要的!因为这个做得不好就会可能导致多台机竞争同时处理一个队列,影响整个集群的效率!在时间很紧的情况下我就用了一个简单的方案,利用redis一个自增key,然后 mod 队列数量算法。这样就很大程度上就保证不会有两台机器同时去竞争一条队列。 + +![](http://img.topjavaer.cn/img/消息订阅推送4.png) + +最后我们算一下整个集群的吞吐量 + +10(机器数) \* 2000(一次拉取数) = 20000。然后以MQ的形式把消息推送到消息中心,发MQ是异步的,算上其它处理0.5s。 + +其实发送20W的推送也就是10几s的事情。 + +到这里我们整个定时任务集群就差不多基本落地好了。反过来仔细思考了一下,有以下可以**优化的点**: + +1、加监控, 集群怎么可以木有监控呢,万一出问题有任务堆积怎么办。 + +2、加上可视化界面。 + +3、最好有智能调度,增加任务优先级。确保优先级高的任务先运行。 + +4、资源调度,万一机器数量不够,力不从心,优先保证重要任务执行。 + + + +目前项目已上线,运行稳定。 + + + +> 参考链接:https://www.cnblogs.com/linlinismine/p/9214299.html diff --git a/docs/advance/system-design/12-second-kill-5-point.md b/docs/advance/system-design/12-second-kill-5-point.md new file mode 100644 index 0000000..42f1ddf --- /dev/null +++ b/docs/advance/system-design/12-second-kill-5-point.md @@ -0,0 +1,99 @@ +--- +sidebar: heading +title: 秒杀系统设计的5个要点 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,秒杀系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 秒杀系统设计的5个要点 + +## 秒杀系统涉及到的知识点 + +- 高并发,cache,锁机制 +- 基于缓存架构redis,Memcached的先进先出队列。 +- 稍微大一点的秒杀,肯定是分布式的集群的,并发来自于多个节点的JVM,synchronized所有在JVM上加锁是不行了 +- 数据库压力 +- 秒杀超卖问题 +- 如何防止用户来刷, 黑名单?IP限制? +- 利用memcached的带原子性特性的操作做并发控制 + +## 秒杀简单设计方案 + +比如有10件商品要秒杀,可以放到缓存中,读写时不要加锁。 当并发量大的时候,可能有25个人秒杀成功,这样后面的就可以直接抛秒杀结束的静态页面。进去的25个人中有15个人是不可能获得商品的。所以可以根据进入的先后顺序只能前10个人购买成功。后面15个人就抛商品已秒杀完。 + +比如某商品10件物品待秒。假设有100台web服务器(假设web服务器是Nginx + Tomcat),n台app服务器,n个数据库 + +第一步 如果Java层做过滤,可以在每台web服务器的业务处理模块里做个计数器AtomicInteger(10)=待秒商品总数,decreaseAndGet()>=0的继续做后续处理,<0的直接返回秒杀结束页面,这样经过第一步的处理只剩下100台*10个=1000个请求。 + +第二步,memcached 里以商品id作为key的value放个10,每个web服务器在接到每个请求的同时,向memcached服务器发起请求,利用memcached的decr(key,1)操作返回值>=0的继续处理,其余的返回秒杀失败页面,这样经过第二步的处理只剩下100台中最快速到达的10个请求。 + +第三步,向App服务器发起下单操作事务。 + +第四步,App服务器向商品所在的数据库请求减库存操作(操作数据库时可以 "update table set count=count-1 where id=商品id and count>0;" update 成功记录数为1,再向订单数据库添加订单记录,都成功后提交整个事务,否则的话提示秒杀失败,用户进入支付流程。 + +## 看看淘宝的秒杀 + +一、前端 + +面对高并发的抢购活动,前端常用的三板斧是【扩容】【静态化】【限流】 + +**扩容**:加机器,这是最简单的方法,通过增加前端池的整体承载量来抗峰值。 + +**静态化**:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。 + +**限流**:一般都会采用IP级别的限流,即针对某一个IP,限制单位时间内发起请求数量。或者活动入口的时候增加游戏或者问题环节进行消峰操作。 + +**有损服务**:最后一招,在接近前端池承载能力的水位上限的时候,随机拒绝部分请求来保护活动整体的可用性。 + +二、那么后端的数据库在高并发和超卖下会遇到什么问题呢 + +- 首先MySQL自身对于高并发的处理性能就会出现问题,一般来说,MySQL的处理性能会随着并发thread上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单thread的性能还要差。 +- 其次,超卖的根结在于减库存操作是一个事务操作,需要先select,然后insert,最后update -1。最后这个-1操作是不能出现负数的,但是当多用户在有库存的情况下并发操作,出现负数这是无法避免的。 +- 最后,当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢InnoDB行锁的问题,导致出现互相等待甚至死锁,从而大大降低MySQL的处理性能,最终导致前端页面出现超时异常。 + +针对上述问题,如何解决呢? 淘宝的高大上解决方案: + +I:**关闭死锁检测**,提高并发处理性能。 + +II:**修改源代码**,将排队提到进入引擎层前,降低引擎层面的并发度。 + +III:**组提交**,降低server和引擎的交互次数,降低IO消耗。 + +**解决方案1:**将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。 + +优点:解决性能问题 + +缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。 + +**解决方案2:**引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。 + +优点:解决超卖问题,略微提升性能。 + +缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。 + +**解决方案3:**将写操作前移到MC中,同时利用MC的轻量级的锁机制CAS来实现减库存操作。 + +优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。 + +缺点:没有实测,基于CAS的特性不知道高并发下是否会出现大量更新失败?不过加锁之后肯定对并发性能会有影响。 + +**解决方案4:**将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作,同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。 + +优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。 + +缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。 + +**总结** + +1、前端三板斧【扩容】【限流】【静态化】 + +2、后端两条路【内存】+【排队】 + diff --git a/docs/advance/system-design/13-permission-system.md b/docs/advance/system-design/13-permission-system.md new file mode 100644 index 0000000..e953920 --- /dev/null +++ b/docs/advance/system-design/13-permission-system.md @@ -0,0 +1,188 @@ +--- +sidebar: heading +title: 全网最全的权限系统设计方案 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,权限系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 全网最全的权限系统设计方案 + +今天和大家聊聊权限系统设计常见的方案。 + +## **1、为什么需要权限管理** + +日常工作中权限的问题时时刻刻伴随着我们,程序员新入职一家公司需要找人开通各种权限,比如网络连接的权限、编码下载提交的权限、监控平台登录的权限、运营平台查数据的权限等等。 + +**在很多时候我们会觉得这么多繁杂的申请给工作带来不便,并且如果突然想要查一些数据,发现没有申请过权限,需要再走审批流程,时间拉得会很长。那为什么还需要这么严格的权限管理呢?** + +举个例子,一家支付公司有运营后台,运营后台可以查到所有的商户信息,法人代表信息,交易信息以及费率配置信息,如果我们把这些信息不加筛选都给到公司的每一个小伙伴,那么跑市场的都可以操作商家的费率信息,如果一个不小心把费率改了会造成巨大的损失。 + +又比如商户的信息都是非常隐秘的,有些居心不良的小伙伴把这些信息拿出来卖给商家的竞争对手,会给商家造成严重的不良后果。虽然这么做都是个别人人为的过错,但是制度上如果本身这些信息不开放出来就能在很大程度上避免违法乱纪的事情发生了。 + +总体来讲**权限管理是公司数据安全的重要保证,针对不同的岗位,不同的级别看到的数据是不一样的,操作数据的限制也是不一样的。**比如涉及到资金的信息只开放给财务的相关岗位,涉及到配置的信息只开放给运营的相关岗位,这样各司其职能避免很多不必要的安全问题。 + +> 如何让各个岗位的人在系统上各司其职,就是权限管理要解决的问题。 + +## **2、权限模型** + +### **2.1 权限设计** + +从业务分类上来讲权限可以分为数据查看权限,数据修改权限等,对应到系统设计中有页面权限、菜单权限、按钮权限等。菜单也分一级菜单、二级菜单甚至三级菜单,以csdn文章编辑页面左侧菜单栏为例是分了两级菜单。菜单对应的页面里又有很多按钮,我们在设计的时候最好把权限设计成树形结构,这样在申请权限的时候就可以一目了然的看到菜单的结构,需要哪些权限就非常的明了了。 + +如下图所示: + +![](http://img.topjavaer.cn/img/权限系统1.png) + +按照这个架构,按钮的父级是二级菜单,二级菜单的父级是一级菜单,这样用户申请权限的时候非常清晰的看到自己需要哪些权限。 + +### **2.2 为什么需要角色** + +权限结构梳理清晰之后,需要思考怎么把权限分配给用户,用户少的情况下,可以直接分配,一个用户可以有多个权限,统一一个权限可以被多个用户拥有,用户-权限的模型结构如下所示: + +![](http://img.topjavaer.cn/img/权限系统2.png) + +这种模型能够满足权限的基本分配能力,但是随着用户数量的增长,这种模型的弊端就凸显出来了,每一个用户都需要去分配权限,非常的浪费管理员的时间和精力,并且用户和权限杂乱的对应关系会给后期带来巨大的维护成本。用户-权限对应关系图: + +![](http://img.topjavaer.cn/img/权限系统3.png) + +这种对应关系在用户多的情况下基本无法维护了。其实很多用户负责同一个业务模块所需要的权限是一样的,这样的话我们是不是可以借助第三个媒介,把需要相同的权限都分配给这个媒介,然后用户和媒介关联起来,用户就拥有了媒介的权限了。这就是经典的RBAC模型,其中媒介就是我们通常所说的角色。 + +### **2.3 权限模型的演进** + +#### 2.3.1 RBAC模型 + +有了角色之后可以把权限分配给角色,需要相同权限的用户和角色对应起来就可以了,一个权限可以分配给多个角色,一个角色可以拥有多个权限,同样一个用户可以分配多个角色,一个角色也可以对应多个用户,对应模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统4.png) + +这就是经典的RBAC模型了(role-based-access-control),在这里面角色起到了桥梁左右,连接了用户和权限的关系,每个角色可以拥有多个权限,每个用户可以分配多个角色,这样用户就拥有了多个角色的多个权限。 + +同时因为有角色作为媒介,大大降低了错综复杂的交互关系,比如一家有上万人的公司,角色可能只需要几百个就搞定了,因为很多用户需要的权限是一样的,分配一样的角色就可以了。这种模型的对应关系图如下所示: + +![](http://img.topjavaer.cn/img/权限系统5.png) + +用户和角色,角色和权限都是多对多的关系,这种模型是最通用的权限管理模型,节省了很大的权限维护成本, 但是实际的业务千变万化,权限管理的模型也需要根据不同的业务模型适当的调整,比如一个公司内部的组织架构是分层级的,层级越高权限越大,因为层级高的人不仅要拥有自己下属拥有的权限,二期还要有一些额外的权限。 + +RBAC模型可以给不同层级的人分配不同的角色,层级高的对应角色的权限就多,这样的处理方式可以解决问题,但是有没有更好的解决办法呢,答案肯定是有的,这就引出**角色继承的RBAC模型**。 + +#### 2.3.2 角色继承的RBAC模型 + +角色继承的RBAC模型又称RBAC1模型。每个公司都有自己的组织架构,比如公司里管理财务的人员有财务总监、财务主管、出纳员等,财务主管需要拥有但不限于出纳员的权限,财务总监需要拥有但不限于财务主管的权限,像这种管理关系向下兼容的模式就需要用到角色继承的RBAC模型。**角色继承的RBAC模型的思路是上层角色继承下层角色的所有权限,并且可以额外拥有其他权限。** + +模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统6.png) + +从模型图中可以看出下级角色拥有的权限,上级角色都拥有,并且上级角色可以拥有其他的权限。角色的层级关系可以分为两种,一种是下级角色只能拥有一个上级角色,但是上级角色可以拥有多个下级角色,这种结构用图形表示是一个树形结构,如下图所示: + +![](http://img.topjavaer.cn/img/权限系统7.png) + +还有一种关系是下级角色可以拥有多个上级角色,上级角色也可以拥有多个下级角色,这种结构用图形表示是一个有向无环图,如下图所示: + +![](http://img.topjavaer.cn/img/权限系统8.png) + +树形图是我们比较常用的,因为一个用户一般情况下不会同时有多个直属上级,比如财务部只能有一个财务总监,但是可以有多个财务主管和收纳员。 + +#### 2.3.3 带约束的RBAC模型 + +带约束的RBAC模型又成RBAC2模型。在实际工作中,为了安全的考虑会有很多约束条件,比如财务部里同一个人不能即是会计又是审核员,跟一个人同一时间不能即是运动员又是裁判员是一个道理的,又比如财务部的审核员不能超过2个,不能1个也没有。因为角色和权限是关联的,所以我们做好角色的约束就可以了。 + +> 常见的约束条件有:角色互斥、基数约束、先决条件约束等。 + +**角色互斥:** 如果角色A和角色B是互斥关系的话,那么一个用户同一时间不能即拥有角色A,又拥有角色B,只能拥有其中的一个角色。 + +> 比如我们给一个用户赋予了会计的角色就不能同时再赋予审核员的角色,如果想拥有审核员的角色就必须先去掉会计的角色。假设提交角色和审核角色是互质的,我们可以用图形表示: + +![](http://img.topjavaer.cn/img/权限系统9.png) + +**基数约束:** 同一个角色被分配的用户数量可以被限制,比如规定拥有超级管理员角色的用户有且只有1个;用户被分配的角色数量也需要被限制,角色被分配的权限数量也可以被限制。 + +**先决条件约束:**用户想被赋予上级角色,首先需要拥有下级角色,比如技术负责人的角色和普通技术员工角色是上下级关系,那么用户想要用户技术负责人的角色就要先拥有普通技术员工的角色。 + +### **2.4 用户划分** + +#### 2.4.1 用户组 + +我们创建角色是为了解决用户数量大的情况下,用户分配权限繁琐以及用户-权限关系维护成本高的问题。抽象出一个角色,把需要一起操作的权限分配给这个角色,把角色赋予用户,用户就拥有了角色上的权限,这样避免了一个个的给用户分配权限,节省了大量的资源。 + +同样的如果有一批用户需要相同的角色,我们也需要一个个的给用户分配角色,比如一个公司的客服部门有500多个人,有一天研发部研发了一套查询后台数据的产品,客服的小伙伴都需要使用,但是客服由于之前并没有统一的一个角色给到所有的客服小伙伴,这时候需要新加一个角色,把权限分配给该角色,然后再把角色一个个分配给客服人员,这时候会发现给500个用户一个个添加角色非常的麻烦。但是客服人员又有共同的属性,所以我们可以创建一个用户组,所有的客服人员都属于客服用户组,把角色分配给客服用户组,这个用户组下面的所有用户就拥有了需要的权限。 + +RBAC模型添加用户组之后的模型图如下所示: + +![](http://img.topjavaer.cn/img/权限系统10.png)很多朋友会问,用户组和角色有什么区别呢?简单的来说,**用户组是一群用户的组合,而角色是用户和权限之间的桥梁。** 用户组把相同属性的用户组合起来,比如同一个项目的开发、产品、测试可以是一个用户组,同一个部门的相同职位的员工可以是一个用户组, 一个用户组可以是一个职级,可以是一个部门,可以是一起做事情的来自不同岗位的人。 + +用户可以分组,权限也可以分组,权限特别多的情况下,可以把一个模块的权限组合起来成为一个权限组,权限组也是解决权限和角色对应关系复杂的问题。 + +比如我们定义权限的时候一级菜单、二级菜单、按钮都可以是权限,一个一级菜单下面有几十个二级菜单,每个二级菜单下面又有几十个按钮,这时候我们把权限一个个分配给角色也是非常麻烦的,可以采用分组的方法把权限分组,然后把分好的组赋予角色就可以了。 + +给权限分组也是个技术活,需要理清楚权限之间的关系,比如支付的运营后台我们需要查各种信息,账务的数据、订单的数据、商户的数据等等,这些查询的数据并不在一个页面,每个页面也有很多按钮,我们可以把这几个页面以及按钮对应的权限组合成一个权限组赋予角色。加入权限组之后的RBAC模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统11.png) + +实际工作中我们很少给权限分组,给用户分组的场景会多一些,有的时候用户组也可以直接和权限关联,这个看实际的业务场景是否需要,权限模型没有统一的,业务越复杂业务模型会约多样化。 + +#### 2.4.2 组织 + +每个公司都有自己的组织架构,很多时候权限的分配可以根据组织架构来划分。因为同一个组织内的小伙伴使用的大部分权限是一样的。如下所示一个公司的组织架构图: + +![](http://img.topjavaer.cn/img/权限系统12.png) + +按照这个组织架构,每一个组织里的成员使用的基础权限很可能是一样的,比如人力资源都需要看到人才招聘的相关信息,市场推广都需要看到行业分析的相关信息,按照组织来分配角色会有很多优势: + +**实现权限分配的自动化:** 和组织关系打通之后,按照组织来分配角色,如果有新入职的用户,被划分在某个组织下面之后,会自动获取该组织下所有的权限,无需人工分配。又比如有用户调岗,只需要把组织关系调整就可以了,权限会跟着组织关系自动调整,也无需人工干预。这么做首先需要把权限和组织关系打通。 + +**控制数据权限:** 把角色关联到组织,组织里的成员只能看到本组织下的数据,比如市场推广和大客定制,市场推广针对的是零散的客户,大可定制针对的是有一定体量的客户,相互的数据虽然在一个平台,但是只能看自己组织下的数据。 + +加入组织之后的RBAC模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统13.png) + +用户可以在多个组织中,因为组织也有层级结构,一个组织里只可以有多个用户,所以用户和组织的关系是多对多的关系,组织和角色的关系是一对一的关系。这个在工作中可以根据实际情况来确定对应关系。 + +#### 2.4.3 职位 + +一个组织下面会有很多职位,比如财务管理会有财务总监、财务主管、会计、出纳员等职位,每个职位需要的权限是不一样的,可以像组织那样根据职位来分配不同的角色,由于一个人的职位是固定的,所以用户跟职位的对应关系时一对一的关系,职位跟角色的对应关系可以是多对多的关系。加入职位的RBAC模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统14.png) + +### **2.5 理想的RBAC模型** + +RBAC模型根据不同业务场景的需要会有很多种演变,实际工作中业务是非常复杂的,权限分配也是非常复杂的,想要做出通用且高效的模型很困难。我们把RBAC模型的演变汇总起来会是一个支撑大数据量以及复杂业务的理想的模型。把RBAC、RBAC1、RBAC2、用户组、组织、职位汇总起来的模型如下所示: + +![](http://img.topjavaer.cn/img/权限系统15.png) + +按照这个模型基本上能够解决所有的权限问题,其中的对应关系可以根据实际的业务情况来确定,一般情况下,组织和职位是一对多的关系,特殊情况下可以有多对多的情况,需要根据实际情况来定。 + +理想的RBAC模型并不是说我们一开始建权限模型就可以这么做,而是数据体量、业务复杂度达到一定程度之后可以使用这个模型来解决权限的问题,如果数据量特别少,比如刚成立的公司只有十几个人,那完全可以用用户-权限模型,都没有必要使用RBAC模型。 + +**3、权限系统表设计** + +### **3.1 标准RBAC模型表设计** + +标准RBAC模型的表是比较简单了,要表示`用户-角色-权限`三者之前的关系,首先要创建用户表、角色表、权限表,用户和角色是多对多的关系,角色和权限是多对多的关系,需要再创建两章关系表,分别是用户-角色关系表和角色-权限关系表。这六张表的ER图如下所示: + +![](http://img.topjavaer.cn/img/权限系统16.png) + +### **3.2 理想RBAC模型表设计** + +理想的RBAC模型是标准RBAC模型经过多次扩展得到的,表结构也会比较复杂,因为要维护很多关系,如下图所示是理想的RBAC模型的ER图: + +![](http://img.topjavaer.cn/img/权限系统17.png) + +这里面需要强调的是角色互斥表,互斥的关系可以放在角色上,也可以放在权限上,看实际工作的需求。 + +4、结语 + +本文从易到难非常详细的介绍了权限模型的设计,在工作中需要根据实际情况来定义模型,千人以内的公司使用RBAC模型是完全够用的,没有必要吧权限模型设计的过于复杂。模型的选择要根据具体情况,比如公司体量、业务类型、人员数量等。总之最适合自己公司的模型就是最好的模型,权限模式和设计模式是一样的,都是为了更好的解决问题,不要为了使用模型而使用模型。 + + + +> 原文:blog.csdn.net/u010482601/article/details/104989532 diff --git a/docs/advance/system-design/15-red-packet.md b/docs/advance/system-design/15-red-packet.md new file mode 100644 index 0000000..040506d --- /dev/null +++ b/docs/advance/system-design/15-red-packet.md @@ -0,0 +1,139 @@ +--- +sidebar: heading +title: 如何设计一个抢红包系统 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,抢红包系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 如何设计一个抢红包系统 + +## 前言 + +本篇分享如何设计一个抢红包系统,希望对大家有所帮助。主要展示抢红包系统的设计,红包算法不是重点,所以没有二倍均值法之类的实现。 + +## 需求分析 + +常见的红包系统,由用户指定金额、红包总数来完成红包的创建,然后通过某个入口将红包下发至目标用户,用户看到红包后,点击红包,随机获取红包,最后,用户可以查看自己抢到的红包。整个业务流程不复杂,难点在于抢红包这个行为可能有很高的并发。所以,系统设计的优化点主要关注在抢红包这个行为上。 + +由于查看红包过于简单,所以本文不讨论。那么系统用例就只剩下`发、抢`两种。 + +1. 发红包:用户设置红包总金额、总数量 +2. 抢红包:用户从总红包中随机获得一定金额 + +没什么好说的,相信大家的微信红包没少抢,一想都明白。看起来业务很简单,却其实还有点小麻烦。首先,抢红包必须保证高可用,不然用户会很愤怒。其次,必须保证系统数据一致性不能超发,不然抢到红包的用户收不到钱,用户会很愤怒。最后一点,系统可能会有很高的并发。 + +OK,分析完直接进行详细设计。所以简简单单只有两个接口: + +1. 发红包 +2. 抢红包 + +## 表结构设计 + +这里直接给出建表语句: + +红包活动表: + +```sql +CREATE TABLE `t_redpack_activity` +( + `id` bigint(20) NOT NULL COMMENT '主键', + `total_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '总金额', + `surplus_amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '剩余金额', + `total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包总数', + `surplus_total` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包剩余总数', + `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号', + `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8; +复制代码 +``` + +红包表: + +```sql +CREATE TABLE `t_redpack` +( + `id` bigint(20) NOT NULL COMMENT '主键', + `activity_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '红包活动ID', + `amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额', + `status` TINYINT(4) NOT NULL DEFAULT 0 COMMENT '红包状态 1可用 2不可用', + `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8; +复制代码 +``` + +明细表: + +```sql +CREATE TABLE `t_redpack_detail` +( + `id` bigint(20) NOT NULL COMMENT '主键', + `amount` decimal(10, 2) NOT NULL DEFAULT '0.00' COMMENT '金额', + `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户编号', + `redpack_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '红包编号', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8; +复制代码 +``` + +活动表,就是你发了多少个红包,并且需要维护剩余金额。明细表是用户抢到的红包明细。红包表是每一个具体的红包信息。为什么需要三个表呢?事实上如果没有红包表也是可以的。但我们的方案`预先分配红包`需要使用一张表来记录红包的信息,所以设计的时候才有此表。 + +OK,分析完表结构其实方案已经七七八八差不多了。请接着看下面的方案,从简单到复杂的过度。 + +## 基于分布式锁的实现 + +![](http://img.topjavaer.cn/img/抢红包1.png) + +基于分布式锁的实现最为简单粗暴,整个抢红包接口以`activityId`作为`key`进行加锁,保证同一批红包抢行为都是串行执行。分布式锁的实现是由`spring-integration-redis`工程提供,核心类是`RedisLockRegistry`。锁通过`Redis`的`lua`脚本实现,且实现了阻塞式本地可重入。 + +## 基于乐观锁的实现 + +![](http://img.topjavaer.cn/img/抢红包2.png) + +第二种方式,为红包活动表增加乐观锁版本控制,当多个线程同时更新同一活动表时,只有一个 clien 会成功。其它失败的 client 进行循环重试,设置一个最大循环次数即可。此种方案可以实现并发情况下的处理,但是冲突很大。因为每次只有一个人会成功,其他 client 需要进行重试,即使重试也只能保证一次只有一个人成功,因此 TPS 很低。当设置的失败重试次数小于发放的红包数时,可能导致最后有人没抢到红包,实际上还有剩余红包。 + +## 基于悲观锁的实现 + +![](http://img.topjavaer.cn/img/抢红包3.png) + +由于红包活动表增加乐观锁冲突很大,所以可以考虑使用使用悲观锁:`select * from t_redpack_activity where id = #{id} for update`,注意悲观锁必须在事务中才能使用。此时,所有的抢红包行为变成了串行。此种情况下,悲观锁的效率远大于乐观锁。 + +## 预先分配红包,基于乐观锁的实现 + +![](http://img.topjavaer.cn/img/抢红包4.png) + +可以看到,如果我们将乐观锁的维度加在红包明细上,那么冲突又会降低。因为之前红包明细是用户抢到后才创建的,那么现在需要预先分配红包,即创建红包活动时即生成 N 个红包,通过状态来控制可用/不可用。这样,当多个 client 抢红包时,获取该活动下所有可用的红包明细,随机返回其中一条然后再去更新,更新成功则代表用户抢到了该红包,失败则代表出现了冲突,可以循环进行重试。如此,冲突便被降低了。 + +## 基于 Redis 队列的实现 + +![](http://img.topjavaer.cn/img/抢红包5.png) + +和上一个方案类似,不过,用户发放红包时会创建相应数量的红包,并且加入到 Redis 队列中。抢红包时会将其弹出。`Redis`队列很好的契合了我们的需求,每次弹出都不会出现重复的元素,用完即销毁。缺陷:抢红包时一旦从队列弹出,此时系统崩溃,恢复后此队列中的红包明细信息已丢失,需要人工补偿。 + +## 基于 Redis 队列,异步入库 + +![](http://img.topjavaer.cn/img/抢红包6.png) + +这种方案的是抢到红包后不操作数据库,而是保存持久化信息到`Redis`中,然后返回成功。通过另外一个线程`UserRedpackPersistConsumer`,拉取持久化信息进行入库。需要注意的是,此时的拉取动作如果使用普通的`pop`仍然会出现`crash point`的问题,所以考虑到可用性,此处使用`Redis`的`BRPOPLPUSH`操作,弹出元素后加入备份到另外一个队列,保证此处崩溃后可以通过备份队列自动恢复。崩溃恢复线程`CrashRecoveryThread`通过定时拉取备份信息,去 DB 中查证是否持久化成功,如果成功则清除此元素,否则进行补偿并清除此元素。如果在操作数据库的过程中出现异常会记录错误日志`redpack.persist.log`,此日志使用单独的文件和格式,方便进行补偿(一般不会触发)。 + +## 后语 + +当然,一个健壮的系统可能还要考虑到方方面面。发红包本身如果是数据量特别大的情况要还需要做多副本方案。本文只是演示各种方案的优缺点,仅供参考。另外,如果采用`Redis`则需要做高可用。 + + + +> 参考链接:https://juejin.cn/post/6925947709517987848 diff --git a/docs/advance/system-design/16-mq-design.md b/docs/advance/system-design/16-mq-design.md new file mode 100644 index 0000000..64bd8a7 --- /dev/null +++ b/docs/advance/system-design/16-mq-design.md @@ -0,0 +1,219 @@ +--- +sidebar: heading +title: 如何设计一个消息队列? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,消息队列设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 如何设计一个消息队列? + +**如果让你来设计一个 MQ,该如何下手?需要考虑哪些问题?又有哪些技术挑战?** + +对于 MQ 来说,不管是 RocketMQ、Kafka 还是其他消息队列,**它们的本质都是:一发一存一消费。**下面我们以这个本质作为根,一起由浅入深地聊聊 MQ。 + +# 从 MQ 的本质说起 + +将 MQ 掰开了揉碎了来看,都是「一发一存一消费」,再直白点就是一个「转发器」。 + +生产者先将消息投递一个叫做「队列」的容器中,然后再从这个容器中取出消息,最后再转发给消费者,仅此而已。 + +![](http://img.topjavaer.cn/img/20230115171219.png) + +上面这个图便是消息队列最原始的模型,它包含了两个关键词:消息和队列。 + +> 1、消息:就是要传输的数据,可以是最简单的文本字符串,也可以是自定义的复杂格式(只要能按预定格式解析出来即可)。 +> +> 2、队列:大家应该再熟悉不过了,是一种先进先出数据结构。它是存放消息的容器,消息从队尾入队,从队头出队,入队即发消息的过程,出队即收消息的过程。 + +# 原始模型的进化 + +再看今天我们最常用的消息队列产品(RocketMQ、Kafka 等等),你会发现:它们都在最原始的消息模型上做了扩展,同时提出了一些新名词,比如:主题(topic)、分区(partition)、队列(queue)等等。 + +要彻底理解这些五花八门的新概念,我们化繁为简,先从消息模型的演进说起(**道理好比:架构从来不是设计出来的,而是演进而来的**) + +## 2.1 队列模型 + +最初的消息队列就是上一节讲的原始模型,它是一个严格意义上的队列(Queue)。消息按照什么顺序写进去,就按照什么顺序读出来。不过,队列没有 “读” 这个操作,读就是出队,从队头中 “删除” 这个消息。 + +![](http://img.topjavaer.cn/img/20230115171317.png) + +这便是队列模型:它允许多个生产者往同一个队列发送消息。但是,如果有多个消费者,实际上是竞争的关系,也就是一条消息只能被其中一个消费者接收到,读完即被删除。 + +## 2.2 发布-订阅模型 + +如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。 + +一个可行的方案是:为每个消费者创建一个单独的队列,让生产者发送多份。这种做法比较笨,而且同一份数据会被复制多份,也很浪费空间。 + +为了解决这个问题,就演化出了另外一种消息模型:发布-订阅模型。 + +![](http://img.topjavaer.cn/img/20230115171400.png) + +在发布-订阅模型中,存放消息的容器变成了 “主题”,订阅者在接收消息之前需要先 “订阅主题”。最终,每个订阅者都可以收到同一个主题的全量消息。 + +仔细对比下它和 “队列模式” 的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费。 + +## 2.3 小结 + +最后做个小结,上面两种模型说白了就是:单播和广播的区别。而且,当发布-订阅模型中只有 1 个订阅者时,它和队列模型就一样了,因此在功能上是完全兼容队列模型的。 + +这也解释了为什么现代主流的 RocketMQ、Kafka 都是直接基于发布-订阅模型实现的?此外,RabbitMQ 中之所以有一个 Exchange 模块?其实也是为了解决消息的投递问题,可以变相实现发布-订阅模型。 + +包括大家接触到的 “消费组”、“集群消费”、“广播消费” 这些概念,都和上面这两种模型相关,以及在应用层面大家最常见的情形:组间广播、组内单播,也属于此范畴。 + +所以,先掌握一些共性的理论,对于大家再去学习各个消息中间件的具体实现原理时,其实能更好地抓住本质,分清概念。 + +# 透过模型看 MQ 的应用场景 + +目前,MQ 的应用场景非常多,大家能倒背如流的是:系统解耦、异步通信和流量削峰。除此之外,还有延迟通知、最终一致性保证、顺序消息、流式处理等等。 + +那到底是先有消息模型,还是先有应用场景呢?答案肯定是:先有应用场景(也就是先有问题),再有消息模型,因为消息模型只是解决方案的抽象而已。 + +MQ 经过 30 多年的发展,能从最原始的队列模型发展到今天百花齐放的各种消息中间件(平台级的解决方案),我觉得万变不离其宗,还是得益于:消息模型的适配性很广。 + +我们试着重新理解下消息队列的模型。它其实解决的是:生产者和消费者的通信问题。那它对比 RPC 有什么联系和区别呢? + +![](http://img.topjavaer.cn/img/20230115171444.png) + +通过对比,能很明显地看出两点差异: + +> 1、引入 MQ 后,由之前的一次 RPC 变成了现在的两次 RPC,而且生产者只跟队列耦合,它根本无需知道消费者的存在。 +> +> 2、多了一个中间节点「队列」进行消息转储,相当于将同步变成了异步。 + +再返过来思考 MQ 的所有应用场景,就不难理解 MQ 为什么适用了?因为这些应用场景无外乎都利用了上面两个特性。 + +举一个实际例子,比如说电商业务中最常见的「订单支付」场景:在订单支付成功后,需要更新订单状态、更新用户积分、通知商家有新订单、更新推荐系统中的用户画像等等。 + +![](http://img.topjavaer.cn/img/20230115171531.png) + +引入 MQ 后,订单支付现在只需要关注它最重要的流程:更新订单状态即可。其他不重要的事情全部交给 MQ 来通知。这便是 MQ 解决的最核心的问题:系统解耦。 + +改造前订单系统依赖 3 个外部系统,改造后仅仅依赖 MQ,而且后续业务再扩展(比如:营销系统打算针对支付用户奖励优惠券),也不涉及订单系统的修改,从而保证了核心流程的稳定性,降低了维护成本。 + +这个改造还带来了另外一个好处:因为 MQ 的引入,更新用户积分、通知商家、更新用户画像这些步骤全部变成了异步执行,能减少订单支付的整体耗时,提升订单系统的吞吐量。这便是 MQ 的另一个典型应用场景:异步通信。 + +除此以外,由于队列能转储消息,对于超出系统承载能力的场景,可以用 MQ 作为 “漏斗” 进行限流保护,即所谓的流量削峰。 + +我们还可以利用队列本身的顺序性,来满足消息必须按顺序投递的场景;利用队列 + 定时任务来实现消息的延时消费 …… + +MQ 其他的应用场景基本类似,都能回归到消息模型的特性上,找到它适用的原因,这里就不一一分析了。 + +总之,就是建议大家多从复杂多变的实践场景再回归到理论层面进行思考和抽象,这样能吃得更透。 + +# 如何设计一个 MQ? + +了解了上面这些理论知识以及应用场景后,下面我们再一起看下:到底如何设计一个 MQ? + +## 4.1 MQ 的雏形 + +我们还是先从简单版的 MQ 入手,如果只是实现一个很粗糙的 MQ,完全不考虑生产环境的要求,该如何设计呢? + +文章开头说过,任何 MQ 无外乎:一发一存一消费,这是 MQ 最核心的功能需求。另外,从技术维度来看 MQ 的通信模型,可以理解成:两次 RPC + 消息转储。 + +有了这些理解,我相信只要有一定的编程基础,不用 1 个小时就能写出一个 MQ 雏形: + +> 1、直接利用成熟的 RPC 框架(Dubbo 或者 Thrift),实现两个接口:发消息和读消息。 +> +> 2、消息放在本地内存中即可,数据结构可以用 JDK 自带的 ArrayBlockingQueue 。 + +## 4.2 写一个适用于生产环境的 MQ + +当然,我们的目标绝不止于一个 MQ 雏形,而是希望实现一个可用于生产环境的消息中间件,那难度肯定就不是一个量级了,具体我们该如何下手呢? + +**1、先把握这个问题的关键点** + +假如我们还是只考虑最基础的功能:发消息、存消息、消费消息(支持发布-订阅模式)。 + +那在生产环境中,这些基础功能将面临哪些挑战呢?我们能很快想到下面这些: + +> 1、高并发场景下,如何保证收发消息的性能? +> +> 2、如何保证消息服务的高可用和高可靠? +> +> 3、如何保证服务是可以水平任意扩展的? +> +> 4、如何保证消息存储也是水平可扩展的? +> +> 5、各种元数据(比如集群中的各个节点、主题、消费关系等)如何管理,需不需要考虑数据的一致性? + +可见,高并发场景下的三高问题在你设计一个 MQ 时都会遇到,「如何满足高性能、高可靠等非功能性需求」才是这个问题的关键所在。 + +**2、整体设计思路** + +先来看下整体架构,会涉及三类角色: + +![](http://img.topjavaer.cn/img/20230115171655.png) + +另外,将「一发一存一消费」这个核心流程进一步细化后,比较完整的数据流如下: + +![](http://img.topjavaer.cn/img/20230115171717.png) + +基于上面两个图,我们可以很快明确出 3 类角色的作用,分别如下: + +> 1、Broker(服务端):MQ 中最核心的部分,是 MQ 的服务端,核心逻辑几乎全在这里,它为生产者和消费者提供 RPC 接口,负责消息的存储、备份和删除,以及消费关系的维护等。 +> +> 2、Producer(生产者):MQ 的客户端之一,调用 Broker 提供的 RPC 接口发送消息。 +> +> 3、Consumer(消费者):MQ 的另外一个客户端,调用 Broker 提供的 RPC 接口接收消息,同时完成消费确认。 + +**3、详细设计** + +下面,再展开讨论下一些具体的技术难点和可行的解决方案。 + +**难点1:RPC 通信** + +解决的是 Broker 与 Producer 以及 Consumer 之间的通信问题。如果不重复造轮子,直接利用成熟的 RPC 框架 Dubbo 或者 Thrift 实现即可,这样不需要考虑服务注册与发现、负载均衡、通信协议、序列化方式等一系列问题了。 + +当然,你也可以基于 Netty 来做底层通信,用 Zookeeper、Euraka 等来做注册中心,然后自定义一套新的通信协议(类似 Kafka),也可以基于 AMQP 这种标准化的 MQ 协议来做实现(类似 RabbitMQ)。对比直接用 RPC 框架,这种方案的定制化能力和优化空间更大。 + +**难点2:高可用设计** + +高可用主要涉及两方面:Broker 服务的高可用、存储方案的高可用。可以拆开讨论。 + +Broker 服务的高可用,只需要保证 Broker 可水平扩展进行集群部署即可,进一步通过服务自动注册与发现、负载均衡、超时重试机制、发送和消费消息时的 ack 机制来保证。 + +存储方案的高可用有两个思路:1)参考 Kafka 的分区 + 多副本模式,但是需要考虑分布式场景下数据复制和一致性方案(类似 Zab、Raft等协议),并实现自动故障转移;2)还可以用主流的 DB、分布式文件系统、带持久化能力的 KV 系统,它们都有自己的高可用方案。 + +**难点3:存储设计** + +消息的存储方案是 MQ 的核心部分,可靠性保证已经在高可用设计中谈过了,可靠性要求不高的话直接用内存或者分布式缓存也可以。这里重点说一下存储的高性能如何保证?这个问题的决定因素在于存储结构的设计。 + +目前主流的方案是:追加写日志文件(数据部分) + 索引文件的方式(很多主流的开源 MQ 都是这种方式),索引设计上可以考虑稠密索引或者稀疏索引,查找消息可以利用跳转表、二份查找等,还可以通过操作系统的页缓存、零拷贝等技术来提升磁盘文件的读写性能。 + +如果不追求很高的性能,也可以考虑现成的分布式文件系统、KV 存储或者数据库方案。 + +**难点4:消费关系管理 +** + +为了支持发布-订阅的广播模式,Broker 需要知道每个主题都有哪些 Consumer 订阅了,基于这个关系进行消息投递。 + +由于 Broker 是集群部署的,所以消费关系通常维护在公共存储上,可以基于 Zookeeper、Apollo 等配置中心来管理以及进行变更通知。 + +**难点5:高性能设计** + +存储的高性能前面已经谈过了,当然还可以从其他方面进一步优化性能。 + +比如 Reactor 网络 IO 模型、业务线程池的设计、生产端的批量发送、Broker 端的异步刷盘、消费端的批量拉取等等。 + +## 4.3 小结 + +再总结下,要回答好:如何设计一个 MQ? + +1、需要从功能性需求(收发消息)和非功能性需求(高性能、高可用、高扩展等)两方面入手。 + +2、功能性需求不是重点,能覆盖 MQ 最基础的功能即可,至于延时消息、事务消息、重试队列等高级特性只是锦上添花的东西。 + +3、最核心的是:能结合功能性需求,理清楚整体的数据流,然后顺着这个思路去考虑非功能性的诉求如何满足,这才是技术难点所在。 + + + +> 参考:https://toutiao.io/posts/ix9hfyh/preview diff --git a/docs/advance/system-design/17-shopping-car.md b/docs/advance/system-design/17-shopping-car.md new file mode 100644 index 0000000..ff6763b --- /dev/null +++ b/docs/advance/system-design/17-shopping-car.md @@ -0,0 +1,230 @@ +--- +sidebar: heading +title: 购物车系统怎么设计? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,购物车系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 购物车系统怎么设计? + +## 1 主要功能 + +在用户选购商品时,下单前,暂存用户想购买的商品。 + +购物车对数据可靠性要求不高,性能也无特别要求,在整个电商系统是相对容易设计和实现的一个子系统。 + +购物车系统的主要功能: + +- 把商品加入购物车(后文称“加购”) +- 购物车列表页 +- 发起结算下单 +- 在所有界面都要显示的购物车小图标 + +支撑这些功能,存储模型如何设计? + +只要一个“购物车”实体。 + +## 2 主要属性 + +打开京东购物车页面:SKUID(商品ID)、数量、加购时间和勾选状态 + +![](http://img.topjavaer.cn/img/20230116134221.png) + +“勾选状态”属性,即在购物车界面,每件商品前面的那个小对号,表示在结算下单时,是否要包含这件商品。至于商品价格和总价、商品介绍等都能实时从其他系统获取,无需购物车系统保存。 + +购物车功能简单,但设计购物车系统的存储时,仍有一些问题需考虑。 + +## 3 原则 + +### 3.1 思考 + +#### 3.1.1 用户未登录,在浏览器中加购,关闭浏览器再打开,刚才加购的商品还在吗? + +存在。 + +若用户未登录,加购的商品也会被保存在用户的电脑。即使关闭浏览器再打开,购物车的商品仍存在。 + +#### 3.1.2 用户未登录,在浏览器中加购,然后登录,刚才加购的商品还在吗? + +存在。 + +若用户先加购,再登录。登录前加购的商品就会被自动合并到用户名下,所以登录后购物车中仍有登录前加购的商品。 + +#### 3.1.3 关闭浏览器再打开,上一步加购的商品还在吗? + +不存在。 + +关闭浏览器再打开,这时又变为未登录状态,但是之前未登录时加购的商品已经被合并到刚刚登录的用户名下了,所以购物车是空的。 + +#### 3.1.4 再打开手机,用相同的用户登录,第二步加购的商品还在吗? + +存在。使用手机登录相同的用户,看到的就是该用户的购物车,这时无论你在手机App、电脑还是微信中登录,只要相同用户,看到就是同一购物车,所以第2步加购的商品是存在的。 + +若不仔细把这些问题考虑清楚,用户使用购物车时,就会感觉不好用,不是加购的商品莫名其妙丢了,就是购物车莫名其妙多出一些商品。 + +要解决上面这些问题,只要在存储设计时,把握如下 + +### 3.2 原则 + +- • 若未登录,需临时暂存购物车的商品 +- • 用户登录时,把暂存购物车的商品合并到用户购物车,并清除暂存购物车 +- • 用户登录后,购物车中的商品,需在浏览器、手机APP和微信等等这些终端保持同步 + +购物车系统需保存两类购物车: + +- • 未登录情况下的“暂存购物车” +- • 登录后的“用户购物车” + +## 4 “暂存购物车”存储设计 + +### 4.1 保存在客户端or服务端? + +若存在服务端,则每个暂存购物车都得有个全局唯一标识,这不易设计。保存在服务端,还要浪费服务端资源。所以,肯定保存在客户端: + +- • 节约服务器存储资源 +- • 无购物车标识问题每个客户端就保存它自己唯一一个购物车即可,无需标识。 + +客户端存储可选择不多: + +- • Session不太合适。SESSION保留时间短,且SESSION的数据实际上还是保存在服务端 +- • Cookie +- • LocalStorage浏览器的LocalStorage和App的本地存储类似,都以LocalStorage代表。Cookie、LocalStorage都可用来保存购物车数据。 + +选择哪种更好?各有优劣。这场景中,使用Cookie和LocalStorage最关键区别: + +- • 客户端、服务端的每次交互,都会自动带着Cookie数据往返,这样服务端可读写客户端Cookie中的数据 +- • LocalStorage里的数据,只能由客户端访问 + +使用Cookie存储,实现简单,加减购物车、合并购物车过程,由于服务端可读写Cookie,这样全部逻辑都可在服务端实现,并且客户端和服务端请求的次数也相对少。 + +使用LocalStorage存储,实现相对复杂,客户端和服务端都要实现业务逻辑,但LocalStorage好在其存储容量比Cookie的4KB上限大得多,而且不用像Cookie那样,无论用不用,每次请求都要带着,可节省带宽。 + +所以,选择Cookie或LocalStorage存储“暂存购物车”都行,根据优劣势选型即可: + +- • 设计的是个小型电商,Cookie存储实现起来更简单 +- • 你的电商是面那种批发的行业用户,用户需加购大量商品,Cookie可能容量不够用,选择LocalStorage更合适 + +不管哪种存储,暂存购物车保存的 + +### 4.2 数据格式 + +都一样。参照实体模型设计,JSON表示: + +``` +{ + "cart": [ + { + "SKUID": 8888, + "timestamp": 1578721136, + "count": 1, + "selected": true + }, + { + "SKUID": 6666, + "timestamp": 1578721138, + "count": 2, + "selected": false + } + ] +} +``` + +## 5 用户购物车 存储设计 + +用户购物车须保证多端数据同步,数据须保存在服务端。常规思路:设计一张购物车表,把数据存在MySQL。表结构同样参照实体模型: + +![](http://img.topjavaer.cn/img/20230116134240.png) + +需在user_id建索引,因为查询购物车表,都以user_id作为查询条件。 + +也可选择更快的Redis保存购物车数据: + +- • 用户ID=Key +- • Redis的HASH=Value,保存购物车中的商品 + +如: + +``` +{ + "KEY": 6666, + "VALUE": [ + { + "FIELD": 8888, + "FIELD_VALUE": { + "timestamp": 1578721136, + "count": 1, + "selected": true + } + }, + { + "FIELD": 6666, + "FIELD_VALUE": { + "timestamp": 1578721138, + "count": 2, + "selected": false + } + } + ] +} +``` + +为便理解,用JSON表示Redis中HASH的数据结构: + +- • KEY中的值6666是用户ID +- • FIELD存放商品ID +- • FIELD_VALUE是个JSON字符串,保存加购时间、商品数量和勾选状态 + +读写性能,Redis比MySQL快得多,Redis就一定比MySQL好吗? + +### 5.1 MySQL V.S Redis 存储 + +- • Redis性能比MySQL高出至少一个量级,响应时间更短,支撑更多并发请求 +- • MySQL数据可靠性好于Redis,因为Redis异步刷盘,若服务器掉电,Redis有可能丢数据。但考虑到购物车里的数据,对可靠性要求不高,丢少量数据的后果也就是,个别用户的购物车少了几件商品,问题不大。所以,购物车场景,Redis数据可靠性不高这个缺点,不是不能接受 +- • MySQL另一优势:支持丰富的查询方式和事务机制,但对购物车核心功能无用。但每个电商系统都有它个性化需求,若需以其他方式访问购物车数据,如统计今天加购的商品总数,这时,使用MySQL存储数据,易实现,而使用Redis存储,查询麻烦且低效 + +综合比较下来,考虑到需求变化,推荐MySQL存储购物车数据。若追求性能或高并发,也可选择使用Redis。 + +设计存储架构过程就是不断抉择过程。很多情况下,可选择方案不止一套,选择时需考虑实现复杂度、性能、系统可用性、数据可靠性、可扩展性等。这些条件每一个都不是绝对不可以牺牲的,不要让一些“所谓的常识”禁锢思维。 + +比如,一般认为数据绝不可丢,即不能牺牲数据可靠性。但用户购物车存储,使用Redis替代MySQL,就是牺牲数据可靠性换取高性能。很低概率的丢失少量数据可接受。性能提升带来的收益远大于丢失少量数据而付出的代价,这选择就值得。 + +如果说不考虑需求变化这个因素,牺牲一点点数据可靠性,换取大幅性能提升,Redis是最优解。 + +## 6 总结 + +- • 购物车系统的主要功能包括:加购、购物车列表页和结算下单 +- • 核心实体:只有一个“购物车”实体 +- • 至少包括:SKUID、数量、加购时间和勾选状态属性 + +在给购物车设计存储时,为确保: + +- • 购物车内的数据在多端一致 +- • 用户登录前后购物车内商品能无缝衔接 + +除了每个用户的“用户购物车”,还要实现一个“暂存购物车”保存用户未登录时加购的商品,并在用户登录后自动合并“暂存购物车”和“用户购物车”。 + +暂存购物车存储在客户端浏览器或App,可存放到Cookie或LocalStorage。用户购物车保存在服务端,可以选择使用: + +- • Redis存储会有更高的性能,可以支撑更多的并发请求 +- • MySQL是更常规通用的方式,便于应对变化,系统扩展性更好 + +## 思考 + +既然用户的购物车数据存放在MySQL或Redis各有优劣。那能否把购物车数据存在MySQL,并用Redis缓存?不就兼顾二者优势?若可行,如何保证Redis中的数据和MySQL数据一致性? + +用Redis给购物车库做缓存,技术可行。但考虑: + +- 值得吗?每个人的购物车都不一样,所以这个缓存它的读写比差距不会很大,缓存命中率不会太高,缓存收益有限,为维护缓存,还会增加系统复杂度。所以我们就要自行权衡一下,是不是值得的问题。除非超大规模系统,否则没必要设置这缓存 +- 若非要做这样一个缓存,用什么缓存更新策略? + + + +> 参考:https://mp.weixin.qq.com/s/3P_f_Vua8rwsGOHrxWubSg diff --git a/docs/advance/system-design/18-register-center.md b/docs/advance/system-design/18-register-center.md new file mode 100644 index 0000000..ba9721d --- /dev/null +++ b/docs/advance/system-design/18-register-center.md @@ -0,0 +1,208 @@ +--- +sidebar: heading +title: 如何设计一个注册中心? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,注册中心,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +## 如何设计一个注册中心? + +今天,给大家分享如何设计一个**注册中心**。 + +不管是出于面试,还是深入学习注册中心,关于如何设计一个注册中心都是一个很好的话题。 + +假设现在我们系统有两个小系统: + +- 订单系统 +- 商品系统 + +单个系统分别部署在不同服务器上,如果我们订单系统需要调用商品系统的某个服务: + +![](http://img.topjavaer.cn/img/注册中心1.png) + + + +#### 怎么调用? + +方法1:商品系统开发的朋友告诉你对应的地址。 + +![](http://img.topjavaer.cn/img/注册中心2.png) + + + +方法2:商品系统开发的朋友把对应`API`地址存放到某个地方。 + +![](http://img.topjavaer.cn/img/注册中心3.png) + + + +方法3:直接通过Nginx,使用域名进行转发到某个实例上。 + +![](http://img.topjavaer.cn/img/注册中心4.png) + +这时候,订单系统就可以通过上述方法调用商品系统的`API`了。 + +#### 问题来了 + +实际线上环境中,很少是单体机构的,很多都是做了集群的,也就是说每个服务会有N个实例,少则几个几十个,多则几百上千上万。如果此时我们还用上面三种方法,当我们的商品系统某个服务下线(宕机了),或者新增实例,此时是非常的头疼。 + +> 所以,注册中心就来了。 + +#### 注册中心来了 + +我们能不能搞一个第三方的节点,这个节点就用来存放我们商品系统的服务信息,这样一来,其他系统需要服务信息,直接去第三方节点上去获取即可。此时,其他系统只要知道这个第三方的节点地址就可以了。这个第三方的节点,我们也称之为`注册中心`。 + +> 下面我们用服务提供方(商品系统)称之为provider,服务调用方(订单系统)我们称之为consumer。 + +#### 如何设计一个注册中心 + +我们需要解决如下几个问题: + +- 服务如何注册 +- consumer如何知道provider +- 服务注册中心如何高可用 +- 服务上下线,消费端如何动态感知 + +##### 服务注册 + +![](http://img.topjavaer.cn/img/注册中心5.png) + +当我们把服务信息注册上去后,就应该是: + +![](http://img.topjavaer.cn/img/注册中心6.png) + +> 服务列表保存通常有三种方式:本地内存、数据库、第三方缓存系统 + +注册上去后,consumer需要服务地址的时候,就可以用相应key去注册中心获取对应的服务列表。 + +![](http://img.topjavaer.cn/img/注册中心7.png) + +> 同一个服务注册中心,我们可以注册多个服务,比如用户服务、商品服务、订单服务... + +##### 服务消费 + +![](http://img.topjavaer.cn/img/注册中心8.png) + +consumer端通过key获取指定的服务地址列表。 + +以上的还是蛮简单的吧,简单来说,我们就是引用了一个第三方的服务来存放我们的服务提供者列表。并且以key-value的形式存储,key我们可以理解为服务名称,value就是服务实例列表。 + +![](http://img.topjavaer.cn/img/注册中心9.png) + + + +##### 注册中心高可用 + +高可用无非就是做集群,我们可以对注册中心部署多个节点。在消费端consumer只需要知道一个服务注册中心集群地址`cluster-url`即可。 + +![](http://img.topjavaer.cn/img/注册中心10.png) + + + +##### 动态感知服务上下线 + +consumer拿到服务列表后,会把服务列表保存起来,保存到本地缓存里。 + +![](http://img.topjavaer.cn/img/注册中心10.png) + + + +consumer通过一定的负载均衡算法,选择出一个地址,最后发起远程的调用。 + +![](http://img.topjavaer.cn/img/注册中心12.png) + + + +如果我们的服务节点挂掉一个了,怎么办? + +![](http://img.topjavaer.cn/img/注册中心13.png) + + + +此时,服务注册中心的服务列表还是之前的列表,如果consumer调用到过掉的节点上,那岂不是会出问题呀。 + +所以,我们的服务注册中心需要知道哪个服务节点挂了,然后从对应服务列表里删除。 + +有种办法叫做心跳检测`heartBeat`,即就是服务注册中心,每隔一定时间去监测一下provider,如果监测到某个服务挂了,那就把对应服务地址从服务列表中删除。 + +> 根据心跳检测,来提出无效服务。 + +![](http://img.topjavaer.cn/img/注册中心13.png) + + + +可是不对呀,此时consumer端本地列表里还有过掉的服务地址,怎么办呢? + +或者是,在增加一个新的服务节点 + +![](http://img.topjavaer.cn/img/注册中心15.png) + + + +对于服务注册中心来说,就是服务列表里增加一个服务地址。 + +但是在消费端存在同样的问题,就是服务注册中心的服务列表和consumer端的服务列表不一样了。 + +如何让consumer端也动态感知呢? + +其实很简单,此时,我们得思维换一下,因为consumer的服务列表是来自于服务注册中心,我们就可以把consumer理解为消费端,服务注册中心理解为服务端。此时,consumer端就可以去服务端(服务注册中心)拉取provider服务列表。 + +通常有两种方案:push和pull + +- push:服务注册中心主动推送服务列表给consumer。 +- pull:consumer主动从注册中心拉取服务列表。 + +![](http://img.topjavaer.cn/img/注册中心15.png) + + + +不管是push还是pull,都会存在consumer和服务注册中心的通信管道。如果他们之间断开了,那就无法获取服务列表了。 + +还有就是服务注册中心知道consumer的地址,比如 + +> 我得知道你的微信好友,不然我怎么把我手里的资源发给你 + +我们的网络通信,必然会存在监听的动作。 + +如果服务注册中心要push到consumer,此时他们之间需要建立一个会话,所以,在服务注册中心会维护一个会话管理的模块。还有一种方式就是consumer提供一个`API`,这个`API`给服务注册中心进行回调。 + +> 本质是我们是使用HTTP协议还是使用Socket监听 + +push有个不好点,那就是服务注册中心需要维护大量的会话,而且还需要对每个会话维持一个心跳,一遍知晓这些会话状态,得确保这些consumer能收到数据, + +另外就是pull,pull其实就相对push就简单多了。pull和我们前面说的心跳机制是类似的,consumer端启动定时任务,每个多久拉取服务注册中心的服务列表。pull也不需要去维护大量的会话,我只需要每隔多久调用接口拉取服务列表即可。但是这里还是会存在一个问题,因为是定时去拉取,所以会存在一定的数据延迟,比如consumer刚刚拉取服务列表,但就在拉取结束的后,某个服务provider挂了,consumer就要等下次拉取才知道对应服务provider挂了。 + +> 如果定时任务是每隔30秒拉去一次,那就是说,延迟最长时间是30秒。 + +还有一种方式long-pull,也叫长轮询,是上面两种方案的优化方案,consumer发起拉取请求时,先把这个请求hold住,当服务注册中心有发生变化后,consumer端能立马感知。 + +关于长轮询: + +> 与简单轮询相似,只是在服务端在没有新的返回数据情况下不会立即响应,而会挂起,直到有数据或即将超时 +> +> `优点`:实现也不复杂,同时相对轮询,节约带宽 +> +> `缺点`:还是存在占用服务端资源的问题,虽然及时性比轮询要高,但是会在没有数据的时候在服务端挂起,所以会一直占用服务端资源,处理能力变少 +> +> `应用`:一些早期的对及时性有一些要求的应用:web IM 聊天 + +这样,我们就搞定了所谓的服务上下线动态感知。 + +通过上面的服务注册、服务消费、注册中心高可用以及动态感知服务的上下线,这就是我们去实现一个服务注册中心的通用模型。 + +##### 小总结 + +关于如何设计一个注册中心,无非重点关以下几点: + +- 服务是如何注册 +- 消费端如何获取服务 +- 如何保证注册中心的高可用 +- 动态感知服务的上下线 diff --git a/docs/advance/system-design/19-high-concurrent-system-design.md b/docs/advance/system-design/19-high-concurrent-system-design.md new file mode 100644 index 0000000..12499df --- /dev/null +++ b/docs/advance/system-design/19-high-concurrent-system-design.md @@ -0,0 +1,63 @@ +--- +sidebar: heading +title: 如何设计一个高并发系统? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,高并发系统,高并发系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 如何设计一个高并发系统? + +## 总览 + +在高并发的情景下进行系统设计, + +可以分为以下 6 点: + +- 系统拆分 + - 熔断 + - 降级 +- 缓存 +- MQ +- 分库分表 +- 读写分离 +- ElasticSearch + +![](http://img.topjavaer.cn/img/高并发系统设计1.png) + +### 系统拆分 + +将一个系统拆分为多个子系统,用 RPC 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,分担系统压力。 + +### 缓存 + +大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存就可以了。毕竟 Redis 轻轻松松单机几万的并发。所以你可以考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。 + +### MQ + +可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改。那高并发绝对搞挂你的系统,你要是用 Redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还很简单,没有事务支持。所以该用 MySQL 还得用 MySQL 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,后边系统消费后慢慢写,控制在 MySQL 承载范围之内。所以你可以考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。 + +### 分库分表 + +分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 SQL 执行的性能。 + +### 读写分离 + +读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。 + +### ElasticSearch + +ES 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 ES 来承载,还有一些全文搜索类的操作,也可以考虑用 ES 来承载。 + + + + + +> 参考链接:https://hadyang.com/interview/docs/architecture/concurrent/design/ diff --git a/docs/advance/system-design/2-order-timeout-auto-cancel.md b/docs/advance/system-design/2-order-timeout-auto-cancel.md new file mode 100644 index 0000000..b100f91 --- /dev/null +++ b/docs/advance/system-design/2-order-timeout-auto-cancel.md @@ -0,0 +1,644 @@ +--- +sidebar: heading +title: 订单30分钟未支付自动取消怎么实现? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,订单取消设计,订单自动取消,订单设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 订单30分钟未支付自动取消怎么实现? + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +**目录** + +- 了解需求 +- 方案 1:数据库轮询 +- 方案 2:JDK 的延迟队列 +- 方案 3:时间轮算法 +- 方案 4:redis 缓存 +- 方案 5:使用消息队列 + +## 了解需求 + +在开发中,往往会遇到一些关于延时任务的需求。 + +例如 + +- 生成订单 30 分钟未支付,则自动取消 +- 生成订单 60 秒后,给用户发短信 + +对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?一共有如下几点区别 + +1、定时任务有明确的触发时间,延时任务没有 + +2、定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 + +3、定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 + +下面,我们以判断订单是否超时为例,进行方案分析 + +## 方案 1:数据库轮询 + +### 思路 + +该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作 + +### 实现 + +可以用 quartz 来实现的,简单介绍一下 + +maven 项目引入一个依赖如下所示 + +``` + + org.quartz-scheduler + quartz + 2.2.2 + +``` + +调用 Demo 类 MyJob 如下所示 + +``` +package com.rjzheng.delay1; + +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; + +public class MyJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("要去数据库扫描啦。。。"); + } + + public static void main(String[] args) throws Exception { + // 创建任务 + JobDetail jobDetail = JobBuilder.newJob(MyJob.class) + .withIdentity("job1", "group1").build(); + // 创建触发器 每3秒钟执行一次 + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("trigger1", "group3") + .withSchedule( + SimpleScheduleBuilder + .simpleSchedule() + .withIntervalInSeconds(3). + repeatForever()) + .build(); + Scheduler scheduler = new StdSchedulerFactory().getScheduler(); + // 将任务及其触发器放入调度器 + scheduler.scheduleJob(jobDetail, trigger); + // 调度器开始调度任务 + scheduler.start(); + } + +} +``` + +运行代码,可发现每隔 3 秒,输出如下 + +``` +要去数据库扫描啦。。。 +``` + +### 优点 + +简单易行,支持集群操作 + +### 缺点 + +- 对服务器内存消耗大 +- 存在延迟,比如你每隔 3 分钟扫描一次,那最坏的延迟时间就是 3 分钟 +- 假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大 + +## 方案 2:JDK 的延迟队列 + +### 思路 + +该方案是利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。 + +DelayedQueue 实现工作流程如下图所示 + +![](http://img.topjavaer.cn/img/订单取消1.png) + +其中 Poll():获取并移除队列的超时元素,没有则返回空 + +take():获取并移除队列的超时元素,如果没有则 wait 当前线程,直到有元素满足超时条件,返回结果。 + +### 实现 + +定义一个类 OrderDelay 实现 Delayed,代码如下 + +``` +package com.rjzheng.delay2; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class OrderDelay implements Delayed { + + private String orderId; + + private long timeout; + + OrderDelay(String orderId, long timeout) { + this.orderId = orderId; + this.timeout = timeout + System.nanoTime(); + } + + public int compareTo(Delayed other) { + if (other == this) { + return 0; + } + OrderDelay t = (OrderDelay) other; + long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS)); + return (d == 0) ? 0 : ((d < 0) ? -1 : 1); + } + + // 返回距离你自定义的超时时间还有多少 + public long getDelay(TimeUnit unit) { + return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS); + } + + void print() { + System.out.println(orderId + "编号的订单要删除啦。。。。"); + } + +} +``` + +运行的测试 Demo 为,我们设定延迟时间为 3 秒 + +``` +package com.rjzheng.delay2; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +public class DelayQueueDemo { + + public static void main(String[] args) { + List list = new ArrayList(); + list.add("00000001"); + list.add("00000002"); + list.add("00000003"); + list.add("00000004"); + list.add("00000005"); + + DelayQueue queue = newDelayQueue < OrderDelay > (); + long start = System.currentTimeMillis(); + for (int i = 0; i < 5; i++) { + //延迟三秒取出 + queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS))); + try { + queue.take().print(); + System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + +} +``` + +输出如下 + +``` +00000001编号的订单要删除啦。。。。 +After 3003 MilliSeconds +00000002编号的订单要删除啦。。。。 +After 6006 MilliSeconds +00000003编号的订单要删除啦。。。。 +After 9006 MilliSeconds +00000004编号的订单要删除啦。。。。 +After 12008 MilliSeconds +00000005编号的订单要删除啦。。。。 +After 15009 MilliSeconds +``` + +可以看到都是延迟 3 秒,订单被删除 + +### 优点 + +效率高,任务触发时间延迟低。 + +### 缺点 + +- 服务器重启后,数据全部消失,怕宕机 +- 集群扩展相当麻烦 +- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 +- 代码复杂度较高 + +## 方案 3:时间轮算法 + +### 思路 + +先上一张时间轮的图(这图到处都是啦) + +![](http://img.topjavaer.cn/img/订单取消2.png) + +时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个 3 个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。 + +如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在 20 秒之后执行怎么办,由于这个环形结构槽数只到 8,如果要 20 秒,指针需要多转 2 圈。位置是在 2 圈之后的 5 上面(20 % 8 + 1) + +### 实现 + +我们用 Netty 的 HashedWheelTimer 来实现 + +给 Pom 加上下面的依赖 + +``` + + io.netty + netty-all + 4.1.24.Final + +``` + +测试代码 HashedWheelTimerTest 如下所示 + +``` +package com.rjzheng.delay3; + +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.TimerTask; + +import java.util.concurrent.TimeUnit; + +public class HashedWheelTimerTest { + + static class MyTimerTask implements TimerTask { + + boolean flag; + + public MyTimerTask(boolean flag) { + this.flag = flag; + } + + public void run(Timeout timeout) throws Exception { + System.out.println("要去数据库删除订单了。。。。"); + this.flag = false; + } + } + + public static void main(String[] argv) { + MyTimerTask timerTask = new MyTimerTask(true); + Timer timer = new HashedWheelTimer(); + timer.newTimeout(timerTask, 5, TimeUnit.SECONDS); + int i = 1; + while (timerTask.flag) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(i + "秒过去了"); + i++; + } + } + +} +``` + +输出如下 + +``` +1秒过去了 +2秒过去了 +3秒过去了 +4秒过去了 +5秒过去了 +要去数据库删除订单了。。。。 +6秒过去了 +``` + +### 优点 + +效率高,任务触发时间延迟时间比 delayQueue 低,代码复杂度比 delayQueue 低。 + +### 缺点 + +- 服务器重启后,数据全部消失,怕宕机 +- 集群扩展相当麻烦 +- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 + +## 方案 4:Redis 缓存 + +### 思路一 + +利用 redis 的 zset,zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值 + +添加元素:ZADD key score member [score member …] + +按顺序查询元素:ZRANGE key start stop [WITHSCORES] + +查询元素 score:ZSCORE key member + +移除元素:ZREM key member [member …] + +测试如下 + +``` +添加单个元素 +redis> ZADD page_rank 10 google.com +(integer) 1 + +添加多个元素 +redis> ZADD page_rank 9 baidu.com 8 bing.com +(integer) 2 + +redis> ZRANGE page_rank 0 -1 WITHSCORES +1) "bing.com" +2) "8" +3) "baidu.com" +4) "9" +5) "google.com" +6) "10" + +查询元素的score值 +redis> ZSCORE page_rank bing.com +"8" + +移除单个元素 +redis> ZREM page_rank google.com +(integer) 1 + +redis> ZRANGE page_rank 0 -1 WITHSCORES +1) "bing.com" +2) "8" +3) "baidu.com" +4) "9" +``` + +那么如何实现呢?我们将订单超时时间戳与订单号分别设置为 score 和 member,系统扫描第一个元素判断是否超时,具体如下图所示 + +![](http://img.topjavaer.cn/img/订单取消3.png) + +### 实现一 + +``` +package com.rjzheng.delay4; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.Tuple; + +import java.util.Calendar; +import java.util.Set; + +public class AppTest { + + private static final String ADDR = "127.0.0.1"; + + private static final int PORT = 6379; + + private static JedisPool jedisPool = new JedisPool(ADDR, PORT); + + public static Jedis getJedis() { + return jedisPool.getResource(); + } + + //生产者,生成5个订单放进去 + public void productionDelayMessage() { + for (int i = 0; i < 5; i++) { + //延迟3秒 + Calendar cal1 = Calendar.getInstance(); + cal1.add(Calendar.SECOND, 3); + int second3later = (int) (cal1.getTimeInMillis() / 1000); + AppTest.getJedis().zadd("OrderId", second3later, "OID0000001" + i); + System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID0000001" + i); + } + } + + //消费者,取订单 + + public void consumerDelayMessage() { + Jedis jedis = AppTest.getJedis(); + while (true) { + Set items = jedis.zrangeWithScores("OrderId", 0, 1); + if (items == null || items.isEmpty()) { + System.out.println("当前没有等待的任务"); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + continue; + } + int score = (int) ((Tuple) items.toArray()[0]).getScore(); + Calendar cal = Calendar.getInstance(); + int nowSecond = (int) (cal.getTimeInMillis() / 1000); + if (nowSecond >= score) { + String orderId = ((Tuple) items.toArray()[0]).getElement(); + jedis.zrem("OrderId", orderId); + System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId); + } + } + } + + public static void main(String[] args) { + AppTest appTest = new AppTest(); + appTest.productionDelayMessage(); + appTest.consumerDelayMessage(); + } + +} +``` + +此时对应输出如下 + +![](http://img.topjavaer.cn/img/订单取消4.png) + +可以看到,几乎都是 3 秒之后,消费订单。 + +然而,这一版存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号,我们上测试代码 ThreadTest + +``` +package com.rjzheng.delay4; + +import java.util.concurrent.CountDownLatch; + +public class ThreadTest { + + private static final int threadNum = 10; + private static CountDownLatch cdl = newCountDownLatch(threadNum); + + static class DelayMessage implements Runnable { + public void run() { + try { + cdl.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + AppTest appTest = new AppTest(); + appTest.consumerDelayMessage(); + } + } + + public static void main(String[] args) { + AppTest appTest = new AppTest(); + appTest.productionDelayMessage(); + for (int i = 0; i < threadNum; i++) { + new Thread(new DelayMessage()).start(); + cdl.countDown(); + } + } + +} +``` + +输出如下所示 + +![](http://img.topjavaer.cn/img/订单取消5.png) + +显然,出现了多个线程消费同一个资源的情况。 + +### 解决方案 + +(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。 + +(2)对 ZREM 的返回值进行判断,只有大于 0 的时候,才消费数据,于是将 consumerDelayMessage()方法里的 + +``` +if(nowSecond >= score){ + String orderId = ((Tuple)items.toArray()[0]).getElement(); + jedis.zrem("OrderId", orderId); + System.out.println(System.currentTimeMillis()+"ms:redis消费了一个任务:消费的订单OrderId为"+orderId); +} +``` + +修改为 + +``` +if (nowSecond >= score) { + String orderId = ((Tuple) items.toArray()[0]).getElement(); + Long num = jedis.zrem("OrderId", orderId); + if (num != null && num > 0) { + System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId); + } +} +``` + +在这种修改后,重新运行 ThreadTest 类,发现输出正常了 + +### 思路二 + +该方案使用 redis 的 Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在 key 失效之后,提供一个回调,实际上是 redis 会给客户端发送一个消息。是需要 redis 版本 2.8 以上。 + +### 实现二 + +在 redis.conf 中,加入一条配置 + +notify-keyspace-events Ex + +运行代码如下 + +``` +package com.rjzheng.delay5; + +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPubSub; + +public class RedisTest { + + private static final String ADDR = "127.0.0.1"; + private static final int PORT = 6379; + private static JedisPool jedis = new JedisPool(ADDR, PORT); + private static RedisSub sub = new RedisSub(); + + public static void init() { + new Thread(new Runnable() { + public void run() { + jedis.getResource().subscribe(sub, "__keyevent@0__:expired"); + } + }).start(); + } + + public static void main(String[] args) throws InterruptedException { + init(); + for (int i = 0; i < 10; i++) { + String orderId = "OID000000" + i; + jedis.getResource().setex(orderId, 3, orderId); + System.out.println(System.currentTimeMillis() + "ms:" + orderId + "订单生成"); + } + } + + static class RedisSub extends JedisPubSub { + @Override + public void onMessage(String channel, String message) { + System.out.println(System.currentTimeMillis() + "ms:" + message + "订单取消"); + + } + } +} +``` + +输出如下 + +![](http://img.topjavaer.cn/img/订单取消6.png) + +可以明显看到 3 秒过后,订单取消了 + +ps:redis 的 pub/sub 机制存在一个硬伤,官网内容如下 + +原:Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost. + +翻: Redis 的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。因此,方案二不是太推荐。当然,如果你对可靠性要求不高,可以使用。 + +### 优点 + +(1) 由于使用 Redis 作为消息通道,消息都存储在 Redis 中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。 + +(2) 做集群扩展相当方便 + +(3) 时间准确度高 + +### 缺点 + +需要额外进行 redis 维护 + +## 方案 5:使用消息队列 + +### 思路 + +我们可以采用 RabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 + +RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter + +lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能。 + +### 优点 + +高效,可以利用 rabbitmq 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。 + +### 缺点 + +本身的易用度要依赖于 rabbitMq 的运维.因为要引用 rabbitMq,所以复杂度和成本变高。 + +--end-- + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**Github地址**:https://github.com/Tyson0314/java-books diff --git a/docs/advance/system-design/20-sharding-smooth-migration.md b/docs/advance/system-design/20-sharding-smooth-migration.md new file mode 100644 index 0000000..781bfed --- /dev/null +++ b/docs/advance/system-design/20-sharding-smooth-migration.md @@ -0,0 +1,72 @@ +--- +sidebar: heading +title: 分库分表如何平滑过渡? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,分库分表,分库分表平滑过渡,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +## 分库分表概述 + +在业务量不大时,单库单表即可支撑。 + +当数据量过大存储不下、或者并发量过大负荷不起时,就要考虑分库分表。 + +### 1.1 分库分表相关术语 + +- 读写分离: 不同的数据库,同步相同的数据,分别只负责数据的读和写; +- 分区: 指定分区列表达式,把记录拆分到不同的区域中(必须是同一服务器,可以是不同硬盘),应用看来还是同一张表,没有变化; +- 分库:一个系统的多张数据表,存储到多个数据库实例中; +- 分表: 对于一张多行(记录)多列(字段)的二维数据表,又分两种情形: +- (1) 垂直分表: 竖向切分,不同分表存储不同的字段,可以把不常用或者大容量、或者不同业务的字段拆分出去; +- (2) 水平分表: 横向切分,按照特定分片算法,不同分表存储不同的记录。 + +### 1.2 真的要采用分库分表? + +需要注意的是,分库分表会为数据库维护和业务逻辑带来一系列复杂性和性能损耗,`除非预估的业务量大到万不得已,切莫过度设计、过早优化`。 + +规划期内的数据量和性能问题,尝试能否用下列方式解决: + +- 当前数据量:如果没有达到几百万,通常无需分库分表; +- 数据量问题:增加磁盘、增加分库(不同的业务功能表,整表拆分至不同的数据库); +- 性能问题:升级CPU/内存、读写分离、优化数据库系统配置、优化数据表/索引、优化 SQL、分区、数据表的垂直切分; +- 如果仍未能奏效,才考虑最复杂的方案:数据表的水平切分。 + +## 分库分表如何平滑过渡? + +现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表**动态切换**到分库分表上? + +单库单表的系统给迁移到分库分表,有以下方案: + +### 停机迁移方案 + +最简单但是有点 low 的方案,在网站或者 app 挂个公告,说凌晨某个时间段进行维护,无法访问。 + +接着到了时间点了就停机,系统停掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个**导数的一次性工具**,将单库单表的数据读出来,写到分库分表里面去。 + +导数完了之后,修改系统的数据库连接配置等,然后启动系统连到新的分库分表上去。 + +具体流程图如下: + +![](http://img.topjavaer.cn/img/database-shard-method-1.png) + +### 双写迁移方案 + +这个比较常用的一种迁移方案,不用停机。 + +简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,**除了对老库增删改,都加上对新库的增删改**,这就是所谓的**双写**,同时写俩库,老库和新库。 + +然后**系统部署**之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要去判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。 + +导完一轮之后,有可能数据还是存在不一致,那么就用脚本自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。 + +当数据完全一致了之后,所有机器使用分库分表的最新代码重新部署一次,此时就完成迁移了。 + +![](http://img.topjavaer.cn/img/database-shard-method-2.png) diff --git a/docs/advance/system-design/21-excel-import.md b/docs/advance/system-design/21-excel-import.md new file mode 100644 index 0000000..c2d6b32 --- /dev/null +++ b/docs/advance/system-design/21-excel-import.md @@ -0,0 +1,272 @@ +--- +sidebar: heading +title: 10w级别数据Excel导入优化 +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,Excel导入优化,海量数据处理,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +# 10w级别数据Excel导入优化 + +# Part1需求说明 + +项目中有一个 Excel 导入的需求:缴费记录导入 + +由实施 / 用户 将别的系统的数据填入我们系统中的 Excel 模板,应用将文件内容读取、校对、转换之后产生欠费数据、票据、票据详情并存储到数据库中。 + +在我接手之前可能由于之前导入的数据量并不多没有对效率有过高的追求。但是到了 4.0 版本,我预估导入时Excel 行数会是 10w+ 级别,而往数据库插入的数据量是大于 3n 的,也就是说 10w 行的 Excel,则至少向数据库插入 30w 行数据。因此优化原来的导入代码是势在必行的。我逐步分析和优化了导入的代码,使之在百秒内完成(最终性能瓶颈在数据库的处理速度上,测试服务器 4g 内存不仅放了数据库,还放了很多微服务应用。处理能力不太行)。具体的过程如下,每一步都有列出影响性能的问题和解决的办法。 + +导入 Excel 的需求在系统中还是很常见的,我的优化办法可能不是最优的,欢迎读者在评论区留言交流提供更优的思路 + +# Part2一些细节 + +- 数据导入:导入使用的模板由系统提供,格式是 xlsx (支持 65535+行数据) ,用户按照表头在对应列写入相应的数据 + +- 数据校验:数据校验有两种: + +- - 字段长度、字段正则表达式校验等,内存内校验不存在外部数据交互。对性能影响较小 + - 数据重复性校验,如票据号是否和系统已存在的票据号重复(需要查询数据库,十分影响性能) + +- 数据插入:测试环境数据库使用 MySQL 5.7,未分库分表,连接池使用 Druid + +# Part3迭代记录 + +## 1第一版:POI + 逐行查询校对 + 逐行插入 + +这个版本是最古老的版本,采用原生 POI,手动将 Excel 中的行映射成 ArrayList 对象,然后存储到 List,代码执行的步骤如下: + +1. 手动读取 Excel 成 List + +2. 循环遍历,在循环中进行以下步骤 + +3. 1. 检验字段长度 + 2. 一些查询数据库的校验,比如校验当前行欠费对应的房屋是否在系统中存在,需要查询房屋表 + 3. 写入当前行数据 + +4. 返回执行结果,如果出错 / 校验不合格。则返回提示信息并回滚数据 + +显而易见的,这样实现一定是赶工赶出来的,后续可能用的少也没有察觉到性能问题,但是它最多适用于个位数/十位数级别的数据。存在以下明显的问题: + +- 查询数据库的校验对每一行数据都要查询一次数据库,应用访问数据库来回的网络IO次数被放大了 n 倍,时间也就放大了 n 倍 +- 写入数据也是逐行写入的,问题和上面的一样 +- 数据读取使用原生 POI,代码十分冗余,可维护性差。 + +## 2第二版:EasyPOI + 缓存数据库查询操作 + 批量插入 + +针对第一版分析的三个问题,分别采用以下三个方法优化 + +### 缓存数据,以空间换时间 + +逐行查询数据库校验的时间成本主要在来回的网络IO中,优化方法也很简单。将参加校验的数据全部缓存到 HashMap 中。直接到 HashMap 去命中。 + +例如:校验行中的房屋是否存在,原本是要用 区域 + 楼宇 + 单元 + 房号 去查询房屋表匹配房屋ID,查到则校验通过,生成的欠单中存储房屋ID,校验不通过则返回错误信息给用户。而房屋信息在导入欠费的时候是不会更新的。并且一个小区的房屋信息也不会很多(5000以内)因此我采用一条SQL,将该小区下所有的房屋以 区域/楼宇/单元/房号 作为 key,以 房屋ID 作为 value,存储到 HashMap 中,后续校验只需要在 HashMap 中命中 + +### 自定义 SessionMapper + +Mybatis 原生是不支持将查询到的结果直接写人一个 HashMap 中的,需要自定义 SessionMapper + +SessionMapper 中指定使用 MapResultHandler 处理 SQL 查询的结果集 + +```java +@Repository +public class SessionMapper extends SqlSessionDaoSupport { + + @Resource + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + super.setSqlSessionFactory(sqlSessionFactory); + } + + // 区域楼宇单元房号 - 房屋ID + @SuppressWarnings("unchecked") + public Map getHouseMapByAreaId(Long areaId) { + MapResultHandler handler = new MapResultHandler(); + + this.getSqlSession().select(BaseUnitMapper.class.getName()+".getHouseMapByAreaId", areaId, handler); + Map map = handler.getMappedResults(); + return map; + } +} +``` + +MapResultHandler 处理程序,将结果集放入 HashMap + +```java +public class MapResultHandler implements ResultHandler { + private final Map mappedResults = new HashMap(); + + @Override + public void handleResult(ResultContext context) { + @SuppressWarnings("rawtypes") + Map map = (Map)context.getResultObject(); + mappedResults.put(map.get("key"), map.get("value")); + } + + public Map getMappedResults() { + return mappedResults; + } +} +``` + +示例 Mapper + +```java +@Mapper +@Repository +public interface BaseUnitMapper { + // 收费标准绑定 区域楼宇单元房号 - 房屋ID + Map getHouseMapByAreaId(@Param("areaId") Long areaId); +} +``` + +### 示例 Mapper.xml + +```xml + + + + + + +``` + +之后在代码中调用 SessionMapper 类对应的方法即可。 + +# Part4使用 values 批量插入 + +MySQL insert 语句支持使用 values (),(),() 的方式一次插入多行数据,通过 mybatis foreach 结合 java 集合可以实现批量插入,代码写法如下: + +```xml + + insert into table(colom1, colom2) + values + + ( #{item.colom1}, #{item.colom2}) + + +``` + +# Part5使用 EasyPOI 读写 Excel + +采用基于注解的导入导出,修改注解就可以修改Excel,非常方便,代码维护起来也容易。 + +## 3第三版:EasyExcel + 缓存数据库查询操作 + 批量插入 + +第二版采用 EasyPOI 之后,对于几千、几万的 Excel 数据已经可以轻松导入了,不过耗时有点久(5W 数据 10分钟左右写入到数据库)不过由于后来导入的操作基本都是开发在一边看日志一边导入,也就没有进一步优化。但是好景不长,有新小区需要迁入,票据 Excel 有 41w 行,这个时候使用 EasyPOI 在开发环境跑直接就 OOM 了,增大 JVM 内存参数之后,虽然不 OOM 了,但是 CPU 占用 100% 20 分钟仍然未能成功读取全部数据。故在读取大 Excel 时需要再优化速度。莫非要我这个渣渣去深入 POI 优化了吗?别慌,先上 GITHUB 找找别的开源项目。这时阿里 EasyExcel 映入眼帘: + +![](http://img.topjavaer.cn/img/百万数据excel导入.png) + +emmm,这不是为我量身定制的吗!赶紧拿来试试。EasyExcel 采用和 EasyPOI 类似的注解方式读写 Excel,因此从 EasyPOI 切换过来很方便,分分钟就搞定了。也确实如阿里大神描述的:41w行、25列、45.5m 数据读取平均耗时 50s,因此对于大 Excel 建议使用 EasyExcel 读取。 + +## 4第四版:优化数据插入速度 + +在第二版插入的时候,我使用了 values 批量插入代替逐行插入。每 30000 行拼接一个长 SQL、顺序插入。整个导入方法这块耗时最多,非常拉跨。后来我将每次拼接的行数减少到 10000、5000、3000、1000、500 发现执行最快的是 1000。结合网上一些对 innodb_buffer_pool_size 描述我猜是因为过长的 SQL 在写操作的时候由于超过内存阈值,发生了磁盘交换。限制了速度,另外测试服务器的数据库性能也不怎么样,过多的插入他也处理不过来。所以最终采用每次 1000 条插入。 + +每次 1000 条插入后,为了榨干数据库的 CPU,那么网络IO的等待时间就需要利用起来,这个需要多线程来解决,而最简单的多线程可以使用 并行流 来实现,接着我将代码用并行流来测试了一下: + +10w行的 excel、42w 欠单、42w记录详情、2w记录、16 线程并行插入数据库、每次 1000 行。插入时间 72s,导入总时间 95 s。 + +![](http://img.topjavaer.cn/img/百万数据excel导入2.png) + +# Part6并行插入工具类 + +并行插入的代码我封装了一个函数式编程的工具类,也提供给大家 + +```java +/** + * 功能:利用并行流快速插入数据 + * + * @author Keats + * @date 2020/7/1 9:25 + */ +public class InsertConsumer { + /** + * 每个长 SQL 插入的行数,可以根据数据库性能调整 + */ + private final static int SIZE = 1000; + + /** + * 如果需要调整并发数目,修改下面方法的第二个参数即可 + */ + static { + System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4"); + } + + /** + * 插入方法 + * + * @param list 插入数据集合 + * @param consumer 消费型方法,直接使用 mapper::method 方法引用的方式 + * @param 插入的数据类型 + */ + public static void insertData(List list, Consumer> consumer) { + if (list == null || list.size() < 1) { + return; + } + + List> streamList = new ArrayList<>(); + + for (int i = 0; i < list.size(); i += SIZE) { + int j = Math.min((i + SIZE), list.size()); + List subList = list.subList(i, j); + streamList.add(subList); + } + // 并行流使用的并发数是 CPU 核心数,不能局部更改。全局更改影响较大,斟酌 + streamList.parallelStream().forEach(consumer); + } +} +``` + +这里多数使用到很多 Java8 的API,不了解的朋友可以翻看我之前关于 Java 的博客。方法使用起来很简单 + +```java +InsertConsumer.insertData(feeList, arrearageMapper::insertList); +``` + +# Part7其他影响性能的内容 + +# 日志 + +**避免在 for 循环中打印过多的 info 日志** + +在优化的过程中,我还发现了一个**特别影响性能**的东西:info 日志,还是使用 41w行、25列、45.5m 数据,在 **开始-数据读取完毕** 之间每 1000 行打印一条 info 日志,**缓存校验数据-校验完毕** 之间每行打印 3+ 条 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盘。下面是打印日志和不打印日志效率的差别 + +### 打印日志 + +![](http://img.topjavaer.cn/img/image-20230204170642838.png) + +### 不打印日志 + +![](http://img.topjavaer.cn/img/image-20230204170652966.png) + +我以为是我选错 Excel 文件了,又重新选了一次,结果依旧 + +![](http://img.topjavaer.cn/img/image-20230204170702029.png) + +缓存校验数据-校验完毕 不打印日志耗时仅仅是打印日志耗时的 1/10 ! + +# Part9总结 + +提升Excel导入速度的方法: + +- 使用更快的 Excel 读取框架(推荐使用阿里 EasyExcel) +- 对于需要与数据库交互的校验、按照业务逻辑适当的使用缓存。用空间换时间 +- 使用 values(),(),() 拼接长 SQL 一次插入多行数据 +- 使用多线程插入数据,利用掉网络IO等待时间(推荐使用并行流,简单易用) +- 避免在循环中打印无用的日志 diff --git a/docs/advance/system-design/3-file-send.md b/docs/advance/system-design/3-file-send.md new file mode 100644 index 0000000..a0e7ac0 --- /dev/null +++ b/docs/advance/system-design/3-file-send.md @@ -0,0 +1,28 @@ +--- +sidebar: heading +title: 如何把一个文件较快的发送到100w个服务器? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,文件发送,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- + +## 如何把一个文件较快的发送到100w个服务器? + +可以采用p2p网络形式,比如树状形式,单个节点既可以从其他节点接收服务又可以向其他节点提供服务。 + +不过,树状结构会有什么问题呢? + +第一,如果树上的某一个节点坏掉了,那么从这个节点往下的所有服务器全部接收不到文件。 + +第二,如果树中的某条路径,因为网络因素等原因,传递速度比较慢,导致传递时间比较长,这样会使传递效率退化。 + +改进的方案如下: + +可以使用**连通图**。100W台服务器相当于有100W个节点的连通图。我们可以在图里生成多颗不同的生成树,在进行数据下发时,同时按照多颗不同的树去传递数据。这样就可以避免某个中间节点宕机,影响到后续的节点。同时这种传递方法实际上是一种依据时间的广度优先遍历,可以避免某条路径过长造成的效率低下。 diff --git "a/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" b/docs/advance/system-design/3-short-url.md similarity index 60% rename from "\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" rename to docs/advance/system-design/3-short-url.md index 7b0003c..99d69d7 100644 --- "a/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241.md" +++ b/docs/advance/system-design/3-short-url.md @@ -1,62 +1,17 @@ -# 超卖问题 - -先到数据库查询库存,在减库存。不是原子操作,会有超卖问题。 - -通过加排他锁解决该问题。 - -- 开始事务。 -- 查询库存,并显式的设置排他锁:SELECT * FROM table_name WHERE … FOR UPDATE -- 生成订单。 -- 去库存,update会隐式的设置排他锁:UPDATE products SET count=count-1 WHERE id=1 -- commit,释放锁。 - -也可以通过乐观锁实现。使用版本号实现乐观锁。 - -假设此时version = 100, num = 1; 100个线程进入到了这里,同时他们select出来版本号都是version = 100。 - -然后直接update的时候,只有其中一个先update了,同时更新了版本号。 - -那么其他99个在更新的时候,会发觉version并不等于上次select的version,就说明version被其他线程修改过了,则放弃此次update,重试直到成功。 - -```mysql -select version from goods WHERE id= 1001 -update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version); -``` - -# 秒杀系统 - -## 系统的特点 - -- 高性能:秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键 -- 一致性:秒杀商品减库存的实现方式同样关键,有限数量的商品在同一时刻被很多倍的请求同时来减库存,在大并发更新的过程中都要保证数据的准确性。 -- 高可用:秒杀时会在一瞬间涌入大量的流量,为了避免系统宕机,保证高可用,需要做好流量限制 - -## 优化思路 - -- 后端优化:将请求尽量拦截在系统上游 - - 限流:屏蔽掉无用的流量,允许少部分流量走后端。假设现在库存为 10,有 1000 个购买请求,最终只有 10 个可以成功,99% 的请求都是无效请求 - - 削峰:秒杀请求在时间上高度集中于某一个时间点,瞬时流量容易压垮系统,因此需要对流量进行削峰处理,缓冲瞬时流量,尽量让服务器对资源进行平缓处理 - - 异步:将同步请求转换为异步请求,来提高并发量,本质也是削峰处理。在真实的秒杀场景中,有三个核心流程:而这三个核心流程中,真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步。 - - 利用缓存:创建订单时,每次都需要先查询判断库存,只有少部分成功的请求才会创建订单,因此可以将商品信息放在缓存中,减少数据库查询 - - 负载均衡:利用 Nginx 做负载均衡,使用多个服务器并发处理请求,减少单个服务器压力 -- 前端优化: - - 限流:使用验证码等,来分散用户的请求 - - 禁止重复提交:限定每个用户发起一次秒杀后,需等待才可以发起另一次请求,从而减少用户的重复请求 - - 本地标记:用户成功秒杀到商品后,将提交按钮置灰,禁止用户再次提交请求 - - 动静分离:使用CDN缓存静态页面资源,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率 -- 防作弊优化: - - 隐藏秒杀接口:如果秒杀地址直接暴露,在秒杀开始前可能会被恶意用户来刷接口,因此需要在没到秒杀开始时间不能获取秒杀接口,只有秒杀开始了,才返回秒杀地址 url 和验证 MD5,用户拿到这两个数据才可以进行秒杀 - - 同一个账号多次发出请求:在前端优化的禁止重复提交可以进行优化;也可以使用 Redis 标志位,每个用户的所有请求都尝试在 Redis 中插入一个 `userId_secondsKill` 标志位,成功插入的才可以执行后续的秒杀逻辑,其他被过滤掉,执行完秒杀逻辑后,删除标志位 - - 多个账号一次性发出多个请求:一般这种请求都来自同一个 IP 地址,可以检测 IP 的请求频率,如果过于频繁则弹出一个验证码 - - 多个账号不同 IP 发起不同请求:这种一般都是僵尸账号,检测账号的活跃度或者等级等信息,来进行限制。比如微博抽奖,用 iphone 的年轻女性用户中奖几率更大。通过用户画像限制僵尸号无法参与秒杀或秒杀不能成功 - - - -## 参考资料 - -[如何设计一个秒杀系统](https://gongfukangee.github.io/2019/06/09/SecondsKill/#%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%89%B9%E7%82%B9) - -[秒杀系统如何设计](https://www.teqng.com/2021/09/07/%E9%9D%A2%E9%9C%B8%EF%BC%9A%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%EF%BC%9F/) +--- +sidebar: heading +title: 怎么设计一个短链系统? +category: 场景设计 +tag: + - 场景设计 +head: + - - meta + - name: keywords + content: 场景设计面试题,短链系统设计,场景设计 + - - meta + - name: description + content: 场景设计常见面试题总结,让天下没有难背的八股文! +--- # 短链系统 @@ -98,7 +53,7 @@ The document has moved 3521 亿 > 58 亿,也就是可以解决目前全球已知的网址。62 进制就是由 10 个数字 + (a-z)26 个小写字母 + (A-Z)26 个大写字母组成的数。 -### 哈希算法 +**哈希算法** 对原始链接取 Hash 值,是一种比较简单的思路。有很多现成的算法可以实现,但是有个避不开的问题就是:Hash 碰撞,所以选一个碰撞率低的算法比较重要。 @@ -114,9 +69,9 @@ The document has moved b,a和b的字符串长度介于1~50之间)。 +例:输入a:“99999”,b=“99998” +输出:“1” + + +## 面经2 +### 华为一面 +1. 项目、论文。 +2. String能否被继承。 +3. Java内存泄露和排查。 +4. Hash方式和Hash冲突解决。 +5. 静态代理和动态代理。 +6. spring boot常用的注解有哪些 +7. spring boot的配置文件 +8. redis集群的几种方式详细说一下 +9. redis缓存雪崩,缓存击穿,缓存穿透是什么,怎么解决 +10. mysql索引相关,为什么用B+树 +11. 手撕代码,链表求和,leetcode原题 + +### 华为二面 +- 是否用过Java、Python做系统的项目 +- 平时熟练使用哪种语言 +- HashMap、HashSet、HashTable、StringBuffer、StringBuilder哪些是线程安全,哪些是线程不安全 +- HashSet数据结构,跟HashMap有什么区别 +- char和varchar的区别 +- mysql建索引的原则,索引是不是越多越好,为什么 +- spring boot用到了哪些设计模式,从源码层面说说你熟悉的以及实现 +- jvm调优你用什么工具,具体怎么做的,怎么调优 diff --git a/docs/campus-recruit/interview/README.md b/docs/campus-recruit/interview/README.md new file mode 100644 index 0000000..4ebcf45 --- /dev/null +++ b/docs/campus-recruit/interview/README.md @@ -0,0 +1,13 @@ + +## 大厂面经汇总 + +- [字节跳动](./1-byte-and-dance.md) +- [腾讯](./2-tencent.md) +- [百度](./3-baidu.md) +- [阿里](./4-ali.md) +- [快手](./5-kuaishou.md) +- [美团](./6-meituan.md) +- [shopee](./7-shopee.md) +- [京东](./8-jingdong.md) +- [华为](./9-huawei.md) +- [网易](./10-netease.md) diff --git a/docs/campus-recruit/lack-project-experience.md b/docs/campus-recruit/lack-project-experience.md new file mode 100644 index 0000000..c8b6848 --- /dev/null +++ b/docs/campus-recruit/lack-project-experience.md @@ -0,0 +1,229 @@ +--- +sidebar: heading +title: 没有项目经验,怎么办? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 项目经验 + - - meta + - name: description + content: 没有项目经验,怎么办? +--- + +# 没有项目经验,怎么办? + +这个问题有很多人问过我了,今天来聊聊具体解决方案。 + +没有项目经验是很多应届生都会遇到的问题,甚至一些工作几年的职场老手,也可能会有这个问题,因为在公司的项目比较简单,都是crud,没用到一些高大上的技术,也需要找一些比较有价值的项目充实简历。 + +那到哪里找项目找呢?有两种方式:1、找一些付费视频或者专栏;2、开源项目。 + +## 付费视频或者专栏 + +付费教程的好处就是,配套资料比较齐全,而且一般会有老师解答问题,不用自己去摸索,节省一些时间。当然也可以到b站或者其他平台找免费资源。 + +建议学习的时候,每一步都自己去尝试去实现,不要照抄代码。多去思考有没有优化的地方,有哪些关键点是可以写到项目上的。 + +## 开源项目 + +Github上有很多优秀的开源项目,可以选择一个适合自己的来研究。在理解了项目整体架构和功能之后,可以尝试加一些功能或者做一些改造,加深对项目的理解。 + +另外,Github项目很多很多,要怎样才能找到一个好的项目呢? + +一个优秀的项目,我认为有以下两点特征: + +1. **star和fork较多**,说明这个项目比较受欢迎,总体质量较高 +2. **文档齐全**,方便上手。题主也说了,写了一个社交网站,但是因为没有前端开发经验最终在奋斗了2个月后流产了。如果项目有比较完善的文档,那么就不会有这种情况了。 + +下面给大家分享一些比较值得初学者学习的项目。 + +# 一些优质的开源项目 + +## newbee-mall + +star:7.8k + +https://github.com/newbee-ltd/newbee-mall + +newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。 后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。 + +![](http://img.topjavaer.cn/image/image-20210827001950033.png) + +## litemall + +star:16.2k + +https://github.com/linlinjava/litemall + +又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 + +小商城功能: + +- 首页 +- 专题列表、专题详情 +- 分类列表、分类详情 +- 品牌列表、品牌详情 +- 新品首发、人气推荐 +- 优惠券列表、优惠券选择 +- ... + +![](http://img.topjavaer.cn/image/litemall.png) + +![](http://img.topjavaer.cn/image/litemall01.png) + +在这里也分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ + +![](http://img.topjavaer.cn/image/image-20211127150136157.png) + +![](http://img.topjavaer.cn/image/image-20220316234337881.png) + +需要的小伙伴可以自行**下载**: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## eladmin + +star:16.2k + +https://github.com/elunez/eladmin + +一个基于 Spring Boot 2.1.0 、 Spring Boot Jpa、 JWT、Spring Security、Redis、Vue的前后端分离的后台管理系统。项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 + +项目提供了非常详细的文档,地址是[https://el-admin.vip](https://el-admin.vip/) + +项目体验地址:[https://el-admin.xin](https://el-admin.xin/) + +使用的技术栈也比较新,给作者点赞! + +![](http://img.topjavaer.cn/image/image-20210826235913747.png) + +![](http://img.topjavaer.cn/image/image-20210826235952292.png) + +## vhr + +star:22.2k + +https://github.com/lenve/vhr + +微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等。 + +![](http://img.topjavaer.cn/image/vhr01.png) + +## My-Blog + +star2.2k + +https://github.com/ZHENFENG13/My-Blog + +My Blog 是由 SpringBoot + Mybatis + Thymeleaf 等技术实现的 Java 博客系统,页面美观、功能齐全、部署简单及完善的代码,一定会给使用者无与伦比的体验! + +![](http://img.topjavaer.cn/image/my-blog.png) + + + +## ForestBlog + +star3.2k + +https://github.com/saysky/ForestBlog + +一个简单漂亮的SSM(Spring+SpringMVC+Mybatis)博客系统。该博客是基于SSM实现的个人博客系统,适合初学SSM和个人博客制作的同学学习。 + +![](http://img.topjavaer.cn/image/forest_blog.png) + +## Blog + +star1.2k + +https://github.com/zhisheng17/blog + +`My-Blog` 使用的是 Docker + SpringBoot + Mybatis + thymeleaf 打造的一个个人博客模板。此项目在 [Tale](https://github.com/otale/tale) 博客系统基础上进行修改的。 + +![](http://img.topjavaer.cn/image/blog.png) + +## community + +star:1.8k + +https://github.com/codedrinker/community + +码问社区。开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。 + +![](http://img.topjavaer.cn/image/image-20210826234936711.png) + + + +## vblog + +star:6.5k + +https://github.com/lenve/VBlog + +V部落,Vue+SpringBoot实现的多用户博客管理平台! + +后端主要采用了: + +1.SpringBoot +2.SpringSecurity +3.MyBatis +4.部分接口遵循Restful风格 +5.MySQL + +前端主要采用了: + +1.Vue +2.axios +3.ElementUI +4.vue-echarts +5.mavon-editor +6.vue-router + +![](http://img.topjavaer.cn/img/202306241059444.png) + +## gpmall + +star:4.3k + +https://github.com/2227324689/gpmall + +【咕泡学院实战项目】基于SpringBoot+Dubbo构建的电商平台。业务模块划分,尽量贴合互联网公司的架构体系。所以,除了业务本身的复杂度不是很高之外,整体的架构基本和实际架构相差无几。 + +后端的主要架构是基于springboot+dubbo+mybatis。 + +![](http://img.topjavaer.cn/image/20220505162427.png) + +![](http://img.topjavaer.cn/image/20220505162154.png) + +## guns + +star:3.4k + +https://github.com/stylefeng/Guns + +Guns是一个现代化的Java应用开发框架,基于主流技术Spring Boot2,Guns的核心理念是提高开发人员开发效率,降低企业信息化系统的开发成本,提高企业整体开发水平。 + +Guns基于**插件化架构**,在建设系统时,可以自由组合细粒度模块依赖,实现不同功能的组合和剔除,让项目体积灵活控制,从而更方便地搭建不同的业务系统。 + +使用Guns可以快速开发出各类信息化管理系统,例如OA办公系统、项目管理系统、商城系统、供应链系统、客户关系管理系统等。 + +![](http://img.topjavaer.cn/image/20220505162031.png) + +## music-website + +star:2.3k + +https://github.com/Yin-Hongwei/music-website + +音乐网站。客户端和管理端使用 **Vue** 框架来实现,服务端使用 **Spring Boot + MyBatis** 来实现,数据库使用了 **MySQL**。 + +前端技术栈:Vue3.0 + TypeScript + Vue-Router + Vuex + Axios + ElementPlus + Echarts。 + +![](http://img.topjavaer.cn/image/20220505161944.jpg) + + + + + diff --git a/docs/campus-recruit/layoffs-solution.md b/docs/campus-recruit/layoffs-solution.md new file mode 100644 index 0000000..6cdfa85 --- /dev/null +++ b/docs/campus-recruit/layoffs-solution.md @@ -0,0 +1,24 @@ +--- +sidebar: heading +--- + +今年确实是互联网寒冬啊! + +从2022年5月中旬以来,包括腾讯、阿里巴巴、字节跳动、美团、拼多多、快手、百度、京东、网易等在内的十余家企业被爆出裁员消息。 + +也有一些校招毁约的公司,真的是。。。 + +对于2023届的同学,这里给几个建议: + +1. **准备充足**,再去面试!今年行情不好,很多互联网中大厂都在缩招,hc比往年少,所以更要准备充分,争取多拿几个offer,才有主动选择的余地! +2. **多投简历**,不要有“非大厂不进”的心态。今年秋招会比往年难些,毕竟僧多粥少,建议海投简历,争取多一些面试机会。 +3. 找师兄师姐**内推**。内推可以避免在简历关被筛掉,见过太多学历不好的同学,投了很多简历,最终面试的机会寥寥无几。多找一些内推渠道,包括你的师兄师姐、牛客网、一些求职交流裙等。 +4. **选择好方向**,不要“三心二意”。有些同学没有规划好自己的职业发展,在秋招会投递多个方向,比如前段时间我的读者就投了嵌入式工程师、Java后台工程师、测试工程师,大彬不建议大家这么做。虽然投递多个岗位方向,可以多几个面试机会,但是相应的你得多准备几个岗位的知识复习,精力会分散。就Java后台的知识点,没有半个月一个月的时间,是复习不完的(大佬除外)。因此,建议大家秋招之前就定好方向,专注一个方向进行复习。 +5. 多看看银行、研究所等**国企**的机会,相对稳定。近两年很多公司爆出校招毁约的劣迹,而国企一般不会轻易毁约,这算是一个优势。 +6. **打好基础**。校招比较注重基础知识,包括操作系统、计算机网络、数据结构与算法等(针对开发岗位),小伙伴们一定要把基础巩固好,不要等到面试的时候,一问三不知,成了秋招的“炮灰”。 + + + +最后,互联网行业,**年年都是最难的一年**,大家放好心态,面试没过也不用气馁,面试通过与否跟很多因素相关(自身知识储备、竞争者的水平等等),面试完多复盘,相信大家最终都能找到自己满意的offer! + + diff --git a/docs/campus-recruit/leetcode-guide.md b/docs/campus-recruit/leetcode-guide.md new file mode 100644 index 0000000..f65e6f8 --- /dev/null +++ b/docs/campus-recruit/leetcode-guide.md @@ -0,0 +1,153 @@ +--- +sidebar: heading +title: LeetCode刷题经验分享 +category: 分享 +tag: + - 刷题 +head: + - - meta + - name: keywords + content: LeetCode刷题经验,刷题 + - - meta + - name: description + content: LeetCode刷题经验分享 +--- + +分享几点我自己的刷题经验,看看我是如何在最短时间内搞定数据结构与算法,达到应付面试的程度的。 + +主要有以下3点技巧: + +1. **按题目分类来刷**。 +2. **难度要循序渐进**。 +3. **做好总结**。 + +## 按题目分类刷题 + +LeetCode上面的题目都有进行分类,建议在一个时间段只刷同一类型的题目,可以更全面的认识这一类型的数据结构or算法,以加深对此类题型的理解。就好比练功夫,前期把一些基本招式都熟悉掌握,后面再串通这些招式,融会贯通。 + +我个人也是比较习惯按照分类来刷题,自我感觉效果还可以。 + +我将LeetCode题目进行了整理分类,大家可以参考下(以下出现的题型都是需要掌握的): + +**数组操作** + +- LeetCode54 螺旋矩阵 +- LeetCode76 最小覆盖子串 +- LeetCode75 颜色分类 +- LeetCode73 矩阵置零 +- LeetCode384 打乱数组 +- LeetCode581 最短无序连续子数组 +- LeetCode945 使数组唯一的最小增量 + +**链表操作** + +- LeetCode206 反转链表 +- LeetCode19 删除链表的倒数第N个节点 +- LeetCode25 k个一组翻转链表 +- LeetCode141 环形链表 +- LeetCode142 环形链表Ⅱ +- LeetCode61 旋转链表 +- LeetCode138 复制带随机指针的链表 +- LeetCode160 相交链表 +- LeetCode707 设计链表 + +**栈** + +- LeetCode20 有效的括号 +- LeetCode32 最长有效括号 +- LeetCode155 最小栈 +- LeetCode224 基本计算器 +- LeetCode232 用栈实现队列 +- LeetCode316 去除重复字母 + +**树的遍历** + +- LeetCode94 二叉树的中序遍历 +- LeetCode102 二叉树的层次遍历 +- LeetCode110 平衡二叉树 +- LeetCode144 二叉树的前序遍历 +- LeetCode145 二叉树的后序遍历 + +**二叉搜索树** + +- LeetCode98 验证二叉搜索树 +- LeetCode450 删除二叉搜索树中的节点 +- LeetCode701 二叉搜索树中的插入操作 + +**递归** + +- LeetCode21 合并两个有序链表 +- LeetCode101 对称二叉树 +- LeetCode104 二叉树的最大深度 +- LeetCode226 翻转二叉树 +- LeetCode236 二叉树的最近公共祖先 + +**双指针/滑动窗口** + +- LeetCode3 无重复字符的最长子串 +- LeetCode11 盛最多水的容器 +- LeetCode15 三数之和 +- LeetCode16 最接近的三数之和 +- LeetCode26 删除排序数组中的重复项 +- LeetCode42 接雨水 +- LeetCode121 买卖股票的最佳时机 +- LeetCode209 长度最小的子数组 + +**快慢指针遍历** + +- LeetCode141 环形链表 +- LeetCode202 快乐数 +- LeetCode876 链表的中间结点 + +**动态规划** + +- LeetCode5 最长回文子串 +- LeetCode53 最大子序和 +- LeetCode62 不同路径 +- LeetCode64 最小路径和 +- LeetCode70 爬楼梯 +- LeetCode118 杨辉三角 +- LeetCode300 最长上升子序列 +- LeetCode1143 最长公共子序列 + +**回溯算法** + +- LeetCode10 正则表达式匹配 +- LeetCode22 括号生成 +- LeetCode40 组合总和2 +- LeetCode46 全排列 + +**贪心算法** + +- LeetCode 11. 盛最多水的容器 +- LeetCode 406. 根据身高重建队列 +- LeetCode 55. 跳跃游戏 +- LeetCode 122. 买卖股票的最佳时机 II +- LeetCode 309. 最佳买卖股票时机含冷冻期 +- LeetCode 714. 买卖股票的最佳时机含手续费 + +**并查集** + +- LeetCode200 岛屿的个数 +- LeetCode547 省份数量 + +**位运算** + +- LeetCode52 N皇后Ⅱ +- LeetCode338 比特位计数 +- LeetCode191 位1的个数 +- LeetCode231 2的幂 + +## 难度要循序渐进 + +这一点是针对初学者来说的,切记一上来就干hard级别的题目,会让你怀疑人生的。。。 + +正确的做法是**循序渐进**,从容易到中等,再过渡到困难级别。不过国内大厂考察算法,一般都是中等难度,困难级别的应该很少考察。 + +## 做好总结 + +**多做总结!多做总结!多做总结!** + +**做好总结很重要**,特别是对于没思路的题目,看了其他大佬的解法之后,多思考有哪些题目也是类似解法,这种题目的关键解题步骤,把自己的理解写下来,方便自己日后查看。 + +虽然总结可能会花费你半个钟甚至更多的时间,但是不总结的话,下次你遇到这个题目,可能会花更多的时间去思考、解答。 diff --git a/docs/campus-recruit/program-language/README.md b/docs/campus-recruit/program-language/README.md new file mode 100644 index 0000000..bd940fa --- /dev/null +++ b/docs/campus-recruit/program-language/README.md @@ -0,0 +1,5 @@ +# 编程语言(更新中) + +- [Java和Golang怎么选?](./java-or-golang.md) +- [Java和C++怎么选?](./java-or-c++.md) + diff --git a/docs/campus-recruit/program-language/java-or-c++.md b/docs/campus-recruit/program-language/java-or-c++.md new file mode 100644 index 0000000..5ae61fe --- /dev/null +++ b/docs/campus-recruit/program-language/java-or-c++.md @@ -0,0 +1,45 @@ +--- +sidebar: heading +title: Java和C++怎么选? +category: 分享 +tag: + - 职业规划 +head: + - - meta + - name: keywords + content: java和c++,语言选择 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +# Java和C++怎么选? + +很多读者私底下问过我这个问题:Java和c++应该选择哪个?哪个好就业、前景更好? + +首先,Java和C++都是主流的编程语言,不存在哪个好哪个差的说法,学得好的话,都很有前途。 + +下面我从几个方面来比较Java和C++的差异。 + +## 学习难度 + +毫无疑问,C++相对于Java更难学。C++面向底层,而Java面向上层。C++要求对于计算机底层的知识更高,更难,学习C++更有利于理解计算机基础。 + +## 使用场景 + +Java 是大型 web 应用后台的首选语言,像淘宝和京东这些大型电商系统的 web 服务器都选用Java技术栈。很多大数据相关的框架,都是用 Java 开发的,所以在大数据领域 Java 有着天然的优势。另外,移动端安卓APP开发语言也是Java。 + +C++在人工智能、机器学习、计算机视觉与图像识别、自动驾驶等新兴技术领域运用更多,这些领域对性能、效率要求更高。 + +## 市场需求 + +从招聘网站的数据来看,Java开发需求是大于C++的,而且相比于C++,Java更好入门,比Java简单一些,很适合短期学习快速上手工作。如果你想从事后台开发方向,那么建议你选择Java,因为互联网公司在后台开发方向招的 Java 程序员会更多一些。C++ 主要是底层应用开发、语音、图形图像、音视频、游戏等方面用得多。 + +## 发展前景 + +Java 和 C++是两门主流的热门开发语言,一直名列世界编程语言排行榜的前几位。在 TIOBE2022 年 5 月最新的世界编程语言排行榜中,C++和 Java 依然稳定在前几位。两者目前发展前景都很好,暂时不存在谁替代谁或者被其他编程语言替代的情况。 + + + +最后,每个人学习能力、职业规划、兴趣不一样,对于选择哪种编程语言,应该根据自身的具体情况,选择相应的编程语言,做出最优的选择。 + diff --git a/docs/campus-recruit/program-language/java-or-golang.md b/docs/campus-recruit/program-language/java-or-golang.md new file mode 100644 index 0000000..524ccab --- /dev/null +++ b/docs/campus-recruit/program-language/java-or-golang.md @@ -0,0 +1,84 @@ +--- +sidebar: heading +title: Java和Golang怎么选? +category: 分享 +tag: + - 职业规划 +head: + - - meta + - name: keywords + content: java和golang,语言选择 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +# Java和Golang怎么选? + +大家好,我是大彬~ + +最近有读者问了我一个问题:Java和Golang怎么选?下面分享我的看法。 + +作为非科班转码的选手,曾经也很纠结这个问题。 + +Java是当前使用最热门的编程语言,Go最近也很火,很多公司比如bilibili后台转向Go开发,头条的后台只用Python和Go。根据最新2022年2月份的TIOBE编程语言指数排行榜,排名前三的分别是Python、C和Java,Go排名在第11位。 + +以下从三个方面来分析: + +## 一、编程语言 + + 从编程语言本身来说,Java在1995年5月首次推出,Go在2009年11月正式推出。 + + 1、Java + +两种语言各有特点,Java经历了20多年,一直在不断更新推出新版本。2009年Oracle收购Sun公司后,Java发展得到了大力支持,现在使用非常多的Java8发布于2014年,当前最新版本是Java18,于2022年3月份发布。和早期版本相比,从Java8开始,吸收了越来越多的现代化编程语言的优点,比如lambda表达式。 + +Java 要求开发人员更多地地关注程序的业务逻辑,知道如何创建、过滤、修改和存储数据。系统底层和数据库方面的东西都是通过配置和注解来完成的(比如通过 Spring Boot 等通用框架)。 + +2、Go + +Go由Google的三位大神开发,Robert Griesemer,Rob Pike 及 Ken Thompson,是一种静态强类型、编译型语言,语法与C相近,功能更丰富。在2016年,Go被软件评价公司TIOBE 选为“TIOBE 2016 年最佳语言”。 + +Go 不是面向对象编程语言。Go 没有类似 Java 的继承机制,因为它没有通过继承实现传统的多态性。实际上,它没有对象,只有结构体。它可以通过接口和让结构体实现接口来模拟一些面向对象特性。此外,你可以在结构体中嵌入结构体,但内部结构体无法访问外部结构体的数据和方法。Go 使用组合而不是继承将一些行为和数据组合在一起。 + +Go 是一种命令式语言,Java 是一种声明式语言。Go 没有依赖注入,我们需要显式地将所有东西包装在一起。因此,在使用 Go 时尽量少用“魔法”之类的东西。一切代码对于代码评审人员来说都应该是显而易见的。Go 程序员应该了解 Go 代码如何使用内存、文件系统和其他资源。 + +## 二、学习难度 + + 1、Java + +Java是一种静态面向对象编程语言,继承了很多的C++优点,功能强大、简单易用、跨平台可移植,具有多线程、分布式等特点。入门学习不难,随着项目经验的积累逐步提升进阶。 + +2、Go + +Go也是一种静态的编译型语言,语法和C相近,但是采用了不同的变量声明方式。Go支持垃圾回收功能,并行模型是以通信顺序进程为基础,自1.8版本开始支持插件Plugin,能动态加载部分函数。从2.0开始支持泛型。 + + 相比Java,Go内嵌了关联数组数据库类型,也称为哈希表Hashes或字典Dictionaries,就像字符串类型一样。 Go 也没有继承多态性。被嵌入到结构体里的结构体只知道其自己的方法,对“宿主”结构体的方法一无所知。 + +对于Java开发人员来说,这尤其具有挑战性。 不过,随着时间的推移,我开始意识到这种处理多态性的方法只是另一种思维方式,而且是有道理的,因为组合比继承更加可靠,并且运行时间是可变的。 错误处理。在 Go 中,完全由你来决定返回什么错误以及如何返回错误,因此作为开发人员,你需要负责返回和传递错误。毫无疑问的是,错误可能会被隐藏掉,这是一个痛点。时刻要记得检查错误并把它们传递出去,这有点烦人,而且不安全。 + +当然,你可以使用 linter 来检查隐藏的错误,但这只是一种辅助手段,不是真正的解决方案。在 Java 中,处理异常要方便得多。如果是 RuntimeException,甚至不必将其添加到函数的签名中。 + +## 三、发展前景 + +考虑发展前景的话,推荐学习Java语言。Java是当前的主流开发语言,普遍使用在Web开发、电商系统、企业信息管理等各种行业场景。 + +不信你打开招聘网站,搜搜Java和Go岗位的招聘量。如下图,同一地区,Java招聘岗位是500+,Go招聘岗位是175。由此看来,Java岗位的需求量还是比较多的(当然Java方向也比较卷)。 + +![](http://img.topjavaer.cn/img/20220620084914.png) + +![](http://img.topjavaer.cn/img/20220620084855.png) + +Java社区非常活跃,各种文档和学习资料非常丰富。因为使用广泛,所以很多同事朋友沟通交流。 开发框架也是降低学习难度的有力工具,Spring框架是Java开发时常用框架,有非常丰富的组件和易用的功能,Spring Boot和Spring Cloud更是简化了开发过程中的琐碎工作,自动化配置依赖模块、开箱即用和约定优于配置,这些策略使得Spring框架在快速开发领域非常受欢迎。 + +> 参考链接:https://juejin.cn/post/6960553709357154311 + + + +**如果你还在这两种语言之间犹豫不定的话,那就来卷Java吧。** + +下面附上Java学习路线,是我**自学Java过程踩坑**总结出来的,希望能帮到你们! + +## 四、Java学习路线 + +[点这里](https://topjavaer.cn/learning-resources/java-learn-guide.html#%E8%87%AA%E5%AD%A6%E8%B7%AF%E7%BA%BF) diff --git a/docs/campus-recruit/project-experience.md b/docs/campus-recruit/project-experience.md new file mode 100644 index 0000000..cad72fa --- /dev/null +++ b/docs/campus-recruit/project-experience.md @@ -0,0 +1,135 @@ +--- +sidebar: heading +title: 项目经验怎么回答 +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 项目经验 + - - meta + - name: description + content: 项目经验怎么回答 +--- + +# 项目经验怎么回答 + +在面试时,经过寒暄后,一般面试官会让介绍项目经验 。常见的问法是,**说下你最近的一个项目**。 + + 根据我们的面试经验,发现有不少同学对此没准备,说起来磕磕巴巴,甚至有人说出项目经验从时间段或技术等方面和简历上的不匹配,这样就会造成如下的后果。 + +1. **第一印象就不好了**,至少会感觉你表述能力不强。 +2. 一般来说,面试官会根据你介绍的项目背景来提问题,假设面试时会问10个问题,那么至少有5个问题会根据所介绍的项目背景来问,如果没说好,那么就**没法很好地引导后继问题**了,就相当于把提问权完全交给面试官了。 + + 面试时7份靠能力,3份靠技能,而刚开始时的介绍项目又是技能中的重中之重,所以本文将从“**介绍**”和“**引导**”两大层面告诉大家如何准备面试时的项目介绍。 + + 好了,如下是正文内容。 + +## 准备项目描述 + + **在面试前准备项目描述,别害怕,因为面试官什么都不知道** + + 面试官是人,不是神,拿到你的简历的时候,是没法核实你的项目细节的。 + + 更何况,你做的项目是以月为单位算的,而面试官最多用30分钟来从你的简历上了解你的项目经验,所以你对项目的熟悉程度要远远超过面试官,所以你一点也不用紧张。 + + 如果你的工作经验比面试官还丰富的话,甚至还可以控制整个面试流程。 + +![](http://img.topjavaer.cn/img/20220711000600.png) + + 既然面试官无法了解你的底细,那么他们怎么来验证你的项目经验和技术?下面总结了一些常用的提问方式。 + +![](http://img.topjavaer.cn/img/20220711000648.png) + +## 准备项目的各种细节 + + **准备项目的各种细节,一旦被问倒了,就说明你没做过** + + 一般来说,在面试前,大家应当准备项目描述的说辞,自信些,因为这部分你说了算,流利些,因为你经过充分准备后,可以知道你要说些什么。而且这些是你实际的项目经验(不是学习经验,也不是培训经验),那么一旦让面试官感觉你都说不上来,那么可信度就很低了。 + + 不少人是拘泥于“项目里做了什么业务,以及代码实现的细节”,这就相当于把后继提问权直接交给面试官。下表列出了一些不好的回答方式。 + +![](http://img.topjavaer.cn/img/20220711000705.png) + + 在避免上述不好的回答的同时,大家可以按下表所给出的要素准备项目介绍。如果可以,也请大家准备一下用英语描述。其实刚毕业的学生,或者工作经验较少的人,英语能力都差不多,但你说了,这就是质的进步。 + +![](http://img.topjavaer.cn/img/20220711000721.png) + + 面试前,你一定要准备,一定要有自信,但也要避免如下的一些情况。 + +![](http://img.topjavaer.cn/img/20220711000733.png) + +## 不露痕迹地说出面试官爱听的话 + + 在项目介绍的时候(当然包括后继的面试),面试官其实很想要听一些关键点,只要你说出来,而且回答相关问题比较好,这绝对是加分项。我在面试别人的时候,一旦这些关键点得到确认,我是绝对会在评语上加上一笔的。 + + 下面列些面试官爱听的关键点和对应的说辞。 + +![](http://img.topjavaer.cn/img/20220711000746.png) + +## 一定要主动,面试官没有义务挖掘你的亮点 + + 面试时,面试官往往会特别提问:你项目里有什么亮点?或者你作为应聘者,有什么其他加分项能帮你成功应聘到这个岗位。即使这样问,还有些人直接说没有。 + + 我这样问已经是处于角色错位了,作为面试者,应当主动说出,而不是等着问,但请注意,说的时候要有技巧,找机会说,通常是找一些开放性的问题说。 + + 比如:在这个项目里用到了什么技术?你除了说一些基本的技术,比如Spring MVC,Hibernate,还有数据库方面的常规技术时,还得说,用到了Java内存管理,这样能减少对虚拟机内存的压力,或者说用到了大数据处理技术等。 + + 也就是说,得找一切机会说出你拿得出手的而且当前也非常热门的技术。 + + **记住:面试官不是你的亲戚,面试官很忙,能挖掘出你的亮点的面试官很少,而说出你的亮点是你的义务。** + + 我在面试别人过程中,根据不同的情况一般会给出如下的评语。 + + 1、回答很简答,但回答里能证明出他对框架等技术确实是做过,我会在评语里些“对框架了解一般,不知道一些深层次的知识(我都问了多次了你都回答很简答,那么对不起了,我只能这么写。 + + 或许你确实技术很强,那也没办法,谁让你不肯说呢?)”,同时会加一句“表达能力很一般,沟通能力不强”,这样即使他通过技术面试,后面的面试他也会很吃力。 + + 2、回答很简单,通过回答我没法验证他是在项目里做过这个技术,还是仅仅在平时学习中学过这个技术。 + + 我就会写“在简历中说用过XX技术,但对某些细节说不上来,没法看出在项目里用到这个技术”,如果这个技术是职务必需点,那么他通过面试的可能性就非常小。 + + 3、回答很简单,而且只通过嗯啊之类的虚词回答,经过提醒还这样,我会敷衍几句结束面试,直接写“技术很薄弱,没法通过面试”。 + + 4、虽然通过回答能很好地展示自己的技能,但逻辑调理不清晰,那么我会让他通过技术面试,但会写上“技能很好,但表达能力一般(或有待提高),请后继面试经理斟酌”。 + + 这样通过后继综合面试的机会就一般了,毕竟综合面试会着重考察表达能力交往能力等非技术因素。 + + 不管怎样,一旦回答简单,不主动说出你的擅长点,或没有条理很清楚地说出你的亮点,就算我让你通过面试,也不会写上“框架细节了解比较深,数据库应用比较熟练”等之类的好评语,你即使通过技术和后面的综合面试,工资也是比较低的。 + +## 一旦有低级错误,可能会直接出局 + + 面试过程中有些方面你是绝对不能出错,所以你在准备过程中需要尤其注意如下的因素。下面列了些会导致你直接出局的错误回答。 + +![](http://img.topjavaer.cn/img/20220711000758.png) + +## 学会引导 + +引导篇:准备些加分点,在介绍时有意提到,但别说全 + + 在做项目介绍的时候,你可以穿插说出一些你的亮点,但请记得,不论在介绍项目还是在回答问题,你当前的职责不是说明亮点而是介绍项目,一旦你详细说,可能会让面试官感觉你跑题了。 + + 所以这时你可以一笔带过,比如你可以说,“我们的项目对数据要求比较大,忙的时候平均每小时要处理几十万条数据”,这样就可以把面试官引入“大数据”的方向。 + + 你在面试前可以根据职位的需求,准备好这种“一笔带过”的话。比如这个职位的需求点是Spring MVC框架,大数据高并发,要有数据库调优经验,那么介绍以往项目时,你就最好突出这些方面你的实际技能。 + +## 你可以引导,但不能自说自话 + + 如果你真的想应聘的话,一定要事先准备,只要别露出太明显的痕迹,面试官不会写上“似乎有准备,没法考察真实技能”这种话,更何况未必每个面试官都能感觉出你准备过。 但你不能凭着有准备而太强势,毕竟面试是面试官主导的。 + + 有时说话太多,主动扩展,问他数据库用什么,不仅回答数据库是什么,自己做了什么,甚至顺便会把大数据处理技术都说出来。其实可能会过犹不及,面试官就会重点考察你说的每个细节,因为怀疑你说的都是你从网上看的,而不是你项目中用到的。 + + 不过话说回来,他如果仅仅说,数据量比较大,但点到为止,不继续说后面的话,我就会深入去问,他自然有机会表达。同时请注意,一般在面试过程中,一旦你亮出加分点,但面试官没接嘴,这个加分点可能就不是项目必备的,也不是他所关注的,当前你就可以别再说了,或者等到你提问题的时候再说。 + +## 总结 + + 到这里,我们已经给出了介绍项目的一些技巧。这些技巧都是从 java web轻量级开发面试教程从摘录的。 + + 两句话,第一,面试前一定要准备,第二,本文给出是的方法,不是教条,大家可以按本文给出的方向结合自己的项目背景做准备,而不是死记硬背本文给出的一些说辞。 + + 当大家介绍好项目背景后,面试才刚刚开始,哪怕你说得再好,哪怕你把问题引导到你准备的范围里,这也得应付Java Web(比如Spring MVC,ORM等)、Java Core(多线程、集合、JDBC等)和数据库等方面的问题。希望大家有所收获。 + + + +> 链接:https://www.nowcoder.com/discuss/150755 diff --git a/docs/campus-recruit/question-ask-me.md b/docs/campus-recruit/question-ask-me.md new file mode 100644 index 0000000..9d2e98e --- /dev/null +++ b/docs/campus-recruit/question-ask-me.md @@ -0,0 +1,134 @@ +--- +sidebar: heading +title: 面试官:你有什么要问我的吗? +category: 分享 +tag: + - 面试题 +head: + - - meta + - name: keywords + content: 面试高频题 + - - meta + - name: description + content: 面试官:你有什么要问我的吗? +--- + +## 你有什么要问我的吗? + +很多时候,在面试接近尾声的时候,面试官会问应聘者一个问题:“你有什么要问我的吗?”。 + +有些人没面试经验比较少,一下子没想到合适的问题,回答“没有”,然后就结束了。其实这样就白白浪费了一个了解公司、团队的好机会,而且可能给面试官一个不好的印象,他可能觉得你对他们公司并不感兴趣。 + +今天就来跟大家聊聊怎样去回答这个问题。 + +大部分公司都是有2~5轮面试,每一面的面试官不一样,一般是项目负责人、部门负责人或者HR,要根据面试官的身份来进行提问。 + +### 1.面试官是项目负责人 + +可以提问跟岗位内容相关的具体问题,比如岗位所需能力、团队分工、工作内容等,重点在于体现专业性和利他性。比如: + +- 您认为我目前的能力跟应聘岗位之间的匹配度如何? +- 请问您所在的团队整体工作流程是怎样的?如何进行分工的? +- 请问我如果有幸加入您的团队,我应该是负责哪些业务?可以给我详细介绍下相关业务吗? + +### 2.面试官是部门负责人 + +如果是面试官是部门负责人的话,可以了解关于工作和岗位、考核相关的问题,比如: + +- 如果我能有幸加入您负责的部门,您期望我能给团队带来哪些价值? +- 您认为考核这个岗位员工的最重要指标有哪些? + +也可以提一些关于行业和企业战略的问题,比如 + +- 您对这个行业未来的发展怎么看? + 您对企业未来的战略有什么想法? + +### 3.面试官是HR + +针对HR面试,可以了解以下内容: + +1. 应聘岗位情况(很多时候招聘网站上放的是通用JD,通用JD的问题就在于岗位的具体需求可能会被遗漏); +2. 职业发展(员工在企业内的晋升空间,轮岗机会以及培训机会) + +具体可以提问的问题如下: + +1. 这个职位未来几年的职业发展是怎样的? +3. 我想了解一下公司的培训机制和学习机制? +4. 请问下我应聘的岗位的晋升机制是怎样的? + +总的提问原则只有一个:**把合适的问题给到合适的人,不给对方制造麻烦**。 + +另外附上网上看到的**面试50问**,供大家参考~ + +## 面试50问 + +**职责篇** + +1. 工作时间/计划是怎样的? +2. 平时会处理哪些任务? +3. 团队中初级人员和高级人员是如何平衡的? +4. 针对新员工有哪些培训? +5. 如果按照工作计划执行,有多少工作是需要自己独立完成的? +6. 完成核心工作大概大概需要多久? +7. 对这个岗位的定义是什么? + +**技术篇** + +1. 你常用的堆栈 +2. 你如何用源代码 +3. 你如何测试代码? +4. 你平时如何追踪bug? +5. 如何集成和部署更改,CI/CD吗? +6. 基础架构 +7. 从规划到完成任务的工作流程是什么? +8. 怎么为灾难恢复 +9. 是否有标准化的开发环境 +10. 可以以多快的速度为产品设置新的本地测试环境 +11. 可以以多快的速度响应代码或依赖项中的安全问题? +12. 是否允许所有开发人员拥有其计算机的本地管理员权限? + +**团队篇** + +1. 这项工作是如何组织的? +2. 团队内/团队间的沟通情况是怎样的? +3. 遇到了意见分歧该如何解决? +4. 设定优先事项/时间表的人是谁? +5. 不能在预期时间内完成会怎样? +6. 每周都开啥会? +7. 产品/服务时间表是怎样的?(可以从多长时间发布一次/持续部署时长/多个发布流 +8. 出现生产事故后怎么处理? +9. 团队正在经历的尚未解决的挑战是什么? + +**公司篇** + +1. 是否有会议/旅行预算,使用规则是什么? +2. 晋升过程是怎样的? +3. 是否设置了单独的技术向或管理向的职业发展 +4. 年假、事假、病假、产假等每年都有多少天? +5. 对多元化招聘有什么看法? +6. 公司内部是否有自己的学习资源,比如电子订阅文档或在线课程等? +7. 有获得认证的预算吗? +8. 公司什么时候会达到成熟阶段? +9. 我能为FOSS项目做贡献吗?是否需要先获得批准? +10. 是否会被要求签署非竞业协议 + +**公司营收情况** + +1. 公司目前赚钱吗? +2. 如果没有,那距离赚钱还有多久? +3. 公司目前的发展资金来自哪里?谁在决定高层次的计划和方向? +4. 公司靠什么赚钱? +5. 是什么阻止公司赚更多的钱? + +**远程工作** + +1. 公司远程工作的员工占比多少? +2. 公司是否提供一些硬件设备,多长时间更新一次? +3. 是否可以通过公司购买额外的物品或家具?预算是什么样的? +4. 预计多长时间来一次办公室? +5. 办公室和会议室是否支持视频会议? + +**工作环境篇** + +1. 办公室布局是什么样的,是开放式/小隔间还是办公室? +2. 我的新团队是否有支持/市场等团队支持? diff --git a/docs/campus-recruit/resume.md b/docs/campus-recruit/resume.md new file mode 100644 index 0000000..b4c1636 --- /dev/null +++ b/docs/campus-recruit/resume.md @@ -0,0 +1,147 @@ +--- +sidebar: heading +title: 简历应该怎么写? +category: 分享 +tag: + - 简历 +head: + - - meta + - name: keywords + content: 简历编写,写简历 + - - meta + - name: description + content: 简历应该怎么写? +--- + +很多同学刚开始找工作时,投出去很多简历,但是都石沉大海了,没有下文。 + +要知道,很多大公司HR经常一天要看几百份,甚至上千份简历,基本都是10秒内看一份简历,就是这**关键的10秒**,HR就决定了是进入面试还是PASS。因此,**掌握好写简历的技巧**非常重要! + +接下来聊一下怎么写好简历。 + +分成以下几个方面: + +- **简历模板** +- **基本信息** +- **教育经历** +- **专业技能** +- **项目经历(实习经历、工作经历)** +- **奖项荣誉** + +这些都是一个简历必要的信息。 + +## 简历模板 + +像我们搞技术的,选个简约的模板就可以了,不要太多花哨的东西,简历太过花里胡哨,往往会引起HR和面试官反感,很有可能就失去了这次面试机会。 + +像下面这份简历就是反例,不够简洁,不仅不能吸引到面试官,反而更容易被面试官PASS掉。 + +![](http://img.topjavaer.cn/img/image-20210903002410309.png) + +而下面这份简历,相比之下就整洁很多,看起来清爽很多,也是我自己个人在用的简历模板。 + +![](http://img.topjavaer.cn/img/image-20210904111225259.png) + +要注意,简历上不要出现错别字,如果简历上出现错别字,会给HR或者技术面试官留下很不好的印象。**所以写完简历以后一定要仔细检查,可以让朋友帮忙检查一下**。 + +还有比较重要的一点是,简历的格式保存为**PDF**之后,再往招聘网站或者邮箱投递,**不要使用word格式的**,因为word很多版本,容易出现版本不兼容的情况,导致排版混乱,影响你的面试。 + +## 基本信息 + +个人信息主要有: + +**姓名**:这个不用说了,总不该写错吧。 + +**电话**:记住填写自己常用的手机号码,有些同学有多个号码的,更要注意!还有就是,千万别写错号码,写错了用人方想电话联系就联系不上了,很可能就错过了这次机会。 + +**电子邮箱**:这个跟电话一样,也是很重要的联系方式。很多笔试面试邀请都是通过邮箱发出,offer发放也是直接发到邮箱,不可以出错。至于用什么邮箱,随个人喜好,保证能正常收到邮件就行。 + +**求职意向**:写明岗位,后端、前端、测试、运维等等。建议每投一个简历,把应聘岗位这一栏修改为对应要投的岗位名称,显得更有诚意,不要一份简历投遍所有公司。 + +**工作年限**:应届生,实习生等不需要写,工作的小伙伴就写上对应的年限。很重要的一点就是,不要造假,实事求是! + +**博客/开源项目**:没有的话,可不写。如果你有好的开源项目或者博客,面试会很加分。但是要有一定的含金量,不是烂大街的项目或者博客。 + +## 教育经历 + +教育经历的时间一般都是**从后往前**的,最高学历放在首位,记得千万不要学历造假! + +成绩排名这块,有个**小技巧**。如果你专业排名前5 ,那么可以直接写年级排名/总人数,比如 5/100 。如果你专业排名不高,那么建议换成Top 20%这样的描述,比 起20/100,看起来印象会更好。 + +## 专业技能 + +见过很多同学简历上出现精通两个字,精通Java、精通MySQL、精通Redis等等,切记不要用这个词,面试官自己可能都不敢说自己精通。一般写熟悉或者熟练掌握等,当然如果你真的牛,能够扛得住面试官的灵魂拷问,那写上精通也没问题。 + +可以到招聘网站上看看应聘岗位的岗位要求,下面是阿里巴巴国际事业部Java开发岗位的要求: + +![](http://img.topjavaer.cn/img/image-20210904110034702.png) + +基本就是Java基础、并发、数据库、中间件、框架等,可以参考以下优秀模板: + +![](http://img.topjavaer.cn/img/image-20220711224619409.png) + +要注意的地方: + +- **专业名词要写对**,区分好大小写,比如Java不要写成java或者JAVA、MySQL不要写成mysql等,注重细节,力争做好每一步。 +- **不要写一些跟技术无关的东西**,比如"PS/PR"、"驾照"、"会用OFFICE"、""会重装系统"等等... +- **最好不要写一些使用 xx 的经验**,比如接入微信 /支付宝支付、接入友盟分享SDK等等,这种基本对着文档撸就好了,没必要写上去。除非特别有技术含量的。 + +## 项目经历 + +项目经历(实习经历、工作经历)非常重要,建议按照以下格式来写。 + +> 对于校招来说,实习是很大的加分项!小伙伴们有机会能去实习的,一定要去实习! + +**项目名称**:主要负责的项目。 + +**项目介绍**:简要描述项目背景、项目内容。 + +**技术栈**:列举项目中的技术栈,如果是当下比较流行的技术,那面试的时候会比较加分。不是比较大众的技术也没关系,只要对所用的技术有深入的了解,面试的时候也能打动面试官。 + +**项目职责**:写清楚你在项目中的角色,以及个人职责,具体负责哪一块业务等等。可以重点突出**攻克的技术问题**,或者参与过的**性能优化**(最好有**数据支撑**,比如SQL执行时间从10秒到50毫秒、xx优化给公司节省了100w的服务器成本等),这些在面试中很加分,比较能体现个人的实战能力。 + +下面是一份写的还不错的简历(来源:牛客网),截取了项目经历部分,写的还不错,大家可以参考下。 + +![](http://img.topjavaer.cn/img/image-20220711230732013.png) + +**注意**: + +1、**写到简历上的项目,一定是自己比较熟悉的,**面试官可能会深挖里面的一些细节,为什么这么设计,有没有其他替代方案等等,只有自己亲手做过的项目,才能应对面试官的逼问,不然很容易就gg了。 + +2、**项目数量不建议太多**,一般2-3个就可以了,把最有亮点的放在第一个。 + +3、**简历上的项目要贴合岗位要求**,比如你投的岗位是Java后端开发工程师,那么不建议在简历上写嵌入式、算法等不相关的项目。很多转行的同学可能就有这个问题。 + +4、**项目经历可以适当包装**,不要太离谱,能够自圆其说就可以。 + +5、**避免流水账式的介绍**,有部分同学会把项目经历写成日记一样,比如"从 xx 年开始做,先经历了 xx,然后发生了 xx"等等,洋洋洒洒几百字,非常不建议这么写。 + +6、**技术无关、很空泛的东西少写**,比如负责项目应用软件开发编码工作、编写相关的技术文档、提供技术支持等等,写了等于没写,没有提供有效的信息。 + +关于项目经历这块,面试常见的问题: + +- 负责项目的哪块内容? +- 选用这些技术的原因? +- xx功能怎么设计? +- 项目中遇到过什么疑难Bug? +- 使用一些开源框架的时候有没有遇到什么问题,怎么解决的? +- ... + +## 奖项荣誉 + +校招的话,如果拿到奖学金、ACM比赛拿过奖,可以写上去,会很加分。有些公司还很看重四六级证书,如果四六级比较高分,也可以放上去。 + +社招的话,一般比较看重项目经验,这一块可以弱化。 + +![](http://img.topjavaer.cn/img/image-20210904102957363.png) + + + +以上就是一份简历必要的基本信息了,希望对你有帮助! + + + +另外,大彬精心整理了**23套精美简历模板**,有需要的小伙伴可以自行下载: + +[23套精美简历模板](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247489358&idx=1&sn=dd1b91f115438c29a4215c674b8761e4&chksm=ce98ea08f9ef631e2c8361269f28c01db73eca1ff5b91ba0a0ec8c9a6b9fdcb46a5e16c57020#rd) + diff --git a/docs/campus-recruit/share/1-23-backend.md b/docs/campus-recruit/share/1-23-backend.md new file mode 100644 index 0000000..7e39e0f --- /dev/null +++ b/docs/campus-recruit/share/1-23-backend.md @@ -0,0 +1,119 @@ +--- +sidebar: heading +title: 双非本,非科班自学转码分享 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 非科班转码,自学java,双非转码 + - - meta + - name: description + content: 自学Java经验分享 +--- + +# 双非本,非科班的自我救赎之路 + +大家好,我是大彬~ + +今天跟大家分享一位学弟的**秋招经历**,他跟大彬一样,也是**非科班转码**的,在今年这样的环境下,能成功”上岸“,非常不容易。 + +![](http://img.topjavaer.cn/img/image-20221113224821052.png) + +接下来一起看看他的经历(学弟花了周末两天时间写的,整整**5000**多字,非常用心),希望他的分享能帮助到正在参加校招的小伙伴们,以及后面的学弟学妹们! + +## 背景介绍 + +先简要说说我的基本情况吧,本人就读于山西某**二本**院校,专业生物方向。 + +大一期间,借助学校的平台报了一个非全的计算机专业的第二学历。 + +大二下开始真正接触到Java。 + +大三下学期(今年上半年)找到了一份实习,下半年七月开始投递简历,于是便打响了我的秋招之战,直到10月下旬,才有两家小公司的意向,其中一家为蚂蚁集团旗下某子公司,base又在老家郑州,也算是符合我的预期。诚然,不敢和大佬相比,本人既无学历光环,手中也无大厂offer。 + +但正如大彬网站首页所说:“**作为一名转码选手,深感这一路的不易**”,这一路走来也踩过不少坑,也积累了不少自己的见解,在这里和大家分享下我的经验,同时也当做是对我这两年的一个总结。大家有不同意见的,欢迎交流。 + +![](http://img.topjavaer.cn/img/image-20221113224616682.png) + +## 修炼历程 + +### 我是如何与Java结缘的? + +都说兴趣是最好的老师,可能大多数转码的同学和我一样,基于各种原因对自己本专业没有学习的欲望。 + +我个人很喜欢理工科,恰巧大一军训结束后,我看到学校可以报一个非全的第二专业,然后就选择了计科。但真正和Java结缘是在大二,大一的时候学的都是像离散数学、操作系统、计算机网络原理这些科目,因为纯靠自学而且这些又都是计算机的底层知识(后来发现很有用),当时差点劝退我这个小白,于是就给自己制定了一个方针:“第一学历的考试及格就行,第二学历尽量认真去学”,就这样坚持了一个学期,后来经过验证,两边都没有耽误,效果还不错。但这时候我对于计算机的理解也仅仅只是那些理论上的东西,完全不知道如果要靠它吃饭的话需要具体学哪些东西又需要学到哪种程度? + +转机出现在大二下学期,因为需要学Java这门课,这也是我接触到的第一种编程语言(后来也学习了c++),然后一位朋友给我分享了B站老杜的Java教程,就发现Java不仅可以开发很酷的网站,也可以做出自己的小游戏,这就像打开了编程世界的大门,这些新奇的事物一扫前期理论知识带给我的枯燥感,每一个都吸引着我去不断的靠近它们,就这样我的Java修炼之路便开始了...... + +### 如何安排学习? + +>相信绝大多数的自学者都是按照网上的路线走的,但是学习路线上的内容那么多,我们需要从校招面试的角度去分析哪些是“常考点”,哪些是“低频考点”,这样我们才能把时间花在刀刃上,做到有针对性的准备校招。下面分享下本人的几点见解: + +- **JavaSE:**这一块我是分 **Java语言特性 + JUC + JVM**由浅入深逐渐去学习的。 + - **Java语言特性**:首先一定要熟练掌握语法,了解其新特性比如函数式编程、stream流的使用等。再者,像集合、IO流、反射、JDK动态代理、多线程这几块是高频面试题,尤其是集合方面,举个例子:面试官可能会以Map为切入点,问你HashMap的put流程,然后延伸到在JDK1.7和1.8的多线程环境下put的时候分别会出现什么问题?出现该问题的原因以及如何解决?像这种情况就得多背八股文了,有精力的也可以去扒源码来加深自己的理解。 + - **JUC:** 并发编程几乎也是**必问**,通过学习JUC能帮助你理解大部分问题。它考察的方式也很灵活,比如面试官可以直接问你理论知识,像线程池的七大参数、四种拒绝策略、线程池的执行原理等等,也可以给你一个场景让你去设计代码考察你的coding能力,我就曾经遇到过这样一个场景“项目中有一个main方法,执行具体的核心业务逻辑前需要开两个线程先校验参数,待参数校验完毕后,main方法才能执行核心业务逻辑,如何设计你的代码实现此需求?” 如果你对JUC下不太了解的话可能就会没有思路,反之,可能你一下子就会想到有一个CountDownLatch的类能解决这个问题。除此之外,像各种锁:ReentrantLock、Synchronized以及Synchronized是否被static关键字修饰、读写锁、可重入锁、CAS等、生产者和消费者问题、集合类不安全的解决方案等等,如果这些不理解的话,面试官随便问一下细节可能你就懵了。因为这一块知识稍微有点难度,建议大家在学习的时候可以从视频入门,在学习的过程中一定不要懒,自己多动手用代码去验证、去实现、弄懂它的大致原理,如果想在深入的话可以去网上找找大佬写的优质博客或读一些相关的书籍。 + - **JVM:** 对付校招的话,出现频率高的像JVM的内存区域,垃圾回收算法以及老年代用什么算法,CMS和G1的区别,程序计数器有什么用,虚拟机栈是否存在溢出以及什么情况下会溢出等等此类问题...我参考的是[**大彬的网站**](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247489295&idx=1&sn=9f82abc8f30b1d05ec7b50f25001e815&chksm=ce98ea49f9ef635f7b6eb0bbd5a22f5d38a113e46290241012e0bd3b5bc3248d773b4be144ee&token=1644038503&lang=zh_CN#rd)。不知道大家有没有遇到过这样的情况,比如你百度一个面试题,要么出来的是长篇大论(讲的很细,内容很多),要么寥寥几句,如果你直接按照搜来的答案说的话,要么很费时间,要么说不了几句就没了,而大彬网站中的大多数问题的答案属于你可以一字不用改动直接背给面试官的那种。当然,大家如果时间允许并且想要了解某个技术细节的话,不能只囫囵吞枣,还是要沉下心慢慢去思考的。 +- **算法:**经常听到有小伙伴问“算法到底要不要刷”,大家不坚定的态度无外乎是因为“工作中用不到”、“算法晦涩难懂,学起来枯燥”、“投入产出比高,需要长期投入才有效果”这几个因素。我想告诉大家的是,如果你想冲击大中厂的话,一定要去刷。就拿我这次秋招来说,做了几十家的笔试题,80%以上都有算法,更何况笔试是你秋招的第一道关卡,如果你算法题做不出来的话连面试都很难进。那如何规划呢,我的建议是如果你时间充足的话(比如正在大二或者大三上),学完JavaSE就能开始刷题了,这样一边刷题一边还能让你掌握语言特性。但是时间不充足的话(比如马上要找实习了技术栈还没学完,或者马上面临校园招聘了),这种情况下我还是建议先完善自己的技术栈,起码先把自己的技术体系形成闭环,待找到实习(工作)后再抽时间刷(我就是后者)。所以说,干什么都要趁早,**早,就是优势**。至于如何刷题,推荐大家去刷代码随想录,然后再刷剑指offer上的那70多道,两者加上接近三百道题了,(这时候已经能手撕大多数企业的题了),时间充裕的话还可以再刷刷hot100。注意,算法题也是需要**多多回顾**的,刷一遍是绝对不够的,建议多刷几遍,这样才能形成自己的理解做到活学活用。 + +- **数据库:** + + - MySQL:面试官经常从“MySQL架构、索引原理、事务原理与锁机制、日志机制、存储引擎执行细节”这几个方面去考察,也有一些笔试题中会出现场景题让你编写SQL。我推荐大家可以先过一遍小林的图解MySQL,这个讲的比较细也比较基础,然后有了基础之后再去看一下《MySQL45讲》会轻松很多,而且里面有很多真实的场景,你也可以稍加修饰吸收成你自己的经验来和面试官进行掰扯,45讲看完之后其实对数据库的各类面试题理解起来就很容易了,此时等到面试前再突击下大彬的MySQL面试题,效率直接起飞。至于SQL场景题的话,只能多练了,这个大家可以去牛客上刷SQL的在线编程题,刷到“SQL进阶挑战”完全就够了,不仅是应付笔试,面对以后工作用到的CRUD也是绰绰有余。 + - Redis: 面试常从“底层数据结构,主从复制,持久化、缓存一致性”等角度考察,学习路线和MySQL差不多,也是小林的图解 + 大彬的面试题 + +- **各种框架:** + + - 首先我想说一下框架在面试中的比重,大厂问的比较少,反倒小公司会问的多一点。该学到哪种程度呢,至少是springboot,像微服务那些是加分项,大厂的话不会太看中你的技术栈有多新,它们更注重你的基础。所以我觉得技术栈的话学到boot就没多大问题了,可以将后面的时间用来夯实你的基础。 + - 如何学习框架 + - 初级阶段:建议还是以视频为主吧,这样入门很快,毕竟框架也是实操性较强的东西,直接看视频效果会更直观一点。 + - 运用阶段:学完框架之后,就一定得多练多敲,一方面强化记忆,另一方面增强对这门技术的熟练度。比如可以从B站上找一个小项目练练手,或者从开源平台上找一些相关的Demo自己调试一下让它跑起来,扒开它的源码看自己能不能优化、改造某个功能等。这些都能让你快速升级,熟练掌握技术栈并养成自己的编程思想。 + - 质变阶段:当你对各类技术栈的使用已相当熟练,你可以去选择专攻某一个模块。比如你可以去研究spring中的某一个组件,比如Bean组件,IOC容器等。除此之外java中的轮子有很多,你也可以自己实现一个功能全面的rpc框架,也能写到你的简历上面,也能和面试官聊上一阵。 + +- **计算机基础:** + + 首先希望大家清楚,任何新技术的出现,都是为了完善旧技术的缺点,我们有学不完的技术,但是计算机基础的知识是永远不会变的。假如你不懂网络原理,可能你理解前后端用HTTP进行交互时就比较抽象、也无法实现自己的rpc框架...如果你不了解操作系统,也很难理解Redis中的大key对持久化的影响...对于校招来说,只需要好好准备操作系统和网络原理就行了。B站上也有很多资源,像王道考研的OS和网络,八股文的话可以看小林的图解,然后在看看大彬的面试题。 + + >可能细心的小伙伴已经发现了,我的路线总结下来基本上是 大彬 (面试)+ 小林图解(基础八股) + 代码随想录(刷题) + +### 遇到问题如何解决? + +>有句话是“旁观者清,近观者迷”,自学的过程本身就是孤独的,在这个过程中我们肯定也会遇到很多大大小小的问题,有时候自己碰到一个没法解决的问题就会陷在一个死胡同里好几天,不仅耽误学习进度,严重的时候还会影响我们的学习士气。当遇到这些情况时我们就要虚心地向别人请教、提问。我也加了很多的技术交流群,下面分享一下我对于"提问"的经验。 + +- 提问之前 + - 我们一定要尝试自己解决,不是说一遇到问题自己都懒得思考就直接去提问,可以和小伙伴交流、通过网上搜索,查阅一些论坛社区等手段来解决问题 + - 如果实在不能解决,就要梳理好自己要问什么,整理好你要表达的话语,尽量做到准确,简明,让别人一看就能明白你不懂的地方在哪。 +- 怎样提问 + - 第一点:因为你是求助者,一定要虚心。如果没有人回答你的问题也不要阴阳怪气,交流群本身是用来交流技术分享经验的一个地方。 + - 第二是准确描述你的问题的前因后果,比如你做过什么样的尝试,得到了什么样的结果,你想要的结果是什么 + +## 实习与秋招 + +>先插一句:无论是找实习还是准备秋招,**一定要趁早,早就是优势,早就代表着更多的机会!** + +### 我的实习历程 + +深知自己与别人的差距,于是我上半年二月份就开始在网上投实习的简历了,到四月底的时候找到了杭州一家公司,我仍清晰地记得那天收到offer时的心情,走上Java这条路以来,第一次感受到了被认可,这也给了我很大的一个信心让我坚定地在这条路上一直走下去。 + +相信大多数实习生进公司之后面对的都是边缘业务的CRUD,接触不到项目的核心,你如果想要得到更多的收获就得主动去找mentor要一些有挑战性的活,比如我实习期间承担难度最大的一件事就是完成了项目中分布式WebSocket的开发,从一开始确定技术解决方案,然后同mentor一起验证方案的可行性,再到具体编码实现,在整个流程中学到了很多东西,这后来也成为了我经常和面试官对线的地方,四个月的实习也是我成长最快的一段时间。 + +总之,我想告诉大家的是,如果自己的起点就比别人低,那就得自己去**争取更多的机会**,这样才能使自己一直**保持着竞争力**。 + +### 我的秋招历程 + +在实习期间的时候,我就开始投递了一小部分提前批(全GG),因为当时是七月份,并没有意识到到今年秋招的寒气,到八月份的时候我就辞职了,便全身心的转移到了秋招的战线上。 + +直到八月末九月初的时候已经投了100家左右,也做了将近二十多家的笔试,算法题平均都能a个75%,但是仅收到了两家面试,面试过程也都还算顺利,但还是都挂了,这个时候已经意识到形势的严峻了,我已经开始有点慌了,中间甚至有过放弃的念头,但我的直觉告诉我不能就这样灰头土脸的收场,无论结果怎样都要给自己一个合理的交代。 + +后来就每天边做笔试边投简历,也转变了投递策略,不能只局限在牛客和力扣上找公司,智联、BOSS、51JOB、甚至在抖音上碰到博主推的公司也开始投,全面广撒网。就这样战线一直拉到十月下旬。 + +两个月的秋招战线,可以说是我整个Java之路上最难熬的一段时间,经历了多次面试前的紧张、面试后的期待、收到感谢信后的失望,也感谢自己没有放弃吧,最后也算是拿到了这一行的敲门砖。 + +## 心得感悟 + +汝之蜜糖,彼之砒霜,这里引用某位网友的一段话“每个人都应经过自己的摸索,找到适合自己的路线。不管是应届生参加校招,还是在校生找实习,求职的过程都注定是**艰难且孤独**的,这时候不光要拼学历门槛、技术能力、语言表达能力,还要拼谁能沉得住气,谁能扛得住一次又一次面试失败后的打击。面试招聘常有金三银四和金九银十的说法,但这都是一个理想状态,在你实力没那么强的情况下,求职可能是一个比较长期的过程。不要给自己设置deadline,不是说十月之后就没有招聘,十月十一月,十二月,可能一家公司结束了招聘,又会有另外一家开始招聘。有人五月份六月份才拿到实习offer,而也有人在临近毕业五月甚至六月的时候才拿到自己满意的offer,一直面下去,**多复盘**,会有好结果的。” + +## 致谢 + +每个人在修炼的路上都是孤独的,我们只有怀着宗教般的意志和初恋般的热情,做任何事情都要抱着“流水不争先,争的是滔滔不绝”的态度去不断地提升自己,才有可能成就某种事业。作为一枚菜鸟小白,一路走来,受到了不少大佬的帮助,感谢我的那位朋友,感谢大彬,感谢卡哥,感谢小林,你们的帮助给这条路上的小白注入了源源不断的动力。最后,祝愿大家考研、找工作的伙伴都能顺利上岸! + diff --git a/docs/campus-recruit/share/2-no-offer.md b/docs/campus-recruit/share/2-no-offer.md new file mode 100644 index 0000000..037ad5b --- /dev/null +++ b/docs/campus-recruit/share/2-no-offer.md @@ -0,0 +1,67 @@ +--- +sidebar: heading +title: 非科班,秋招还没offer,该怎么办 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 非科班转码,秋招没offer,23秋招,秋招 + - - meta + - name: description + content: 秋招经验分享 +--- + +# 非科班,秋招还没offer,该怎么办 + +前两天有个学弟微信私聊我,他是非科班的(985硕士,机械专业),因为秋招准备的比较晚,目前还没拿offer,感觉秋招已经没希望了,只能春招再战了。学弟很焦虑,作为一个985硕士,周围很多人都拿到offer了,只有他还在“苦苦挣扎”,问我有没有什么**学习建议**? + +学弟目前**基本情况**如下: + +**已经学习的知识**: + +- 计算机基础:操作系统、数据结构 、计网、组成原理、数据库 +- Java 基础、集合、JVM、Java并发 +- 框架:Spring Boot、Mybatis、SpringMVC、Spring Cloud +- LeetCode 刷了几百道题 +- 其他的像Redis、RabbitMQ、Kafka这些都学了 + +项目经验是烂大街的秒杀系统,没有什么亮点。 + +因为我也是非科班出身,比较了解转码人秋招的“**痛**”,周末花了时间整理了一下,给了几点**建议**: + +## 校招是最好的机会 + +一定要明白**校招的重要性**,如果校招没拿到offer,那么只能走社招,而社招跟校招的难度不是一个级别的,看重的是你的项目经历,只会更难。无论是秋招,还是春招,对于应届毕业生来说,都是拿offer的最好机会,一定要抓住这个机会!秋招没把握住机会,那么春招更应该加足马力,争取拿到满意的offer。 + +## 实习经验很重要 + +**特别对于非科班同学**。如果现在还没有实习经验,最好找一下实习。非科班选手,如果不是名校出身,学历不够突出,没有相关实习经验,可能简历都过不了。 + +## 项目经验同样很重要 + +现在大部分应届生简历上的项目都是秒杀商城、RPC、外卖系统等等,这些项目也不是说没亮点,只是亮点都差不多,给人的感觉就是抄网上现成的,不是从头到尾自己实现的。假如面试时让你介绍一下这个项目,**你觉得这个项目亮点在哪里?你自己实现的功能有哪些?有没有其他解决方案**?这些都是需要自己仔细去考虑的。 + +可以看看类似的开源项目,取长补短,**包装**自己的项目经验,当然,包装要适度,要经得住面试的“灵魂拷问”,否则得不偿失。 + +## 面向面试准备 + +多在网上搜索面经,看看心仪的公司面试是个什么难度,喜欢考察什么类型的问题,思考怎么去准备,有**针对性**的进行查漏补缺。 + +## 关于算法题 + +学弟刷了400道LeetCode题目了,如果不是“无效刷题”,其实已经完全够用了。挺多人刷题量非常大,但是到最后发现还是效果不好,一道题做完之后,隔了半个月再来看,依旧一头雾水,这样刷再多题也没用。**建议是每刷完一道题,将解题思路写下来**,方便后续复习查看,这样刷题的效率会更高。 + +## 抓住秋招的尾巴 + +**秋招尚未结束,抓住秋招的尾巴**。11月-12月这个时间段,会有公司针对秋招时没有招满的岗位进行补录,要抓住这个机会。秋招补录的流程很快,一般只有整个流程几天内可以走完。 + + + + + +最后,如果秋招面试过程有疑问、offer抉择问题、简历问题等,可以扫码加大彬的微信交流~ + +![](http://img.topjavaer.cn/img/个人微信索隆.jpg) + diff --git a/docs/campus-recruit/share/2-years-tech-upgrade.md b/docs/campus-recruit/share/2-years-tech-upgrade.md new file mode 100644 index 0000000..5322d98 --- /dev/null +++ b/docs/campus-recruit/share/2-years-tech-upgrade.md @@ -0,0 +1,61 @@ +--- +sidebar: heading +title: 工作两年多,技术水平没有很大提升,该怎么办? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,技术提升 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作两年多,技术水平没有很大提升,该怎么办? + +最近在大彬的[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了问题:**工作两年多,技术水平没有很大提升,该怎么办?** + +**原问题如下**: + +大彬大佬能不能给点学习建议,我是**非计算机专业**,**培训**的java后端,现在开发工作两年多了,越是工作其实就越会发现自己的**知识面很窄**,没办法提升技术水平,所以想要自己学习提升下,但是**没啥方向**,不知道该先学什么,所以想请大佬指点下,谢谢! + +--- + +**大彬的回答**: + +最简单的一个方法就是找一个比你现在公司**技术方面强一些**的公司,到他们招聘网站看看**岗位职责描述**(1-3年工作经验的Java开发),对比下自己缺少哪些技能,**查漏补缺**。以跳槽到更好的公司为目标进行学习,这样既有动力也有学习方向。 + +> 附上阿里菜鸟1-3年的JD: +> +> 1. 扎实的编程基础,精通java开发语言,熟悉jvm,web开发、缓存,分布式架构、消息中间件等核心技术; +> 2. 掌握多线程编码及性能调优,有丰富的高并发、高性能系统、幂等设计和开发经验; +> 3. 精通Java EE相关的主流开源框架,能了解到它的原理和机制,如SpringBoot、Spring、Mybatis等; +> 4. 熟悉Oracle、MySql等数据库技术,对sql优化有一定的经验; +> 5. 思路清晰,良好的沟通能力与技术学习能力; +> 6. 有大型网站构建经验优先考虑; + +如果你在一个小公司或外包公司的话,一般一到两年时间就把用到的技术栈基本都摸透了,因为业务量不大,很难接触到像**高并发、分布式、灾备、异地多活、分片**等,每天都是重复的增删改查,**很难有技术沉淀**。 + +工作久了之后,你就会发现,到职业中后期,**公司的技术上限也是你的技术上限**,单靠自己盲目去学,缺少实践机会,技术上也很难精进。只有去更大的平台,你能接触到的业务场景、技术就会更多,技术能力也就能随着慢慢变强了。 + +--- + +最后,推荐大家加入我的[知识星球](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有200多位小伙伴加入了,星球已经更新了多篇**高质量文章、优质资源、经验分享**,利用好的话价值是**远超**门票的。 + + + +星球提供以下这些**服务**: + +1. 星球内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享、面试资料**,让你少走一些弯路 +2. 四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 +3. **一对一答疑**,我会尽自己最大努力为你答疑解惑 +4. **免费的简历修改、面试指导服务**,绝对赚回门票 +5. **中大厂内推**,助你更快走完流程、拿到offer +6. 各个阶段的**优质学习资源**(新手小白到架构师),包括一些大彬自己花钱买的课程,都分享到星球了,超值 +7. 打卡学习、读书分享活动,**大学自习室的氛围**,一起蜕变成长 + +**加入方式**:**扫描二维码**领取优惠券即可加入~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/campus-recruit/share/3-power-grid-vs-pdd.md b/docs/campus-recruit/share/3-power-grid-vs-pdd.md new file mode 100644 index 0000000..e3db044 --- /dev/null +++ b/docs/campus-recruit/share/3-power-grid-vs-pdd.md @@ -0,0 +1,147 @@ +--- +sidebar: heading +title: 国家电网还是拼多多,怎么选? +category: 分享 +tag: + - offer选择 +head: + - - meta + - name: keywords + content: offer选择,秋招offer比较 + - - meta + - name: description + content: 秋招offer选择经验分享 +--- + + +今天在知乎上看到这个职业选择的问题:“**国家电网还是拼多多,怎么选?**”,其中高赞的观点我觉得非常具有参考价值,今天分享出来,也希望能给球友们一些启发和帮助。 + +**原问题** + +本人是某 top3 本硕,EE 专业,秋招拿到了老家不差的地级市国网(东部沿海,非江浙山东)供电局与平多多的 offer。现在在纠结,应该选择哪一个。 + +选择国网供电局的 offer 的话,在老家,日子可以过得比较舒适。主要是稳定性使得失业的心理焦虑比较小,未来的确定性较大。在老家,房价也比较低。在老家,可以住较大的房子,可以有更多的时间陪伴家人。 + +不足之处是本人在国网中没有任何关系,本来国网中就讲关系,然后我老家所在城市更是如此,估计一辈子都在基层了。选择了国网也意味着放弃了大城市,我们老家城市未来的发展我是看不透的。 + +还有一点是,国网的钱是真的少,工作也很辛苦,估计以后的钱会更少。至于稳定性,这一点其实也是有隐忧的,因为 98 年国企员工下岗潮让我觉得,稳定性只是大家根据国网以前的形势得来的结论,随着电改的进行,以后是否那么稳定就不一定了。以及,到了市供电局分配岗位,是随机的,就怕被分配到不好的岗位和县局。 + +选择拼多多的话,钱倒是很多,只是在上海的话,房价高昂,可能用尽全力也买不了什么学区房,孩子只能上“菜小”,对其未来肯定不利。同时,除了买房以后,还有高昂的教育消费。 + +在拼多多,可以说是没有个人时间,没有生活,然后对身体伤害也比较大。而且自己是自学转码,其实自己目前技术似乎也不好,如果没有个人时间自学的话,估计很难有什么提升,程序员这条路的发展怕是会后续无力。 + +到了三四十岁,估计不得不面临被优化。只是,觉得在大城市,可以免去小城市的扰人的关系往来,也觉得自己有比较广阔的未来(想想而已,毕竟在上海是艰难求生存,不知道未来是如何),还有就是钱多。 + +钱多还是挺有吸引力的,毕竟世界上只有一种病,那就是穷病。 + +**总结一下,在国网工作有以下优缺点:** + +优点: + +- 小城市性价比较高,我们城市的房价还算是合理的; +- 确定性和稳定性较大; +- 相对来说,可以兼顾家庭和工作;以后也可以参与到孩子教育里。 + +缺点: + +- 钱少;真的是穷啊;而且有进一步下降的可能 +- 加班也不少 +- 有一定概率发生安全事故; +- 没有关系,估计一辈子当个基层;而且工人相对不体面(是一个学姐说的) + +**在 pdd 工作有以下优缺点:** + +优点: + +- 钱多; +- 接触到的牛人多,工作环境比较好; +- 在上海生活,上海生活有很多便利。 + +缺点: + +- 势必会与女友交流减少,以后对家庭有所忽视 +- 上海房价高昂(不过上海房价高昂也无可厚非,优质资源就那么多,没有房价门槛,资源都不够分) +- 失业焦虑与年龄压力比较大; +- 对身体健康不太友好,长时间的加班可能会导致身体吃不消。 + +**两个千赞回答** + +**回答一** + +> 作者:大魔王 链接:https://www.zhihu.com/question/423207447/answer/1715155394 + +兄弟,俺也是 top3 的,刚从南方电网某单位跳出来。这题俺有发言权。这俩选择都不是最优,相信我。目前我了解到的,周围学 ee 比较好的出路,一是去互联网大厂做服务器架设偏硬件的,一个是去做云相关的。 + +为啥是这俩,因为这俩目前需求极大还不内卷,可以在一线城市拿高薪镀金后,回到二线城市找个不错的工作,甚至部门小主管。一个朋友是本科西交美硕,学的 ee,去百度做了三年服务器架设,每年到手大概 45,然后跳到了成都的大数据中心,类似的大数据中心,各地都在建设,对于此类人才需求量极大。 + +另一个朋友是北理工学的光纤通信相关,在浪潮和深信服干了三年,现在跑到成都某央企旗下新成立的云业务子公司,到手 30 个,工作量很养生。另外,为啥从干了三年的电网跳出来,你可以听听我的看法。 + +有好几个朋友关心我跳槽的原因和去向。 + +先说说我的情况吧,我是 17 年入职南网超高压输电公司下面的检修中心。19 年家里人出首付买了房,并不是留不下什么的。到手收入算上公积金,按年份依次是 15、18、21,差不多每年涨两万多这样,直到 30w 专责或者班组长,大家可以算算需要多少年,隔壁的超高压广州局差不多 16、19、22、25,值长上限能到 40。 + +评论里有人说南网数研院不行的,但数研院比超高压的收入会高不少,大概几 w?具体的钱,因为发放模式不一样,所以说不准。 + +离职的原因总结起来就是,在一线城市进体制混日子没必要,和家里的体制内发展差不多,反正都没发展,家里房价压力小还不用背井离乡,在一线城市就要干有前景的工作,即便没混上去要回家也是赚了几年快钱,混上去自然更好。 + +具体说说为啥混不上去吧,同单位的数个 985 本硕,甚至是清华本硕,干了七八年了,丝毫不见升上去的可能性,升的上去的首先是关系户,别觉得关系户都是硬提拔,他们首先是干了出成绩的工作,至于为啥是他们干出成绩的工作,这还用说?其次是善于揣摩领导心意的干了很多年的舔狗,更多的是舔了也上不去。虽然卷是新常态,但电网的卷从学历素质而言确实是独一份。 + +**虽然轻松好混,但就如我总结的,要混为啥不回家混。** + +所以干了三年后看明白这一点就一直在准备跳槽,但电网的坑就在于,进去之后,出来太难了,工作经历经验在外界完全用不上。想过考公务员,但一不是研究生,二不是党员,三专业受限,能考的好岗位属实有限。好在家乡重庆发展给力,在云平台大数据物联网这一块有很多新企业。刚好有个新成立的央企在拓展电力行业的物联网业务,经过好几轮面试总算回来了,岗位是偏前端做产品的。回家以后收入只低了一点,个人感觉更有发展空间了。 + +人生的有趣之处就在于未来有希望。这是个极速变革的时代,过早进入电网这样一个与世隔绝的围城是一种遗憾。“坐观垂钓者,徒有羡鱼情。” + +**我们都会过上不错的生活,可在那之后呢?最大的快乐莫过于自我价值的实现。** + +**回答二** + +> 作者:匿名用户 链接:https://www.zhihu.com/question/423207447/answer/1656487127 + +这道题我做过,不想武断的给建议,我就跟大家说说选“错”了会发生什么吧。 + +我跟题主大概率是校友(但是年龄大他不少),浙大电院硕士,本科中流 985,但是我的求学生涯比较曲折,硕士毕业的时候已经 29 了。这是背景。 + +2014 年,我毕业二选一(其他 offer 没进决赛),上海电网和海康威视。按理说在确认能拿到上海户口的情况下,要我现在选我肯定选上海电网,承诺的起薪更高(当时我记得 15w),工作强度更小,又稳定,又能落户上海,想不出来怎么会不选? + +要知道当年闵行的房价只有两万六。 + +但我也不知道怎么想的,就觉得在上海肯定过的很苦,杭州更容易定居,于是就选了海康威视。其实当年我都是想象的,我连杭州的房价都不太知道,只是被上海两万六的房价吓坏了。后来的事情,包括上海杭州房价疯长,杭州变成奋斗比之都,我都完全没有预知。 + +然后就去海康上班了,第一年连奖金税前拿了 17w。后来每年都涨点,但是涨得不多,第三年税前全加起来能拿到 25w。这时候还是买不起房子,杭州的房子开始疯长,但是家里不是很支持我在杭州定居,拿不出首付,只能干看着滨江的小户型从不到 200w 涨到 400 多 w,彻底断了念想。 + +在杭州难以定居之后,我本来想着回老家省会城市工作,结果半路被阿里巴巴发了个 offer,就在杭州又工作了几年,但是仍然没买房子。其中的过程就不说了,反正没买上。 + +倒是图便宜在老家省会买了个房子,贷款 100 多万,没想到买了就开始降价。这也算是求仁得仁吧,怕自己没地方住,慌不择路,果然成了韭菜。很快就来到了 35 岁,高强度的工作实在有点顶不住,业绩也不太好,再加上总有人鼓吹 35 岁要找后路,想来想去再也不想在杭州待了,一天也不想多待,放弃了一笔股票,灰溜溜回老家省会找了个民营企业的 it 部门工作。 + +说实话,离开杭州的火车上,我哭了好几次。35 了,什么也没挣着,为数不多的存款被我投资不慎亏了个底掉,除了一幢仍然在降价的房子和 100 万欠款,我什么也没留下。 + +一路上都在想,为啥一直错?如果当时签了上海电网踏踏实实干,怎么也能像大学同学们那样混个专责,人生会明朗很多。就算留在杭,如果在合适的时候买房而不是拿去投资做生意,自己已经是个完成了地域跃迁的新杭州人了。 + +北方和南方,就是未来的东德和西德,北京就是柏林。而我,就是那个浪费了定居法兰克福的机会,灰溜溜跑回德累斯顿的可怜虫——这个想法一直都在我脑中萦绕,甚至到今天都没有消散…… + +我说这些,不是想让大家可怜我,也不是贩卖负能量,我就是想跟大家说——我算是那种错上加错,近于不可挽回的案例了吧?快 30 才毕业,工作选错,房子选错,永远不合时宜。我都很怀疑,浙大尤其是浙大电院,还有没有比我愚蠢+倒霉的毕业生? + +**但是事实证明,尽管我浪费了太多机会,后续的生活也并没有像我想象的那么悲观。** + +在老家工作天天可以回家住,自己的房子谁管涨价降价?自己装修的好好的,短时间又不可能卖。而且跟老婆孩子不需要分开,周末还能看看两边父母,幸福感挺高的。 + +老家也未必就是“东德”,尽管经济上有明显差距,但是文化上并非很落后,年轻人的观念其实也都差不多,不管是工作上还是生活中,都没有遇到太多不适。 + +当然,最重要的还是工作。 + +按理说像我这样的,兜了一大圈回老家,还进不了体制内,如果当年毕业的时候就想办法回到老家体制内,才是最正确的选择——我确实有特别多中小学同学都在当地的政府、事业单位,或者电网烟草等国企工作,现在也都是头头脑脑的,混的好的已经成了处长。 + +但是说实话,我也没有比人家过的差太多。工作压力确实大一些,毕竟身在民营企业,总没有体制内那么惬意。但是我回老家这两年,放下了漂泊的焦虑,反而可以真正把心思放在工作上,事业发展的也挺好,现在做到了部门负责人岗位,薪水已经打平了我在杭州的最后一年,后续也不是没有上升的空间。 + +**太长远的事情不敢说,至少目前,我觉得自己生活的还挺好的。** + +我啰嗦这么一大堆,就是想告诉题主这样比较优秀的年轻人(我姑且觉得自己还行吧,至少干活的能力还行),其实没有必要那么紧张。只要你的基本面在,哪怕真的选错了,结果也不至于太坏。我相信你们不管怎样,总比我要明智一点吧?放轻松,没有那么多“万劫不复”,与其追求全对的人生,不如让自己茁壮一点,能担得起选错了的阵痛。 + +**ending** + +分享的最后,想说的是: + +**任何选择,都可能伴有不甘和遗憾,我们普通人很难有洞见未来的能力,但我们还是可以基于过往去做出最适合自己的决定。** + +那做完决定之后,最重要的是,不要陷入自我怀疑当中,如果纠错的成本可以接受,那完全可以反悔,就像你秋招接了一个不是自己预期的 offer 保底,心有不甘,春招又冲了一个理想的 offer,那就赔偿违约金就好了。 diff --git a/docs/campus-recruit/share/4-agricultural-bank.md b/docs/campus-recruit/share/4-agricultural-bank.md new file mode 100644 index 0000000..1e4c071 --- /dev/null +++ b/docs/campus-recruit/share/4-agricultural-bank.md @@ -0,0 +1,80 @@ +--- +sidebar: heading +title: 农业银行软件开发工作体验 +category: 分享 +tag: + - 工作体验 +head: + - - meta + - name: keywords + content: 工作体验,银行工作体验 + - - meta + - name: description + content: 农业银行软件开发工作体验 +--- + +分享一位22届的学弟分享自己在入职农业银行-软件开发岗位2个月后的体验。 + +我是22届的学生一枚,秋招季选择了农业银行软件开发一职,现在入职大概2个月了,也就是九月份,趁着这段时间就聊聊这段时间的工作现状吧。 + +![](http://img.topjavaer.cn/img/bank-work.png) + +## 目前工作状态 + +无时无刻不在期待着各种假期,比如国庆十一小长假,这就是我目前最真实的工作状态。 + +因为集中培训刚结束不久,现在还穿插着各种部门和组内培训,目前仍处于学习居多的状态。 + +虽然之前秋招的时候也积累了很多知识,但很多在工作中都不太适用,再加上公司一般都有一套自己的技术体系,所以需要进一步的学习。 + +现在除了学习之外,还会做一些指导人给分配的简单工作,所以目前基本属于边学边做的状态,属于是还在新手村打怪升级的状态。 + +## 入职后的那些人和事 + +跟自己同批入职的都是校招生,大家也都刚从校园的环境中脱离出来而且年龄相仿,所以会有很多共同话题,很容易打成一片。 + +正式入职后先是参加了一个月的集中培训,在培训期间氛围还是挺轻松的,每天就是坐在自己的位置上听课(摸鱼,bushi ) + +本以为成为了社畜以后就跟考试彻底说拜拜了, 但是谁能想到一个月的培训竟然穿插了5个考试,而且考试成绩跟最终是否成功转正有关系。 + +正因为有这5次考试, 大家虽然嘴上都说着躺平无所谓,但身体却都很诚实,所以慢慢地氛围就变得卷了起来。 + +每次考完试以后都有一众大佬拿了优秀,再低头看看自己惨不忍睹的分数,实在是悔不当初啊 ,摸鱼的时光很欢乐,但分数也很现实。 + +求学时最避免不了的就是同辈压力,成为社畜以后同样也避不开这个话题。 + +**上学时比拼的是成绩,面对的是升学压力;** + +**工作以后比拼的是绩效,面对的是升职压力。** + +当代年轻人很多都喜欢把躺平挂在嘴边,但我认为还是应该以积极的态度去面对工作和生活,因为最后可能只有你真正躺平了,就好像进群时大家都说自己是菜鸡,最后发现群除我佬。 + +## 个人工作体验和氛围 + +前面说到培训的时光确实很欢快,除了考试以外也没啥太大压力,但真正进组以后就开始焦虑起来了。 + +培训的时候因为都是萌新,大家很容易就能熟络起来,也有很多共同话题可以聊,但进组以后面对的就是陌生的工作环境,大家基本都各忙各的,没有过多的交流。 + +刚开始的时候我整个人的情绪都很低落,很难适应这种陌生的环境,每天在工位上就是盼着下班,但是下班以后回到家也感到很不开心。 + +就这样持续了一段时间,等慢慢的和同事熟悉了以后,这种有点压抑的感觉渐渐消失,工作也慢慢步入正轨。 + +工作氛围嘛,我觉得我所在小组的氛围还是挺好的,对我们新人特别照顾,尤其是指导人特别nice,每当有不明白的问题咨询她,总是能得到耐心且细致的答复。 + +工作节奏,可能是我们是新人的缘故,并没有分配给我们很多活,不过老员工的工作节奏也不是很紧张,并没有很push的感觉。 + +## 面临的问题和成长 + +初入职场,面临的问题还是很多的,一方面是自己的能力,另一方面是自己的心态。 + +虽然在学校的时候积累了很多的知识和技能,但真正面对工作时还是有很多不会的地方,需要在工作中慢慢去学习。 + +当遇到一个问题时,在思考了一段时间后仍然无果,最快的解决方式就是咨询同组的前辈,他们的经验可以帮你快速解决问题。 + +所以说不要羞于提问,也不要怕自己问的问题有多么简单,他们同样也是这样过来的,多跟前辈沟通,多吸取他们的经验,自己能够成长的更快。 + +除了提高自己能力之外,心态也尤为重要,刚开始有很多不会的东西,心态很容易炸裂,这个时候一定要慢慢来,不要太急于求成,可以允许自己成长慢一点,只要一直在进步,慢一点也没关系。 + +## 未来展望 + +希望接下来的几个月通过自己的努力,能够逐渐胜任目前的工作,然后顺利转正,趁自己的学习和记忆能力还没有下降太多,把该考的证书都考下来,希望能更上一层楼吧 。 diff --git a/docs/campus-recruit/share/5-feizhu-meituan-internship.md b/docs/campus-recruit/share/5-feizhu-meituan-internship.md new file mode 100644 index 0000000..87e703e --- /dev/null +++ b/docs/campus-recruit/share/5-feizhu-meituan-internship.md @@ -0,0 +1,222 @@ +--- +sidebar: heading +title: 阿里和美团实习的经历分享 +category: 分享 +tag: + - 工作体验 +head: + - - meta + - name: keywords + content: 实习经历分享 + - - meta + - name: description + content: 阿里飞猪和美团基础架构组实习的经历分享 +--- + +分享一位学弟在阿里飞猪和美团基础架构组实习的经历,很不错的分享,非常用心! + +## 为什么选飞猪 + +在飞猪实习了将近 10 个月,这 10 个月真的对我来说太珍贵了,半年后我决定回忆梳理一下实习经历,留一个纪念。 + +去年春招刚开始的时候,我的预期是进个中小厂先实习积累经验,然后再跳大厂进行实习。一开始确实是面试挺不顺的,加上当时因为疫情上半学期的考试周推迟到下半学期开学了(也就是春招的时候), 然后我这边和前女友也正不顺,所以前期都感觉想放弃了。 + +但是去年的确是形式比较好(今年春招实习十个月的我,简历更漂亮,反而不行了),考试周结束以及分手后有幸拿了 4 个公司的 offer: + +- 百度智能小程序的 Java 岗 +- 美团酒旅的 Java 后端开发 +- 腾讯 TEG 的 CDN +- 飞猪的 Java 后端开发 + +去年的我其实还不知道“组 > 项目 > 部门 > 公司”这条朴素的真理,自然心中只有阿里和腾讯了。 + +然后,身为一个刚转行后端大半年,甚至春招简历上的项目都是前端的转专业菜鸡,连 CURD 的项目其实都没搞过,我当时对自己十分缺乏自信,所以不敢去腾讯零基础学习 C++ 进行服务器底层的开发。 + +综上,飞猪是当时我认为的最佳选择。现在事后诸葛亮地来说,还是一个不错的选择,因为去了一个团队氛围还是相当和谐的组。 + +## 租房惨痛教训 + +实习第一件事自然就是租房了,其实一开始我没把这放在心上,因为我自认为睡眠能力很强,本科两个室友打呼噜,有一个甚至磨牙,我都能不受影响地睡四年。加上我小时候生活在农村,差一点的环境我也不 care。 + +结果年轻的我被上了惨痛的一课。 + +当时我听别人说不要上自如,会比自己找贵很多。我就去豆瓣的租房群搜索余杭租房,结果年轻的小白遇到了二房东。不得不吐槽一下,豆瓣上等平台的租房消息已经被二房东给霸占了,很难从中找到真正房东发布的房子。 + +我当时图便宜,看中一个月租 1750 的,因为没经验,过去看了看就决定租了。结果房子是隔断房,不隔音,隔壁的室友真的吵,大半夜突然就唱歌。 + +这还不是最致命的,这个小区临着马路,但是外窗不怎么隔音。我看房的时候是白天,根本不吵,没有察觉到。当时余杭正搞基建呢,越到半夜,大货车就越多,声音巨吵。 + +我尝试好多方法,比如耳塞、戴耳机听音乐等,但都不管用。忍了 20 来天,决定跑路,押一付一 3500 元全没了。 + +我知道阿里员工在自如可以免费换房子,就选择自如了,这一次运气比较好,2000 多在欧美金融城那个小区租了不错的房间。 + +但是自如需要押一付三,加上前面损失的押一付一,第一个月我还没挣钱,就有 1w 多的支出了。当时也是各种借钱,惨的一批。 + +我的教训就是: + +- 不要急着租到房,哪怕多住几天宾馆,也得货比三家,找到真正合适的房子,睡眠充足才是最重要的。 +- 小心二房东,不管对方怎么说,只要一般朋友圈都是房源的,肯定是二房东。 +- 别选隔断房,能选择小区合租还是小区合租。 +- 隔音,隔音,还是隔音! + +## 团队氛围 + +也是从飞猪实习开始,我才体会到 "组 > 公司"这个道理。 + +不过幸运的是,我们组内氛围相当好,没有传说中的 PUA 和阿里味。 + +每天中午大家都一起去食堂吃饭,并且围着大楼转个两圈散散步。平时有任何不会的问题,可以找组内任何一个师兄问,他们都会给我耐心解答,还会告诉我到哪里去学习。 + +每个月都会去撸个串,喝点啤酒,吹吹牛聊聊天。周末还去打了几次麻将,不过我每次都是送财童子,基本凭着感觉打。 + +今年年初还去团建滑雪+泡温泉。 + +实习十个月,我基本和每个师兄的关系都还不错,不会存在和谁一起尴尬的情况。哪怕离职很久了,有时候还会聊聊天,吐吐槽。其中 leader 在今年寒冬下,还说如果去飞猪,可以帮我搞一份 offer,真的是很感动。第一份实习就能遇到这么一帮人,真的可以说是很幸运。 + +## 技术成长 + +毫无疑问,飞猪的 10 个月我的技术得到很大成长。 + +毕竟我的底子太烂了,在此之前连 CURD 的玩具项目都没做过。当我和师兄一起做项目的时候,才第一次听说 RPC、消息中间件这些东西。这十个月虽然干的也都是一些算不上有技术含量的活,但是也算是对基本的开发流程、开发工具、中间件等有了从无到有的了解。 + +实习的十个月也是见证了不少难处理的线上 BUG, 比如慢 SQL、本地缓存太大导致频繁 GC,甚至还有一次我感觉差点造成很大资损的 BUG。虽然每次都是师兄排查解决的,但是旁观排除过程、询问排查思路,也让我学到了很多排查问题的方法。 + +师兄们对我也比较信任和放权,经常给我一个独立的小模块,让我自己来负责,他们给我把关技术方案,让我和产品、运营、测试、上下游以及跨团体、跨部门(比如淘宝、CTO)的开发人员进行沟通协调。(ps:也学会了打太极,找产品要排期)。 + +师兄们的技术普遍都还不错,其中龙哥真的是我之前遇到过技术最好的人,主要负责团队的架构和增效工具。真的感觉不管问他什么问题,他都会。而且给了我很多技术学习上的建议,是我梦想中想要成为的样子。 + +在我离职的时候,不管上产品、运营、测试对我的评价都是还挺靠谱的小伙子,这对我来说是很大的鼓舞,让我从之前一个很不自信的转专业菜鸡变成一个有自信的软件研究生,相信自己能在这条路上走下去。 + +不过我自己还是有点偷懒,基本都是师兄给我安排的 CURD 的活,而没有主动要一些有挑战性的工作。导致我今年秋招的时候,面试官问我有什么技术上的挑战,其实回答都是我编的(哈哈,看过的就是我的)。现在想想有点浪费机会,十个月其实到后期成长并不大了。 + +个人对实习生的建议是不要害怕沟通,也不要害怕自己能力不够,积极地向师兄要一些有挑战性的活,这样子不管是转正还是秋招都是加分项(ps:主要是实习生不用背锅,干嘛不试试呢?)。 + +## 拥抱变化 + +实习十个月真的是感觉不停地在拥抱变化,实习十个月经历了两个产品(但是是 ABA,相当于三任)、四个测试,每一次都要重新磨合,也让我感受到了互联网公司架构调整的频繁。 + +而且去年年底的一次架构调整,我原来的团队负责做另一个比较重要的项目,不需要实习生参与了。 + +我就告别带我的两个师兄,带着项目投奔另一个组了(ps:其实就隔了几排桌子)。在过渡期间,我短暂地成为了 aone 上面的项目 owner。但是我个人能力根本无法胜任,每天一堆人找我问接口,一堆线上工单都提给我,每周还要被拉一堆会议,真的是很痛苦的过渡期。 + +## 我的精神内耗 + +身为本科水利,研究生才转行软件的跨行狗,对自己一步步走来还是很自豪的,但是有的时候还是会突然有一种很遗憾的感觉,觉得自己浪费了好多时间。 + +尤其是和我一起实习的本科实习生,感觉人家本科阶段就看了好多源码,折腾了好多技术,水平远远在我之上。还有一些同学,他们科班出身,研究生就稳稳地研究数据库等技术含量很高的领域,两三年也有了知识壁垒,然后顺理成章地就去了高门槛的领域。 + +而我自己一步步走来,好像一直在火急火燎地学习一些很基础的东西,研究生才学习 Java Web, 接着开始业务开发的实习,马上又秋招,履历和实力也无法进入那些高门槛的领域,真的没感觉自己比培训班出来的强多少。 + +有时候感受到自己和别人的差距或者夜深人静的时候,就难免会想像如果自己本科就选了 CS 或者软件,没有浪费四年的话,可能会迈向更深的领域吧。 + +至今还没有解决这个内耗,或许将来我成为一个更优秀的码农的时候,会彻底翻篇吧。 + +## 启航下一站 + +天下没有不散的宴席,实习了十个月,我还是决定迈出舒适区,想去体验不同的方向,就提了离职。当然也正是这十个月给了我勇气和信心,让我相信自己确实有能力吃上这碗饭,相信自己能够胜任哪怕更底层的方向。Just move on ! + +## 美团 + +去年在飞猪做了 10 个月的业务之后,我觉得作为一个实习生,不可能负责真正有技术含量的工作。再怎么实习下去,也无非是熟练度的问题,不会再有技术上的提升了。再加上最后一段时间,我的时间都花费线上工单、问题解答和开会上面了,并没有怎么写代码。 + +于是,我希望能换一个更有技术挑战的地方实习,于是我就投了美团的基础研发团队,也非常幸运地拿到了 offer。 + +不得不说,从春招的时候,我就隐隐约约感受到今年形势的艰难,当时我投腾讯 ieg,面了两轮,然后听说腾讯要大裁员了,后面果然我的流程中止了。 + +我实习的时候,美团的师兄也告诉我,在我之后其实还是有几个实习生通过了流程,但是美团也开始裁员了,把他们的流程都中止了。 + +说实话,还是有一点小幸运的,从现在来看,美团实习转正真的是今年为数不多的一股清流了。让我秋招并没有那么紧张。 + +## 避不开的疫情 + +今年实习,其实是很难避开疫情的。我三月多从飞猪离职,本来定的 4 月多去美团实习。 + +但是北京健康宝一直有弹窗,每天各种打市民热线反馈也没用。 + +然后,我申请远程办公,领导没有批准,说要保证实习质量。然后到了 4 月底,我的弹窗终于消失了,立马出发去北京。 + +但是造化弄人,我刚到北京,北京正好爆发一波疫情,全员居家办公,相当于白来了。 + +很快我就体会到为啥领导之前不批准远程实习了,5 月份一个月居家办公对我一个实习生来说,真的是纯纯等于摸鱼。 + +值得一提的是,5 月居家我自己做饭一个月,居然瘦了十斤。不过后来又迅速肥了起来。 + +## 团队氛围 + +不得不说,我是相当幸运的,每次去的团队氛围都不错。 + +我在美团的组内,大家关系也是很和谐,每天吃完饭,都会一起围着恒电东侧的小树林逛一圈,聊聊技术和事实。 + +内部还有个吹水群,今年实习四个月,不得不说世界风起云涌,有的是吐槽的新闻。 + +团队虽然人也不多,才 10 人,但是还是有不少技术大神的,有几个师兄经常在美团技术博客上发表文章,和他们一起讨论,真的是能学到很多。 + +今年八月初,大家还用团建费包车去赛上草原去团建。 + +这也是我第一次去草原,不管是草原、骑马、烤羊肉、还是越野车兜风,都是我第一次参加。而且之前身为直男的我,从来不怎么拍照片, 这次领导、师兄、师姐们还是给我拍了不少照片,留作回忆。 + +## 技术成长 + +一说技术成长,不得不说很惭愧,在美团每天都在带薪学习,爽得飞起。这也要感谢 mentor 和坐我旁边的 leader 的包容。 + +美团对实习生的培养,还是要点赞的。 + +之前我在阿里实习的时候,实习生的权限是非常小的,连 atta 的权限都没有。 + +而美团这边基本各种资料、博客我都有权限看。 + +因为团队是基础研发,会有很多和操作系统、网络相关的东西。之前做业务的时候,我从来不需要考虑到这些,所以周会的时候我一般都一脸懵逼。 + +好在美团侧重文档文化,师兄师姐们的 wiki 还是写的很详细的,从中真的是受益匪浅。 + +而且美团基础架构团队的技术分享还是很多的,我就经常会听一些大佬在内部的技术分享。 + +实习四个月,真的是从 0 开始学了好多,每次学习都感觉是递归式的学习,学 A 发现 B 不会,学 B 发现 C 不会,以此类推,不过最后总能到操作系统上。 + +于是我也开始学习操作系统了,之前 408 和八股真的是硬背式的纸上谈兵,其实我对操作系统一窍不通。虽然学习的时候感觉无比艰难,但是不知不觉也学到了很多很多的东西,之前零拷贝、page cache、两阶段提交等知识,其实我只是背背八股,但是现在真的在项目中看到他们如何使用了。 + +此外,实习四个月,团队内部每周会轮流做技术分享,我有幸做了两次技术分享。真的感觉做技术分享式的学习和普通的自学根本不同,会更加系统化和深入。感觉每次基本都是到分享当前,我的稿子都在不断地大改。 + +当然,最大的收获莫过于让我破除了对底层的恐惧。 + +在我刚转行乃至搞业务这么长时间,我其实对底层技术一直心生恐惧的。 + +比如使用消息中间件和 RPC 的时候,我最多看看别人写的技术分析,但是我从来不会自己扒源码来看。难道是自己真的不可能看懂么,不,其实是害怕。 + +但是现在的话,我对自己扒代码看懂更加感兴趣和有成就感。 + +## 关于玩 + +相对于杭州,北京的熟人还是不少的,只不过因为疫情,大家出来聚一次也不容易。和我几个高中同学聚了几次。 + +期间,我的一个大学哥们,到美团当了一周产品跑路帝国理工了,但是在我这住了将近一个月。一到周末,俩人一起就是胡吃海喝,我的身材彻底 GG。 + +在北京没怎么出去玩过,就刚来北京的时候出去逛了逛。能自由活动的时候都已经六七月了,当时天气太热了,我丧失了动力。最遗憾的是,一直想去天大看看,也没能去成。 + +但即便这样,有时候还是难免感觉到孤单,可能这就是北漂的感受吧。可惜我的圈子太小了,根本找不到对象。 + +## 下一站,家里蹲 + +美团实习工资还是太低了,在北京过得并不舒服。 + +趁着没毕业前,我想回家躺平,毕竟毕业就没机会了,就离职了。 + +没想到回到家躺平是躺平了,但是真的好无聊啊,同学朋友一个都没有。 + +如果有河南、周口或者其他地方的 xdm 也像我一样躺平没事干,可以交流一波。 + +之前几个师兄暗示我最后的结果可能挺棒的,所以今年秋招也是很佛系。 + +但是昨天开奖了,给的价格并不满意。我知道基础架构开的更高的基本都是校招,可能美团更喜欢刮彩票,而不是实习生吧。 + +又到了人生下一个节点的岔路口了,目前下一站会向何方,我也有那么一丝丝的犹豫和迷茫。一直以来都在匆忙赶路,都在焦虑,都在卷自己,没有静下好好思考自己究竟想要什么。 + +但现实是,这样并不是真正"有效"的付出。希望这段时间难得在家有时间,能静下心好好想想自己究竟想要什么吧。 + +最后,有时候真的也感觉赶上今年真的挺倒霉的,哎。 + +大家明明学历、实习、面试都表现其实都不错,但就是比往年更难收到 offer。即使地狱模式收到的 offer, 价位其实也不尽人意。 + +但我们也不能放弃,争取拿到全局最差区间的最优解吧。 + +共勉,人生的道路还很长,一时的 offer 也说明不了什么。最近看到阿里和虾皮的 22 届,明明那么优秀,收到这么高的 offer,但是最终应届被裁一场空,也感觉很悲哀的。 diff --git a/docs/campus-recruit/share/6-2023-autumn-recruit.md b/docs/campus-recruit/share/6-2023-autumn-recruit.md new file mode 100644 index 0000000..787bec9 --- /dev/null +++ b/docs/campus-recruit/share/6-2023-autumn-recruit.md @@ -0,0 +1,176 @@ +--- +sidebar: heading +title: 23届秋招,寒气逼人。。 +category: 分享 +tag: + - 校招 +head: + - - meta + - name: keywords + content: 秋招经历分享 + - - meta + - name: description + content: 2023 届秋招经历分享 +--- + +## 23届秋招,寒气逼人。。 + +分享一篇牛客网友的 2023 届秋招经历分享,写的很不错,很真实。 + +下面是正文。 + +> 原文链接:https://www.nowcoder.com/users/291384568 + +## 一、自我介绍 + +> 就叫我 OliQ 吧(《白鲸》式的开局)! + +我来自杭州的一所双非一本学校,是一名普通的本科生,专业【软件工程】。 + +### 1.1 初学编程 + +事实上,我从高中毕业起就开始思考未来的工作了,一开始网上都是 Python 相关的新闻,因此从高中毕业的暑假就开始学 Python,当时在新华书店,捧着一本入门书天天看; + +但是看了并没有什么用,除了大一的时候吹牛皮,啥都没学到 😓; + +然后自 2020 年初(大一寒假) 疫情爆发,学校线上授课;课程中有【面向对象语言】的学习,自此开始正式的跟着视频学习 Java 了; + +### 1.2 第一次实习 + +2021 年暑假(大二暑假),我的绩点排名在学校保研线边缘徘徊,但又不愿去刷那些水课的绩点,因此决定 考研 或者 工作,期间比较迷茫; + +当时在网上得到一位大数据方向前辈的指点,**他说了一句话:“早,就是优势。”** + +因此,我决定先去实习,当时在杭州人工智能小镇找了家公司实习; + +虽说是实习,但其实基本每天上班啥也不干,主管也没分配任务,就是一直在看书,期间看完了周志明老师的 JVM,以及几本讲并发编程的书; + +### 1.3 第二次实习 + +大三上时,眼看着 Java 越来越卷,自己开始学习了大数据相关的组件,像 Hadoop、HBase、Flume 等等组件,一直学到了实时计算之前; + +大三下时,我明白自己是一个心态非常不稳定的人,考研对我来说,最后几个月会非常的难熬,并且考研失败的风险也让我望而却步,因此下定决心**本科就业**! + +寒假的时候跟着视频完成了【谷粒商城】那个项目,免费的,之后立刻着手准备找实习。 + +你可以在 B 站或者尚硅谷官网找到谷粒商城对应的视频教程。个人不建议再用这个作为自己的项目经验,用的人实在是太多了,面试官都看腻了,性价比太低太低。 + +当然了,商城类项目在自己学习的时候用来实践还是不错的,涉及到的知识点比较多。 + +![](http://img.topjavaer.cn/img/秋招回顾1.png) + +也就是在这第二段实习过程中(2022 上半年),我真正的学到了一些实际的开发技巧; + +实习期间,看完了几本深入讲中间件 ZK、Redis、Spring 源码 和 代码重构的书。 + +本次实习,让我受益良多,由衷感谢我的 mentor 和 主管! + +## 二、秋招情况 + +> 以勇敢的胸膛面对逆境! + +![](http://img.topjavaer.cn/img/秋招回顾2.png) + +我从 6 月底开始复习准备,因为准备得比较晚,所以基本没参加提前批; + +正式批总共投递了近 150 家公司,笔试了 30 家,面试了 15 个公司,除了海康威视,其他基本都意向或排序了; + +大致情况如下: + +- **offer**:兴业数金 +- **意向**:猿辅导,Aloudata +- **排序 / 审批**:华为,网易雷火,荣耀,招银网络,古茗奶茶,CVTE,以及一众独角兽公司 +- **面试挂**:海康威视 + +## 三、复习方式 + +> 心之何如,有似万丈迷津,遥亘千里,其中并无舟子可以渡人,除了自渡,他人爱莫能助。 + +### 3.1 关于焦虑 + +我们先要肯定一点,在复习的时候,【焦虑】是一件必然的事情,我们要正视焦虑。 + +就拿我自己举例子吧,【双非本科】的学历会把我放到一个最最糟糕的位置; + +自开始复习时,我内心就非常非常的焦虑,胸膛经常会像要爆炸一样的沉闷(真的)。。 + +而我的缓解方式主要分为两种吧: + +1. **运动** :背一会八股或者刷一会题之后就去走走,每天晚上去操场跑步。 +2. **心理慰藉** :面试前,我会像《三傻大闹宝莱坞》里的阿米尔汗一样,拍着自己的胸口对自己说 **“Aal izz well”** 。另外,我还会给自己想好一个下下策,如果秋招真的找不到工作该怎么办?那至少还有春招,对比明年考研失利的同学,我至少积累了经验! + +### 3.2 复习流程 + +我的整体复习流程分为三步: + +1. 处理基础知识 +2. 看八股 +3. 查漏补缺 + +#### 3.2.1 阶段一 + +我自知《计网》和《操作系统》这两门课学的很差,所以一开始就复习这部分知识。 + +当时先把两门课的教材翻了一遍,然后做了一些摘抄,但说实话基本没用。 + +这部分知识,我在面试过程中,大概有 50% 的几率会被问到操作系统,但从来没被问到过计网(幸运)。 + +之后复习《设计模式》,先跟着一个 csdn 上的博客边看别写,之后找了一个很老的(2003 年)博客总结,反复背诵,基本能手写大部分的模式实现了。 + +这部分知识,我在面试过程中,要求写过 单例 、三大工厂 和 发布订阅 的实现,问过项目中和 Spring 以及其它中间件中用到的设计模式。 + +#### 3.2.2 阶段二 + +全面进军 Java 八股文。 + +我先看了自己在实习前准备的那些文档,之后网上找了 JavaGuide、JavaKeeper 这两份文档作为补充。 + +因为自己之前有过两段的实习经验,因此背过很多次八股。 + +但考虑到本次秋招可能会把战线拉得比较长,因此就自己总结了一份脑图。 + +![](http://img.topjavaer.cn/img/秋招回顾3.png) + +### 3.2.3 阶段三 + +经过几轮面试,逐渐察觉到了自己的一些不足,之后针对性的去完善了一下。 + +这里随便列举几个点,供其它同学参考: + +1. 为什么说进程切换开销比线程大? +2. NIO 到底有没有阻塞,NIO 到底能不能提高 IO 效率? +3. Redis 分布式锁的限制,RedLock 的实现? +4. ZK 明明有了有序的指令队列,为什么还要用 zxid 来辅助排序? +5. basic paxos 和 multi paxos 的使用? +6. 为什么拜占庭将军无解? +7. 还有一些业务场景的选择问题。。。 + +## 四、总结 + +> 你们总在悲痛或需要的时候祈祷,我愿你们也在完满的欢乐中及丰富的日子里祈祷。 + +**我一直提醒自己:你是一个双非本科生,这个秋招你如果再不拼命,你就要完蛋了;** + +我想,我是幸运的: + +- 我很幸运 在实习的时候,有一个好的 mentor,带我开发了字节码相关的组件,让我的简历不容易挂; +- 我很幸运 在复习的时候,有几位好的朋友,分享经验,加油鼓励,让我没有被焦虑击倒; +- 我很幸运 在面试的时候,有无私的舍友们,能在我需要笔试面试时,把宿舍让给我,让我没有后顾之忧; + +当然,也会有遗憾。每个人心中都有着大厂梦,而今年进大厂确实很难: + +- 我从大一开始就非常渴望进入阿里巴巴,实习的时候五面阿里不得,秋招全部简历挂; +- 百度+度小满,投了 4 个岗位,全部简历挂; +- 字节,一开始担心算法没敢投,之后担心基础知识也没敢投,也很遗憾了; + +人生,有所得就有所失,有所失就有所得。 + +最后,想给其他明后年参加秋招的同学一些提醒: + +1. 一定要早做准备,早点实习,早点刷算法题,**早就是优势**; +2. 人生无常,意外太多,绝对不要 all in 一家公司; +3. 鞋合不合适只有脚知道,自己总结的八股会更适合自己; +4. 多刷 力扣 Hot 100,或者 Codetop 热门题,反复刷; +5. **选择大于努力**; + +**在寒气逼人的 2022,我们需要抱团取暖。** diff --git a/docs/campus-recruit/share/README.md b/docs/campus-recruit/share/README.md new file mode 100644 index 0000000..38c2a9b --- /dev/null +++ b/docs/campus-recruit/share/README.md @@ -0,0 +1,5 @@ +# 校招分享(持续更新中) + +- [双非本,非科班的自我救赎之路](./1-23-backend.md) +- [非科班,秋招还没offer,该怎么办](./2-no-offer.md) + diff --git a/docs/career-plan/3-years-reflect.md b/docs/career-plan/3-years-reflect.md new file mode 100644 index 0000000..21e4cfc --- /dev/null +++ b/docs/career-plan/3-years-reflect.md @@ -0,0 +1,99 @@ +--- +sidebar: heading +title: 工作3年半,最近岗位有变动,有点迷茫 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作3年半,最近岗位有变动,有点迷茫 + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于职业规划的和自学方面的问题,挺有有代表性的,跟大家分享一下。 + +**圈友提问**: + +本人是19年毕业至今工作了**3年半**左右,一直在同一个岗位上,近段时间因为公司项目组资金不足被调岗了,新岗位和自己的技术栈**不太匹配**,觉得如果一直变动会影响自己的**职业发展**。 + +这么些年来一直有点**混吃等死**,想在现在开始寻求改变,希望通过学习,让自己有所变化。不过对于学习方式以及学习路线很是**迷茫**,希望大彬老师可以帮忙解惑,非常感谢。 + +**目前掌握的技术栈**: + +有一定的Java基础,对JVM原理,体系结构,垃圾回收机制等有部分了解 + +熟悉Scala&Spark开发, 了解部署spark server项目及脚本 + +掌握spring, springCloud微服务架构开发, + +了解Elastic search,Mule gateway, Feign等的使用 + +熟悉Jenkins, Kubernates, PCF, G3等devops部署及调试 + +熟悉AppDynamic, patrol, LWM等程序监控平台的使用及配置 + +了解NexusIQ,Checkmarx,sonar等代码质量监控平台的使用及调优 + +熟悉sqlServer及Mongodb数据库 + +--- + +**大彬的回答**: + +我一直都是认为,在现在这个就业环境下,“专才”的竞争力是要大于“全才”的,**专注一个方向**,对你的职业发展更为有利。 + +从你的技术栈来看,相对还是偏“杂”一些,Java、大数据、devops等都有涉及。而且现在还有调岗的可能,我建议可以准备跳槽,**跳出舒适圈**。 + +至于怎么去学习,我建议你到招聘网站看看Java开发3年经验(Java高级开发)都是什么要求,**面向面试学习**,这样学习效果比较好。 + +比如阿里巴巴和OPPO 3 年左右工作经验的JD,我整理了一下,大概有这些点: + +1、JAVA**基础扎实**,理解io、多线程、集合等基础框架,了解JVM原理;(基础必须要掌握好) + +2、熟悉分布式系统的设计和应用,熟悉**高并发、分布式**、缓存、消息等机制;能对分布式常用技术进行合理应用,解决问题;(高并发、分布式) + +3、对用过的开源框架,能了解到它的**原理**和机制(框架源码) + +4、**性能调优**,解决疑难问题的能力;(平时要注意积累这种能力) + +基本就是这几个要求,对着JD看看自己哪一块薄弱,平时抽空针对性进行查漏补缺,像高并发分布式这种,可以结合工作项目业务场景去思考。平时有遇到性能调优方面的问题(不一定是自己遇到的,也可以是其他人处理的问题,你可以**主动参与**进去,了解怎么去处理),也要记得**复盘总结**,这些都是宝贵的经验,面试能派上用场。 + + + +关于**项目经验**,如果这块有疑问,可以参考下面两篇文章: + +你在项目里遇到的最大困难是什么?https://t.zsxq.com/09KAo4zZU + +项目经验怎么回答?https://t.zsxq.com/09KejfE2I + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) + diff --git a/docs/career-plan/4-years-reflect.md b/docs/career-plan/4-years-reflect.md new file mode 100644 index 0000000..8fcf541 --- /dev/null +++ b/docs/career-plan/4-years-reflect.md @@ -0,0 +1,38 @@ +--- +sidebar: heading +title: 4年工作经验的程序员分享 +category: 分享 +tag: + - 工作经历分享 +head: + - - meta + - name: keywords + content: 工作经历分享 + - - meta + - name: description + content: 4年工作经验的程序员分享 +--- + +今天给大家分享一个**4年工作经验的程序员**的经历,应该对各位会有所启发。 + +2022年,我彻底失业了。**在面试了10多家单位后,居然没有一个人给offer**,为此对自己做出了反思。 + +首先总结一下面试,**第一点我真的太不会表达了**,十多年的技术生涯,使我欠缺交流的机会。第二点我的技术真的很菜,我做了4年的程序员,1年的前端,3年c#,这样听起来好像应该是一个经验丰富的程序员了。应该是在市场上很吃香。我当时也是这么天真地认为的。 + +为什么会到今天这个地步呢。**4年程序员生涯,面试的时候我要求的待遇一降再降**,从9k-10k。在从8.5k-10k,再从7-8k。这是在广州呢。7-8k不就顶一个本科生刚毕业的新手吗。为什么连这个数都没有企业愿意给呢?我越面试越没有信心,首先我要深刻地反思自己。 + +**原因如下:首先我技术真的菜**,不要因为3年时间就能拿高工资,你不追求技术进步,可能十年工资还是一样的。甚至还要贬值。3年间,我基本上班摸鱼,没什么任务给自己锻炼,身处其中的时候觉得挺好的,殊不知这是一个巨大的危机,以为自己碰上了一个好领导,殊不知严厉的领导才能督促你的技术进步。对于技术的理解只是达到能完成公司的任务是远远不够的。但是我当时天真的就是按照这个最低的标准进行的。写代码就是靠百度。功能跑起来没错就行了。从来不会去思考或者去主动记忆一些技术。然而我这三年就是这么过来的。一个混子程序员迟早要遭到报应的。 + +对于上面的问题。其实我在入职上家公司3个月的时候就已经意识到了。当时想着是不是要跳槽。因为领导总是安排一些我已经会了的功能给我做。就是简单重复机械的工作给我。当时还觉得领导是不是想把我赶走呢。但是由于一些原因我还是留下来了。但是领导还是给我安排这样的工作给我做,我就产生了消极怠工的思想。也就不怎么积极了。然后领导就再也没有安排有价值的任务给我做了。(之前有一段时间有的安排一下能稍微学到点东西的任务给我做)慢慢地我变成了一个混子。对于编程语言产生了遗忘,还不如刚刚学的时候。 + +现在看来我应该早点离开那家公司就好了,不至于浪费了3年的时光,人也废掉了。因为那家公司就是维护。修bug而已,然后前面程序员留下的代码惨不忍睹的。你无法从阅读代码中学到任何设计模式。而且你还要按照他的写法才能让程序跑起来。我应该在工作半年后走人的。 + +**还有就是自己对于技术的追求只要求能完成工作任务就行**。那么领导天天给你安排些简单任务。那你岂不是只有简单的水平了。事实上领导看我没啥进步也确实都安排简单的任务给我做。恶性循环导致自己水平越来越菜。而且还有我那可怜的策略,当时对自己的要求:对于技术只要能完成任务就可以了。一切以任务为导向。现在回过味来,才知道多么的可怕了。我应该以市场为导向的,而且是以更高工资的收入所要求的的对技术的要求和市场常见的技术栈为导向,明确下一份工作方向。直到我现在找工作才知道,我原来的公司技术栈是市场上最窄的技术栈。从那种公司出来,自己的身价不增反而贬值。(个人成分也很大,我在公司做的都是些边角料的开发任务)其实应该多多跳槽的。 + +**总结前面说的。**首先在一家公司发现对自己发展不利,要立刻跳槽。不然白白浪费大把的时间。第二对于自己技术的要求要按照市场技术栈为导向和更高工资要求的技术等级为导向。第三从事技术的年限不能决定你的技术高度,你付出的精力才决定了你的技术高度和工资。我做了三年的开发连7,8k都没公司给。其他人做了一年开发他可以到15k。所以大家要出工也要出力。不能自欺欺人。 + +这30多天的面试算是给我深深地上了一课吧,去掉了所有的不切实际的幻想。面试的失败当然和社会环境(疫情)也有不小的原因,如果你不在年轻了,工作了很多年的开发,水平还是很一般(面试又不会吹牛),那么极有可能会找不到下家公司。我现在体会到了程序员真的是一个高淘汰率的职位。技术不行就会被后来者代替。我还不到30岁,35岁危机就提前到来了。对于技术做不到顶尖的程序员来说,还是早做准备吧。程序员不能干一辈子,程序员职业更多是赢者通吃的格局 + +然后30岁了。发现有点干不动了。(一到下午就困地不行情绪浮躁,思维混乱,很难集中注意力在工作上,工作10分钟休息半小时)网上说的30多岁身体跟不上是真的。如果加上自己技术一般的话。其实不会多少技术精进的欲望的。30多岁干基层的活真的是干不动的(他需要脑子的灵活和反应快,不需要什么太多智慧技巧,这种年轻人显然更合适)高级的靠智慧的架构管理工作除外,其实应该考虑转型,可以在计算机行业里面,目前我打算做实施。主要脑子反应没有之前快了。做不了机械重复的脑力劳动了,而且想做一做和人打交道的工作,突破一下自己。 + +**以上是对自己的4年程序生涯的总结**,兄弟们会看到自己的影子吗。或者会被启发到吗。此文案例真实,希望能帮助大家规避类似错误,找到自己在技术圈的真实定位。 diff --git a/docs/career-plan/guoqi-programmer.md b/docs/career-plan/guoqi-programmer.md new file mode 100644 index 0000000..5db7cc2 --- /dev/null +++ b/docs/career-plan/guoqi-programmer.md @@ -0,0 +1,82 @@ +--- +sidebar: heading +title: 在国企做开发,是什么样的体验 +category: 分享 +tag: + - 国企 +head: + - - meta + - name: keywords + content: 国企工作,工作经历分享 + - - meta + - name: description + content: 4年工作经验的程序员分享 +--- + +## 在国企做开发,是什么样的体验 + +> 本文已经收录到Github仓库,该仓库包含**计算机基础、Java核心知识点、多线程、JVM、常见框架、分布式、微服务、设计模式、架构**等核心知识点,欢迎star~ +> +> Github地址:https://github.com/Tyson0314/Java-learning +> +> Gitee地址:https://gitee.com/tysondai/Java-learning + +有读者咨询我,在国企做开发怎么样? + +当然是有利有弊,国企相对稳定,加班总体没有互联网多,不过相对而言,工资收入没有互联网高,而且国企追求稳定,往往技术栈比较保守,很难接触新的技术,导致技术水平进步缓慢。 + +下面分享一位国企程序员的经历,希望能给大家一些参考价值。 + +> 下文中的“我”代表故事主人公 + +我校招加入了某垄断央企,在里面从事研发工程师的工作。下面我将分享一些入职后的一些心得体会。 + +在国企中,开发是最底层最苦B的存在,在互联网可能程序员还能够和产品经理argue,但是在国企中,基本都是领导拍脑袋的决定,即便这个需求不合理,或者会造成很多问题等等,你所需要的就是去执行,然后完成领导的任务。下面我会分享一些国企开发日常。 + +## 1、大量内部项目 + +在入职前几个月,我们都要基于一种国产编辑器培训,说白了集团的领导看市场上有eclipse,idea这样编译器,然后就说咱们内部也要搞一个国产的编译器,所有的项目都要强制基于这样一个编译器。 + +在国企里搞开发,通常会在项目中塞入一大堆其他项目插件,本来一个可能基于eclipse轻松搞定的事情,在国企需要经过2、3个项目跳转。但国企的项目本来就是领导导向,只需给领导演示即可,并不具备实用性。所以在一个项目集成多个项目后,可以被称为X山。你集成的其他项目会突然出一些非常奇怪的错误,从而导致自己项目报错。但是这也没有办法,在国企中搞开发,有些项目或者插件是被要求必须使用的。 + +## 2、外包 + +说到开发,在国企必然是离不开外包的。在我这个公司,可以分为直聘+劳务派遣两种用工形式,劳务派遣就是我们通常所说的外包,直聘就是通过校招进来的校招生。 + +直聘的优势在于会有公司的统一编制,可以在系统内部调动。当然这个调动是只存在于规定中,99.9%的普通员工是不会调动。劳务派遣通常是社招进来的或者外包。在我们公司中,项目干活的主力都是外包。我可能因为自身本来就比较喜欢技术,并且觉得总要干几年技术才能对项目会有比较深入的理解,所以主动要求干活,也就是和外包一起干活。一开始我认为外包可能学历都比较低或者都不行,但是在实际干活中,某些外包的技术执行力是很强的,大多数项目的实际控制权在外包上,我们负责管理给钱,也许对项目的了解的深度和颗粒度上不如外包。 + +上次我空闲时间与一个快40岁的外包聊天,才发现他之前在腾讯、京东等互联网公司都有工作过,架构设计方面都特别有经验。然后我问他为什么离开互联网公司,他就说身体受不了。所以身体如果不是特别好的话,国企也是一个不错的选择。 + +## 3、技术栈 + +在日常开发中,国企的技术一般不会特别新。我目前接触的技术,前端是JSP,后端是Springboot那一套。开发的过程一般不会涉及到多线程,高并发等技术。基本上都是些表的设计和增删改查。如果个人对技术没啥追求,可能一天的活2,3小时就干完了。如果你对技术有追求,可以在剩余时间去折腾新技术,自由度比较高。 + +所以在国企,作为普通基层员工,一般会有许多属于自己的时间,你可以用这些时间去刷手机,当然也可以去用这些时间去复盘,去学习新技术。在社会中,总有一种声音说在国企呆久了就待废了,很多时候并不是在国企待废了,而是自己让自己待废了。 + +## 4、升职空间 + +每个研发类央企都有自己的职级序列,一般分为技术和管理两种序列。 + +首先,管理序列你就不用想了,那是留给有关系+有能力的人的。其实,个人觉得在国企有关系也是一种有能力的表现,你的关系能够给公司解决问题那也行。 + +其次,技术序列大多数情况也是根据你的工龄长短和PPT能力。毕竟,国企研发大多数干的活不是研发与这个系统的接口,就是给某个成熟互联网产品套个壳。技术深度基本上就是一个大专生去培训机构培训3个月的结果。你想要往上走,那就要学会去PPT,学会锻炼自己的表达能力,学会如何讲到领导想听到的那个点。既然来了国企,就不要再想钻研技术了,除非你想跳槽互联网。 + +最后,在国企底层随着工龄增长工资增长(不当领导)还是比较容易的。但是,如果你想当领导,那还是天时地利人和缺一不可。 + +## 5、钱 + +在前面说到,我们公司属于成本单位,到工资这一块就体现为钱是总部发的。工资构成分由工资+年终奖+福利组成。 + +1.**工资构成中没有绩效**,没有绩效,没有绩效,重要的事情说三遍。工资是按照你的级别+职称来决定的,公司会有严格的等级晋升制度。但是基本可以概括为混年限。年限到了,你的级别就上去了,年限没到,你天天加班,与工资没有一毛钱关系。 + +2.年终奖,是总部给公司一个大的总包,然后大领导根据实际情况对不同部门分配,部门领导再根据每个人的工作情况将奖金分配到个人。所以,你干不干活,活干得好不好只和你的年终奖相关。据我了解一个部门内部员工的年终奖并不会相差太多。 + +3.最后就是**福利**了,以我们公司为例,大致可以分为通信补助+房补+饭补+一些七七八八的东西,大多数国企都是这样模式。 + +## 总结 + +1、老生常谈了。在国企,工资待遇可以保证你在一线城市吃吃喝喝和基本的生活需要没问题,当然房子是不用想的了。 + +2、国企搞开发,**技术不会特别新**,很多时候是项目管理的角色。工作内容基本体现为领导的决定。 + +3、国企研究技术没有意义,想当领导,就多学习做PPT和领导搞好关系。或者当一个平庸的人,混吃等死,把时间留给家人,也不乏是一种好选择。 diff --git a/docs/career-plan/how-to-prepare-job-hopping.md b/docs/career-plan/how-to-prepare-job-hopping.md new file mode 100644 index 0000000..59aee75 --- /dev/null +++ b/docs/career-plan/how-to-prepare-job-hopping.md @@ -0,0 +1,88 @@ +--- +sidebar: heading +title: 工作一年想要跳槽,不知道应该怎么准备? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,跳槽怎么准备,程序员跳槽 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作一年想要跳槽,不知道应该怎么准备? + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴问我怎么准备跳槽、面试,在这里跟大家分享一下。 + +**圈友提问**: + +先说一下自己**目前的情况**: + +大四,秋招进了一家公司(珠海的一家公司),过年后去公司实习。 + +自己的技能情况:我通过看视频的方式学到了springboot ,中间件有redis,elasticsearch,zookeeper,double……,springcloud只看了一点资料,基本等于没学。 + +找到工作之后就处于摆烂状态,所以知识点我感觉忘记的差不多了,就连基本的spring注解都有些忘了。之前学习的中间件因为没怎么使用加没有复习也跟没学差不多,但是印象还是有。 + +我想在明年的秋招跳槽,但是我不知道应该怎么准备,求彬哥指点一二。 + +最后祝彬哥新年快乐,万事如意! + +--- + +**大彬的回答**: + +师弟新年好~ + +你现在大四,明年跳槽的话,只能参加**社招**了,社招会更加注重你的**实战能力**,所以在项目方面需要花一些功夫。 如果你可以联系上你现在的导师或者Leader的话,可以先问下项目组主要的技术栈是哪些,自己提前自学一下。进入公司实习或者正式工作的时候,多花时间去梳理你负责的项目的**项目架构、业务逻辑、设计上有哪些亮点**等,这些都是面试官会问的,要提前准备好。 + +很多时候面试官会结合你的项目去考察你对知识点的掌握情况,所以还是要把项目“**吃透**”。 + +另外,算法题和八股文也要准备好,如果想进中大厂的话,建议力扣刷个300道左右(高频题),按标签去刷,做好总结。八股文可以看看**[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)内部的面试手册**(在置顶帖),这本面试手册是我花了挺长时间整理的,应该比较全面了。 + + + +总结一下,就是**项目**+**算法**(中大厂会考察,小厂考察的比较少)+**八股文** + + + +最后,附上一年工作经验需要掌握的技能(可以参考一下): + +1、Java基础扎实,理解IQ、线程、集合等基础框架,对JVM原理有一定的了解; + +2、熟悉使用MySQL或者Oracle等关系型数据库; + +3、熟悉使用主流框架(SpringBoot、Mybatis、Springmvc、Zookeeper、Dubbo等); + +4、熟悉Redis缓存、ElasticSearch等开发经验; + +5、具备定位解决生产环境问题的能力; + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/java-or-bigdata.md b/docs/career-plan/java-or-bigdata.md new file mode 100644 index 0000000..cf98ec1 --- /dev/null +++ b/docs/career-plan/java-or-bigdata.md @@ -0,0 +1,62 @@ +--- +sidebar: heading +title: 24届校招,Java开发和大数据开发怎么选 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,岗位选择,Java还是大数据 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 24届校招,Java开发和大数据开发怎么选 + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于方向选择的问题:**24届校招,Java开发和大数据开发怎么选**? + +**原问题如下**: + +想请教一下大彬,对于**java开发和大数据开发**的明年秋招情况的预测。java岗位多卷度也高,每家公司基本都有相关的职位,大数据开发的话培训班比岗位多,只有大公司和数据公司会有这类岗位。24年秋招的话**选择哪个方向**会比较合适呢? + +--- + +**大彬的回答**: + +建议选大数据吧。就这几年校招来看,大数据岗位拿offer的**难度**相比Java还是比较小一些的。而且距离24年秋招还有一年多时间,转大数据完全来得及,而且有了Java基础,再来学大数据,应该会比较快入门。 + +再说下大数据和后端的**差异**。大数据门槛比Java高,除了熟悉数据库的操作之外,还要学习大数据整个生态,需要会分布式、数仓、数据分析统计等知识。因为大数据的学习门槛比 Java 高,所以市场上培训大数据的相比Java会少一些,**竞争也相对小**,没有Java那么卷。 + +另外,大数据**薪资**总体会比Java开发高一些(同一家公司同一级别,普通开发岗比大数据开发薪资会少一点),这也是大数据方向的一个优势。 + +不过呢,小点的公司,可能没有大数据的需求,毕竟业务量不大(小公司通常也不建议去,坑多)。 + +综上所述,还是建议你选择**大数据**方向。 + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/computer-basic/algorithm.md b/docs/computer-basic/algorithm.md new file mode 100644 index 0000000..3b02e9f --- /dev/null +++ b/docs/computer-basic/algorithm.md @@ -0,0 +1,1456 @@ +--- +sidebar: heading +title: 常见算法总结 +category: 计算机基础 +tag: + - 算法 +head: + - - meta + - name: keywords + content: 算法知识总结,二叉树遍历,排序算法,动态规划,回溯算法,贪心算法,双指针 + - - meta + - name: description + content: 算法常见知识点总结 +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 二叉树的遍历 + +二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。 + +二叉树的先序、中序和后序属于深度优先遍历DFS,层次遍历属于广度优先遍历BFS。 + +![](http://img.topjavaer.cn/img/前序中序后序.png) + +四种主要的遍历思想为: + +前序遍历:根结点 ---> 左子树 ---> 右子树 + +中序遍历:左子树---> 根结点 ---> 右子树 + +后序遍历:左子树 ---> 右子树 ---> 根结点 + +层次遍历:只需按层次遍历即可 + +### 前序遍历 + +遍历思路:根结点 ---> 左子树 ---> 右子树。 + +根据前序遍历的顺序,优先访问根结点,然后在访问左子树和右子树。所以,对于任意结点node,第一部分即直接访问之,之后在判断左子树是否为空,不为空时即重复上面的步骤,直到其为空。若为空,则需要访问右子树。注意,在访问过左孩子之后,需要反过来访问其右孩子,可以是栈这种数据结构来支持。对于任意一个结点node,具体步骤如下: + +1. 访问结点,并把结点node入栈,当前结点置为左孩子; +2. 判断结点node是否为空,若为空,则取出栈顶结点并出栈,将右孩子置为当前结点;否则重复a)步直到当前结点为空或者栈为空(可以发现栈中的结点就是为了访问右孩子才存储的) + +```java + +public void preOrderTraverse2(TreeNode root) { + LinkedList stack = new LinkedList<>(); + TreeNode pNode = root; + while (pNode != null || !stack.isEmpty()) { + if (pNode != null) { + System.out.print(pNode.val+" "); + stack.push(pNode); + pNode = pNode.left; + } else { //pNode == null && !stack.isEmpty() + TreeNode node = stack.pop(); + pNode = node.right; + } + } +} +``` + +### 中序遍历 + +遍历思路:左子树 ---> 根结点 ---> 右子树 + +```java + public List inorderTraversal(TreeNode root) { + List res = new ArrayList<>(); + Deque deque = new ArrayDeque<>(); + + while (!deque.isEmpty() || root != null) { + while (root != null) { + deque.push(root); + root = root.left; + } + root = deque.pop(); + res.add(root.val); + root = root.right; + } + + return res; + } +``` + +### 后序遍历 + +遍历思路:左子树 ---> 右子树 ---> 根结点。 + +使用 null 作为标志位,访问到 null 说明此次递归调用结束。 + +```java +class Solution { + public List postorderTraversal(TreeNode root) { + List res = new LinkedList<>(); + if (root == null) { + return res; + } + + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + root = stack.pop(); + if (root != null) { + stack.push(root);//最后访问 + stack.push(null); + if (root.right != null) { + stack.push(root.right); + } + if (root.left != null) { + stack.push(root.left); + } + } else { //值为null说明此次递归调用结束,将节点值存进结果 + res.add(stack.pop().val); + } + } + + return res; + } +} +``` + +### 层序遍历 + +只需要一个队列即可,先在队列中加入根结点。之后对于任意一个结点来说,在其出队列的时候,访问之。同时如果左孩子和右孩子有不为空的,入队列。 + +``` +public void levelTraverse(TreeNode root) { + if (root == null) { + return; + } + LinkedList queue = new LinkedList<>(); + queue.offer(root); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); + System.out.print(node.val+" "); + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + } +} +``` + +## 排序算法 + +常见的排序算法主要有:冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序、基数排序。各种排序算法的时间空间复杂度、稳定性见下图。 + +![](http://img.topjavaer.cn/img/排序算法时间空间复杂度.png) + +### 冒泡排序 + +冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。 + +思路: + +- 比较相邻的元素。如果第一个比第二个大,就交换它们两个; +- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数; +- 针对所有的元素重复以上的步骤,除了最后一个; +- 重复步骤1~3,直到排序完成。 + +代码实现: + +```java +public void bubbleSort(int[] arr) { + if (arr == null) { + return; + } + boolean flag; + for (int i = arr.length - 1; i > 0; i--) { + flag = false; + for (int j = 0; j < i; j++) { + if (arr[j] > arr[j + 1]) { + int tmp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = tmp; + flag = true; + } + } + if (!flag) { + return; + } + } +} +``` + +### 插入排序 + +插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。 + +算法描述: + +一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下: + +- 从第一个元素开始,该元素可以认为已经被排序; +- 取出下一个元素,在已经排序的元素序列中从后向前扫描; +- 如果该元素(已排序)大于新元素,将该元素移到下一位置; +- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置; +- 将新元素插入到该位置后; +- 重复步骤2~5。 + +代码实现: + +```java +public void insertSort(int[] arr) { + if (arr == null) { + return; + } + for (int i = 1; i < arr.length; i++) { + int tmp = arr[i]; + int j = i; + for (; j > 0 && tmp < arr[j - 1]; j--) { + arr[j] = arr[j - 1]; + } + arr[j] = tmp; + } +} +``` + +### 选择排序 + +表现**最稳定的排序算法之一**,因为**无论什么数据进去都是O(n2)的时间复杂度**,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。 + +选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 + +思路:n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下: + +- 初始状态:无序区为R[1..n],有序区为空; +- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区; +- n-1趟结束,数组有序化了。 + +代码实现: + +```java + public void selectionSort(int[] arr) { + if (arr == null) { + return; + } + for (int i = 0; i < arr.length - 1; i++) { + for (int j = i + 1; j < arr.length; j++) { + if (arr[i] > arr[j]) { + int tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + } + } + } +``` + +### 希尔排序 + +希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。 + +**希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。** + +代码实现: + +```java +public static int[] ShellSort(int[] array) { + int len = array.length; + int temp, gap = len / 2; + while (gap > 0) { + for (int i = gap; i < len; i++) { + temp = array[i]; + int preIndex = i - gap; + while (preIndex >= 0 && array[preIndex] > temp) { + array[preIndex + gap] = array[preIndex]; + preIndex -= gap; + } + array[preIndex + gap] = temp; + } + gap /= 2; + } + return array; +} +``` + +### 基数排序 + +基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数; + +基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。 + +算法描述: + +- 取得数组中的最大数,并取得位数; +- arr为原始数组,从最低位开始取每个位组成radix数组; +- 对radix进行计数排序(利用计数排序适用于小范围数的特点); + +代码实现: + +```java +public static int[] RadixSort(int[] array) { + if (array == null || array.length < 2) + return array; + // 1.先算出最大数的位数; + int max = array[0]; + for (int i = 1; i < array.length; i++) { + max = Math.max(max, array[i]); + } + int maxDigit = 0; + while (max != 0) { + max /= 10; + maxDigit++; + } + int mod = 10, div = 1; + ArrayList> bucketList = new ArrayList>(); + for (int i = 0; i < 10; i++) + bucketList.add(new ArrayList()); + for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) { + for (int j = 0; j < array.length; j++) { + int num = (array[j] % mod) / div; + bucketList.get(num).add(array[j]); + } + int index = 0; + for (int j = 0; j < bucketList.size(); j++) { + for (int k = 0; k < bucketList.get(j).size(); k++) + array[index++] = bucketList.get(j).get(k); + bucketList.get(j).clear(); + } + } + return array; +} +``` + +### 计数排序 + +计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。 + +计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。 + +```java +public static int[] CountingSort(int[] array) { + if (array.length == 0) return array; + int bias, min = array[0], max = array[0]; + for (int i = 1; i < array.length; i++) { + if (array[i] > max) + max = array[i]; + if (array[i] < min) + min = array[i]; + } + bias = 0 - min; + int[] bucket = new int[max - min + 1]; + Arrays.fill(bucket, 0); + for (int i = 0; i < array.length; i++) { + bucket[array[i] + bias]++; + } + int index = 0, i = 0; + while (index < array.length) { + if (bucket[i] != 0) { + array[index] = i - bias; + bucket[i]--; + index++; + } else + i++; + } + return array; +} +``` + + + +### 快速排序 + +快速排序是由**冒泡排序**改进而得到的,是一种排序执行效率很高的排序算法,它利用**分治法**来对待排序序列进行分治排序,它的思想主要是通过一趟排序将待排记录分隔成独立的两部分,其中的一部分比关键字小,后面一部分比关键字大,然后再对这前后的两部分分别采用这种方式进行排序,通过递归的运算最终达到整个序列有序。 + +快速排序的过程如下: + +1. 在待排序的N个记录中任取一个元素(通常取第一个记录)作为基准,称为基准记录; +2. 定义两个索引 left 和 right 分别表示首索引和尾索引,key 表示基准值; +3. 首先,尾索引向前扫描,直到找到比基准值小的记录,并替换首索引对应的值; +4. 然后,首索引向后扫描,直到找到比基准值大于的记录,并替换尾索引对应的值; +5. 若在扫描过程中首索引等于尾索引(left = right),则一趟排序结束;将基准值(key)替换首索引所对应的值; +6. 再进行下一趟排序时,待排序列被分成两个区:[0,left-1]和[righ+1,end] +7. 对每一个分区重复以上步骤,直到所有分区中的记录都有序,排序完成 + +快排为什么比冒泡效率高? + +快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小于基准值的都交换到左边,大于基准值的都交换到右边,这样不会像冒泡一样每次都只交换相邻的两个数,因此比较和交换的此数都变少了,速度自然更高。 + +快速排序的平均时间复杂度是O(nlgn),最坏时间复杂度是O(n^2)。 + +```java + public void quickSort(int[] arr) { + if (arr == null) { + return; + } + quickSortHelper(arr, 0, arr.length - 1); + } + private void quickSortHelper(int[] arr, int left, int right) { + if (left > right) { + return; + } + int tmp = arr[left]; + int i = left; + int j = right; + while (i < j) { + //j先走,最终循环终止时,j停留的位置就是arr[left]的正确位置 + //改为i<=j,则会进入死循环,[1,5,5,5,5]->[1] 5 [5,5,5]->[5,5,5],会死循环 + while (i < j && arr[j] >= tmp) { + j--; + } + while (i < j && arr[i] <= tmp) { + i++; + } + if (i < j) { + int tmp1 = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp1; + } else { + break; + } + } + + //当循环终止的时候,i=j,因为是j先走的,j所在位置的值小于arr[left],交换arr[j]和arr[left] + arr[left] = arr[j]; + arr[j] = tmp; + + quickSortHelper(arr, left, j - 1); + quickSortHelper(arr, j + 1, right); + } +``` + +### 归并排序 + +归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。 + +两路归并排序算法思路是递归处理。每个递归过程涉及三个步骤 + +- 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素 +- 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作 +- 合并: 合并两个排好序的子序列,生成排序结果 + +![](http://img.topjavaer.cn/img/20220327151830.png) + +时间复杂度:对长度为n的序列,需进行logn次二路归并,每次归并的时间为O(n),故时间复杂度是O(nlgn)。 + +空间复杂度:归并排序需要辅助空间来暂存两个有序子序列归并的结果,故其辅助空间复杂度为O(n) + +```java +public class MergeSort { + public void mergeSort(int[] arr) { + if (arr == null || arr.length == 0) { + return; + } + //辅助数组 + int[] tmpArr = new int[arr.length]; + mergeSort(arr, tmpArr, 0, arr.length - 1); + } + + private void mergeSort(int[] arr, int[] tmpArr, int left, int right) { + if (left < right) { + int mid = (left + right) >> 1; + mergeSort(arr, tmpArr, left, mid); + mergeSort(arr, tmpArr, mid + 1, right); + merge(arr, tmpArr, left, mid, right); + } + } + + private void merge(int[] arr, int[] tmpArr, int left, int mid, int right) { + int i = left; + int j = mid + 1; + int tmpIndex = left; + while (i <= mid && j <= right) { + if (arr[i] < arr[j]) { + tmpArr[tmpIndex++] = arr[i]; + i++; + } else { + tmpArr[tmpIndex++] = arr[j]; + j++; + } + } + + while (i <= mid) { + tmpArr[tmpIndex++] = arr[i++]; + } + + while (j <= right) { + tmpArr[tmpIndex++] = arr[j++]; + } + + for (int m = left; m <= right; m++) { + arr[m] = tmpArr[m]; + } + } +} +``` + +### 堆排序 + +堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 + +![](https://img-blog.csdn.net/20150312212515074) + +**Top大问题**解决思路:使用一个固定大小的**最小堆**,当堆满后,每次添加数据的时候与堆顶元素比较,若小于堆顶元素,则舍弃,若大于堆顶元素,则删除堆顶元素,添加新增元素,对堆进行重新排序。 + +对于n个数,取Top m个数,时间复杂度为O(nlogm),这样在n较大情况下,是优于nlogn(其他排序算法)的时间复杂度的。 + +PriorityQueue 是一种基于优先级堆的优先级队列。每次从队列中取出的是具有最高优先权的元素。如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头。优先级队列用数组实现,但是数组大小可以动态增加,容量无限。 + +```java +//找出前k个最大数,采用小顶堆实现 +public static int[] findKMax(int[] nums, int k) { + PriorityQueue pq = new PriorityQueue<>(k);//队列默认自然顺序排列,小顶堆,不必重写compare + + for (int num : nums) { + if (pq.size() < k) { + pq.offer(num); + } else if (pq.peek() < num) {//如果堆顶元素 < 新数,则删除堆顶,加入新数入堆 + pq.poll(); + pq.offer(num); + } + } + + int[] result = new int[k]; + for (int i = 0; i < k&&!pq.isEmpty(); i++) { + result[i] = pq.poll(); + } + return result; +} +``` + + + +## 动态规划 + +动态规划常常适用于有重叠子问题的问题。动态规划的基本思想:若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。 + +动态规划法试图仅仅解决每个子问题一次,一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次遇到同一个子问题的时候直接查表得到解。 + +动态规划的解题思路:1、状态定义;2、状态转移方程;3、初始状态。 + +### 不同路径 + +[不同路径](../leetcode/hot120/62-unique-paths.md) + +### 最长回文子串 + +从给定的字符串 `s` 中找到最长的回文子串的长度。 + +例如 `s = "babbad"` 的最长回文子串是 `"abba"` ,长度是 `4` 。 + +解题思路: + +1. 定义状态。`dp[i][j]` 表示子串 `s[i..j]` 是否为回文子串 + +2. 状态转移方程:`dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]` +3. 初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 `true`,即 `dp[i][i] = true` 。 +4. 只要一得到 `dp[i][j] = true`,就记录子串的长度和起始位置 + +注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。 + +![](http://img.topjavaer.cn/img/image-20201115230411764.png) + +时间复杂度O(N2),空间复杂度O(N2),因为使用了二维数组。 + +```java +public class Solution { + + public String longestPalindrome(String s) { + // 特判 + int len = s.length(); + if (len < 2) { + return s; + } + + int maxLen = 1; + int begin = 0; + + // dp[i][j] 表示 s[i, j] 是否是回文串 + boolean[][] dp = new boolean[len][len]; + char[] charArray = s.toCharArray(); + + for (int i = 0; i < len; i++) { + dp[i][i] = true; + } + for (int j = 1; j < len; j++) { + for (int i = 0; i < j; i++) { + if (charArray[i] != charArray[j]) { + dp[i][j] = false; + } else { + if (j - i < 3) { + dp[i][j] = true; + } else { + dp[i][j] = dp[i + 1][j - 1]; + } + } + + // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置 + if (dp[i][j] && j - i + 1 > maxLen) { + maxLen = j - i + 1; + begin = i; + } + } + } + return s.substring(begin, begin + maxLen); //substring(i, j)截取i到j(不包含j)的字符串 + } +} +``` + + + +### 最大子数组和 + +**题目描述**:给你一个整数数组 `nums` ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +**示例**: + +```java +输入: nums = [-2,1,-3,4,-1,2,1,-5,4] +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。 +``` + +1、首先确定dp数组(dp table)以及下标的含义。 + +dp[i]表示以nums[i]结尾的子数组的最大和。 + +2、确定递推公式。 + +`dp[i] = dp[i - 1] > 0 ? ( dp[i - 1] + nums[i]) : nums[i]` + +dp[i+1]取决于dp[i]的值,不需要使用数组保存状态,只需要一个变量sum来保存上一个状态即可。 + +3、dp数组如何初始化。 + +从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。 + +dp[0]应该是多少呢?根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。 + +示例代码如下: + +```java +class Solution { + public int maxSubArray(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + + int max = nums[0]; + int sum = 0; + + for (int num : nums) { + if (sum > 0) { + sum += num; + } else { + sum = num; + } + max = Math.max(max, sum); + } + + return max; + } +} +``` + +### 最长公共子序列 + +一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 +例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 + +动态规划。`dp[i][j]`表示text1以i-1结尾的子串和text2以j-1结尾的子串的最长公共子序列的长度。dp横坐标或纵坐标为0表示空字符串,`dp[0][j] = dp[i][0] = 0`,无需额外处理base case。 + +![](http://img.topjavaer.cn/img/longestCommonSubsequence.png) + +```java +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + char[] arr1 = text1.toCharArray(); + char[] arr2 = text2.toCharArray(); + //dp[0][x]和dp[x][0]表示有一个为空字符串 + //dp[1][1]为text1第一个字符和text2第一个字符的最长公共子序列的长度 + //dp[i][j]表示text1以i-1结尾的子串和text2以j-1结尾的子串的最长公共子序列的长度 + int len1 = arr1.length; + int len2 = arr2.length; + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 1; i < len1 + 1; i++) { + for (int j = 1; j < len2 + 1; j++) { + if (arr1[i - 1] == arr2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[len1][len2]; + } +} +``` + +`dp[i][j]`表示text1以i结尾的子串和text2以j结尾的子串的最长公共子序列的长度。需要处理base case。 + +```java +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + char[] arr1 = text1.toCharArray(); + char[] arr2 = text2.toCharArray(); + + int len1 = arr1.length; + int len2 = arr2.length; + //`dp[i][j]`表示text1以i结尾的子串和text2以j结尾的子串的最长公共子序列的长度。 + int[][] dp = new int[len1][len2]; + + if (arr1[0] == arr2[0]) { + dp[0][0] = 1; + } + for (int i = 1; i < len1; i++) { + if (arr1[i] == arr2[0]) { + dp[i][0] = 1; + } else { + dp[i][0] = dp[i - 1][0]; + } + } + for (int i = 1; i < len2; i++) { + if (arr1[0] == arr2[i]) { + dp[0][i] = 1; + } else { + dp[0][i] = dp[0][i - 1]; + } + } + + for (int i = 1; i < len1; i++) { + for (int j = 1; j < len2; j++) { + if (arr1[i] == arr2[j]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[len1 - 1][len2 - 1]; + } +} +``` + + + +### 接雨水 + +给定 `n` 个非负整数表示每个宽度为 `1` 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 + +![](http://img.topjavaer.cn/img/接雨水.png) + +示例: + +```java +输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +输出:6 +解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 +``` + +动态规划,使用两个数组空间。leftMax[i] 代表第 `i` 列左边(不包含自身)最高的墙的高度,rightMax[i] 代表第 `i` 列右边最高的墙的高度。 + +```java +class Solution { + public int trap(int[] height) { + int len = height.length; + int res = 0; + int[] leftMax = new int[len]; + int[] rightMax = new int[len]; + + for (int i = 1; i < len; i++) { + leftMax[i] = Math.max(leftMax[i - 1], height[i - 1]); + } + + for (int j = len - 2; j > 0; j--) { + rightMax[j] = Math.max(rightMax[j + 1], height[j + 1]); + } + + for (int i = 1; i < len - 1; i++) { + int min = Math.min(leftMax[i], rightMax[i]); + if (min > height[i]) { + res += min - height[i]; + } + } + + return res; + } +} +``` + + + +### 单词拆分 + +![](http://img.topjavaer.cn/img/word-break.png) + +```java +class Solution { + public boolean wordBreak(String s, List wordDict) { + int len = s.length(), maxw = 0; + //dp[i]表示前i个字母组成的字符串是否可以被拆分 + boolean[] dp = new boolean[len + 1]; + //状态转移方程初始化条件 + dp[0] = true; + Set set = new HashSet(); + for(String str : wordDict){ + set.add(str); + maxw = Math.max(maxw, str.length()); + } + for(int i = 1; i < len + 1; i++){ + for(int j = i; j >= 0 && j >= i - maxw; j--){ + if(dp[j] && set.contains(s.substring(j, i))){ + dp[i] = true; + break; + } + } + } + return dp[len]; + } +} +``` + +## 回溯算法 + +回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。 + +### 组合总和 + +题目描述:给定一个**无重复元素**的数组 `candidates` 和一个目标数 `target` ,找出 `candidates` 中所有可以使数字和为 `target` 的组合。 + +示例: + +``` +输入:candidates = [2,3,6,7], target = 7, +输出: +[ + [7], + [2,2,3] +] +``` + +使用回溯算法。 + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> combinationSum2(int[] candidates, int target) { + if (candidates == null || candidates.length == 0) { + return ans; + } + Arrays.sort(candidates);//排序方便回溯剪枝 + Deque path = new ArrayDeque<>();//作为栈来使用,效率高于Stack;也可以作为队列来使用,效率高于LinkedList;线程不安全 + combinationSum2Helper(candidates, target, 0, path); + return ans; + } + + public void combinationSum2Helper(int[] arr, int target, int start, Deque path) { + if (target == 0) { + ans.add(new ArrayList(path)); + } + + for (int i = start; i < arr.length; i++) { + if (target < arr[i]) {//剪枝 + return; + } + if (i > start && arr[i] == arr[i - 1]) {//在一个层级,会产生重复 + continue; + } + path.addLast(arr[i]); + combinationSum2Helper(arr, target - arr[i], i + 1, path); + path.removeLast(); + } + } +} +``` + + + +### 全排列 + +给定一个 **没有重复** 数字的序列,返回其所有可能的全排列。 + +示例: + +``` +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] +``` + +使用回溯。注意与组合总和的区别(数字有无顺序)。 + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> permute(int[] nums) { + boolean[] flag = new boolean[nums.length]; + ArrayDeque path = new ArrayDeque<>(); + permuteHelper(nums, flag, path); + + return ans; + } + + private void permuteHelper(int[] nums, boolean[] flag, ArrayDeque path) { + if (path.size() == nums.length) { + ans.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (flag[i]) { + continue;//继续循环 + } + path.addLast(nums[i]); + flag[i] = true; + permuteHelper(nums, flag, path); + path.removeLast(); + flag[i] = false; + } + } +} +``` + + + +### 全排列II + +给定一个可包含重复数字的序列,返回所有不重复的全排列。注意与组合总和的区别。 + +1、排序;2、同一层级相同元素剪枝。 + +![](http://img.topjavaer.cn/img/permutations-ii.png) + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> permuteUnique(int[] nums) { + if (nums == null || nums.length == 0) { + return ans; + } + ArrayDeque path = new ArrayDeque<>(); + boolean[] used = new boolean[nums.length]; + Arrays.sort(nums);//切记 + dps(nums, used, path); + + return ans; + } + + private void dps(int[] nums, boolean[] used, ArrayDeque path) { + if (path.size() == nums.length) { + ans.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + if ((i > 0 && nums[i] == nums[i - 1]) && !used[i - 1]) {//同一层相同的元素,剪枝 + continue;//继续循环,不是return退出循环 + } + path.addLast(nums[i]); + used[i] = true; + dps(nums, used, path); + path.removeLast(); + used[i] = false; + } + } +} +``` + +## 贪心算法 + +贪心算法,是寻找**最优解问题**的常用方法,这种方法模式一般将求解过程分成**若干个步骤**,但每个步骤都应用贪心原则,选取当前状态下**最好/最优的选择**(局部最有利的选择),并以此希望最后堆叠出的结果也是最好/最优的解。 + +**贪婪法的基本步骤:** + +1. 从某个初始解出发; +2. 采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一部分解,缩小问题规模; +3. 将所有解综合起来。 + +### 买卖股票的最佳时机 II + +**题目描述**: + +给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。 + +在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。 + +返回 你能获得的 最大 利润 。 + +**示例**: + +```java +输入:prices = [1,2,3,4,5] +输出:4 +解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 +  总利润为 4 。 +``` + +思路:可以尽可能地完成更多的交易,但不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +```java +//输入: [7,1,5,3,6,4] +//输出: 7 +class Solution { + public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + int tmp = prices[i] - prices[i - 1]; + if (tmp > 0) { + profit += tmp; + } + } + + return profit; + } +} +``` + +### 跳跃游戏 + +**题目描述** + +给定一个非负整数数组 `nums` ,你最初位于数组的 **第一个下标** 。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个下标。 + +**示例**: + +```java +输入:nums = [2,3,1,1,4] +输出:true +解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 +``` + +解题思路: + +1. 如果某一个作为 起跳点 的格子可以跳跃的距离是 3,那么表示后面 3 个格子都可以作为 起跳点 +2. 可以对每一个能作为 起跳点 的格子都尝试跳一次,把 能跳到最远的距离 不断更新 +3. 如果可以一直跳到最后,就成功了 + +```java +class Solution { + public boolean canJump(int[] nums) { + if (nums == null || nums.length == 0) { + return true; + } + + int maxIndex = nums[0]; + for (int i = 1; i < nums.length; i++) { + if (maxIndex >= i) { + maxIndex = Math.max(maxIndex, i + nums[i]); + } else { + return false; + } + } + + return true; + } +} +``` + +### 加油站 + +**题目描述** + +在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 + +你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 + +给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。 + +**示例** + +```java +输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] +输出: 3 +解释: +从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 +开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 +开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 +开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 +开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 +开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 +因此,3 可为起始索引。 +``` + +**思路**: + +1. 遍历一周,总获得的油量少于要花掉的油量必然没有结果; +2. 先苦后甜,记录遍历时所存的油量最少的站点,由于题目有解只有唯一解,所以从当前站点的下一个站点开始是唯一可能成功开完全程的。 + +```java +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int minIdx=0; + int sum=Integer.MAX_VALUE; + int num=0; + for (int i = 0; i < gas.length; i++) { + num+=gas[i]-cost[i]; + if(num 0) { + fast = fast.next; + } + + while (fast.next != null) { + fast = fast.next; + slow = slow.next; + } + slow.next = slow.next.next; + + return tmp.next; + } +} +``` + +### 三数之和 + +[题目链接](https://leetcode.cn/problems/3sum/) + +**题目描述** + +给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。 + +注意:答案中不可以包含重复的三元组。 + +**示例** + +```java +输入:nums = [-1,0,1,2,-1,-4] +输出:[[-1,-1,2],[-1,0,1]] +``` + +**思路**: + +- 首先对数组进行排序,排序后固定一个数 nums[i]nums[i],再使用左右指针指向 nums[i]nums[i]后面的两端,数字分别为 nums[L]nums[L] 和 +- nums[R]nums[R],计算三个数的和 sumsum 判断是否满足为 00,满足则添加进结果集 +- 如果 nums[i]nums[i]大于 00,则三数之和必然无法等于 00,结束循环 +- 如果 nums[i]nums[i] == nums[i-1]nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过 +- 当 sumsum == 00 时,nums[L]nums[L] == nums[L+1]nums[L+1] 则会导致结果重复,应该跳过,L++L++ +- 当 sumsum == 00 时,nums[R]nums[R] == nums[R-1]nums[R−1] 则会导致结果重复,应该跳过,R--R−− + +**参考代码**: + +```java +class Solution { + public List> threeSum(int[] nums) { + List> res = new ArrayList<>(); + Arrays.sort(nums); + + for (int i = 0; i < nums.length; i++) { + if (nums[i] > 0) { //最左边的数字大于0,则sum不会等于0,退出 + break; + } + if (i > 0 && nums[i] == nums[i - 1]) { //去重复 + continue; + } + + int left = i + 1; + int right = nums.length - 1; + + while (left < right) { + int sum = nums[i] + nums[left] + nums[right]; + if (sum == 0) { + res.add(Arrays.asList(nums[i], nums[left], nums[right])); ///array to list + while (left < right && nums[left] == nums[left + 1]) { + left++; + } + while (left < right && nums[right] == nums[right - 1]) { + right--; + } + left++; + right--; + } else if (sum > 0) { + right--; + } else { + left++; + } + } + } + + return res; + } +} +``` + +### 环形链表 + +[题目链接](https://leetcode.cn/problems/linked-list-cycle/) + +**题目描述** + +给你一个链表的头节点 head ,判断链表中是否有环。 + +如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。 + +如果链表中存在环 ,则返回 true 。 否则,返回 false 。 + +**示例** + +```java +输入:head = [3,2,0,-4], pos = 1 +输出:true +解释:链表中有一个环,其尾部连接到第二个节点。 +``` + +**思路** + +快慢指针。快指针每次走两步,慢指针走一步,相当于慢指针不动,快指针每次走一步,如果是环形链表,则一定会相遇。 + +```java +public class Solution { + public boolean hasCycle(ListNode head) { + if (head == null) { + return false; + } + + ListNode quick = head; + ListNode slow = head; + + while (quick != null && quick.next != null) { + slow = slow.next; + quick = quick.next.next; + + if (slow == quick) { + return true; + } + } + + return false; + } +} +``` + +### 环形链表II + +[题目链接](https://leetcode.cn/problems/linked-list-cycle-ii/) + +**题目描述** + +给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 + +如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。 + +不允许修改 链表。 + +**示例** + +```java +输入:head = [3,2,0,-4], pos = 1 +输出:返回索引为 1 的链表节点 +解释:链表中有一个环,其尾部连接到第二个节点 +``` + +**解题思路** + +方法一:头结点到入环结点的距离为a,入环结点到相遇结点的距离为b,相遇结点到入环结点的距离为c。然后,当fast以slow的两倍速度前进并和slow相遇时,fast走过的距离是s的两倍,即有等式:a+b+c+b = 2(a+b) ,可以得出 a = c ,所以说,让fast和slow分别从相遇结点和头结点同时同步长出发,他们的相遇结点就是入环结点。 + +```java +public class Solution { + public ListNode detectCycle(ListNode head) { + ListNode fast = head; + ListNode slow = head; + + while (true) { + if (fast == null || fast.next == null) { + return null; + } + fast = fast.next.next; + slow = slow.next; + if (fast == slow) { + break; + } + } + + fast = head; + + while (slow != fast) { + slow = slow.next; + fast = fast.next; + } + + return fast; + } +} +``` + +方法二:先算出环的大小n,快指针先走n步,然后快慢指针一起走,相遇的地方即是环的入口。 + +```java +public class Solution { + public ListNode detectCycle(ListNode head) { + ListNode slow = head; + ListNode fast = head; + //快慢指针找出环的大小 + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + if (fast == slow) { + break; + } + } + + if (fast == null || fast.next == null) { + return null; + } + + int cycleSize = 1; + while (fast.next != slow) { + cycleSize++; + fast = fast.next; + } + + //快慢指针重新从链表首部出发,快指针先走sizeOfCycle步 + //然后两个指针同时一起走,步长为1,相遇节点即是环的入口 + fast = head; + slow = head; + while (cycleSize-- > 0) { + fast = fast.next; + } + while (fast != slow) { + fast = fast.next; + slow = slow.next; + } + + return fast; + } +} +``` + diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" b/docs/computer-basic/data-structure.md similarity index 59% rename from "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" rename to docs/computer-basic/data-structure.md index 9bb6945..e6e60be 100644 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ b/docs/computer-basic/data-structure.md @@ -1,3 +1,25 @@ +--- +sidebar: heading +title: 常见数据结构总结 +category: 计算机基础 +tag: + - 数据结构 +head: + - - meta + - name: keywords + content: 数据结构知识总结,数据结构应用场景,数组,链表,哈希表,栈,队列,树,图 + - - meta + - name: description + content: 数据结构常见知识点总结 +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## 各种数据结构应用场景 - 栈:逆序输出;语法检查,符号成对判断;方法调用 @@ -77,21 +99,50 @@ ### AVL树 -平衡二叉搜索树,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1。 +**平衡二叉搜索树**,它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1。 + +![](http://img.topjavaer.cn/img/image-20220722233208322.png) + +简单了解一下**左旋与右旋**的概念。 - - 左左单旋转 - ![在这里插入图片描述](https://img-blog.csdn.net/20181005222022420?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) +左旋与右旋就是为了解决不平衡问题而产生的,我们构建一颗AVL树的过程会出现结点平衡因子(平衡因子就是二叉排序树中每个结点的左子树和右子树的高度差。)绝对值大于1的情况,这时就可以通过左旋或者右旋操作来达到平衡的目的。 -- 左右双旋转 - ![在这里插入图片描述](https://img-blog.csdn.net/20181005222244445?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) +四种旋转情况 + +![](http://img.topjavaer.cn/img/四种旋转情况.png) + +### 红黑树 + +红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。查找性能并没有提高,查找的时间复杂度是O(logn)。红黑树通过左旋、右旋和变色维持平衡。 + +![](http://img.topjavaer.cn/img/20220619165116.png) + +对于插入节点,AVL和红黑树都是最多两次旋转来实现平衡。对于删除节点,avl需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而红黑树最多只需旋转3次。 -- 四种旋转情况 +红黑树的特性: - ![在这里插入图片描述](https://img-blog.csdnimg.cn/20181206115511699.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) +- 每个节点或者是黑色,或者是红色。 +- 根节点和叶子节点是黑色,叶子节点为空。 +- 红色节点的子节点必须是黑色的。 +- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点,保证没有一条路径会比其他路径长一倍。 + +优点:相比avl树,红黑树插入删除的效率更高。红黑树维持红黑性质所做的红黑变换和旋转的开销,相较于avl树维持平衡的开销要小得多。 + +**应用场景** + +- Java ConcurrentHashMap & TreeMap +- C++ STL: map & set +- linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块 +- epoll在内核中的实现,用红黑树管理事件块 +- nginx中,用红黑树管理timer等 ### B树 -也称B-树,属于多叉树又名平衡多路查找树。规则: +也称B-树,属于多叉树又名平衡多路查找树。 + +![](http://img.topjavaer.cn/img/B-树.png) + +规则: - 1<子节点数<=m,m代表一个树节点最多有多少个查找路径 - 每个节点最多有m-1个关键字,非根节点至少有m/2个关键字,根节点最少可以只有1个关键字 @@ -105,46 +156,81 @@ B-树的特性: B+树是B-树的变体,也是一种多路搜索树。B+的搜索与B-树基本相同,区别是B+树只有达到叶子结点才命中,B-树可以在非叶子结点命中。B+树更适合文件索引系统。 -B-和B+树的区别 +![](http://img.topjavaer.cn/img/B+树.jpg) + +B-和B+树的区别: - B+树的非叶子结点不包含data,叶子结点使用链表连接,便于区间查找和遍历。B-树需要遍历整棵树,范围查询性能没有B+树好。 - B-树的非树节点存放数据和索引,搜索可能在非叶子结点结束,访问更快。 -### 红黑树 +## 图 -红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。查找性能并没有提高,查找的时间复杂度是O(logn)。红黑树通过左旋、右旋和变色维持平衡。 +图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为: G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 -对于插入节点,AVL和红黑树都是最多两次旋转来实现平衡。对于删除节点,avl需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而红黑树最多只需旋转3次。 +和线性表,树的差异: -特性: -(1) 每个节点或者是黑色,或者是红色。 -(2) 根节点和叶子节点是黑色,叶子节点为空。 -(4)红色节点的子节点必须是黑色的。 -(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点,保证没有一条路径会比其他路径长一倍。 +- 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。 +- 线性表可以没有元素,称为空表;树中可以没有节点,称为空树;但是,在图中不允许没有顶点(有穷非空性)。 +- 线性表中的各元素是线性关系,树中的各元素是层次关系,而图中各顶点的关系是用边来表示(边集可以为空)。 -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220619165116.png) +![](http://img.topjavaer.cn/img/20220619165442.png) -优点:相比avl树,红黑树插入删除的效率更高。红黑树维持红黑性质所做的红黑变换和旋转的开销,相较于avl树维持平衡的开销要小得多。 +### 相关术语 -应用:主要用来存储有序的数据,Java中的TreeSet和TreeMap都是通过红黑树实现的。 +- 顶点的度 +顶点Vi的度(Degree)是指在图中与Vi相关联的边的条数。对于有向图来说,有入度(In-degree)和出度(Out-degree)之分,有向图顶点的度等于该顶点的入度和出度之和。 +- 邻接 -## 图 +若无向图中的两个顶点V1和V2存在一条边(V1,V2),则称顶点V1和V2邻接(Adjacent); + +若有向图中存在一条边,则称顶点V3与顶点V2邻接,且是V3邻接到V2或V2邻接直V3; + +- 路径 + +在无向图中,若从顶点Vi出发有一组边可到达顶点Vj,则称顶点Vi到顶点Vj的顶点序列为从顶点Vi到顶点Vj的路径(Path)。 + +- 连通 + +若从Vi到Vj有路径可通,则称顶点Vi和顶点Vj是连通(Connected)的。 + +- 权(Weight) + +有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。 -图由顶点集(vertex set)和边集(edge set)所组成。 +### 类型 -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220619165442.png) +无向图 -图的存储结构有邻接矩阵、邻接表和边集数组三种。 +如果图中任意两个顶点之间的边都是无向边(简而言之就是没有方向的边),则称该图为无向图(Undirected graphs)。 -1、邻接矩阵 +无向图中的边使用小括号“()”表示; 比如 (V1,V2); -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220619165526.png) +有向图 -2、邻接表 +如果图中任意两个顶点之间的边都是有向边(简而言之就是有方向的边),则称该图为有向图(Directed graphs)。 -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220619165617.png) +有向图中的边使用尖括号“<>”表示; 比如/ + +完全图 + +- `无向完全图`: 在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。(含有n个顶点的无向完全图有(n×(n-1))/2条边) +- `有向完全图: 在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。(含有n个顶点的有向完全图有n×(n-1)条边 + +### 图的存储结构 + +1、**邻接矩阵** + +图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。 + +![](http://img.topjavaer.cn/img/20220619165526.png) + +2、**邻接表** + +邻接表由表头节点和表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。 + +![](http://img.topjavaer.cn/img/20220619165617.png) 下面给出建立图的邻接表中所使用的边结点类的定义 @@ -186,6 +272,8 @@ public interface Graph } ``` +### 图的遍历 + 深度优先遍历 ```java diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md new file mode 100644 index 0000000..831488a --- /dev/null +++ b/docs/computer-basic/network.md @@ -0,0 +1,48 @@ +--- +sidebar: heading +title: 计算机网络常见面试题 +category: 计算机基础 +tag: + - 网络 +head: + - - meta + - name: keywords + content: 计算机网络常见面试题 + - - meta + - name: description + content: 计算机网络常见面试题,努力打造最优质的Java学习网站 +--- + +**计算机网络重要知识点&高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到**Java面试手册完整版**。 + +![](http://img.topjavaer.cn/img/面试手册详情1.png) + +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 + +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +另外星球也提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) + +![](http://img.topjavaer.cn/img/image-20230102210715391.png) + +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237\351\235\242\350\257\225\351\242\230.md" b/docs/computer-basic/operate-system.md similarity index 94% rename from "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237\351\235\242\350\257\225\351\242\230.md" rename to docs/computer-basic/operate-system.md index c8e073c..64e08f0 100644 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\223\215\344\275\234\347\263\273\347\273\237/\346\223\215\344\275\234\347\263\273\347\273\237\351\235\242\350\257\225\351\242\230.md" +++ b/docs/computer-basic/operate-system.md @@ -1,3 +1,25 @@ +--- +sidebar: heading +title: 操作系统常见面试题总结 +category: 计算机基础 +tag: + - 操作系统 +head: + - - meta + - name: keywords + content: 操作系统面试题,进程线程,并发和并行,协程,进程通信,死锁,进程调度策略,分页分段,页面置换算法,用户态和内核态,IO多路复用 + - - meta + - name: description + content: 操作系统常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## 操作系统的四个特性? 并发:同一段时间内多个程序执行(与并行区分,并行指的是同一时刻有多个事件,多处理器系统可以使程序并行执行) @@ -69,7 +91,7 @@ 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方持有的资源,所以这两个线程就会互相等待而进入死锁状态。 -![](https://raw.githubusercontent.com/Tyson0314/img/master/死锁.png) +![](http://img.topjavaer.cn/img/死锁.png) 下面通过例子说明线程死锁,代码来自并发编程之美。 @@ -160,7 +182,7 @@ Thread[线程 2,5,main]waiting get resource1 进程一共有`5`种状态,分别是创建、就绪、运行(执行)、终止、阻塞。 -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220613090034.png) +![](http://img.topjavaer.cn/img/20220613090034.png) - 运行状态就是进程正在`CPU`上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。 - 就绪状态就是说进程已处于准备运行的状态,即进程获得了除`CPU`之外的一切所需资源,一旦得到`CPU`即可运行。 @@ -285,4 +307,4 @@ Linux下,进程不能直接读写内存物理地址,只能访问【虚拟内 -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220612101342.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/computer-basic/tcp.md b/docs/computer-basic/tcp.md new file mode 100644 index 0000000..f351994 --- /dev/null +++ b/docs/computer-basic/tcp.md @@ -0,0 +1,217 @@ +--- +sidebar: heading +title: TCP常见面试题总结 +category: 计算机基础 +tag: + - TCP +head: + - - meta + - name: keywords + content: TCP面试题,三次握手,四次挥手,TCP协议,滑动窗口,粘包,TCP和UDP,TCP特点,TCP可靠性 + - - meta + - name: description + content: TCP常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +# TCP协议面试题 + +## 为什么需要TCP协议? + +IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。 + +因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。 + +## 说说TCP的三次握手 + +假设发送端为客户端,接收端为服务端。开始时客户端和服务端的状态都是`CLOSED`。 + +![](http://img.topjavaer.cn/img/三次握手图解.png) + +1. 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位`SYN=1`,序列号`seq=x`。第一次握手前客户端的状态为`CLOSE`,第一次握手后客户端的状态为`SYN-SENT`。此时服务端的状态为`LISTEN`。 +2. 第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位`SYN=1`,`ACK=1`,序列号`seq=y`,确认号`ack=x+1`。第二次握手前服务端的状态为`LISTEN`,第二次握手后服务端的状态为`SYN-RCVD`,此时客户端的状态为`SYN-SENT`。(其中`SYN=1`表示要和客户端建立一个连接,`ACK=1`表示确认序号有效) +3. 第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位`ACK=1`,序列号`seq=x+1`,确认号`ack=y+1`。第三次握手前客户端的状态为`SYN-SENT`,第三次握手后客户端和服务端的状态都为`ESTABLISHED`。**此时连接建立完成。** + +## 两次握手可以吗? + +之所以需要第三次握手,主要为了**防止已失效的连接请求报文段**突然又传输到了服务端,导致产生问题。 + +- 比如客户端A发出连接请求,可能因为网络阻塞原因,A没有收到确认报文,于是A再重传一次连接请求。 +- 然后连接成功,等待数据传输完毕后,就释放了连接。 +- 然后A发出的第一个连接请求等到连接释放以后的某个时间才到达服务端B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段。 +- 如果不采用三次握手,只要B发出确认,就建立新的连接了,**此时A不会响应B的确认且不发送数据,则B一直等待A发送数据,浪费资源。** + +## 说说TCP的四次挥手 + +![](http://img.topjavaer.cn/img/四次挥手0.png) + +1. A的应用进程先向其TCP发出连接释放报文段(`FIN=1,seq=u`),并停止再发送数据,主动关闭TCP连接,进入`FIN-WAIT-1`(终止等待1)状态,等待B的确认。 +2. B收到连接释放报文段后即发出确认报文段(`ACK=1,ack=u+1,seq=v`),B进入`CLOSE-WAIT`(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。 +3. A收到B的确认后,进入`FIN-WAIT-2`(终止等待2)状态,等待B发出的连接释放报文段。 +4. B发送完数据,就会发出连接释放报文段(`FIN=1,ACK=1,seq=w,ack=u+1`),B进入`LAST-ACK`(最后确认)状态,等待A的确认。 +5. A收到B的连接释放报文段后,对此发出确认报文段(`ACK=1,seq=u+1,ack=w+1`),A进入`TIME-WAIT`(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间`2MSL`(最大报文段生存时间)后,A才进入`CLOSED`状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。 + +## 第四次挥手为什么要等待2MSL? + +- **保证A发送的最后一个ACK报文段能够到达B**。这个`ACK`报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在`2MSL`时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到`CLOSED`状态,若A在`TIME-WAIT`状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到`CLOSED`状态。 +- **防止已失效的连接请求报文段出现在本连接中**。A在发送完最后一个`ACK`报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。 + +## 为什么是四次挥手? + +因为当Server端收到Client端的`SYN`连接请求报文后,可以直接发送`SYN+ACK`报文。**但是在关闭连接时,当Server端收到Client端发出的连接释放报文时,很可能并不会立即关闭SOCKET**,所以Server端先回复一个`ACK`报文,告诉Client端我收到你的连接释放报文了。只有等到Server端所有的报文都发送完了,这时Server端才能发送连接释放报文,之后两边才会真正的断开连接。故需要四次挥手。 + +## SIN/FIN不包含数据却要消耗序列号 + +凡是需要对端确认的,一定消耗TCP报文的序列号。SYN和FIN需要对端的确认,因此需要消耗一个序列号。 + +SYN作为三次握手的确认。FIN作为四次挥手的确认。如果没有序列号,会导致SYN请求多次重发,服务端多次处理,造成资源浪费 + +## 说说TCP报文首部有哪些字段,其作用又分别是什么? + +![](http://img.topjavaer.cn/img/tcp报文.png) + +- **16位端口号**:源端口号,主机该报文段是来自哪里;目标端口号,要传给哪个上层协议或应用程序 +- **32位序号**:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。 +- **32位确认号**:用作对另一方发送的tcp报文段的响应。其值是收到的TCP报文段的序号值加1。 +- **4位头部长度**:表示tcp头部有多少个32bit字(4字节)。因为4位最大能标识15,所以TCP头部最长是60字节。 +- **6位标志位**:URG(紧急指针是否有效),ACk(表示确认号是否有效),PSH(缓冲区尚未填满),RST(表示要求对方重新建立连接),SYN(建立连接消息标志接),FIN(表示告知对方本端要关闭连接了) +- **16位窗口大小**:是TCP流量控制的一个手段。这里说的窗口,指的是接收通告窗口。它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。 +- **16位校验和**:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障。 +- **16位紧急指针**:一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。 + +## TCP有哪些特点? + +- TCP是**面向连接**的运输层协议。 +- **点对点**,每一条TCP连接只能有两个端点。 +- TCP提供**可靠交付**的服务。 +- TCP提供**全双工通信**。 +- **面向字节流**。 + +## TCP和UDP的区别? + +1. TCP**面向连接**;UDP是无连接的,即发送数据之前不需要建立连接。 +2. TCP提供**可靠的服务**;UDP不保证可靠交付。 +3. TCP**面向字节流**,把数据看成一连串无结构的字节流;UDP是面向报文的。 +4. TCP有**拥塞控制**;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。 +5. 每一条TCP连接只能是**点到点**的;UDP支持一对一、一对多、多对一和多对多的通信方式。 +6. TCP首部开销20字节;UDP的首部开销小,只有8个字节。 + +## TCP 和 UDP 分别对应的常见应用层协议有哪些? + +**基于TCP的应用层协议有:HTTP、FTP、SMTP、TELNET、SSH** + +- **HTTP**:HyperText Transfer Protocol(超文本传输协议),默认端口80 +- **FTP**: File Transfer Protocol (文件传输协议), 默认端口(20用于传输数据,21用于传输控制信息) +- **SMTP**: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口25 +- **TELNET**: Teletype over the Network (网络电传), 默认端口23 +- **SSH**:Secure Shell(安全外壳协议),默认端口 22 + +**基于UDP的应用层协议:DNS、TFTP、SNMP** + +- **DNS** : Domain Name Service (域名服务),默认端口 53 +- **TFTP**: Trivial File Transfer Protocol (简单文件传输协议),默认端口69 +- **SNMP**:Simple Network Management Protocol(简单网络管理协议),通过UDP端口161接收,只有Trap信息采用UDP端口162。 + +## TCP的粘包和拆包 + +TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一**个完整的包可能会被TCP拆分成多个包进行发送**,**也有可能把多个小的包封装成一个大的数据包发送**,这就是所谓的TCP粘包和拆包问题。 + +**为什么会产生粘包和拆包呢?** + +- 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包; +- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包; +- 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包; +- 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。 + +**解决方案:** + +- 发送端将每个数据包封装为固定长度 +- 在数据尾部增加特殊字符进行分割 +- 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。 + +## 说说TCP是如何确保可靠性的呢? + +- TCP的连接是基于**三次握手**,而断开则是基于**四次挥手**。确保连接和断开的可靠性。 +- TCP的可靠性,还体现在**有状态**。TCP会记录哪些数据发送了,哪些数据被接收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。 +- 确认和重传机制:建立连接时三次握手同步双方的“序列号 + 确认号 + 窗口大小信息”,是确认重传、流控的基础。传输过程中,如果Checksum校验失败、丢包或延时,发送端重传 +- 流量控制:窗口和计时器的使用。TCP窗口中会指明双方能够发送接收的最大数据量 +- 拥塞控制 + +## TCP的重传机制是什么? + +由于TCP的下层网络(网络层)可能出现丢失、重复或失序的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传,一是基于时间,二是基于确认信息。 + +TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。 + +## 说下TCP的滑动窗口机制 + +TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。 + +![](http://img.topjavaer.cn/img/image-20210921112213523.png) + + +TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。 + +## 详细讲一下拥塞控制? + +防止过多的数据注入到网络中。 几种拥塞控制方法:慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。 + +![](http://img.topjavaer.cn/img/拥塞控制.jpg) + +**慢开始** + +把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。 + + 当 cwnd < ssthresh 时,使用慢开始算法。 + + 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 + + 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。 + +**拥塞避免** + +让拥塞窗口cwnd缓慢地增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长。 + +无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。 + +**快重传** + +有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。 + +快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。 + +发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。 + +**快恢复** + +当发送方连续收到三个重复确认,就会把慢开始门限ssthresh减半,接着把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 + +在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得TCP的性能有明显的改进。 + +## 什么是 SYN 攻击? + +我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到 一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。 + + + +## 如何唯一确定一个TCP连接呢? + +TCP 四元组可以唯一的确定一个连接,四元组包括如下: 源地址 源端口 目的地址 目的端口。 + +源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。 + +源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。 + +## 说说TCP KeepAlive 的基本原理? + +TCP 的连接,实际上是一种纯软件层面的概念,在物理层面并没有“连接”这种概念。TCP 通信双方建立交互的连接,但是并不是一直存在数据交互,有些连接会在数据交互完毕后,主动释放连接,而有些不会。在长时间无数据交互的时间段内,交互双方都有可能出现掉电、死机、异常重启等各种意外,当这些意外发生之后,这些 TCP 连接并未来得及正常释放,在软件层面上,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,为了解决这个问题,在传输层可以利用 TCP 的 KeepAlive 机制实现来实现。主流的操作系统基本都在内核里支持了这个特性。 + +TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。 + +> 参考链接:https://hit-alibaba.github.io/interview/basic/network/TCP.html diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\345\233\276\350\247\243HTTP.md" "b/docs/computer-basic/\345\233\276\350\247\243HTTP.md" similarity index 98% rename from "\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\345\233\276\350\247\243HTTP.md" rename to "docs/computer-basic/\345\233\276\350\247\243HTTP.md" index abf68ff..c9def51 100644 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\345\233\276\350\247\243HTTP.md" +++ "b/docs/computer-basic/\345\233\276\350\247\243HTTP.md" @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: HTTP常见知识点总结 +category: 计算机基础 +tag: + - TCP +head: + - - meta + - name: keywords + content: HTTP面试题,HTTP + - - meta + - name: description + content: HTTP常见知识点总结,让天下没有难背的八股文! +--- + ## 简介 访问baidu.com的过程: @@ -541,4 +556,4 @@ WebDAV 是一个可对 Web 服务器上的内容直接进行文件复制、编 主动攻击是攻击者通过直接访问 Web 应用,把攻击代码传入的攻击模式。主动攻击模式里具有代表性的攻击是 SQL 注入攻击和 OS 命令注入攻击。 -被动攻击是利用圈套策略执行攻击代码的攻击模式,攻击者不直接对目标 Web 应用访问发起攻击。被动攻击模式中具有代表性的攻击是跨站脚本攻击和跨站点请求伪造。 \ No newline at end of file +被动攻击是利用圈套策略执行攻击代码的攻击模式,攻击者不直接对目标 Web 应用访问发起攻击。被动攻击模式中具有代表性的攻击是跨站脚本攻击和跨站点请求伪造。 diff --git a/docs/database/es/1-es-architect.md b/docs/database/es/1-es-architect.md new file mode 100644 index 0000000..065404c --- /dev/null +++ b/docs/database/es/1-es-architect.md @@ -0,0 +1,68 @@ +--- +sidebar: heading +title: ES 的分布式架构原理 +category: 数据库 +tag: + - ES +head: + - - meta + - name: keywords + content: es面试题,非关系型数据库,elastic search面试题,es,ES 的分布式架构原理 + - - meta + - name: description + content: MongoDB常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## ES 的分布式架构原理 + +ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)? + +## 面试官心理分析 + +在搜索这块,lucene 是最流行的搜索库。几年前业内一般都问,你了解 lucene 吗?你知道倒排索引的原理吗?现在早已经 out 了,因为现在很多项目都是直接用基于 lucene 的分布式搜索引擎—— ElasticSearch,简称为 ES。 + +而现在分布式搜索基本已经成为大部分互联网行业的 Java 系统的标配,其中尤为流行的就是 ES,前几年 ES 没火的时候,大家一般用 solr。但是这两年基本大部分企业和项目都开始转向 ES 了。 + +所以互联网面试,肯定会跟你聊聊分布式搜索引擎,也就一定会聊聊 ES,如果你确实不知道,那你真的就 out 了。 + +如果面试官问你第一个问题,确实一般都会问你 ES 的分布式架构设计能介绍一下么?就看看你对分布式搜索引擎架构的一个基本理解。 + +## 面试题剖析 + +ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。 + +ES 中存储数据的**基本单位是索引**,比如说你现在要在 ES 中存储一些订单数据,你就应该在 ES 中创建一个索引 `order_idx` ,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。 + +``` +index -> type -> mapping -> document -> field +``` + +这样吧,为了做个更直白的介绍,我在这里做个类比。但是切记,不要划等号,类比只是为了便于理解。 + +index 相当于 mysql 里的一张表。而 type 没法跟 mysql 里去对比,一个 index 里可以有多个 type,每个 type 的字段都是差不多的,但是有一些略微的差别。假设有一个 index,是订单 index,里面专门是放订单数据的。就好比说你在 mysql 中建表,有些订单是实物商品的订单,比如一件衣服、一双鞋子;有些订单是虚拟商品的订单,比如游戏点卡,话费充值。就两种订单大部分字段是一样的,但是少部分字段可能有略微的一些差别。 + +所以就会在订单 index 里,建两个 type,一个是实物商品订单 type,一个是虚拟商品订单 type,这两个 type 大部分字段是一样的,少部分字段是不一样的。 + +很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况(**注意**, `mapping types` 这个概念在 ElasticSearch 7. X 已被完全移除,详细说明可以参考[官方文档](https://github.com/elastic/elasticsearch/blob/6.5/docs/reference/mapping/removal_of_types.asciidoc)),你可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个 type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的**表结构定义**,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。 + +![](http://img.topjavaer.cn/img/image-20221206001831784.png) + +你搞一个索引,这个索引可以拆分成多个 `shard` ,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是**支持横向扩展**,比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;二是**提高性能**,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。 + +接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 `primary shard` ,负责写入数据,但是还有几个 `replica shard` 。 `primary shard` 写入数据之后,会将数据同步到其他几个 `replica shard` 上去。 + +![](http://img.topjavaer.cn/img/image-20221206001848582.png) + +通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。 + +ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。 + +如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。 + +说得更简单一点,就是说如果某个非 master 节点宕机了。那么此节点上的 primary shard 不就没了。那好,master 会让 primary shard 对应的 replica shard(在其他机器上)切换为 primary shard。如果宕机的机器修复了,修复后的节点也不再是 primary shard,而是 replica shard。 + +其实上述就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。 + + + +> 来源:https://github.com/doocs/advanced-java diff --git a/docs/database/es/es-basic.md b/docs/database/es/es-basic.md new file mode 100644 index 0000000..96f2fd4 --- /dev/null +++ b/docs/database/es/es-basic.md @@ -0,0 +1,674 @@ +--- +sidebar: heading +title: ES常见知识点总结 +category: 数据库 +tag: + - ES +head: + - - meta + - name: keywords + content: ES常见知识点总结,非关系型数据库,elastic search面试题,es + - - meta + - name: description + content: ES常见知识点和面试题总结,让天下没有难背的八股文! +--- + +跟大家分享Elasticsearch的基础知识,它是做什么的以及它的使用和基本原理。 + +## 一、生活中的数据 + +搜索引擎是对数据的检索,所以我们先从生活中的数据说起。我们生活中的数据总体分为两种: + +- 结构化数据 +- 非结构化数据 + +**结构化数据:** 也称作行数据,是由二维表结构来逻辑表达和实现的数据,严格地遵循数据格式与长度规范,主要通过关系型数据库进行存储和管理。指具有固定格式或有限长度的数据,如数据库,元数据等。 + +**非结构化数据:** 又可称为全文数据,不定长或无固定格式,不适于由数据库二维表来表现,包括所有格式的办公文档、XML、HTML、Word 文档,邮件,各类报表、图片和咅频、视频信息等。 + +说明:如果要更细致的区分的话,XML、HTML 可划分为半结构化数据。因为它们也具有自己特定的标签格式,所以既可以根据需要按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。 + +根据两种数据分类,搜索也相应的分为两种: + +- 结构化数据搜索 +- 非结构化数据搜索 + +对于结构化数据,因为它们具有特定的结构,所以我们一般都是可以通过关系型数据库(MySQL,Oracle 等)的二维表(Table)的方式存储和搜索,也可以建立索引。 + +对于非结构化数据,也即对全文数据的搜索主要有两种方法: + +- 顺序扫描 +- 全文检索 + +**顺序扫描:** 通过文字名称也可了解到它的大概搜索方式,即按照顺序扫描的方式查询特定的关键字。 + +例如给你一张报纸,让你找到该报纸中“平安”的文字在哪些地方出现过。你肯定需要从头到尾把报纸阅读扫描一遍然后标记出关键字在哪些版块出现过以及它的出现位置。 + +这种方式无疑是最耗时的最低效的,如果报纸排版字体小,而且版块较多甚至有多份报纸,等你扫描完你的眼睛也差不多了。 + +**全文搜索:** 对非结构化数据顺序扫描很慢,我们是否可以进行优化?把我们的非结构化数据想办法弄得有一定结构不就行了吗? + +将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。 + +这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之为索引。 + +这种方式的主要工作量在前期索引的创建,但是对于后期搜索却是快速高效的。 + +## 二、先说说 Lucene + +通过对生活中数据的类型作了一个简短了解之后,我们知道关系型数据库的 SQL 检索是处理不了这种非结构化数据的。 + +这种非结构化数据的处理需要依赖全文搜索,而目前市场上开放源代码的最好全文检索引擎工具包就属于 Apache 的 Lucene了。 + +但是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。 + +目前以 Lucene 为基础建立的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。 + +Solr 和 Elasticsearch 都是比较成熟的全文搜索引擎,能完成的功能和性能也基本一样。 + +但是 ES 本身就具有分布式的特性和易安装使用的特点,而 Solr 的分布式需要借助第三方来实现,例如通过使用 ZooKeeper 来达到分布式协调管理。 + +不管是 Solr 还是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是因为它实现了倒排索引的查询结构。 + +如何理解倒排索引呢? 假如现有三份数据文档,文档的内容如下分别是: + +- Java is the best programming language. +- PHP is the best programming language. +- Javascript is the best programming language. + +为了创建倒排索引,我们通过分词器将每个文档的内容域拆分成单独的词(我们称它为词条或 Term),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。 + +结果如下所示: + +``` +Term Doc_1 Doc_2 Doc_3 +------------------------------------- +Java | X | | +is | X | X | X +the | X | X | X +best | X | X | X +programming | x | X | X +language | X | X | X +PHP | | X | +Javascript | | | X +------------------------------------- +``` + +这种结构由文档中所有不重复词的列表构成,对于其中每个词都有一个文档列表与之关联。 + +这种由属性值来确定记录的位置的结构就是倒排索引。带有倒排索引的文件我们称为倒排文件。 + +我们将上面的内容转换为图的形式来说明倒排索引的结构信息,如下图所示: + +![](http://img.topjavaer.cn/img/es1.png) + +其中主要有如下几个核心术语需要理解: + +- **词条(Term):** 索引里面最小的存储和查询单元,对于英文来说是一个单词,对于中文来说一般指分词后的一个词。 +- **词典(Term Dictionary):** 或字典,是词条 Term 的集合。搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。 +- **倒排表(Post list):** 一个文档通常由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。 +- **倒排文件(Inverted File):** 所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。 + +从上图我们可以了解到倒排索引主要由两个部分组成: + +- 词典 +- 倒排文件 + +词典和倒排表是 Lucene 中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排文件是分两部分存储的,词典在内存中而倒排文件存储在磁盘上。 + +## 三、ES 核心概念 + +一些基础知识的铺垫之后我们正式进入今天的主角 Elasticsearch 的介绍。 + +ES 是使用 Java 编写的一种开源搜索引擎,它在内部使用 Lucene 做索引与搜索,通过对 Lucene 的封装,隐藏了 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。 + +然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 + +它可以被下面这样准确的形容: + +- 一个分布式的实时文档存储,每个字段可以被索引与搜索。 +- 一个分布式实时分析搜索引擎。 +- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。 + +官网对 Elasticsearch 的介绍是 Elasticsearch 是一个分布式、可扩展、近实时的搜索与数据分析引擎。 + +我们通过一些核心概念来看下 Elasticsearch 是如何做到分布式,可扩展和近实时搜索的。 + +### 集群(Cluster) + +ES 的集群搭建很简单,不需要依赖第三方协调管理组件,自身内部就实现了集群的管理功能。 + +ES 集群由一个或多个 Elasticsearch 节点组成,每个节点配置相同的 cluster.name 即可加入集群,默认值为 “elasticsearch”。 + +确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。 + +一个 Elasticsearch 服务启动实例就是一个节点(Node)。节点通过 node.name 来设置节点名称,如果不设置则在启动时给节点分配一个随机通用唯一标识符作为名称。 + +#### ①发现机制 + +那么有一个问题,ES 内部是如何通过一个相同的设置 cluster.name 就能将不同的节点连接到同一个集群的?答案是 Zen Discovery。 + +Zen Discovery 是 Elasticsearch 的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举 Master 节点)。 + +它提供单播和基于文件的发现,并且可以扩展为通过插件支持云环境和其他形式的发现。 + +Zen Discovery 与其他模块集成,例如,节点之间的所有通信都使用 Transport 模块完成。节点使用发现机制通过 Ping 的方式查找其他节点。 + +Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。 + +如果集群的节点运行在不同的机器上,使用单播,你可以为 Elasticsearch 提供一些它应该去尝试连接的节点列表。 + +当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 Master 节点,并加入集群。 + +这意味着单播列表不需要包含集群中的所有节点, 它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了。 + +如果你使用 Master 候选节点作为单播列表,你只要列出三个就可以了。这个配置在 elasticsearch.yml 文件中: + +``` +discovery.zen.ping.unicast.hosts: ["host1", "host2:port"] +``` + +节点启动后先 Ping ,如果 `discovery.zen.ping.unicast.hosts` 有设置,则 Ping 设置中的 Host ,否则尝试 ping localhost 的几个端口。 + +Elasticsearch 支持同一个主机启动多个节点,Ping 的 Response 会包含该节点的基本信息以及该节点认为的 Master 节点。 + +选举开始,先从各节点认为的 Master 中选,规则很简单,按照 ID 的字典序排序,取第一个。如果各节点都没有认为的 Master ,则从所有节点中选择,规则同上。 + +这里有个限制条件就是 `discovery.zen.minimum_master_nodes` ,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。 + +最后选举结果是肯定能选举出一个 Master ,如果只有一个 Local 节点那就选出的是自己。 + +如果当前节点是 Master ,则开始等待节点数达到 `discovery.zen.minimum_master_nodes`,然后提供服务。 + +如果当前节点不是 Master ,则尝试加入 Master 。Elasticsearch 将以上服务发现以及选主的流程叫做 Zen Discovery 。 + +由于它支持任意数目的集群( 1- N ),所以不能像 Zookeeper 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则。 + +只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。 + +但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题。 + +大多数解决方案就是设置一个 Quorum 值,要求可用节点必须大于 Quorum(一般是超过半数节点),才能对外提供服务。 + +而 Elasticsearch 中,这个 Quorum 的配置就是 `discovery.zen.minimum_master_nodes` 。 + +#### ②节点的角色 + +每个节点既可以是候选主节点也可以是数据节点,通过在配置文件 ../config/elasticsearch.yml 中设置即可,默认都为 true。 + +``` +node.master: true //是否候选主节点 +node.data: true //是否数据节点 +``` + +数据节点负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(Data 节点)对机器配置要求比较高,对 CPU、内存和 I/O 的消耗很大。 + +通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。 + +候选主节点可以被选举为主节点(Master 节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。 + +主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。 + +![](http://img.topjavaer.cn/img/es2.png) + +一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对 CPU、内存核 I/O 消耗都很大。 + +所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响。 + +因此为了提高集群的健康性,我们应该对 Elasticsearch 集群中的节点做好角色上的划分和隔离。可以使用几个配置较低的机器群作为候选主节点群。 + +主节点和其他节点之间通过 Ping 的方式互检查,主节点负责 Ping 所有其他节点,判断是否有节点已经挂掉。其他节点也通过 Ping 的方式判断主节点是否处于可用状态。 + +虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发。 + +这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色。 + +#### ③脑裂现象 + +同时如果由于网络或其他原因导致集群中选举出多个 Master 节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不同的节点对于 Master 的选择出现了分歧,出现了多个 Master 竞争。 + +“脑裂”问题可能有以下几个原因造成: + +- **网络问题:** 集群间的网络延迟导致一些节点访问不到 Master,认为 Master 挂掉了从而选举出新的 Master,并对 Master 上的分片和副本标红,分配新的主分片。 +- **节点负载:** 主节点的角色既为 Master 又为 Data,访问量较大时可能会导致 ES 停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。 +- **内存回收:** 主节点的角色既为 Master 又为 Data,当 Data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。 + +为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施: + +- **适当调大响应时间,减少误判。** 通过参数 discovery.zen.ping_timeout 设置节点状态的响应时间,默认为 3s,可以适当调大。 + +如果 Master 在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,`discovery.zen.ping_timeout:6`),可适当减少误判。 + +- **选举触发。** 我们需要在候选集群中的节点的配置文件中设置参数 `discovery.zen.munimum_master_nodes` 的值。 + +这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是 1,官方建议取值(`master_eligibel_nodes2)+1`,其中 `master_eligibel_nodes` 为候选主节点的个数。 + +这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于 `discovery.zen.munimum_master_nodes` 个候选节点存活,选举工作就能正常进行。 + +当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。 + +- **角色分离。** 即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。 + +### 分片(Shards) + +ES 支持 PB 级全文搜索,当索引上的数据量太大的时候,ES 通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据库块称之为一个分片。 + +这类似于 MySQL 的分库分表,只不过 MySQL 分库分表需要借助第三方组件而 ES 内部自身实现了此功能。 + +在一个多分片的索引中写入数据时,通过路由来确定具体写入哪一个分片中,所以在创建索引的时候需要指定分片的数量,并且分片的数量一旦确定就不能修改。 + +分片的数量和下面介绍的副本数量都是可以通过创建索引时的 Settings 来配置,ES 默认为一个索引创建 5 个主分片, 并分别为每个分片创建一个副本。 + +``` +PUT /myIndex +{ + "settings" : { + "number_of_shards" : 5, + "number_of_replicas" : 1 + } +} +``` + +ES 通过分片的功能使得索引在规模上和性能上都得到提升,每个分片都是 Lucene 中的一个索引文件,每个分片必须有一个主分片和零到多个副本。 + +### 副本(Replicas) + +副本就是对分片的 Copy,每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作。 + +主分片和对应的副本分片是不会在同一个节点上的,所以副本分片数的最大值是 N-1(其中 N 为节点数)。 + +对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。 + +ES 为了提高写入的能力这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES 通过乐观锁的方式控制,每个文档都有一个` _version` (版本)号,当文档被修改时版本号递增。 + +一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。 + +![](http://img.topjavaer.cn/img/es3.png) + +从上图可以看出为了达到高可用,Master 节点会避免将主分片和副本分片放在同一个节点上。 + +假设这时节点 Node1 服务宕机了或者网络不可用了,那么主节点上主分片 S0 也就不可用了。 + +幸运的是还存在另外两个节点能正常工作,这时 ES 会重新选举新的主节点,而且这两个节点上存在我们所需要的 S0 的所有数据。 + +我们会将 S0 的副本分片提升为主分片,这个提升主分片的过程是瞬间发生的。此时集群的状态将会为 Yellow。 + +为什么我们集群状态是 Yellow 而不是 Green 呢?虽然我们拥有所有的 2 个主分片,但是同时设置了每个主分片需要对应两份副本分片,而此时只存在一份副本分片。所以集群不能为 Green 的状态。 + +如果我们同样关闭了 Node2 ,我们的程序依然可以保持在不丢失任何数据的情况下运行,因为 Node3 为每一个分片都保留着一份副本。 + +如果我们重新启动 Node1 ,集群可以将缺失的副本分片再次进行分配,那么集群的状态又将恢复到原来的正常状态。 + +如果 Node1 依然拥有着之前的分片,它将尝试去重用它们,只不过这时 Node1 节点上的分片不再是主分片而是副本分片了,如果期间有更改的数据只需要从主分片上复制修改的数据文件即可。 + +#### 小结: + +- 将数据分片是为了提高可处理数据的容量和易于进行水平扩展,为分片做副本是为了提高集群的稳定性和提高并发量。 +- 副本是乘法,越多消耗越大,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。 +- 副本越多,集群的可用性就越高,但是由于每个分片都相当于一个 Lucene 的索引文件,会占用一定的文件句柄、内存及 CPU。并且分片间的数据同步也会占用一定的网络带宽,所以索引的分片数和副本数也不是越多越好。 + +### 映射(Mapping) + +映射是用于定义 ES 对索引中字段的存储类型、分词方式和是否存储等信息,就像数据库中的 Schema ,描述了文档可能具有的字段或属性、每个字段的数据类型。 + +只不过关系型数据库建表时必须指定字段类型,而 ES 对于字段类型可以不指定然后动态对字段类型猜测,也可以在创建索引时具体指定字段的类型。 + +对字段类型根据数据格式自动识别的映射称之为动态映射(Dynamic Mapping),我们创建索引时具体定义字段类型的映射称之为静态映射或显示映射(Explicit Mapping)。 + +在讲解动态映射和静态映射的使用前,我们先来了解下 ES 中的数据有哪些字段类型?之后我们再讲解为什么我们创建索引时需要建立静态映射而不使用动态映射。 + +ES(v6.8)中字段数据类型主要有以下几类: + +![](http://img.topjavaer.cn/img/es4.png) + +Text 用于索引全文值的字段,例如电子邮件正文或产品说明。这些字段是被分词的,它们通过分词器传递 ,以在被索引之前将字符串转换为单个术语的列表。 + +分析过程允许 Elasticsearch 搜索单个单词中每个完整的文本字段。文本字段不用于排序,很少用于聚合。 + +Keyword 用于索引结构化内容的字段,例如电子邮件地址,主机名,状态代码,邮政编码或标签。它们通常用于过滤,排序,和聚合。Keyword 字段只能按其确切值进行搜索。 + +通过对字段类型的了解我们知道有些字段需要明确定义的,例如某个字段是 Text 类型还是 Keyword 类型差别是很大的,时间字段也许我们需要指定它的时间格式,还有一些字段我们需要指定特定的分词器等等。 + +如果采用动态映射是不能精确做到这些的,自动识别常常会与我们期望的有些差异。 + +所以创建索引的时候一个完整的格式应该是指定分片和副本数以及 Mapping 的定义,如下: + +``` +PUT my_index +{ + "settings" : { + "number_of_shards" : 5, + "number_of_replicas" : 1 + } + "mappings": { + "_doc": { + "properties": { + "title": { "type": "text" }, + "name": { "type": "text" }, + "age": { "type": "integer" }, + "created": { + "type": "date", + "format": "strict_date_optional_time||epoch_millis" + } + } + } + } +} +``` + +## 四、ES 的基本使用 + +在决定使用 Elasticsearch 的时候首先要考虑的是版本问题,Elasticsearch (排除 0.x 和 1.x)目前有如下常用的稳定的主版本:2.x,5.x,6.x,7.x(current)。 + +你可能会发现没有 3.x 和 4.x,ES 从 2.4.6 直接跳到了 5.0.0。其实是为了 ELK(ElasticSearch,Logstash,Kibana)技术栈的版本统一,免的给用户带来混乱。 + +在 Elasticsearch 是 2.x (2.x 的最后一版 2.4.6 的发布时间是 July 25, 2017) 的情况下,Kibana 已经是 4.x(Kibana 4.6.5 的发布时间是 July 25, 2017)。 + +那么在 Kibana 的下一主版本肯定是 5.x 了,所以 Elasticsearch 直接将自己的主版本发布为 5.0.0 了。 + +统一之后,我们选版本就不会犹豫困惑了,我们选定 Elasticsearch 的版本后再选择相同版本的 Kibana 就行了,不用担忧版本不兼容的问题。 + +Elasticsearch 是使用 Java 构建,所以除了注意 ELK 技术的版本统一,我们在选择 Elasticsearch 的版本的时候还需要注意 JDK 的版本。 + +因为每个大版本所依赖的 JDK 版本也不同,目前 7.2 版本已经可以支持 JDK11。 + +### 安装使用 + +①下载和解压 Elasticsearch,无需安装解压后即可用,解压后目录: + +- `bin`:二进制系统指令目录,包含启动命令和安装插件命令等。 +- `config`:配置文件目录。 +- `data`:数据存储目录。 +- `lib`:依赖包目录。 +- `logs`:日志文件目录。 +- `modules`:模块库,例如 x-pack 的模块。 +- `plugins`:插件目录。 + +②安装目录下运行 `bin/elasticsearch` 来启动 ES。 + +③默认在 9200 端口运行,请求 curl http://localhost:9200/ 或者浏览器输入 http://localhost:9200,得到一个 JSON 对象,其中包含当前节点、集群、版本等信息。 + +``` +{ + "name" : "U7fp3O9", + "cluster_name" : "elasticsearch", + "cluster_uuid" : "-Rj8jGQvRIelGd9ckicUOA", + "version" : { + "number" : "6.8.1", + "build_flavor" : "default", + "build_type" : "zip", + "build_hash" : "1fad4e1", + "build_date" : "2019-06-18T13:16:52.517138Z", + "build_snapshot" : false, + "lucene_version" : "7.7.0", + "minimum_wire_compatibility_version" : "5.6.0", + "minimum_index_compatibility_version" : "5.0.0" + }, + "tagline" : "You Know, for Search" +} +``` + +### 集群健康状态 + +要检查群集运行状况,我们可以在 Kibana 控制台中运行以下命令 GET /_cluster/health,得到如下信息: + +``` +{ + "cluster_name" : "wujiajian", + "status" : "yellow", + "timed_out" : false, + "number_of_nodes" : 1, + "number_of_data_nodes" : 1, + "active_primary_shards" : 9, + "active_shards" : 9, + "relocating_shards" : 0, + "initializing_shards" : 0, + "unassigned_shards" : 5, + "delayed_unassigned_shards" : 0, + "number_of_pending_tasks" : 0, + "number_of_in_flight_fetch" : 0, + "task_max_waiting_in_queue_millis" : 0, + "active_shards_percent_as_number" : 64.28571428571429 +} +``` + +集群状态通过 绿,黄,红 来标识: + +- 绿色:集群健康完好,一切功能齐全正常,所有分片和副本都可以正常工作。 +- 黄色:预警状态,所有主分片功能正常,但至少有一个副本是不能正常工作的。此时集群是可以正常工作的,但是高可用性在某种程度上会受影响。 +- 红色:集群不可正常使用。某个或某些分片及其副本异常不可用,这时集群的查询操作还能执行,但是返回的结果会不准确。对于分配到这个分片的写入请求将会报错,最终会导致数据的丢失。 + +当集群状态为红色时,它将会继续从可用的分片提供搜索请求服务,但是你需要尽快修复那些未分配的分片。 + +## 五、ES 机制原理 + +ES 的基本概念和基本操作介绍完了之后,我们可能还有很多疑惑: + +- 它们内部是如何运行的? +- 主分片和副本分片是如何同步的? +- 创建索引的流程是什么样的? +- ES 如何将索引数据分配到不同的分片上的?以及这些索引数据是如何存储的? +- 为什么说 ES 是近实时搜索引擎而文档的 CRUD (创建-读取-更新-删除) 操作是实时的? +- 以及 Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据? +- 还有为什么删除文档不会立刻释放空间? + +带着这些疑问我们进入接下来的内容。 + +### 写索引原理 + +下图描述了 3 个节点的集群,共拥有 12 个分片,其中有 4 个主分片(S0、S1、S2、S3)和 8 个副本分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点 1 是主节点(Master 节点)负责整个集群的状态。 + +![](http://img.topjavaer.cn/img/es5.png) + +写索引是只能写在主分片上,然后同步到副本分片。这里有四个主分片,一条数据 ES 是根据什么规则写到特定分片上的呢? + +这条索引数据为什么被写到 S0 上而不写到 S1 或 S2 上?那条数据为什么又被写到 S3 上而不写到 S0 上了? + +首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。 + +实际上,这个过程是根据下面这个公式决定的: + +``` +shard = hash(routing) % number_of_primary_shards +``` + +Routing 是一个可变值,默认是文档的` _id` ,也可以设置成一个自定义的值。 + +Routing 通过 Hash 函数生成一个数字,然后这个数字再除以 `number_of_primary_shards` (主分片的数量)后得到余数。 + +这个在 0 到 `number_of_primary_shards-1` 之间的余数,就是我们所寻求的文档所在分片的位置。 + +这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。 + +由于在 ES 集群中每个节点通过上面的计算公式都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。 + +在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。 + +![](http://img.topjavaer.cn/img/es6.png) + +假如此时数据通过路由计算公式取余后得到的值是 `shard=hash(routing)%4=0`。 + +则具体流程如下: + +- 客户端向 ES1 节点(协调节点)发送写请求,通过路由计算公式得到值为 0,则当前数据应被写到主分片 S0 上。 +- ES1 节点将请求转发到 S0 主分片所在的节点 ES3,ES3 接受请求并写入到磁盘。 +- 并发将数据复制到两个副本分片 R0 上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点 ES3 将向协调节点报告成功,协调节点向客户端报告成功。 + +### 存储原理 + +上面介绍了在 ES 内部索引的写处理流程,这个流程是在 ES 的内存中执行的,数据被分配到特定的分片和副本上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。 + +具体的存储路径可在配置文件 `../config/elasticsearch.yml `中进行设置,默认存储在安装目录的 Data 文件夹下。 + +建议不要使用默认值,因为若 ES 进行了升级,则有可能导致数据全部丢失: + +``` +path.data: /path/to/data //索引数据 +path.logs: /path/to/logs //日志记录 +``` + +#### ①分段存储 + +索引文档以段的形式存储在磁盘上,何为段?索引文件被拆分为多个子文件,则每个子文件叫作段,每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。 + +在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。 + +段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。 + +一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。 + +段的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。 + +如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。 + +索引文件分段存储并且不可修改,那么新增、更新和删除如何处理呢? + +- 新增,新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段就可以了。 +- 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除而是通过新增一个 .del 文件,文件中会列出这些被删除文档的段信息。这个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。 +- 更新,不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在 .del 文件中标记删除,然后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。 + +段被设定为不可修改具有一定的优势也有一定的缺点,优势主要表现在: + +- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。 +- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。 +- 其它缓存(像 Filter 缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。 +- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和需要被缓存到内存的索引的使用量。 + +段的不变性的缺点如下: + +- 当对旧数据进行删除时,旧数据不会马上被删除,而是在 .del 文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。 +- 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。 +- 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。 +- 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。 + +#### ②延迟写策略 + +介绍完了存储的形式,那么索引写入到磁盘的过程是怎样的?是否是直接调 Fsync 物理性地写入磁盘? + +答案是显而易见的,如果是直接写入到磁盘上,磁盘的 I/O 消耗上会严重影响性能。 + +那么当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。如果真是这样 ES 也就不会称之为近实时全文搜索引擎了。 + +为了提升写的性能,ES 并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。 + +每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存。 + +当达到默认的时间(1 秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点。 + +这里的内存使用的是 ES 的 JVM 内存,而文件缓存系统使用的是操作系统的内存。 + +新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。 + +由内存刷新到文件缓存系统的时候会生成新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。 + +在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 Refresh (即内存刷新到文件缓存系统)。 + +默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是近实时搜索,因为文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。 + +我们也可以手动触发 Refresh,`POST /_refresh` 刷新所有索引,`POST /nba/_refresh` 刷新指定的索引。 + +Tips:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产>环境下每次索引一个文档都去手动刷新。而且并不是所有的情况都需要每秒刷新。 + +可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是>近实时搜索。 + +这时可以在创建索引时在 Settings 中通过调大 `refresh_interval = "30s" `的值 , 降低每个索引的刷新频率,设值时需要注意后面带上时间单位,否则默认是毫秒。当 `refresh_interval=-1` 时表示关闭索引的自动刷新。 + +虽然通过延时写的策略可以减少数据往磁盘上写的次数提升了整体的写入能力,但是我们知道文件缓存系统也是内存空间,属于操作系统的内存,只要是内存都存在断电或异常情况下丢失数据的危险。 + +为了避免丢失数据,Elasticsearch 添加了事务日志(Translog),事务日志记录了所有还没有持久化到磁盘的数据。 + +![](http://img.topjavaer.cn/img/es7.png)图片 + +添加了事务日志后整个写索引的流程如上图所示: + +- 一个新文档被索引之后,先被写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志中。 + + 不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。 + +- 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 Refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,但是可以提供文档的检索功能且不能被修改。 + +- 随着新文档索引不断被写入,当日志数据大小超过 512M 或者时间超过 30 分钟时,会触发一次 Flush。 + + 内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过 Fsync 刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。 + +通过这种方式当断电或需要重启时,ES 不仅要根据提交点去加载已经持久化过的段,还需要工具 Translog 里的记录,把未持久化的数据重新持久化到磁盘上,避免了数据丢失的可能。 + +③段合并 + +由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 + +每一个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。 + +Elasticsearch 通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。 + +段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。 + +![](http://img.topjavaer.cn/img/es8.png) + +段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。 + +合并结束后老的段会被删除,新的段被 Flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。 + +段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。 + +Elasticsearch 在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。 + +## 六、ES 的性能优化 + +### 存储设备 + +磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。 + +这里有一些优化磁盘 I/O 的技巧: + +- 使用 SSD。就像其他地方提过的, 他们比机械磁盘优秀多了。 +- 使用 RAID 0。条带化 RAID 会提高磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。 +- 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面。 +- 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。 +- 如果你用的是 EC2,当心 EBS。即便是基于 SSD 的 EBS,通常也比本地实例的存储要慢。 + +### 内部索引优化 + +![](http://img.topjavaer.cn/img/es9.png) + +Elasticsearch 为了能快速找到某个 Term,先将所有的 Term 排个序,然后根据二分法查找 Term,时间复杂度为 logN,就像通过字典查找一样,这就是 Term Dictionary。 + +现在再看起来,似乎和传统数据库通过 B-Tree 的方式类似。但是如果 Term 太多,Term Dictionary 也会很大,放内存不现实,于是有了 Term Index。 + +就像字典里的索引页一样,A 开头的有哪些 Term,分别在哪页,可以理解 Term Index是一棵树。 + +这棵树不会包含所有的 Term,它包含的是 Term 的一些前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个 Offset,然后从这个位置再往后顺序查找。 + +在内存中用 FST 方式压缩 Term Index,FST 以字节的方式存储所有的 Term,这种压缩方式可以有效的缩减存储空间,使得 Term Index 足以放进内存,但这种方式也会导致查找时需要更多的 CPU 资源。 + +对于存储在磁盘上的倒排表同样也采用了压缩技术减少存储所占用的空间。 + +### 调整配置参数 + +调整配置参数建议如下: + +- 给每个文档指定有序的具有压缩良好的序列模式 ID,避免随机的 UUID-4 这样的 ID,这样的 ID 压缩比很低,会明显拖慢 Lucene。 + +- 对于那些不需要聚合和排序的索引字段禁用 Doc values。Doc Values 是有序的基于 `document=>field value` 的映射列表。 + +- 不需要做模糊检索的字段使用 Keyword 类型代替 Text 类型,这样可以避免在建立索引前对这些文本进行分词。 + +- 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 `index.refresh_interval` 改到 30s 。 + + 如果你是在做大批量导入,导入期间你可以通过设置这个值为 -1 关掉刷新,还可以通过设置 `index.number_of_replicas: 0` 关闭副本。别忘记在完工的时候重新开启它。 + +- 避免深度分页查询建议使用 Scroll 进行分页查询。普通分页查询时,会创建一个 `from+size` 的空优先队列,每个分片会返回 `from+size` 条数据,默认只包含文档 ID 和得分 Score 给协调节点。 + + 如果有 N 个分片,则协调节点再对(from+size)×n 条数据进行二次排序,然后选择需要被取回的文档。当 from 很大时,排序过程会变得很沉重,占用 CPU 资源严重。 + +- 减少映射字段,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如 Hbase,在 ES 中得到结果后再去 Hbase 查询这些字段。 + +- 创建索引和查询时指定路由 Routing 值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。 + +### JVM 调优 + +JVM 调优建议如下: + +- 确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。Elasticsearch 默认安装后设置的堆内存是 1GB。可通过` ../config/jvm.option` 文件进行配置,但是最好不要超过物理内存的50%和超过 32GB。 +- GC 默认采用 CMS 的方式,并发但是有 STW 的问题,可以考虑使用 G1 收集器。 +- ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。 diff --git a/docs/database/mongodb.md b/docs/database/mongodb.md new file mode 100644 index 0000000..78cab53 --- /dev/null +++ b/docs/database/mongodb.md @@ -0,0 +1,308 @@ +--- +sidebar: heading +title: MongoDB常见面试题总结 +category: 数据库 +tag: + - MongoDB +head: + - - meta + - name: keywords + content: MongoDB面试题,非关系型数据库,MongoDB特点,MongoDB使用场景,MongoDB优势,MongoDB + - - meta + - name: description + content: MongoDB常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## mongodb是什么? + +MongoDB 是由 C++语言编写的,是一个基于分布式文件存储的开源数据库系统。 再高负载的情况下,添加更多的节点,可以保证服务器性能。 MongoDB 旨在给 WEB 应用提供可扩展的高性能数据存储解决方案。 + +MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。 MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 + +## mongodb有哪些特点? + +(1)MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。 + +(2)你可以在 MongoDB 记录中设置任何属性的索引 (如: FirstName="Sameer",Address="8 Gandhi Road")来实现更快的排序。 + +(3)你可以通过本地或者网络创建数据镜像,这使得 MongoDB 有更强的扩展性。 + +(4)如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其他节点上这就是所谓的分片。 + +(5)Mongo 支持丰富的查询表达式。查询指令使用 JSON 形式的标记,可轻易查询文档中内嵌的对象及数组。 + +(6)MongoDb 使用 update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。 + +(7)Mongodb 中的 Map/reduce 主要是用来对数据进行批量处理和聚合操作。 + +(8)Map 和 Reduce。 Map 函数调用 emit(key,value)遍历集合中所有的记录,将 key 与 value 传给 Reduce 函数进行处理。 + +(9)Map 函数和 Reduce 函数是使用 Javascript 编写的,并可以通过 db.runCommand 或 mapreduce 命令来执行 MapReduce 操作。 + +(10)GridFS 是 MongoDB 中的一个内置功能,可以用于存放大量小文件。 + +(11) MongoDB 允许在服务端执行脚本, 可以用 Javascript 编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。 + +## 什么是非关系型数据库 + +非关系型数据库是对不同于传统关系型数据库的统称。非关系型数据库的显著特点是不使用SQL作为查询语言,数据存储不需要特定的表格模式。由于简单的设计和非常好的性能所以被用于大数据和Web Apps等 + +## 为什么用MongoDB? + +- 架构简单 +- 没有复杂的连接 +- 深度查询能力,MongoDB支持动态查询。 +- 容易调试 +- 容易扩展 +- 不需要转化/映射应用对象到数据库对象 +- 使用内部内存作为存储工作区,以便更快的存取数据。 + +## 在哪些场景使用MongoDB + +- 大数据 +- 内容管理系统 +- 移动端Apps +- 数据管理 + +## MySQL与MongoDB之间最基本的差别是什么? + +MySQL和MongoDB两者都是免费开源的数据库。MySQL和MongoDB有许多基本差别包括数据的表示(data representation),查询,关系,事务,schema的设计和定义,标准化(normalization),速度和性能。 + +通过比较MySQL和MongoDB,实际上我们是在比较关系型和非关系型数据库,即数据存储结构不同。 + +## MongoDB成为最好NoSQL数据库的原因是什么? + +以下特点使得MongoDB成为最好的NoSQL数据库: + +- 面向文件的 +- 高性能 +- 高可用性 +- 易扩展性 +- 丰富的查询语言 + +## journal回放在条目(entry)不完整时(比如恰巧有一个中途故障了)会遇到问题吗? + +每个journal (group)的写操作都是一致的,除非它是完整的否则在恢复过程中它不会回放。 + +## 分析器在MongoDB中的作用是什么? + +MongoDB中包括了一个可以显示数据库中每个操作性能特点的数据库分析器。通过这个分析器你可以找到比预期慢的查询(或写操作);利用这一信息,比如,可以确定是否需要添加索引。 + +## 名字空间(namespace)是什么? + +MongoDB存储BSON对象在丛集(collection)中。数据库名字和丛集名字以句点连结起来叫做名字空间(namespace)。 + +## 允许空值null吗? + +对于对象成员而言,是的。然而用户不能够添加空值(null)到数据库丛集(collection)因为空值不是对象。然而用户能够添加空对象{}。 + + + +## 更新操作立刻fsync到磁盘? + +不会,磁盘写操作默认是延迟执行的。写操作可能在两三秒(默认在60秒内)后到达磁盘。例如,如果一秒内数据库收到一千个对一个对象递增的操作,仅刷新磁盘一次。(注意,尽管fsync选项在命令行和经过getLastError_old是有效的) + +## 如何执行事务/加锁? + +MongoDB没有使用传统的锁或者复杂的带回滚的事务,因为它设计的宗旨是轻量,快速以及可预计的高性能。可以把它类比成MySQLMylSAM的自动提交模式。通过精简对事务的支持,性能得到了提升,特别是在一个可能会穿过多个服务器的系统里。 + +## 启用备份故障恢复需要多久? + +从备份数据库声明主数据库宕机到选出一个备份数据库作为新的主数据库将花费10到30秒时间。这期间在主数据库上的操作将会失败--包括 + +写入和强一致性读取(strong consistent read)操作。然而,你还能在第二数据库上执行最终一致性查询(eventually consistent query)(在slaveOk模式下),即使在这段时间里。 + +## 什么是master或primary? + +它是当前备份集群(replica set)中负责处理所有写入操作的主要节点/成员。在一个备份集群中,当失效备援(failover)事件发生时,一个另外的成员会变成primary。 + +## 什么是secondary或slave? + +Seconday从当前的primary上复制相应的操作。它是通过跟踪复制oplog(local.oplog.rs)做到的。 + +## 应该启动一个集群分片(sharded)还是一个非集群分片的 MongoDB 环境? + +为开发便捷起见,我们建议以非集群分片(unsharded)方式开始一个 MongoDB 环境,除非一台服务器不足以存放你的初始数据集。从非集群分片升级到集群分片(sharding)是无缝的,所以在你的数据集还不是很大的时候没必要考虑集群分片(sharding)。 + +## 分片(sharding)和复制(replication)是怎样工作的? + +每一个分片(shard)是一个分区数据的逻辑集合。分片可能由单一服务器或者集群组成,我们推荐为每一个分片(shard)使用集群。 + +## 数据在什么时候才会扩展到多个分片(shard)里? + +MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块(chunk)中。只有当存在多余一个块的时后,才会有多个分片获取数据的选项。现在,每个默认块的大小是 64Mb,所以你需要至少 64 Mb 空间才可以实施一个迁移。 + +## 如果在一个分片(shard)停止或者很慢的时候,发起一个查询会怎样? + +如果一个分片(shard)停止了,除非查询设置了“Partial”选项,否则查询会返回一个错误。如果一个分片(shard)响应很慢,MongoDB则会等待它的响应。 + +## 当更新一个正在被迁移的块(Chunk)上的文档时会发生什么? + +更新操作会立即发生在旧的块(Chunk)上,然后更改才会在所有权转移前复制到新的分片上。 + +## MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗? + +不会,只会在A:{B,C}上使用索引。 + +## 如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样? + +如果一个分片停止了,除非查询设置了“Partial”选项,否则查询会返回一个错误。如果一个分片响应很慢,MongoDB会等待它的响应。 + +## MongoDB支持存储过程吗?如果支持的话,怎么用? + +MongoDB支持存储过程,它是javascript写的,保存在db.system.js表中。 + +## 如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件? + +GridFS是一种将大型文件存储在MongoDB中的文件规范。使用GridFS可以将大文件分隔成多个小文档存放,这样我们能够有效的保存大文档,而且解决了BSON对象有限制的问题。 + +## mongodb的数据结构 + +数据库中存储的对象设计bson,一种类似json的二进制文件,由键值对组成。 + +## MongoDB的优势有哪些 + +- 面向文档的存储:以 JSON 格式的文档保存数据。 +- 任何属性都可以建立索引。 +- 复制以及高可扩展性。 +- 自动分片。 +- 丰富的查询功能。 +- 快速的即时更新。 +- 来自 MongoDB 的专业支持。 + +## 什么是集合 + +集合就是一组 MongoDB 文档。它相当于关系型数据库(RDBMS)中的表这种概念。集合位于单独的一个数据库中。一个集合内的多个文档可以有多个不同的字段。一般来说,集合中的文档都有着相同或相关的目的。 + +## 什么是文档 + +文档由一组key value组成。文档是动态模式,这意味着同一集合里的文档不需要有相同的字段和结构。在关系型数据库中table中的每一条记录相当于MongoDB中的一个文档。 + +## 什么是”mongod“ + +mongod是处理MongoDB系统的主要进程。它处理数据请求,管理数据存储,和执行后台管理操作。当我们运行mongod命令意味着正在启动MongoDB进程,并且在后台运行。 + +## "mongod"参数有什么 + +- 传递数据库存储路径,默认是"/data/db" +- 端口号 默认是 "27017" + +## 什么是"mongo" + +它是一个命令行工具用于连接一个特定的mongod实例。当我们没有带参数运行mongo命令它将使用默认的端口号和localhost连接 + +## MongoDB哪个命令可以切换数据库 + +MongoDB 用 use +数据库名称的方式来创建数据库。 use 会创建一个新的数据库,如果该数据库存在,则返回这个数据库。 + +## MongoDB中的命名空间是什么意思? + +MongoDB内部有预分配空间的机制,每个预分配的文件都用0进行填充。 + +数据文件每新分配一次,它的大小都是上一个数据文件大小的2倍,每个数据文件最大2G。 + +MongoDB每个集合和每个索引都对应一个命名空间,这些命名空间的元数据集中在16M的*.ns文件中,平均每个命名占用约 628 字节,也即整个数据库的命名空间的上限约为24000。 + +如果每个集合有一个索引(比如默认的_id索引),那么最多可以创建12000个集合。如果索引数更多,则可创建的集合数就更少了。同时,如果集合数太多,一些操作也会变慢。 + +要建立更多的集合的话,MongoDB 也是支持的,只需要在启动时加上“--nssize”参数,这样对应数据库的命名空间文件就可以变得更大以便保存更多的命名。这个命名空间文件(.ns文件)最大可以为 2G。 + +每个命名空间对应的盘区不一定是连续的。与数据文件增长相同,每个命名空间对应的盘区大小都是随分配次数不断增长的。目的是为了平衡命名空间浪费的空间与保持一个命名空间数据的连续性。 + +需要注意的一个命名空间$freelist,这个命名空间用于记录不再使用的盘区(被删除的Collection或索引)。每当命名空间需要分配新盘区时,会先查看$freelist是否有大小合适的盘区可以使用,如果有就回收空闲的磁盘空间。 + +## 在MongoDB中如何创建一个新的数据库 + +MongoDB 用 use + 数据库名称 的方式来创建数据库。 use 会创建一个新的数据库,如果该数据库存在,则返回这个数据库。 + +## MongoDB中的分片是什么意思 + +分片是将数据水平切分到不同的物理节点。当应用数据越来越大的时候,数据量也会越来越大。当数据量增长时,单台机器有可能无法存储数据或可接受的读取写入吞吐量。利用分片技术可以添加更多的机器来应对数据量增加以及读写操作的要求。 + +## 什么是复制 + +复制是将数据同步到多个服务器的过程,通过多个数据副本存储到多个服务器上增加数据可用性。复制可以保障数据的安全性,灾难恢复,无需停机维护(如备份,重建索引,压缩),分布式读取数据。 + +## 在MongoDB中如何在集合中插入一个文档 + +要想将数据插入 MongoDB 集合中,需要使用 insert() 或 save() 方法。 + +```stylus +>db.collectionName.insert({"key":"value"}) +>db.collectionName.save({"key":"value"}) +``` + +## 为什么要在MongoDB中使用分析器 + +数据库分析工具(Database Profiler)会针对正在运行的mongod实例收集数据库命令执行的相关信息。包括增删改查的命令以及配置和管理命令。分析器(profiler)会写入所有收集的数据到 system.profile集合,一个capped集合在管理员数据库。分析器默认是关闭的你能通过per数据库或per实例开启。 + +## MongoDB支持主键外键关系吗 + +默认MongoDB不支持主键和外键关系。 用Mongodb本身的API需要硬编码才能实现外键关联,不够直观且难度较大。 + +## MongoDB支持哪些数据类型 + +String、Integer、Double、Boolean、Object、Object ID、Arrays、Min/Max Keys、Datetime、Code、Regular Expression等 + +## 86、"ObjectID"由哪些部分组成 + +一共有四部分组成:时间戳、客户端ID、客户进程ID、三个字节的增量计数器 + +_id是一个 12 字节长的十六进制数,它保证了每一个文档的唯一性。在插入文档时,需要提供 _id 。如果你不提供,那么 MongoDB 就会为每一文档提供一个唯一的 id。 _id 的头 4 个字节代表的是当前的时间戳,接着的后 3 个字节表示的是机器 id 号,接着的 2 个字节表示MongoDB 服务器进程 id,最后的 3 个字节代表递增值。 + +## MongoDb索引 + +索引用于高效的执行查询。没有索引MongoDB将扫描查询整个集合中的所有文档这种扫描效率很低,需要处理大量数据。索引是一种特殊的数据结构,将一小块数据集保存为容易遍历的形式。索引能够存储某种特殊字段或字段集的值,并按照索引指定的方式将字段值进行排序。 + +## 如何添加索引 + +使用 db.collection.createIndex() 在集合中创建一个索引 + +```reasonml +>db.collectionName.createIndex({columnName:1}) +``` + +## 在MongoDB中如何更新数据 + +update() 与 save() 方法都能用于更新集合中的文档。 update() 方法更新已有文档中的值,而 save() 方法则是用传入该方法的文档来替换已有文档。 + +## 如何删除文档 + +MongoDB 利用 remove() 方法 清除集合中的文档。它有 2 个可选参数: + +- deletion criteria:(可选)删除文档的标准。 +- justOne:(可选)如果设为 true 或 1,则只删除一个文档。 + +```maxima +>db.collectionName.remove({key:value}) +``` + +## 在MongoDB中如何排序 + +MongoDB 中的文档排序是通过 sort() 方法来实现的。 sort() 方法可以通过一些参数来指定要进行排序的字段,并使用 1 和 -1 来指定排 + +序方式,其中 1 表示升序,而 -1 表示降序。 + +```stylus +>db.connectionName.find({key:value}).sort({columnName:1}) +``` + +## 什么是聚合 + +聚合操作能够处理数据记录并返回计算结果。聚合操作能将多个文档中的值组合起来,对成组数据执行各种操作,返回单一的结果。它相当于 SQL 中的 count(*) 组合 group by。对于 MongoDB 中的聚合操作,应该使用 aggregate() 方法。 + +```stylus +>db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION) +``` + +## 在MongoDB中什么是副本集 + +在MongoDB中副本集由一组MongoDB实例组成,包括一个主节点多个次节点,MongoDB客户端的所有数据都写入主节点(Primary),副节点从主节点同步写入数据,以保持所有复制集内存储相同的数据,提高数据可用性。 + diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL\345\237\272\347\241\200.md" b/docs/database/mysql-basic-all.md similarity index 93% rename from "\346\225\260\346\215\256\345\272\223/MySQL\345\237\272\347\241\200.md" rename to docs/database/mysql-basic-all.md index 8c66844..2eb6b8f 100644 --- "a/\346\225\260\346\215\256\345\272\223/MySQL\345\237\272\347\241\200.md" +++ b/docs/database/mysql-basic-all.md @@ -1,8 +1,6 @@ -## 简介 +# 简介 -SQL 结构化查询语言。 - -### 数据类型 +## 数据类型 主要包括以下五大类: @@ -16,7 +14,7 @@ SQL 结构化查询语言。 其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等 -#### 整型 +# 整型 | MySQL数据类型 | 含义(有符号) | | ------------- | ------------------------------------ | @@ -28,7 +26,7 @@ SQL 结构化查询语言。 int(m) m 用于指定显示宽度,int(5)表示5位数的宽度。注意显示宽度属性不能控制列可以存储的值范围,显示宽度属性通常由应用程序用于格式化整数值。 -#### 浮点型 +### 浮点型 | MySQL数据类型 | 含义 | | ------------- | ------------------------------------- | @@ -39,13 +37,13 @@ int(m) m 用于指定显示宽度,int(5)表示5位数的宽度。注意显示 不论是定点还是浮点类型,如果用户指定的精度超出精度范围,则会四舍五入进行处理。 -#### 定点数 +### 定点数 浮点型在数据库中存放的是近似值,而定点类型在数据库中存放的是精确值,不会丢失精度。定点数以字符串形式存储。 decimal(m,d) 参数m是总个数,d是小数位。 -#### 字符串 +### 字符串 | MySQL数据类型 | 含义 | | ------------- | ------------------------------- | @@ -66,13 +64,13 @@ nvarchar(存储的是Unicode数据类型的字符)不管是一个字符还 text:不需要指定存储长度,能用varchar就不用text。 -#### 二进制数据(BLOB) +### 二进制数据(BLOB) 二进制数据类型可存储任何数据,如图像、多媒体、文档等。 BLOB和TEXT存储方式不同,TEXT以文本方式存储,英文存储区分大小写;而Blob是以二进制方式存储,不区分大小写。 -#### 日期时间类型 +### 日期时间类型 | MySQL数据类型 | 含义 | | ------------- | ----------------------------- | @@ -85,9 +83,9 @@ BLOB和TEXT存储方式不同,TEXT以文本方式存储,英文存储区分 -## 基本命令 +# 基本命令 -### 启动 +## 启动 启动服务:`service mysqld start` @@ -95,7 +93,7 @@ BLOB和TEXT存储方式不同,TEXT以文本方式存储,英文存储区分 启动客户端:`mysql -uroot -p` -u 后不要有空格(Ubuntu有空格) -### 数据库操作 +## 数据库操作 ```mysql SHOW DATABASES; @@ -104,7 +102,7 @@ USE db_name; DROP DATABASE db_name; ``` -### 表 +## 表 创建表:`create table user (id int, name varchar(10))` @@ -114,7 +112,7 @@ DROP DATABASE db_name; `SHOW CREATE db` | `SHOW CREATE table`:显示创建特定数据库或表的MySQL语句 -### 检索 +## 检索 检索不同的行: @@ -131,7 +129,7 @@ FROM products LIMIT 0, 5; #开始位置,行数|返回从第0行开始的5行数据 ``` -### 排序 +## 排序 ```mysql SELECT prod_name @@ -150,7 +148,7 @@ LIMIT 1; # 仅返回一行 子句顺序:FORM -- ORDER BY -- LIMIT,顺序不对会报错。 -### 过滤 +## 过滤 子句操作符: @@ -163,7 +161,7 @@ LIMIT 1; # 仅返回一行 | <= | 小于等于 | | BETWEEN | 两值之间 | -#### 不匹配检查: +### 不匹配检查: ```mysql SELECT vend_id, prod_name @@ -171,7 +169,7 @@ FROM products WHERE vend_id <> 1003; ``` -#### 范围查询: +### 范围查询: ```mysql SELECT prod_name, prod_price @@ -179,7 +177,7 @@ FROM products WHERE prod_price BETWEEN 5 AND 10; ``` -#### 空值检查 +### 空值检查 ```mysql SELECT prod_name @@ -187,7 +185,7 @@ FROM products WHERE prod_price IS NULL; ``` -#### 计算次序 +### 计算次序 ```mysql SELECT prod_name, prod_price @@ -195,7 +193,7 @@ FROM products WHERE vend_id = 1002 OR vend_id = 1003 AND prod_price >= 10; # AND优先级大于OR ``` -#### IN 操作符 +### IN 操作符 ```mysql SELECT prod_name, product_price @@ -206,7 +204,7 @@ ORDER BY prod_name; IN操作符一般比OR操作符清单执行更快。IN的最大优点是可以包含其他SELECT语句,使得能够更动态地建立WHERE子句。 -#### NOT操作符 +### NOT操作符 MySQL支持使用NOT 对IN 、BETWEEN 和EXISTS子句取反。 @@ -216,7 +214,7 @@ FROM products WHERE vend_id NOT IN (1002, 1003) ``` -#### LIKE操作符 +### LIKE操作符 % 匹配0到多个任意字符。 @@ -236,11 +234,11 @@ WHERE prod_name LIKE '_jet_'; 通配符搜索比其他简单搜索耗时,不能过度使用通配符。 -#### LIMIT +### LIMIT limit 0,4 :从第0条记录开始,取4条 -#### 正则表达式 +### 正则表达式 OR 匹配: @@ -316,9 +314,9 @@ ORDER BY prod_name; SELECT 'hello' REGEXP '[0-9]';#REGEXP检查返回0或1;此处返回0 ``` -## 计算字段 +# 计算字段 -### 拼接字段 +## 拼接字段 MySQL使用Concat()函数实现拼接。 @@ -331,7 +329,7 @@ ORDER BY vend_name; 返回值:`ACME (USA)` 使用别名:`SELECT dept AS department FROM t_dept;` -### 计算字段 +## 计算字段 ```mysql SELECT prod_id, quantity, item_price, quantity*item_price AS expanded_price @@ -339,9 +337,9 @@ FROM orderitems WHERE order_num = 2005; ``` -## 函数 +# 函数 -### 文本处理 +## 文本处理 ```mysql SELECT vend_name, Upper(vend_name) AS vend_name_upcase @@ -359,9 +357,9 @@ WHERE Soundex(cust_contact) = Soundex('Y Lie'); 返回数据:`Tyson Y lee` -### 日期处理函数 +## 日期处理函数 -![](http://img.dabin-coder.cn/image/20220530235607.png) +![](http://img.topjavaer.cn/img/20220530235607.png) 查找2005年9月的所有订单: ```mysql @@ -378,13 +376,13 @@ FROM orders WHERE Year(order_date) = 2005 AND Month(order_date) = 9; ``` -### 数值处理函数 +## 数值处理函数 -![](http://img.dabin-coder.cn/image/20220530233617.png) +![](http://img.topjavaer.cn/img/20220530233617.png) -## 汇总数据 +# 汇总数据 -### 聚集函数 +## 聚集函数 Sum:求和 Avg:求平均数 @@ -406,7 +404,7 @@ FROM products WHERE vend_id = 1003; ``` -## 分组 +# 分组 单独地使用group by没意义,它只能显示出每组记录的第一条记录。 @@ -415,7 +413,7 @@ SELECT * FROM orders GROUP BY cust_id; ``` -![](http://img.dabin-coder.cn/image/20220530233523.png) +![](http://img.topjavaer.cn/img/20220530233523.png) 除聚集计算语句外,SELECT语句中的每个列都必须在GROUP BY子句中给出。 @@ -427,7 +425,7 @@ GROUP BY vend_id; GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。 -### 过滤分组 +## 过滤分组 having 用来分组查询后指定一些条件来输出查询结果,having作用和where类似,但是having只能用在group by场合,并且必须位于group by之后order by之前。 @@ -438,7 +436,7 @@ GROUP BY cust_id HAVING COUNT(*) >= 2; ``` -### having和where区别 +## having和where区别 ```mysql SELECT cust_id FROM orders GROUP BY cust_id HAVING COUNT(cust_id) >= 2; @@ -463,7 +461,7 @@ HAVING COUNT(*) >= 2; WHERE子句过滤所有prod_price至少为10的行。然后按vend_id分组数据,HAVING子句过滤计数为2或2以上的分组。 -### SELECT 子句顺序 +## SELECT 子句顺序 ```mysql SELECT @@ -477,7 +475,7 @@ LIMIT -## 子查询 +# 子查询 由于性能的限制,不能嵌套太多的子查询。 @@ -491,9 +489,9 @@ WHERE order_num IN (SELECT order_num -## 连接 +# 连接 -### 内连接 +## 内连接 找出供应商生产的产品。 @@ -513,7 +511,7 @@ WHERE vendors.vend_id = products.vend_id; 没有给出连接条件的话,会得到两张表的笛卡尔积。 -### 自连接 +## 自连接 找出生产nike的供应商生产的所有物品。 @@ -524,7 +522,7 @@ WHERE p1.vend_id = p2.vend_id AND p2.prod_id = 'nike'; ``` -### 自然连接 +## 自然连接 natural join是对两张表中字段名和数据类型都相同的字段进行**等值连接**,并返回符合条件的结果 。 @@ -534,9 +532,9 @@ SELECT * FROM role NATURAL JOIN user_role; 返回结果: -![](http://img.dabin-coder.cn/image/20220530235619.png) +![](http://img.topjavaer.cn/img/20220530235619.png) -### 内连接 +## 内连接 显示符合连接条件的记录。没有设置连接条件则返回笛卡尔积的结果。join 默认是 inner join。 @@ -546,7 +544,7 @@ SELECT * FROM role INNNER JOIN user_role 返回结果: -![](http://img.dabin-coder.cn/image/20220530235640.png) +![](http://img.topjavaer.cn/img/20220530235640.png) join…using(column)按指定的属性做等值连接。 join…on tableA.column1 = tableB.column2 指定条件。 @@ -557,9 +555,9 @@ SELECT * FROM role INNER JOIN user_role ON role.role_id = user_role.role_id 返回结果: -![](http://img.dabin-coder.cn/image/20220530235654.png) +![](http://img.topjavaer.cn/img/20220530235654.png) -### 外连接 +## 外连接 左外联接(Left Outer Join):除了匹配2张表中相关联的记录外,还会匹配左表中剩余的记录,右表中未匹配到的字段用NULL表示。 右外联接(Right Outer Join):除了匹配2张表中相关联的记录外,还会匹配右表中剩余的记录,左表中未匹配到的字段用NULL表示。 @@ -573,7 +571,7 @@ FROM customers LEFT OUTER JOIN orders ON customers.cust_id = order.cust_id; ``` -### 多表连接 +## 多表连接 ```mysql SELECT goal.player, eteam.teamname, game.stadium, game.mdate @@ -586,9 +584,9 @@ WHERE eteam.id = 'GRE' -## 组合查询 +# 组合查询 -### UNION +## UNION UNION中的每个查询必须包含相同的列、表达式或聚集函数。列数据类型必须兼容。 @@ -611,7 +609,7 @@ UNION 默认会去掉重复的行,使用 UNION ALL可以返回所有匹配行 -## 全文搜索 +# 全文搜索 为了进行全文本搜索,必须索引被搜索的列,而且要随着数据的改变不断地重新索引。在对表列进行适当设计后,MySQL会自动进行所有的索引和重新索引。 @@ -653,9 +651,9 @@ FROM productnotes; -## 表操作 +# 表操作 -### 创建表 +## 创建表 ```mysql CREATE TABLE productnotes @@ -670,7 +668,7 @@ CREATE TABLE productnotes 主键中只能使用NOT NULL值的列。 -### 更新表 +## 更新表 数据库表的更改不能撤销,应先做好备份。 @@ -708,7 +706,7 @@ MODIFY vend_phone CHAR(16); - 重新创建触发器、存储过程、索引和外键。 -### 约束 +## 约束 添加主键约束: @@ -737,19 +735,33 @@ ADD FOREIGN KEY(vendor_id) REFERENCES vendors(vendor_id); ALTER TABLE products DROP FOREIGN KEY vendor_id; ``` -### 删除表 +## 删除表 `DROP TABLE cumstomers` -### 重命名表 +## truncate、delete与drop区别 + +**相同点:** + +1. truncate和不带where子句的delete、以及drop都会删除表内的数据。 + +2. drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。 + +**不同点:** + +1. truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引; + +2. 速度,一般来说: drop> truncate > delete。 + +## 重命名表 `RENAME TABLE cusmtomers TO cust` -## 列操作 +# 列操作 -### 插入数据 +## 插入数据 MySQL用单条INSERT语句处理多个插入比使用多条INSERT语句快。 @@ -761,7 +773,7 @@ INSERT INTO customers(cust_name, cust_city) INSERT操作可能很耗时(特别是有很多索引需要更新时),而且它可能降低等待处理的SELECT语句的性能。降低INSERT语句的优先级:`INSERT LOW_PRIORITY INTO` -### 更新数据 +## 更新数据 如果用UPDATE语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个UPDATE操作被取消。为了在发生错误时也继续进行更新,可使用IGNORE关键字:`UPDATE IGNORE customers...` @@ -773,7 +785,7 @@ WHERE cust_id = 1005; 返回值是受影响的记录数。 -### 删除数据 +## 删除数据 如果想从表中删除所有行,不要使用DELETE。可使用TRUNCATE TABLE语句,它完成相同的工作,但速度更快(TRUNCATE实际是删除原来的表并重新创建一个表,而不是逐行删除表中的数据)。 @@ -795,23 +807,9 @@ AND EXISTS WHERE b.id = a.id); ``` -### truncate、delete与drop区别 - -**相同点:** - -1. truncate和不带where子句的delete、以及drop都会删除表内的数据。 - -2. drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。 - -**不同点:** - -1. truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引; - -2. 速度,一般来说: drop> truncate > delete。 - -## 引擎 +# 引擎 InnoDB是一个可靠的事务处理引擎,它不支持全文本搜索; @@ -823,23 +821,23 @@ MyISAM是一个性能极高的引擎,它支持全文本搜索,但不支持 -## 视图 +# 视图 视图为虚拟的表。视图提供了一种MySQL的SELECT语句层次的封装,可用来简化数据处理以及重新格式化基础数据或保护基础数据。 -### 应用 +## 应用 - 重用SQL语句。 - 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。 - 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。 -### 限制 +## 限制 - 与表一样,视图必须唯一命名 - 视图不能索引,也不能有关联的触发器或默认值。 - 视图可以和表一起使用。例如,编写一条联结表和视图的SELECT语句。 - ORDER BY可以用在视图中,但如果从该视图检索数据SELECT中也含有ORDER BY,那么该视图中的ORDER BY将被覆盖。 -### 语法 +## 语法 `CREATE VIEW`:创建视图 @@ -848,7 +846,7 @@ MyISAM是一个性能极高的引擎,它支持全文本搜索,但不支持 `DROP VIEW viewname`:删除视图 `CREATE ORREPLACE VIEW`:更新视图,相当于先用`DROP`再用`CREATE` -### 简化复杂连接 +## 简化复杂连接 创建一个视图,返回订购了任意产品的客户列表。 ```mysql CREATE VIEW productcustomers AS @@ -863,20 +861,20 @@ SELECT cust_name, cust_contact FROM productcustomers WHERE prod_id = 'nike'; ``` -### 更新视图 +## 更新视图 对视图增加或删除行,实际上是对其基表增加或删除行。视图主要用于数据检索。 -## 存储过程 +# 存储过程 为以后的使用而保存的一条或多条MySQL语句的集合。可将其视为批文件。 为什么使用存储过程: - 把复杂处理进行封装,简化复杂的操作; - 提高性能,存储过程比单独SQL语句更快; -### 创建 +## 创建 返回产品平均价格的存储过程: ```mysql CREATE PROCEDURE productpricing() # 可以接受参数 @@ -886,12 +884,12 @@ BEGIN END; ``` BEGIN/END 用来限定存储过程体。此段代码仅创建了存储过程,未执行。 -### 调用 +## 调用 `CALL productpricing()` -### 删除 +## 删除 存储过程在创建之后,被保存在服务器上以供使用,直至被删除。 `DROP PROCEDURE productpricing IF EXISTS` -### 参数 +## 参数 MySQL支持IN(传递给存储过程)、OUT(从存储过程传出)和INOUT(对存储过程传入和传出)类型的参数。 接受订单号并返回该订单的金额: ```mysql @@ -908,7 +906,7 @@ END; ``` 调用存储过程:`CALL ordertotal(20, @total);` 显示订单金额:`SELECT @total;` -### 实例 +## 实例 获取订单税后金额(订单金额+税收)。 ```mysql CREATE PROCEDURE ordertotal( @@ -936,7 +934,7 @@ END; CALL ordertotal(20005, 1, @total); SELECT @total; ``` -### 查看 +## 查看 创建存储过程的 CREATE 语句。 ```mysql SHOW CREATE PROCEDURE ordertotal; @@ -949,9 +947,9 @@ SHOW CREATE PROCEDURE ordertotal; SHOW PROCEDURE status; ``` -## 游标 +# 游标 存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。MySQL游标只能用于存储过程(和函数)。 -### 创建游标 +## 创建游标 DECLARE 命名游标。存储过程处理完成后,游标便消失(游标只存在于存储过程)。定义游标之后,便可以打开它。 ```mysql CREATE PROCEDURE processorders() @@ -961,7 +959,7 @@ BEGIN SELECT order_num FROM orders; END; ``` -### 使用游标 +## 使用游标 `OPEN ordernumbers` 打开游标。 `CLOSE ordernumbers` CLOSE释放游标使用的所有内部内存和资源。 ```mysql @@ -1002,20 +1000,20 @@ END; -## 触发器 +# 触发器 -提供SQL语句自动执行的功能。DELETE/INSERT/UPDATE支持触发器,其他SQL语句不支持。 -### 创建 +触发器提供SQL语句自动执行的功能。DELETE/INSERT/UPDATE支持触发器,其他SQL语句不支持。 +## 创建 创建触发器四要素:1.唯一的触发器名(MySQL5规定触发器名在表中唯一,数据库没要求);2.触发器关联的表;3.相应的SQL语句;4.何时执行(处理之前或者之后)。 ```mysql CREATE TRIGGER newproduct AFTER INSERT ON products #插入之后执行 FOR EACH ROW SELECT 'product added'; #对每个插入行执行 ``` 只有表支持触发器,视图不支持。单一触发器不能与多个事件或多个表关联,如果需要对INSERT和UPDATE操作执行触发器,则应该定义两个触发器。 -### 删除 +## 删除 `DROP TRIGGER newproduct` -### 使用触发器 +## 使用触发器 INSERT 触发器可饮用名为 NEW 的虚拟表,访问被插入的行。NEW中的值也可以被更新(允许更改被插入的值)。 @@ -1048,7 +1046,7 @@ FOR EACH ROW SET NEW.vend_state = Upper(NEW.vend_state); -## 事务处理 +# 事务处理 事务处理可以用来维护数据库的完整性。它保证成批的MySQL操作要么完全执行,要么完全不执行。 @@ -1056,6 +1054,8 @@ CREATE/DROP 操作不能回退,即便可以执行回退操作,回退不会 执行事务过程,一旦某个SQL失败,则之前执行成功的SQL会被自动撤销。 +## 语法 + ```mysql START TRANSACTION; DELETE FROM orderitems WHERE order_num = 20010; @@ -1065,7 +1065,7 @@ COMMIT; 当COMMIT或ROLLBACK语句执行后,事务会自动关闭。 -### 保留点 +## 保留点 为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符。 @@ -1080,11 +1080,11 @@ ROLLBACK TO delete1; -## 权限 +# 权限 -### 管理用户 +MySQL用户账号和信息存储在名为mysql的MySQL数据库中。 -MySQL用户账号和信息存储在名为mysql的MySQL数据库中。获取用户账号列表。 +获取用户账号列表: ```mysql USE mysql; @@ -1108,15 +1108,16 @@ SELECT user FROM user; 撤销权限:`REVOKE SELECT, INSERT ON mall.* FROM tyson`,被撤销的访问权限必须存在,否则会出错。 GRANT和REVOKE可在几个层次上控制访问权限: - 整个服务器,使用GRANT ALL和REVOKE ALL; - 整个数据库,使用ON database.*; - 特定的表,使用ON database.table; - 特定的列; - 特定的存储过程。 +- 整个服务器,使用GRANT ALL和REVOKE ALL; +- 整个数据库,使用ON database.*; +- 特定的表,使用ON database.table; +- 特定的列; +- 特定的存储过程。 -## 优化性能 + +# 性能优化 使用EXPLAIN语句让MySQL解释它将如何执行一条SELECT语句。 @@ -1127,6 +1128,7 @@ GRANT和REVOKE可在几个层次上控制访问权限: LIKE很慢,最好是使用FULLTEXT而不是LIKE。 很多高性能的应用都会对关联查询进行分解,有如下的优势: + 1 、让缓存效率更高。如果某张表很少变化,那么基于该表的查询就可以重复利用查询缓存结果。 2 、将查询分解后,执行单个查询可以减少锁的竞争。 3 、在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。 @@ -1136,9 +1138,9 @@ LIKE很慢,最好是使用FULLTEXT而不是LIKE。 -## 索引 +# 索引 -### 创建索引 +## 创建索引 ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 @@ -1157,7 +1159,7 @@ CREATE UNIQUE INDEX index_name ON table_name (column_list) 在创建索引时,可以规定索引能否包含重复值。如果不包含,则索引应该创建为PRIMARY KEY或UNIQUE索引。 -### 删除索引 +## 删除索引 ```mysql DROP INDEX index_name ON talbe_name @@ -1165,7 +1167,7 @@ ALTER TABLE table_name DROP INDEX index_name ALTER TABLE table_name DROP PRIMARY KEY #只有一个主键,不需要指定索引名 ``` -### 查看索引 +## 查看索引 ```mysql show index from tblname; diff --git a/docs/database/mysql-basic/1-data-type.md b/docs/database/mysql-basic/1-data-type.md new file mode 100644 index 0000000..6818dcc --- /dev/null +++ b/docs/database/mysql-basic/1-data-type.md @@ -0,0 +1,82 @@ +# 数据类型 + +主要包括以下五大类: + +整数类型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、 INT、 BIG INT + +浮点数类型:FLOAT、DOUBLE、DECIMAL + +字符串类型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB + +日期类型:Date、DateTime、TimeStamp、Time、Year + +其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection等 + +## 整型 + +| MySQL数据类型 | 含义(有符号) | +| ------------- | ------------------------------------ | +| tinyint(m) | 1个字节 范围(-128~127) | +| smallint(m) | 2个字节 范围(-32768~32767) | +| mediumint(m) | 3个字节 范围(-8388608~8388607) | +| int(m) | 4个字节 范围(-2147483648~2147483647) | +| bigint(m) | 8个字节 范围(+-9.22*10的18次方) | + +int(m) m 用于指定显示宽度,int(5)表示5位数的宽度。注意显示宽度属性不能控制列可以存储的值范围,显示宽度属性通常由应用程序用于格式化整数值。 + +## 浮点型 + +| MySQL数据类型 | 含义 | +| ------------- | ------------------------------------- | +| float(m,d) | 单精度浮点型 4字节 m总个数,d小数位 | +| double(m,d) | 双精度浮点型 8字节 m总个数,d小数位 | + +浮点型数据类型会有精度丢失的问题,比如小数位设置6位,存入0.45,0.45转换成二进制是个无限循环小数0.01110011100...,无法准确表示,存储的时候会发生精度丢失。 + +不论是定点还是浮点类型,如果用户指定的精度超出精度范围,则会四舍五入进行处理。 + +## 定点数 + +浮点型在数据库中存放的是近似值,而定点类型在数据库中存放的是精确值,不会丢失精度。定点数以字符串形式存储。 + +decimal(m,d) 参数m是总个数,d是小数位。 + +## 字符串 + +| MySQL数据类型 | 含义 | +| ------------- | ------------------------------- | +| char(n) | 固定长度,最多255个字节 | +| varchar(n) | 可变长度,最多65535个字节 | +| tinytext | 可变长度,最多255个字节 | +| text | 可变长度,最多65535个字节 | +| mediumtext | 可变长度,最多2的24次方-1个字节 | +| longtext | 可变长度,最多2的32次方-1个字节 | + +查询速度:char > varchar > text + +char:定长,效率高,一般用于固定长度的表单提交数据存储 ;例如:身份证号,手机号,电话,密码等。char长度不足时,在右边使用空格填充,而varchar值保存时只保存需要的字符数。 + +varchar:不定长,效率偏低,内容开头用1到2个字节表示实际长度(长度超过255时需要2个字节),因此最大长度不能超过65535。 + +nvarchar(存储的是Unicode数据类型的字符)不管是一个字符还是一个汉字,都存为2个字节 ,一般用作中文或者其他语言输入,这样不容易乱码 ;varchar存储汉字是2个字节,其他字符存为1个字节 ,varchar适合输入英文和数字。 + +text:不需要指定存储长度,能用varchar就不用text。 + +## 二进制数据(BLOB) + +二进制数据类型可存储任何数据,如图像、多媒体、文档等。 + +BLOB和TEXT存储方式不同,TEXT以文本方式存储,英文存储区分大小写;而Blob是以二进制方式存储,不区分大小写。 + +## 日期时间类型 + +| MySQL数据类型 | 含义 | +| ------------- | ----------------------------- | +| date | 日期 '2008-12-2' | +| time | 时间 '12:25:36' | +| datetime | 日期时间 '2008-12-2 22:06:44' | +| timestamp | 自动存储记录修改时间 | + +若定义一个字段为timestamp,这个字段里的时间数据会随其他字段修改的时候自动刷新,所以这个数据类型的字段可以存放这条记录最后被修改的时间。 + + diff --git a/docs/database/mysql-basic/10-view.md b/docs/database/mysql-basic/10-view.md new file mode 100644 index 0000000..f770fb5 --- /dev/null +++ b/docs/database/mysql-basic/10-view.md @@ -0,0 +1,50 @@ +# 视图 + +视图为虚拟的表。视图提供了一种MySQL的SELECT语句层次的封装,可用来简化数据处理以及重新格式化基础数据或保护基础数据。 + +## 应用 + +- 重用SQL语句。 +- 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。 +- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。 + +## 限制 + +- 与表一样,视图必须唯一命名 +- 视图不能索引,也不能有关联的触发器或默认值。 +- 视图可以和表一起使用。例如,编写一条联结表和视图的SELECT语句。 +- ORDER BY可以用在视图中,但如果从该视图检索数据SELECT中也含有ORDER BY,那么该视图中的ORDER BY将被覆盖。 + +## 语法 + +`CREATE VIEW`:创建视图 + +`SHOW CREATE VIEW viewname`:查看创建视图的语句 + +`DROP VIEW viewname`:删除视图 + +`CREATE ORREPLACE VIEW`:更新视图,相当于先用`DROP`再用`CREATE` + +## 简化复杂连接 + +创建一个视图,返回订购了任意产品的客户列表。 + +```mysql +CREATE VIEW productcustomers AS +SELECT cust_name, orders, orderitems +FROM customers, orders, orderitems +WHERE orderitems.order_num = orders.order_num + AND customers.cust_id = orders.cust_id; +``` + +使用视图: + +```mysql +SELECT cust_name, cust_contact +FROM productcustomers +WHERE prod_id = 'nike'; +``` + +## 更新视图 + +对视图增加或删除行,实际上是对其基表增加或删除行。视图主要用于数据检索。 \ No newline at end of file diff --git a/docs/database/mysql-basic/11-procedure.md b/docs/database/mysql-basic/11-procedure.md new file mode 100644 index 0000000..f03c531 --- /dev/null +++ b/docs/database/mysql-basic/11-procedure.md @@ -0,0 +1,101 @@ +# 存储过程 + +为以后的使用而保存的一条或多条MySQL语句的集合。可将其视为批文件。 +为什么使用存储过程: + +- 把复杂处理进行封装,简化复杂的操作; +- 提高性能,存储过程比单独SQL语句更快; + +## 创建 + +返回产品平均价格的存储过程: + +```mysql +CREATE PROCEDURE productpricing() # 可以接受参数 +BEGIN + SELECT Avg(prod_price) AS priceaverage + FROM products; +END; +``` + +BEGIN/END 用来限定存储过程体。此段代码仅创建了存储过程,未执行。 + +## 调用 + +`CALL productpricing()` + +## 删除 + +存储过程在创建之后,被保存在服务器上以供使用,直至被删除。 +`DROP PROCEDURE productpricing IF EXISTS` + +## 参数 + +MySQL支持IN(传递给存储过程)、OUT(从存储过程传出)和INOUT(对存储过程传入和传出)类型的参数。 +接受订单号并返回该订单的金额: + +```mysql +CREATE PROCEDURE ordertotal( + IN ordernum INT, + OUT ordersum DECIMAL(8, 2) +) +BEGIN + SELECT Sum(item_price * quantity) + FROM orderitems + WHERE order_num = ordernum + INTO ordersum; +END; +``` + +调用存储过程:`CALL ordertotal(20, @total);` +显示订单金额:`SELECT @total;` + +## 实例 + +获取订单税后金额(订单金额+税收)。 + +```mysql +CREATE PROCEDURE ordertotal( + IN onum INT, + IN taxable BOOLEAN, # 是否计税 + OUT ototal DECIMAL(8, 2) +) COMMENT 'order total, adding tax' +BEGIN + DECLARE total DECIMAL(8, 2); + DECLARE taxrate INT DEFAULT 6; + + SELECT Sum(item_price * quanlity) + FROM orderitems + WHERE order_num = onum + INTO total; + + IF taxable THEN + SELECT total + (total / 100 * taxrate) INTO total; + END IF; + -- SELECT total INTO ototal; +END; +``` + +调用存储过程: + +```mysql +CALL ordertotal(20005, 1, @total); +SELECT @total; +``` + +## 查看 + +创建存储过程的 CREATE 语句。 + +```mysql +SHOW CREATE PROCEDURE ordertotal; +``` + +获得包括何时、由谁创建等详细信息的存储过程列表,使用`SHOW PROCEDURE STATUS LIKE 'ordertotal';` + +查看存储过程状态: + +```mysql +SHOW PROCEDURE status; +``` + diff --git a/docs/database/mysql-basic/12-cursor.md b/docs/database/mysql-basic/12-cursor.md new file mode 100644 index 0000000..dad75b4 --- /dev/null +++ b/docs/database/mysql-basic/12-cursor.md @@ -0,0 +1,58 @@ +# 游标 + +存储了游标之后,应用程序可以根据需要滚动或浏览其中的数据。MySQL游标只能用于存储过程(和函数)。 + +## 创建游标 + +DECLARE 命名游标。存储过程处理完成后,游标便消失(游标只存在于存储过程)。定义游标之后,便可以打开它。 + +```mysql +CREATE PROCEDURE processorders() +BEGIN + DECLARE ordernumbers CURSOR + FOR + SELECT order_num FROM orders; +END; +``` + +## 使用游标 + +`OPEN ordernumbers` 打开游标。 +`CLOSE ordernumbers` CLOSE释放游标使用的所有内部内存和资源。 + +```mysql +CREATE PROCEDURE processorders() +BEGIN + + DECLARE done BOOLEAN DEFAULT 0; + DECLARE o INT; + DECLARE t DECIMAL(8, 2); + + DECLARE ordernumbers CURSOR + FOR + SELECT order_num FROM orders; + + DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1; #游标移到最后 + + CREATE TABLE IF NOT EXISTS ordertotals + (order_num INT, total DECIMAL(8,2)); + -- 打开游标 + OPEN ordernumbers; + + -- 循环 + REPEAT + FETCH ordernumbers INTO o; + CALL ordertotal(o, 1, t); + + -- 插入订单号和订单金额 + INSERT INTO ordertotals(order_num, total) + VALUES(o, t); + + -- done为1结束循环 + UNTIL done END REPEAT; + + CLOSE ordernumbers; +END; +``` + +存储过程还在运行中创建了一个新表,。这个表将保存存储过程生成的结果。FETCH取每个order_num,然后用CALL执行另一个存储过程,计算每个订单税后金额。最后,用INSERT保存每个订单的订单号和金额。 \ No newline at end of file diff --git a/docs/database/mysql-basic/13-trigger.md b/docs/database/mysql-basic/13-trigger.md new file mode 100644 index 0000000..f05b92f --- /dev/null +++ b/docs/database/mysql-basic/13-trigger.md @@ -0,0 +1,51 @@ +# 触发器 + +触发器提供SQL语句自动执行的功能。DELETE/INSERT/UPDATE支持触发器,其他SQL语句不支持。 + +## 创建 + +创建触发器四要素:1.唯一的触发器名(MySQL5规定触发器名在表中唯一,数据库没要求);2.触发器关联的表;3.相应的SQL语句;4.何时执行(处理之前或者之后)。 + +```mysql +CREATE TRIGGER newproduct AFTER INSERT ON products #插入之后执行 +FOR EACH ROW SELECT 'product added'; #对每个插入行执行 +``` + +只有表支持触发器,视图不支持。单一触发器不能与多个事件或多个表关联,如果需要对INSERT和UPDATE操作执行触发器,则应该定义两个触发器。 + +## 删除 + +`DROP TRIGGER newproduct` + +## 使用 + +INSERT 触发器可饮用名为 NEW 的虚拟表,访问被插入的行。NEW中的值也可以被更新(允许更改被插入的值)。 + +```mysql +CREATE TRIGGER neworder AFTER INSERT ON order +FOR EACH ROW SELECT NEW.order_num; #返回新的订单号 +``` + +DELETE 触发器可以引用名为 OLD 的虚拟表,访问被删除的行。OLD中的值全都是只读的,不能更新。 + +```mysql +CREATE TRIGGER deleteorder BEFORE DELETE ON orders +FOR EACH ROW +BEGIN + INSERT INTO archive_orders(order_num, cust_id) + VALUES(OLD.order_num, OLD.cust_id); +END; +``` + +订单删除之前保存订单信息到存档表。 + +UPDATE 触发器可以引用名为 OLD 的虚拟表访问以前的值,引用一个名为NEW的虚拟表访问新更新的值。NEW 值可被更新,OLD 值是只读的。 + +下面的例子保证州名缩写总是大写。 + +```mysql +CREATE TRIGGER updatevendor BEFORE UPDATE ON vendor +FOR EACH ROW SET NEW.vend_state = Upper(NEW.vend_state); +``` + + diff --git a/docs/database/mysql-basic/14-transaction.md b/docs/database/mysql-basic/14-transaction.md new file mode 100644 index 0000000..1526284 --- /dev/null +++ b/docs/database/mysql-basic/14-transaction.md @@ -0,0 +1,32 @@ +# 事务处理 + +事务处理可以用来维护数据库的完整性。它保证成批的MySQL操作要么完全执行,要么完全不执行。 + +CREATE/DROP 操作不能回退,即便可以执行回退操作,回退不会有效果。 + +执行事务过程,一旦某个SQL失败,则之前执行成功的SQL会被自动撤销。 + +## 语法 + +```mysql +START TRANSACTION; +DELETE FROM orderitems WHERE order_num = 20010; +DELETE FROM orders WHERE order_num = 20010; +COMMIT; +``` + +当COMMIT或ROLLBACK语句执行后,事务会自动关闭。 + +## 保留点 + +为了支持回退部分事务处理,必须能在事务处理块中合适的位置放置占位符。这样,如果需要回退,可以回退到某个占位符。 + +保留点在事务处理完成后自动释放。 + +```mysql +... +SAVEPOINT delete1; +... +ROLLBACK TO delete1; +``` + diff --git a/docs/database/mysql-basic/15-permission.md b/docs/database/mysql-basic/15-permission.md new file mode 100644 index 0000000..e65754c --- /dev/null +++ b/docs/database/mysql-basic/15-permission.md @@ -0,0 +1,35 @@ +# 权限 + +MySQL用户账号和信息存储在名为mysql的MySQL数据库中。 + +获取用户账号列表: + +```mysql +USE mysql; +SELECT user FROM user; +``` + +创建用户账号:`CREATE USER tyson IDENTIFIED BY 'abc123'` + +修改密码:`SET PASSWORD FOR tyson = Password('xxx');`,新密码需传递到Password()函数进行加密。 + +设置当前用户密码:`SET PASSWORD = Password('xxx');` + +重命名账号:`RENAME USER tyson TO tom` + +删除用户账号:`DROP USER tyson` + +查看访问权限:`SHOW GRANTS FOR tyson`,返回`USAGE ON *.*`则表示没有权限。 + +授予访问权限:`GRANT SELECT ON mall.# TO tyson`,允许用户在mall数据库所有表使用SELECT。 + +撤销权限:`REVOKE SELECT, INSERT ON mall.* FROM tyson`,被撤销的访问权限必须存在,否则会出错。 + +GRANT和REVOKE可在几个层次上控制访问权限: + +- 整个服务器,使用GRANT ALL和REVOKE ALL; +- 整个数据库,使用ON database.*; +- 特定的表,使用ON database.table; +- 特定的列; +- 特定的存储过程。 + diff --git a/docs/database/mysql-basic/16-performace-optimization.md b/docs/database/mysql-basic/16-performace-optimization.md new file mode 100644 index 0000000..8fe6e47 --- /dev/null +++ b/docs/database/mysql-basic/16-performace-optimization.md @@ -0,0 +1,18 @@ +# 性能优化 + +使用EXPLAIN语句让MySQL解释它将如何执行一条SELECT语句。 + +如果一个简单的WHERE子句返回结果所花的时间太长,则可以断定其中使用的某些列就是需要索引的对象。 + +避免使用OR。通过使用多条SELECT语句和连接它们的UNION语句,会有极大的性能改进。 + +LIKE很慢,最好是使用FULLTEXT而不是LIKE。 + +很多高性能的应用都会对关联查询进行分解,有如下的优势: + +1 、让缓存效率更高。如果某张表很少变化,那么基于该表的查询就可以重复利用查询缓存结果。 +2 、将查询分解后,执行单个查询可以减少锁的竞争。 +3 、在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。 +4 、查询本身效率也可能会有所提升。例如 IN()代替关联查询,可能比随机的关联更高效。 +5 、减少冗余记录得查询。 +6 、更进一步,这样做相当于在应用中实现了哈希关联,而不是使用 MySQL 得嵌套循环关联。某些场景哈希关联得效率要高很多。 \ No newline at end of file diff --git a/docs/database/mysql-basic/17-index.md b/docs/database/mysql-basic/17-index.md new file mode 100644 index 0000000..a28dc43 --- /dev/null +++ b/docs/database/mysql-basic/17-index.md @@ -0,0 +1,36 @@ +# 索引 + +## 创建索引 + +ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 + +```mysql +ALTER TABLE table_name ADD INDEX index_name (column_list) +ALTER TABLE table_name ADD UNIQUE (column_list) +ALTER TABLE table_name ADD PRIMARY KEY (column_list) +``` + +CREATE INDEX可对表增加普通索引或UNIQUE索引。 + +```mysql +CREATE INDEX index_name ON table_name (column_list) +CREATE UNIQUE INDEX index_name ON table_name (column_list) +``` + +在创建索引时,可以规定索引能否包含重复值。如果不包含,则索引应该创建为PRIMARY KEY或UNIQUE索引。 + +## 删除索引 + +```mysql +DROP INDEX index_name ON talbe_name +ALTER TABLE table_name DROP INDEX index_name +ALTER TABLE table_name DROP PRIMARY KEY #只有一个主键,不需要指定索引名 +``` + +## 查看索引 + +```mysql +show index from tblname; +show keys from tblname; +``` + diff --git a/docs/database/mysql-basic/2-basic-command.md b/docs/database/mysql-basic/2-basic-command.md new file mode 100644 index 0000000..621b0b4 --- /dev/null +++ b/docs/database/mysql-basic/2-basic-command.md @@ -0,0 +1,230 @@ +# 基本命令 +## 启动 + +启动服务:`service mysqld start` + +关闭服务:`service mysqld stop` + +启动客户端:`mysql -uroot -p` -u 后不要有空格(Ubuntu有空格) + +## 数据库操作 + +```mysql +SHOW DATABASES; +CREATE DATABASE db_name; +USE db_name; +DROP DATABASE db_name; +``` + +## 表 + +创建表:`create table user (id int, name varchar(10))` + +清空表数据:`truncate table user;` + +查看表结构:`desc table_name/select columns from table_name` + +`SHOW CREATE db` | `SHOW CREATE table`:显示创建特定数据库或表的MySQL语句 + +## 检索 + +检索不同的行: + +```mysql +SELECT DISTINCT vend_id +FROM products; +``` + +限制结果: + +```mysql +SELECT prod_name +FROM products +LIMIT 0, 5; #开始位置,行数|返回从第0行开始的5行数据 +``` + +## 排序 + +```mysql +SELECT prod_name +FROM products +ORDER BY prod_name, prod_price DESC; #先按名称排序,再按价格排序 | DESC降序排列,默认ASC升序 +``` + +找出最贵的物品: + +```mysql +SELECT prod_price +FROM products +ORDER BY prod_price DESC +LIMIT 1; # 仅返回一行 +``` + +子句顺序:FORM -- ORDER BY -- LIMIT,顺序不对会报错。 + +## 过滤 + +子句操作符: + +| 操作符 | 说明 | +| ------- | -------- | +| = | 等于 | +| <> | 不等于 | +| != | 不等于 | +| < | 小于 | +| <= | 小于等于 | +| BETWEEN | 两值之间 | + +### 不匹配检查: + +```mysql +SELECT vend_id, prod_name +FROM products +WHERE vend_id <> 1003; +``` + +### 范围查询: + +```mysql +SELECT prod_name, prod_price +FROM products +WHERE prod_price BETWEEN 5 AND 10; +``` + +### 空值检查 + +```mysql +SELECT prod_name +FROM products +WHERE prod_price IS NULL; +``` + +### 计算次序 + +```mysql +SELECT prod_name, prod_price +FROM products +WHERE vend_id = 1002 OR vend_id = 1003 AND prod_price >= 10; # AND优先级大于OR +``` + +### IN 操作符 + +```mysql +SELECT prod_name, product_price +FROM products +WHERE vend_id IN (1002, 1003) +ORDER BY prod_name; +``` + +IN操作符一般比OR操作符清单执行更快。IN的最大优点是可以包含其他SELECT语句,使得能够更动态地建立WHERE子句。 + +### NOT操作符 + +MySQL支持使用NOT 对IN 、BETWEEN 和EXISTS子句取反。 + +```mysql +SELECT prod_name, product_price +FROM products +WHERE vend_id NOT IN (1002, 1003) +``` + +### LIKE操作符 + +% 匹配0到多个任意字符。 + +```mysql +SELECT prod_id, prod_name +FROM products +WHERE prod_name LIKE '%jet%'; +``` + +_ 匹配单个字符。 + +```mysql +SELECT prod_id, prod_name +FROM products +WHERE prod_name LIKE '_jet_'; +``` + +通配符搜索比其他简单搜索耗时,不能过度使用通配符。 + +### LIMIT + +limit 0,4 :从第0条记录开始,取4条 + +### 正则表达式 + +OR 匹配: + +```mysql +SELECT prod_name +FROM products +WHERE prod_name REGEXP '1000|2000' +ORDER BY prod_name; +``` + +匹配特定字符: + +```mysql +SELECT prod_name +FROM products +WHERE prod_name REGEXP '[123] Rely' #匹配1或2或3 [^123]取反 +ORDER BY prod_name; +``` + +匹配范围: + +```mysql +SELECT prod_name +FROM products +WHERE prod_anem REGEXP '[1-5] Ton';#匹配1-5任意一个数字,[a-z]同理 +``` + +匹配特殊字符: + +```mysql +SELECT prod_name +FROM products +WHERE prod_anem REGEXP '\\.';#转义 +``` + +匹配多个实例: + +```mysql +SELECT prod_name +FROM products +WHERE prod_anem REGEXP '\\([0-9] sticks?\\)'; #?匹配它前面的任何字符出现0次或1次 +``` + +匹配连着的四个数: + +```mysql +SELECT prod_name +FROM products +WHERE prod_anem REGEXP '[[:digit:]]{4}'; #[:digit:]匹配任意数字 +``` + +定位符: + +| 元字符 | 说明 | +| ------- | -------- | +| ^ | 文本开始 | +| $ | 文本结束 | +| [[:<:]] | 词开始 | +| [[:>:]] | 词结束 | + +查找一个数(包括小数点开始的数)开始的所有产品: + +```mysql +SELECT prod_name +FROM products +WHERE prod_name REGEXP '^[0-9\\.]' +ORDER BY prod_name; +``` + +简单的正则表达式测试: + +```mysql +SELECT 'hello' REGEXP '[0-9]';#REGEXP检查返回0或1;此处返回0 +``` + diff --git a/docs/database/mysql-basic/3-function.md b/docs/database/mysql-basic/3-function.md new file mode 100644 index 0000000..53c384c --- /dev/null +++ b/docs/database/mysql-basic/3-function.md @@ -0,0 +1,42 @@ +# 函数 +## 文本处理 + +```mysql +SELECT vend_name, Upper(vend_name) AS vend_name_upcase +FROM vendors +ORDER BY vend_name; +``` + +Soundex()函数,匹配所有同音字符串。 + +```mysql +SELECT cust_name, cust_contact +FROM customers +WHERE Soundex(cust_contact) = Soundex('Y Lie'); +``` + +返回数据:`Tyson Y lee` + +## 日期处理函数 + +![](http://img.topjavaer.cn/img/20220530235607.png) +查找2005年9月的所有订单: + +```mysql +SELECT cust_id, order_num +FROM orders +WHERE Date(order_date) BETWEEN '2005-09-01' AND '2005-09-30'; +``` + +或者 + +```mysql +SELECT cust_id, order_num +FROM orders +WHERE Year(order_date) = 2005 AND Month(order_date) = 9; +``` + +## 数值处理函数 + +![](http://img.topjavaer.cn/img/20220530233617.png) + diff --git a/docs/database/mysql-basic/4-sum.md b/docs/database/mysql-basic/4-sum.md new file mode 100644 index 0000000..c2f38ec --- /dev/null +++ b/docs/database/mysql-basic/4-sum.md @@ -0,0 +1,22 @@ +# 聚集函数 + +Sum:求和 +Avg:求平均数 +Max:求最大值 + Min:求最小值 + Count:求记录 + +```mysql +SELECT SUM(item_price*quanlity) AS total_price +FROM orderitems +WHERE order_num = 2005; +``` + +聚集不同值: + +```mysql +SELECT AVG(DISTINCT prod_price) AS avg_price #只考虑不同价格 +FROM products +WHERE vend_id = 1003; +``` + diff --git a/docs/database/mysql-basic/5-group.md b/docs/database/mysql-basic/5-group.md new file mode 100644 index 0000000..4be1e3a --- /dev/null +++ b/docs/database/mysql-basic/5-group.md @@ -0,0 +1,56 @@ +# 分组 + +单独地使用group by没意义,它只能显示出每组记录的第一条记录。 + +```mysql +SELECT * FROM orders +GROUP BY cust_id; +``` + +![](http://img.topjavaer.cn/img/20220530233523.png) + +除聚集计算语句外,SELECT语句中的每个列都必须在GROUP BY子句中给出。 + +```mysql +SELECT vend_id, COUNT(*) AS num_prods #vend_id在GROUP BY子句给出 +FROM products +GROUP BY vend_id; +``` + +GROUP BY子句必须出现在WHERE子句之后,ORDER BY子句之前。 + +## 过滤分组 + +having 用来分组查询后指定一些条件来输出查询结果,having作用和where类似,但是having只能用在group by场合,并且必须位于group by之后order by之前。 + +```mysql +SELECT cust_id, COUNT(*) AS orders +FROM orders +GROUP BY cust_id +HAVING COUNT(*) >= 2; +``` + +## having和where区别 + +```mysql +SELECT cust_id FROM orders GROUP BY cust_id HAVING COUNT(cust_id) >= 2; +SELECT cust_id FROM orders GROUP BY cust_id WHERE COUNT(cust_id) >= 2; #Error Code : 1064 +``` + +第一个sql语句可以执行,但是第二个会报错。 + +- WHERE子句不起作用,因为过滤是基于分组聚集值而不是特定行值的。 + +- 二者作用的对象不同,where子句作用于表和视图,having作用于组。 + +- WHERE在数据分组前进行过滤,HAVING在数据分组后进行过滤。 + +```mysql +SELECT vend_id, COUNT(*) AS num_prods +FROM products +WHERE prod_price >= 10 +GROUP BY vend_id +HAVING COUNT(*) >= 2; +``` + +WHERE子句过滤所有prod_price至少为10的行。然后按vend_id分组数据,HAVING子句过滤计数为2或2以上的分组。 diff --git a/docs/database/mysql-basic/6-join.md b/docs/database/mysql-basic/6-join.md new file mode 100644 index 0000000..ff91f74 --- /dev/null +++ b/docs/database/mysql-basic/6-join.md @@ -0,0 +1,92 @@ +# 连接 +## 内连接 + +找出供应商生产的产品。 + +```mysql +SELECT vend_name, prod_name +FROM vendors INNER JOIN products +ON vendors.vend_id = products.vend_id; #连接条件使用on子句 +``` + +等价于: + +```mysql +SELECT vend_name, prod_name +FROM vendors, products +WHERE vendors.vend_id = products.vend_id; +``` + +没有给出连接条件的话,会得到两张表的笛卡尔积。 + +## 自连接 + +找出生产nike的供应商生产的所有物品。 + +```mysql +SELECT prod_id, prod_name +FROM products AS p1, products AS p2 +WHERE p1.vend_id = p2.vend_id + AND p2.prod_id = 'nike'; +``` + +## 自然连接 + +natural join是对两张表中字段名和数据类型都相同的字段进行**等值连接**,并返回符合条件的结果 。 + +```mysql +SELECT * FROM role NATURAL JOIN user_role; +``` + +返回结果: + +![](http://img.topjavaer.cn/img/20220530235619.png) + +## 内连接 + +显示符合连接条件的记录。没有设置连接条件则返回笛卡尔积的结果。join 默认是 inner join。 + +```mysql +SELECT * FROM role INNNER JOIN user_role +``` + +返回结果: + +![](http://img.topjavaer.cn/img/20220530235640.png) + +join…using(column)按指定的属性做等值连接。 +join…on tableA.column1 = tableB.column2 指定条件。 + +```mysql +SELECT * FROM role INNER JOIN user_role ON role.role_id = user_role.role_id +``` + +返回结果: + +![](http://img.topjavaer.cn/img/20220530235654.png) + +## 外连接 + +左外联接(Left Outer Join):除了匹配2张表中相关联的记录外,还会匹配左表中剩余的记录,右表中未匹配到的字段用NULL表示。 +右外联接(Right Outer Join):除了匹配2张表中相关联的记录外,还会匹配右表中剩余的记录,左表中未匹配到的字段用NULL表示。 +在判定左表和右表时,要根据表名出现在Outer Join的左右位置关系。 + +查找所有客户及其订单,包括没有下过订单的客户。使用左外连接,保留左边表的所有记录。 + +```mysql +SELECT customer.cust_id, order.order_num +FROM customers LEFT OUTER JOIN orders +ON customers.cust_id = order.cust_id; +``` + +## 多表连接 + +```mysql +SELECT goal.player, eteam.teamname, game.stadium, game.mdate +FROM game JOIN goal +ON game.id = goal.matchid +JOIN eteam +ON eteam.id = goal.teamid +WHERE eteam.id = 'GRE' +``` + diff --git a/docs/database/mysql-basic/7-full-text-query.md b/docs/database/mysql-basic/7-full-text-query.md new file mode 100644 index 0000000..f122e0e --- /dev/null +++ b/docs/database/mysql-basic/7-full-text-query.md @@ -0,0 +1,39 @@ +# 全文搜索 +为了进行全文本搜索,必须索引被搜索的列,而且要随着数据的改变不断地重新索引。在对表列进行适当设计后,MySQL会自动进行所有的索引和重新索引。 + +启动全文搜索(仅在MyISAM数据库引擎中支持全文本搜索): + +```mysql +CREATE TABLE productnotes +( + note_id int NOT NULL AUTO_INCREMENT, + note_text text NULL, + PRIMARY KEY(note_id), + FULLTEXT(note_text) +) ENGINE=MyISAM; +``` + +在定义之后,MySQL自动维护该索引。在增加、更新或删除行时,索引随之自动更新。 + +不要在导入数据时使用FULLTEXT,。应该首先导入所有数据,然后再修改表,定义FULLTEXT,这样可以更快导入数据。 + +使用全文搜索: + +```mysql +SELECT note_text +FROM productnotes +WHERE Match(note_text) Against('shoe'); #Match指定搜索列,Against指定搜索词 +``` + +返回结果:`nike shoes is good`,搜索不区分大小写。 + +全文搜索会对返回结果进行排序,具有高等级的行先返回: + +```mysql +SELECT note_text, + Match(note_text) Against('shoes') AS rank #等级由MySQL根据行中词的数目、唯一词的数目、整个索引中词的总数以及包含该词的行的数目计算出来。 +FROM productnotes; +``` + +全文搜索数据是有索引的,速度快。 + diff --git a/docs/database/mysql-basic/8-table-operate.md b/docs/database/mysql-basic/8-table-operate.md new file mode 100644 index 0000000..e3f6fd9 --- /dev/null +++ b/docs/database/mysql-basic/8-table-operate.md @@ -0,0 +1,105 @@ +# 表操作 + +## 创建表 + +```mysql +CREATE TABLE productnotes +( + note_id int NOT NULL AUTO_INCREMENT, + note_text text NULL, + quanlity int NOT NULL DEFAULT 1, # 默认值,只支持常量 + PRIMARY KEY(note_id), + FULLTEXT(note_text) +) ENGINE=MyISAM; +``` + +主键中只能使用NOT NULL值的列。 + +## 更新表 + +数据库表的更改不能撤销,应先做好备份。 + +添加列: + +```mysql +ALTER TABLE vendors +ADD vend_phone CHAR(20); +``` + +删除列: + +```mysql +ALTER TABLE vendors +DROP COLUMN vend_phone; +``` + +更改列属性: + +```mysql +ALTER TABLE vendors +MODIFY vend_phone CHAR(16); +``` + +复杂的表结构更改一般需要手动删除过程: + +- 用新的列布局创建一个新表; + +- 使用INSERT SELECT语句,从旧表复制数据到新表; + +- 检验包含所需数据的新表; +- 重命名旧表(如果确定,可以删除它); + +- 用旧表原来的名字重命名新表; + +- 重新创建触发器、存储过程、索引和外键。 + +## 约束 + +添加主键约束: + +```mysql +ALTER TABLE vendors +ADD CONSTRAINT pk_vendors PRIMARY KEY(vend_id); +``` + +删除主键约束: + +```mysql +ALTER TABLE vendors +DROP PRIMARY KEY; +``` + +添加外键约束: + +```mysql +ALTER TABLE products +ADD FOREIGN KEY(vendor_id) REFERENCES vendors(vendor_id); +``` + +删除外键约束: + +```mysql +ALTER TABLE products DROP FOREIGN KEY vendor_id; +``` + +## 删除表 + +`DROP TABLE cumstomers` + +## truncate、delete与drop区别 + +**相同点:** + +1. truncate和不带where子句的delete、以及drop都会删除表内的数据。 + +2. drop、truncate都是DDL语句(数据定义语言),执行后会自动提交。 + +**不同点:** + +1. truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引; + +2. 速度,一般来说: drop> truncate > delete。 + +## 重命名表 + +`RENAME TABLE cusmtomers TO cust` diff --git a/docs/database/mysql-basic/9-column-operate.md b/docs/database/mysql-basic/9-column-operate.md new file mode 100644 index 0000000..536c1ee --- /dev/null +++ b/docs/database/mysql-basic/9-column-operate.md @@ -0,0 +1,48 @@ +# 列操作 + +## 插入数据 + +MySQL用单条INSERT语句处理多个插入比使用多条INSERT语句快。 + +```mysql +INSERT INTO customers(cust_name, cust_city) + VALUES('Tyson', 'GD'), + ('sophia','GZ'); +``` + +INSERT操作可能很耗时(特别是有很多索引需要更新时),而且它可能降低等待处理的SELECT语句的性能。降低INSERT语句的优先级:`INSERT LOW_PRIORITY INTO` + +## 更新数据 + +如果用UPDATE语句更新多行,并且在更新这些行中的一行或多行时出一个现错误,则整个UPDATE操作被取消。为了在发生错误时也继续进行更新,可使用IGNORE关键字:`UPDATE IGNORE customers...` + +```mysql +UPDATE customers +SET cust_city = NULL +WHERE cust_id = 1005; +``` + +返回值是受影响的记录数。 + +## 删除数据 + +如果想从表中删除所有行,不要使用DELETE。可使用TRUNCATE TABLE语句,它完成相同的工作,但速度更快(TRUNCATE实际是删除原来的表并重新创建一个表,而不是逐行删除表中的数据)。 + +```mysql +DELETE FROM customers +WHERE cust_id = 1006; +``` + +如果执行DELETE语句而不带WHERE子句,表的所有数据都将被删除。MySQL没有撤销操作,应该非常小心地使用UPDATE和DELETE。 + +delete使用别名的时候,要在delete和from间加上删除表的别名。 + +```mysql +DELETE a #加上删除表的别名 +FROM table1 a +WHERE a.status = 0 +AND EXISTS + (SELECT b.id FROM table2 b + WHERE b.id = a.id); +``` + diff --git a/docs/database/mysql-basic/README.md b/docs/database/mysql-basic/README.md new file mode 100644 index 0000000..0625b14 --- /dev/null +++ b/docs/database/mysql-basic/README.md @@ -0,0 +1,31 @@ +--- +title: MySQL基础 +icon: linux +date: 2022-08-06 +category: MySQL +star: true +--- + +**本专栏是大彬学习MySQL基础知识的学习笔记,如有错误,可以在评论区指出**~ + +![](http://img.topjavaer.cn/img/MySQL知识点总结.jpg) + +## MySQL基础总结 + +- [数据类型](./1-data-type.md) +- [基本命令](./2-basic-command.md) +- [函数](./3-function.md) +- [聚集函数](./4-sum.md) +- [分组](./5-group.md) +- [连接](./6-join.md) +- [全文搜索](./7-full-text-query.md) +- [表操作](./8-table-operate.md) +- [列操作](./9-column-operate.md) +- [视图](./10-view.md) +- [存储过程](./11-procedure.md) +- [游标](./12-cursor.md) +- [触发器](./13-trigger.md) +- [事务处理](./14-transaction.md) +- [权限](./15-permission.md) +- [性能优化](./16-performace-optimization.md) +- [索引](./17-index.md) diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL\346\211\247\350\241\214\350\256\241\345\210\222.md" b/docs/database/mysql-execution-plan.md similarity index 83% rename from "\346\225\260\346\215\256\345\272\223/MySQL\346\211\247\350\241\214\350\256\241\345\210\222.md" rename to docs/database/mysql-execution-plan.md index eaffa77..c67de0e 100644 --- "a/\346\225\260\346\215\256\345\272\223/MySQL\346\211\247\350\241\214\350\256\241\345\210\222.md" +++ b/docs/database/mysql-execution-plan.md @@ -1,38 +1,6 @@ - - - - -- [id](#id) -- [select_type](#select_type) -- [table](#table) -- [partitions](#partitions) -- [type](#type) - - [system](#system) - - [const](#const) - - [eq_ref](#eq_ref) - - [ref](#ref) - - [ref_or_null](#ref_or_null) - - [index_merge](#index_merge) - - [range](#range) - - [index](#index) - - [all](#all) -- [possible_keys](#possible_keys) -- [key](#key) -- [ref](#ref-1) -- [rows](#rows) -- [filtered](#filtered) -- [extra](#extra) - - [using where](#using-where) - - [using index](#using-index) - - [Using where&Using index](#using-whereusing-index) - - [null](#null) - - [using index condition](#using-index-condition) - - [using temporary](#using-temporary) - - [filesort](#filesort) - - [using join buffer](#using-join-buffer) -- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - +--- +sidebar: heading +--- 使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息: @@ -91,7 +59,7 @@ where blog_id = ( 三个表依次嵌套,发现最里层的子查询 `id`最大,最先执行。 -![](http://img.dabin-coder.cn/image/explain-id.png) +![](http://img.topjavaer.cn/img/explain-id.png) ## select_type @@ -107,13 +75,13 @@ where blog_id = ( 查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表。当from子句中有子查询时,table列是 ``的格式,表示当前查询依赖 id为N的查询,会先执行 id为N的查询。 -![](http://img.dabin-coder.cn/image/image-20210804083523885.png) +![](http://img.topjavaer.cn/img/image-20210804083523885.png) ## partitions 查询时匹配到的分区信息,对于非分区表值为`NULL`,当查询的是分区表时,`partitions`显示分区表命中的分区情况。 -![](http://img.dabin-coder.cn/image/image-20210802022931773.png) +![](http://img.topjavaer.cn/img/image-20210802022931773.png) ## type @@ -123,25 +91,25 @@ where blog_id = ( 当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。比如,Mysql系统表proxies_priv在Mysql服务启动时候已经加载在内存中,对这个表进行查询不需要进行磁盘 IO。 -![](http://img.dabin-coder.cn/image/image-20210801233419732.png) +![](http://img.topjavaer.cn/img/image-20210801233419732.png) ### const 单表操作的时候,查询使用了主键或者唯一索引。 -![](http://img.dabin-coder.cn/image/explain-const.png) +![](http://img.topjavaer.cn/img/explain-const.png) ### eq_ref **多表关联**查询的时候,主键和唯一索引作为关联条件。如下图的sql,对于user表(外循环)的每一行,user_role表(内循环)只有一行满足join条件,只要查找到这行记录,就会跳出内循环,继续外循环的下一轮查询。 -![](http://img.dabin-coder.cn/image/image-20210801232638027.png) +![](http://img.topjavaer.cn/img/image-20210801232638027.png) ### ref 查找条件列使用了索引而且不为主键和唯一索引。虽然使用了索引,但该索引列的值并不唯一,这样即使使用索引查找到了第一条数据,仍然不能停止,要在目标值附近进行小范围扫描。但它的好处是不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内做扫描。 -![](http://img.dabin-coder.cn/image/explain-ref.png) +![](http://img.topjavaer.cn/img/explain-ref.png) ### ref_or_null @@ -151,13 +119,13 @@ where blog_id = ( 使用了索引合并优化方法,查询使用了两个以上的索引。新建comment表,id为主键,value_id为非唯一索引,执行`explain select content from comment where value_id = 1181000 and id > 1000;`,执行结果显示查询同时使用了id和value_id索引,type列的值为index_merge。 -![](http://img.dabin-coder.cn/image/image-20210802001215614.png) +![](http://img.topjavaer.cn/img/image-20210802001215614.png) ### range 有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。像between、and、'>'、'<'、in和or都是范围索引扫描。 -![](http://img.dabin-coder.cn/image/explain-range.png) +![](http://img.topjavaer.cn/img/explain-range.png) ### index @@ -165,17 +133,17 @@ index包括select索引列,order by主键两种情况。 1. order by主键。这种情况会按照索引顺序全表扫描数据,拿到的数据是按照主键排好序的,不需要额外进行排序。 - ![](http://img.dabin-coder.cn/image/image-20210801225045980.png) + ![](http://img.topjavaer.cn/img/image-20210801225045980.png) 2. select索引列。type为index,而且extra字段为using index,也称这种情况为索引覆盖。所需要取的数据都在索引列,无需回表查询。 - ![](http://img.dabin-coder.cn/image/image-20210801225942948.png) + ![](http://img.topjavaer.cn/img/image-20210801225942948.png) ### all 全表扫描,查询没有用到索引,性能最差。 -![](http://img.dabin-coder.cn/image/explain-all.png) +![](http://img.topjavaer.cn/img/explain-all.png) ## possible_keys @@ -249,13 +217,13 @@ CREATE TABLE `t_orderdetail` ( 查询的列未被索引覆盖,where筛选条件非索引的前导列。对存储引擎返回的结果进行过滤(Post-filter,后过滤),一般发生在MySQL服务器,而不是存储引擎层。 -![](http://img.dabin-coder.cn/image/image-20210802232729417.png) +![](http://img.topjavaer.cn/img/image-20210802232729417.png) ### using index 查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过**索引查找**就能直接找到符合条件的数据,不需要回表查询数据。 -![](http://img.dabin-coder.cn/image/image-20210802232357282.png) +![](http://img.topjavaer.cn/img/image-20210802232357282.png) ### Using where&Using index @@ -265,17 +233,17 @@ CREATE TABLE `t_orderdetail` ( - where筛选条件不符合最左前缀原则 - ![](http://img.dabin-coder.cn/image/image-20210802233120283.png) + ![](http://img.topjavaer.cn/img/image-20210802233120283.png) - where筛选条件是索引列前导列的一个范围 - ![](http://img.dabin-coder.cn/image/image-20210802233455880.png) + ![](http://img.topjavaer.cn/img/image-20210802233455880.png) ### null 查询的列未被索引覆盖,并且where筛选条件是索引的前导列,也就是用到了索引,但是部分字段未被索引覆盖,必须回表查询这些字段,Extra中为NULL。 -![](http://img.dabin-coder.cn/image/image-20210802234122321.png) +![](http://img.topjavaer.cn/img/image-20210802234122321.png) ### using index condition @@ -283,11 +251,11 @@ CREATE TABLE `t_orderdetail` ( 不使用ICP的情况(`set optimizer_switch='index_condition_pushdown=off'`),如下图,在步骤4中,没有使用where条件过滤索引: -![](http://img.dabin-coder.cn/image/no-icp.png) +![](http://img.topjavaer.cn/img/no-icp.png) 使用ICP的情况(`set optimizer_switch='index_condition_pushdown=on'`): -![](http://img.dabin-coder.cn/image/icp.png) +![](http://img.topjavaer.cn/img/icp.png) 下面的例子使用了ICP: @@ -296,11 +264,11 @@ explain select user_id, order_id, order_status from t_order where user_id > 1 and user_id < 5\G; ``` -![](http://img.dabin-coder.cn/image/image-20210803084617433.png) +![](http://img.topjavaer.cn/img/image-20210803084617433.png) 关掉ICP之后(`set optimizer_switch='index_condition_pushdown=off'`),可以看到extra列为using where,不会使用索引下推。 -![](http://img.dabin-coder.cn/image/image-20210803084815503.png) +![](http://img.topjavaer.cn/img/image-20210803084815503.png) ### using temporary @@ -314,7 +282,7 @@ from t_order where user_id > 1 and user_id < 5\G; - select 查询字段不全是索引字段 - select 查询字段都是索引字段,但是 order by 字段和索引字段的顺序不一致 -![](http://img.dabin-coder.cn/image/image-20210804084029239.png) +![](http://img.topjavaer.cn/img/image-20210804084029239.png) ### using join buffer diff --git a/docs/database/mysql-lock.md b/docs/database/mysql-lock.md new file mode 100644 index 0000000..230db1b --- /dev/null +++ b/docs/database/mysql-lock.md @@ -0,0 +1,184 @@ +--- +sidebar: heading +title: MySQL锁相关面试题 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,MySQL锁面试题,MySQL锁,意向锁,全局锁,排他锁,乐观锁,悲观锁 + - - meta + - name: description + content: MySQL锁常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# MySQL锁相关面试题 + +## 为什么需要加锁 + +如果有多个并发请求存取数据,在数据就可能会产生多个事务同时操作同一行数据。如果并发操作不加控制,不加锁的话,就可能写入了不正确的数据,或者导致读取了不正确的数据,破坏了数据的一致性。因此需要考虑加锁。 + +## 表级锁和行级锁有什么区别? + +MyISAM 仅仅支持表级锁,一锁就锁整张表,这在并发写的情况下性非常差。 + +InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。 + +**表级锁和行级锁对比** : + +- **表级锁:** MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。 +- **行级锁:** MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 + +## 共享锁和排他锁有什么区别? + +不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类: + +- **共享锁(S 锁)** :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。 +- **排他锁(X 锁)** :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。 + +排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。 + +| | S 锁 | X 锁 | +| ---- | ------ | ---- | +| S 锁 | 不冲突 | 冲突 | +| X 锁 | 冲突 | 冲突 | + +由于 MVCC 的存在,对于一般的 `SELECT` 语句,InnoDB 不会加任何锁。不过, 你可以通过以下语句显式加共享锁或排他锁。 + +```sql +# 共享锁 +SELECT ... LOCK IN SHARE MODE; +# 排他锁 +SELECT ... FOR UPDATE; +``` + +## 意向锁有什么作用? + +如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到一个叫做意向锁的东东来快速判断是否可以对某个表使用表锁。 + +意向锁是表级锁,共有两种: + +- **意向共享锁(Intention Shared Lock,IS 锁)**:事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。 +- **意向排他锁(Intention Exclusive Lock,IX 锁)**:事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。 + +意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。 + +意向锁之间是互相兼容的。 + +| | IS 锁 | IX 锁 | +| ----- | ----- | ----- | +| IS 锁 | 兼容 | 兼容 | +| IX 锁 | 兼容 | 兼容 | + +意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。 + +| | IS 锁 | IX 锁 | +| ---- | ----- | ----- | +| S 锁 | 兼容 | 互斥 | +| X 锁 | 互斥 | 互斥 | + +## InnoDB 有哪几类行锁? + +**按锁粒度分类**,有行级锁、表级锁和页级锁。 + +1. 行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁的类型主要有三类: + - Record Lock,记录锁,也就是仅仅把一条记录锁上; + - Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身; + - Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 +2. 表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。 +3. 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。 + +**按锁级别分类**,有共享锁、排他锁和意向锁。 + +1. 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 +2. 排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。 +3. 意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁: + +意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁; + +意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。 + +意向锁是 InnoDB 自动加的,不需要用户干预。 + +对于INSERT、UPDATE和DELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。 + +共享锁:`SELECT … LOCK IN SHARE MODE;` + +排他锁:`SELECT … FOR UPDATE;` + +## 什么是死锁?如何防止死锁? + +**什么是死锁?** + +死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 + +**如何防止死锁?** + +- 尽量约定固定的顺序访问表,因为交叉访问更容易造成事务等待回路。 +- 尽量避免大事务,建议拆成多个小事务。因为大事务占用的锁资源越多,越容易出现死锁。 +- 降低数据库隔离级别,比如RR降低为RC,因为RR隔离级别,存在GAP锁,死锁概率大很多。 +- 死锁与索引是密不可分的,合理优化你的索引,死锁概率降低。 +- 如果业务处理不好可以用分布式事务锁或者使用乐观锁 + +## 如何处理死锁? + +通过innodblockwait_timeout来设置超时时间,一直等待直到超时。 + +发起死锁检测,发现死锁之后,主动回滚死锁中的事务,不需要其他事务继续。 + +## 什么是全局锁?它的应用场景有哪些? + +全局锁就是对整个数据库实例加锁,它的典型使用场景就是做全库逻辑备份,这个命令可以使用整个库处于只读状态,使用该命令之后,数据更新语句,数据定义语句,更新类事务的提交语句等操作都会被阻塞。 + +## 使用全局锁会导致的问题? + +如果在主库备份,在备份期间不能更新,业务停止,所以更新业务会处于等待状态。 + +如果在从库备份,在备份期间不能执行主库同步的binlog,导致主从延迟。 + +## 乐观锁和悲观锁是什么? + +数据库中的并发控制是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观锁和悲观锁是并发控制主要采用的技术手段。 + +* 悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。 +* 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加`version`字段,在修改提交之前检查`version`与原来取到的`version`值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或`CAS`算法实现。 + +## select for update加的是表锁还是行锁 + +需要情况讨论:RR和RC隔离级别,还有查询条件(唯一索引、主键、一般索引、无索引) + +**在RC隔离级别下** + +- 如果查询条件是唯一索引,会加`IX`意向排他锁(表级别的锁,不影响插入)、两把`X`排他锁(行锁,分别对应唯一索引,主键索引) +- 如果查询条件是主键,会加`IX`意向排他锁(表级别的锁,不影响插入)、一把对应主键的`X`排他锁(行锁,会锁住主键索引那一行)。 +- 如果查询条件是普通索引,**如果查询命中记录**,会加`IX`意向排他锁(表锁)、两把`X`排他锁(行锁,分别对应普通索引的`X`锁,对应主键的`X`锁);**如果没有命中数据库表的记录**,只加了一把`IX`意向排他锁(表锁,不影响插入) +- 如果查询条件是无索引,会加两把锁,IX意向排他锁(表锁)、一把X排他锁(行锁,对应主键的X锁)。 + +> 查询条件是无索引,为什么不锁表呢? MySQL会走聚簇(主键)索引进行全表扫描过滤。每条记录都会加上X锁。但是,为了效率考虑,MySQL在这方面进行了改进,在扫描过程中,若记录不满足过滤条件,会进行解锁操作。同时优化违背了2PL原则。 + +**在RR隔离级别** + +- 如果查询条件是唯一索引,命中数据库表记录时,一共会加三把锁:一把IX意向排他锁 (表锁,不影响插入),一把对应主键的X排他锁(行锁),一把对应唯一索引的X排他锁 (行锁)。 +- 如果查询条件是主键,会加`IX`意向排他锁(表级别的锁,不影响插入)、一把对应主键的`X`排他锁(行锁,会锁住主键索引那一行)。 +- 如果查询条件是普通索引,命中查询记录的话,除了会加X锁(行锁),IX锁(表锁,不影响插入),还会加Gap 锁(间隙锁,会影响插入)。 +- 如果查询条件是无索引,会加一个IX锁(表锁,不影响插入),每一行实际记录行的X锁,还有对应于supremum pseudo-record的虚拟全表行锁。这种场景,通俗点讲,其实就是锁表了。 + +> 参考链接:https://juejin.cn/post/7199666255884009532 + +## 优化锁方面有什么建议? + +- 尽量使用较低的隔离级别。 + +- 精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会。 + +- 选择合理的事务大小,小事务发生锁冲突的几率也更小。 + +- 给记录集显示加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁。 + +- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会。 + +- 不要申请超过实际需要的锁级别。 + +- 除非必须,查询时不要显示加锁。 MySQL 的 MVCC 可以实现事务中的查询不用加锁,优化事务性能; + diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" b/docs/database/mysql.md similarity index 68% rename from "\346\225\260\346\215\256\345\272\223/MySQL\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" rename to docs/database/mysql.md index 123d7cc..c8b0032 100644 --- "a/\346\225\260\346\215\256\345\272\223/MySQL\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" +++ b/docs/database/mysql.md @@ -1,3 +1,35 @@ +--- +sidebar: heading +title: MySQL常见面试题总结 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,事务特性,事务隔离级别,MySQL编码和字符集,MySQL索引,索引分类,最左匹配原则,聚集索引,覆盖索引,索引失效,索引下推,存储引擎,MVCC原理,MySQL日志,MySQL分区,MySQL主从同步,MySQL深分页,MySQL慢查询,分库分表,乐观锁和悲观锁 + - - meta + - name: description + content: MySQL常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 更新记录 + +- 2024.06.05,更新[MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](###MySQL查询 limit 1000,10 和limit 10 速度一样快吗?) + +- 2024.5.15,新增[B树和B+树的区别?](###B树和B+树的区别?) + +## 什么是MySQL + +MySQL是一个关系型数据库,它采用表的形式来存储数据。你可以理解成是Excel表格,既然是表的形式存储数据,就有表结构(行和列)。行代表每一行数据,列代表该行中的每个值。列上的值是有数据类型的,比如:整数、字符串、日期等等。 + ## 事务的四大特性? **事务特性ACID**:**原子性**(`Atomicity`)、**一致性**(`Consistency`)、**隔离性**(`Isolation`)、**持久性**(`Durability`)。 @@ -126,25 +158,21 @@ utf8 就像是阉割版的utf8mb4,只支持部分字符。比如`emoji`表情 而mysql支持的字符集里,第三列,**collation**,它是指**字符集的比较规则**。 -比如,**"debug"和"Debug"**是同一个单词,但它们大小写不同,该不该判为同一个单词呢。 +比如,"debug"和"Debug"是同一个单词,但它们大小写不同,该不该判为同一个单词呢。 这时候就需要用到collation了。 通过`SHOW COLLATION WHERE Charset = 'utf8mb4';`可以查看到`utf8mb4`下支持什么比较规则。 -![](http://img.dabin-coder.cn/image/20220527084528.png) +![](http://img.topjavaer.cn/img/20220527084528.png) 如果`collation = utf8mb4_general_ci`,是指使用utf8mb4字符集的前提下,**挨个字符进行比较**(`general`),并且不区分大小写(`_ci,case insensitice`)。 -这种情况下,**"debug"和"Debug"是同一个单词**。 +这种情况下,"debug"和"Debug"是同一个单词。 如果改成`collation=utf8mb4_bin`,就是指**挨个比较二进制位大小**。 -于是**"debug"和"Debug"就不是同一个单词。** - -如果改成`collation=utf8mb4_bin`,就是指**挨个比较二进制位大小**。 - -于是**"debug"和"Debug"就不是同一个单词。** +于是"debug"和"Debug"就不是同一个单词。 **那utf8mb4对比utf8有什么劣势吗?** @@ -158,7 +186,9 @@ utf8 就像是阉割版的utf8mb4,只支持部分字符。比如`emoji`表情 ### 什么是索引? -索引是存储引擎用于提高数据库表的访问速度的一种**数据结构**。 +索引是存储引擎用于提高数据库表的访问速度的一种**数据结构**。它可以比作一本字典的目录,可以帮你快速找到对应的记录。 + +索引一般存储在磁盘的文件中,它是占用物理空间的。 ### 索引的优缺点? @@ -201,7 +231,7 @@ B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B 在 B+ 树中,节点中的 `key` 从左到右递增排列,如果某个指针的左右相邻 `key` 分别是 keyi 和 keyi+1,则该指针指向节点的所有 `key` 大于等于 keyi 且小于等于 keyi+1。 -![](http://img.dabin-coder.cn/image/B+树索引0.png) +![](http://img.topjavaer.cn/img/B+树索引0.png) 进行查找操作时,首先在根节点进行二分查找,找到`key`所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出`key`所对应的数据项。 @@ -238,6 +268,18 @@ Index_comment: - 哈希索引**不支持模糊查询**及多列索引的最左前缀匹配。 - 因为哈希表中会**存在哈希冲突**,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。 +### B树和B+树的区别? + +![](http://img.topjavaer.cn/img/202405150843832.png) + +![](http://img.topjavaer.cn/img/202405150843657.png) + +1. **数据存储方式**:在B树中,每个节点都包含键和对应的值,叶子节点存储了实际的数据记录;而B+树中,仅仅只有叶子节点存储了实际的数据记录,非叶子节点只包含键信息和指向子节点的指针。 +2. **数据检索方式**:在B树中,由于非叶子节点也存储了数据,所以查询时可以直接在非叶子节点找到对应的数据,具有更短的查询路径; + 而B+树的所有数据都存储在叶子节点上,只有通过子节点才能获取到完整的数据。 +3. **范围查询效率**:由于B+树的所有数据都存储在叶子节点上,并且叶子节点之间使用链表连接,所以范围查询的效率比较高。而在B树中,范围查询需要通过遍历多个层级的节点,效率相对较低。 +4. **适用场景**:B树适合进行随机读写操作,因为每个节点都包含了数据。而B+树适合进行范围查询和顺序访问,因为数据都存储在叶子节点上,并且叶子节点之间使用链表连接,便于进行顺序遍历。 + ### 为什么B+树比B树更适合实现数据库索引? - 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。 @@ -273,7 +315,7 @@ ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...); 如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。直接执行`b = 2`这种查询条件无法使用索引。 -![最左前缀](http://img.dabin-coder.cn/image/最左前缀.png) +![最左前缀](http://img.topjavaer.cn/img/最左前缀.png) 当a的值确定的时候,b是有序的。例如`a = 1`时,b值为1,2是有序的状态。当`a = 2`时候,b的值为1,4也是有序状态。 当执行`a = 1 and b = 2`时a和b字段能用到索引。而执行`a > 1 and b = 2`时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段无法使用索引。 @@ -307,13 +349,17 @@ explain select user_id from user_like where blog_id = 1; `explain`结果的`Extra`列为`Using where; Using index`, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过**索引扫描**找到符合条件的数据,也不需要回表查询数据。 -![](http://img.dabin-coder.cn/image/cover-index.png) +![](http://img.topjavaer.cn/img/cover-index.png) ### 索引的设计原则? +- 对于经常作为查询条件的字段,应该建立索引,以提高查询速度 +- 为经常需要排序、分组和联合操作的字段建立索引 - 索引列的**区分度越高**,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。 +- 避免给"大字段"建立索引。尽量使用数据量小的字段作为索引。因为`MySQL`在维护索引的时候是会将字段值一起维护的,那这样必然会导致索引占用更多的空间,另外在排序的时候需要花费更多的时间去对比。 - 尽量使用**短索引**,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。 - 索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。 +- 频繁增删改的字段不要建立索引。假设某个字段频繁修改,那就意味着需要频繁的重建索引,这必然影响MySQL的性能 - 利用**最左前缀原则**。 ### 索引什么时候会失效? @@ -391,15 +437,46 @@ ARCHIVE存储引擎非常适合存储大量独立的、作为历史记录的数 ## MyISAM和InnoDB的区别? -1. **是否支持行级锁** : `MyISAM` 只有表级锁,而`InnoDB` 支持行级锁和表级锁,默认为行级锁。 +1. **存储结构的区别**。每个MyISAM在磁盘上存储成三个文件。文件的名字以表的名字开始,扩展名指出文件类型。 .frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。 +2. **存储空间的区别**。MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。InnoDB需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。 +3. **可移植性、备份及恢复**。MyISAM数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。对于InnoDB,可行的方案是拷贝数据文件、备份 binlog,或者用mysqldump,在数据量达到几十G的时候就相对麻烦了。 +4. **是否支持行级锁**。MyISAM 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。而InnoDB 支持行级锁和表级锁,默认为行级锁。行锁大幅度提高了多用户并发操作的性能。 +5. **是否支持事务和崩溃后的安全恢复**。 MyISAM 不提供事务支持。而InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。 +6. **是否支持外键**。MyISAM不支持,而InnoDB支持。 +7. **是否支持MVCC**。MyISAM不支持,InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效。 +8. **是否支持聚集索引**。MyISAM不支持聚集索引,InnoDB支持聚集索引。 +9. **全文索引**。MyISAM支持 FULLTEXT类型的全文索引。InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。 +10. **表主键**。MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见)。 +11. **表的行数**。MyISAM保存有表的总行数,如果`select count(*) from table`;会直接取出该值。InnoDB没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了where条件后,MyISAM和InnoDB处理的方式都一样。 -2. **是否支持事务和崩溃后的安全恢复**: `MyISAM` 不提供事务支持。而`InnoDB `提供事务支持,具有事务、回滚和崩溃修复能力。 +## MySQL有哪些锁? -3. **是否支持外键:** `MyISAM`不支持,而`InnoDB`支持。 +**按锁粒度分类**,有行级锁、表级锁和页级锁。 -4. **是否支持MVCC** :`MyISAM`不支持,`InnoDB`支持。应对高并发事务,MVCC比单纯的加锁更高效。 +1. 行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁的类型主要有三类: + - Record Lock,记录锁,也就是仅仅把一条记录锁上; + - Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身; + - Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 +2. 表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。 +3. 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。 -5. `MyISAM`不支持聚集索引,`InnoDB`支持聚集索引。 +**按锁级别分类**,有共享锁、排他锁和意向锁。 + +1. 共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 +2. 排他锁又称写锁、独占锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。 +3. 意向锁是表级锁,其设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。InnoDB 中的两个表锁: + +意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁; + +意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。 + +意向锁是 InnoDB 自动加的,不需要用户干预。 + +对于INSERT、UPDATE和DELETE,InnoDB 会自动给涉及的数据加排他锁;对于一般的SELECT语句,InnoDB 不会加任何锁,事务可以通过以下语句显式加共享锁或排他锁。 + +共享锁:`SELECT … LOCK IN SHARE MODE;` + +排他锁:`SELECT … FOR UPDATE;` ## MVCC 实现原理? @@ -412,12 +489,12 @@ MVCC(`Multiversion concurrency control`) 就是同一份数据保留多版本的 MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。 - `DB_TRX_ID`:当前事务id,通过事务id的大小判断事务的时间顺序。 -- `DB_ROLL_PRT`:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成`undo log`版本链。 -- `DB_ROLL_ID`:主键,如果数据表没有主键,InnoDB会自动生成主键。 +- `DB_ROLL_PTR`:回滚指针,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成`undo log`版本链。 +- `DB_ROW_ID`:主键,如果数据表没有主键,InnoDB会自动生成主键。 每条表记录大概是这样的: -![](http://img.dabin-coder.cn/image/mvcc9.png) +![](http://img.topjavaer.cn/img/mvcc9.png) 使用事务更新行记录的时候,就会生成版本链,执行过程如下: @@ -429,15 +506,15 @@ MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实 1、初始数据如下,其中`DB_ROW_ID`和`DB_ROLL_PTR`为空。 -![](http://img.dabin-coder.cn/image/mvcc2.png) +![](http://img.topjavaer.cn/img/mvcc2.png) 2、事务A对该行数据做了修改,将`age`修改为12,效果如下: -![](http://img.dabin-coder.cn/image/mvcc7.png) +![](http://img.topjavaer.cn/img/mvcc7.png) 3、之后事务B也对该行记录做了修改,将`age`修改为8,效果如下: -![](http://img.dabin-coder.cn/image/mvcc11.png) +![](http://img.topjavaer.cn/img/mvcc11.png) 4、此时undo log有两行记录,并且通过回滚指针连在一起。 @@ -457,7 +534,7 @@ MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实 **前提**:`DATA_TRX_ID` 表示每个数据行的最新的事务ID;`up_limit_id`表示当前快照中的最先开始的事务;`low_limit_id`表示当前快照中的最慢开始的事务,即最后一个事务。 -![](http://img.dabin-coder.cn/image/read_view10.png) +![](http://img.topjavaer.cn/img/read_view10.png) - 如果`DATA_TRX_ID` < `up_limit_id`:说明在创建`read view`时,修改该数据行的事务已提交,该版本的记录可被当前事务读取到。 - 如果`DATA_TRX_ID` >= `low_limit_id`:说明当前版本的记录的事务是在创建`read view`之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的记录对当前事务的可见性。 @@ -482,7 +559,7 @@ MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实 1、首先,user表只有两条记录,具体如下: -![](http://img.dabin-coder.cn/image/image-20210922232259664.png) +![](http://img.topjavaer.cn/img/image-20210922232259664.png) 2、事务a和事务b同时开启事务`start transaction`; @@ -500,7 +577,7 @@ update user set user_name = 'a'; 5、事务b然后执行查询,查到了事务a中插入的数据。(下图左边是事务b,右边是事务a。事务开始之前只有两条记录,事务a插入一条数据之后,事务b查询出来是三条数据) -![](http://img.dabin-coder.cn/image/幻读1.png) +![](http://img.topjavaer.cn/img/幻读1.png) 以上就是当前读出现的幻读现象。 @@ -579,7 +656,7 @@ MySQL主要分为 Server 层和存储引擎层: 垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。 -![](http://img.dabin-coder.cn/image/垂直划分.png) +![](http://img.topjavaer.cn/img/垂直划分.png) **优点**:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。 @@ -593,7 +670,7 @@ MySQL主要分为 Server 层和存储引擎层: 水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。 -![](http://img.dabin-coder.cn/image/水平划分.png) +![](http://img.topjavaer.cn/img/水平划分.png) **优点**:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。 @@ -848,7 +925,7 @@ select * from xxx where id >=(select id from xxx order by id limit 500000, 1) o 在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了 -``` +```mysql select * from xxx where id >=500000 order by id limit 10; ``` @@ -858,12 +935,10 @@ select * from xxx where id >=500000 order by id limit 10; 将所有的数据**根据id主键进行排序**,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。 -``` +```mysql select * from xxx where id > start_id order by id limit 10; ``` -mysql - 通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。 ## 高度为3的B+树,可以存放多少数据? @@ -917,12 +992,244 @@ B+树中**非叶子节点存的是key + 指针**;**叶子节点存的是数据 当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: * 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描 +* 索引优化,SQL优化。索引要符合最左匹配原则等,参考:https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95 * 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能 * 利用缓存。利用Redis等缓存热点数据,提高查询效率 * 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内 * 读写分离。经典的数据库拆分方案,主库负责写,从库负责读 * 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分 +* 数据异构到es +* 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中 +* 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、TiDB等) +## 说说count(1)、count(*)和count(字段名)的区别 + +嗯,先说说count(1) and count(字段名)的区别。 + +两者的主要区别是 + +1. count(1) 会统计表中的所有的记录数,包含字段为null 的记录。 +2. count(字段名) 会统计该字段在表中出现的次数,忽略字段为null 的情况。即不统计字段为null 的记录。 + +接下来看看三者之间的区别。 + +执行效果上: + +- count(*)包括了所有的列,相当于行数,在统计结果的时候,**不会忽略列值为NULL** +- count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,**不会忽略列值为NULL** +- count(字段名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,**即某个字段值为NULL时,不统计**。 + +执行效率上: + +- 列名为主键,count(字段名)会比count(1)快 +- 列名不为主键,count(1)会比count(列名)快 +- 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*) +- 如果有主键,则 select count(主键)的执行效率是最优的 +- 如果表只有一个字段,则 select count(*)最优。 + +## MySQL中DATETIME 和 TIMESTAMP有什么区别? + +嗯,`TIMESTAMP`和`DATETIME`都可以用来存储时间,它们主要有以下区别: + +1.表示范围 + +- DATETIME:1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999 +- TIMESTAMP:'1970-01-01 00:00:01.000000' UTC 到 '2038-01-09 03:14:07.999999' UTC + +`TIMESTAMP`支持的时间范围比`DATATIME`要小,容易出现超出的情况。 + +2.空间占用 + +- TIMESTAMP :占 4 个字节 +- DATETIME:在 MySQL 5.6.4 之前,占 8 个字节 ,之后版本,占 5 个字节 + +3.存入时间是否会自动转换 + +`TIMESTAMP`类型在默认情况下,insert、update 数据时,`TIMESTAMP`列会自动以当前时间(`CURRENT_TIMESTAMP`)填充/更新。`DATETIME`则不会做任何转换,也不会检测时区,你给什么数据,它存什么数据。 + +4.`TIMESTAMP`比较受时区timezone的影响以及MYSQL版本和服务器的SQL MODE的影响。因为`TIMESTAMP`存的是时间戳,在不同的时区得出的时间不一致。 + +5.如果存进NULL,两者实际存储的值不同。 + +- TIMESTAMP:会自动存储当前时间 now() 。 +- DATETIME:不会自动存储当前时间,会直接存入 NULL 值。 + +## 说说为什么不建议用外键? + +外键是一种约束,这个约束的存在,会保证表间数据的关系始终完整。外键的存在,并非全然没有优点。 + +外键可以保证数据的完整性和一致性,级联操作方便。而且使用外键可以将数据完整性判断托付给了数据库完成,减少了程序的代码量。 + +虽然外键能够保证数据的完整性,但是会给系统带来很多缺陷。 + +1、并发问题。在使用外键的情况下,每次修改数据都需要去另外一个表检查数据,需要获取额外的锁。若是在高并发大流量事务场景,使用外键更容易造成死锁。 + +2、扩展性问题。比如从`MySQL`迁移到`Oracle`,外键依赖于数据库本身的特性,做迁移可能不方便。 + +3、不利于分库分表。在水平拆分和分库的情况下,外键是无法生效的。将数据间关系的维护,放入应用程序中,为将来的分库分表省去很多的麻烦。 + +## 使用自增主键有什么好处? + +自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。 + +## 自增主键保存在什么地方? + +不同的引擎对于自增值的保存策略不同: + +- MyISAM引擎的自增值保存在数据文件中。 +- 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。 + +## 自增主键一定是连续的吗? + +不一定,有几种情况会导致自增主键不连续。 + +1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。 + +2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。 + +3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略: + +- 语句执行过程中,第一次申请自增id,分配1个自增id +- 1个用完以后,第二次申请,会分配2个自增id +- 2个用完以后,第三次申请,会分配4个自增id +- 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用) + +如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。 + +4、自增步长不是1,也会导致自增主键不连续。 + +## InnoDB的自增值为什么不能回收利用? + +主要为了提升插入数据的效率和并行度。 + +假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。 + +假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。 + +事务 B 正确提交了,但事务 A 出现了唯一键冲突。 + +如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。 + +接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。 + +而为了解决这个主键冲突,有两种方法: + +- 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。 +- 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。 + +可见,这两个方法都会导致性能问题。 + +因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。 + +## MySQL数据如何同步到Redis缓存? + +> 参考:https://cloud.tencent.com/developer/article/1805755 + +有两种方案: + +1、通过MySQL自动同步刷新Redis,**MySQL触发器+UDF函数**实现。 + +过程大致如下: + +1. 在MySQL中对要操作的数据设置触发器Trigger,监听操作 +2. 客户端向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数 +3. UDF函数可以把数据写入到Redis中,从而达到同步的效果 + +2、**解析MySQL的binlog**,实现将数据库中的数据同步到Redis。可以通过canal实现。canal是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费。 + +canal的原理如下: + +1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议 +2. mysql master收到dump请求,开始推送binary log给canal +3. canal解析binary log对象(原始为byte流),将数据同步写入Redis。 + + + +## 为什么阿里Java手册禁止使用存储过程? + +先看看什么是存储过程。 + +存储过程是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。 + +存储过程主要有以下几个缺点。 + +1. **存储过程难以调试**。存储过程的开发一直缺少有效的 IDE 环境。SQL 本身经常很长,调试式要把句子拆开分别独立执行,非常麻烦。 +2. **移植性差**。存储过程的移植困难,一般业务系统总会不可避免地用到数据库独有的特性和语法,更换数据库时这部分代码就需要重写,成本较高。 +3. **管理困难**。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。 +4. 存储过程是**只优化一次**,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。 + +## MySQL update 是锁行还是锁表? + +首先,InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。 + +1. 当执行update语句时,where中的过滤条件列,如果用到索引,就是锁行;如果无法用索引,就是锁表。 +2. 如果两个update语句同时执行,第一个先执行触发行锁,但是第二个没有索引触发表锁,因为有个行锁住了,所以还是会等待行锁释放,才能锁表。 +3. 当执行insert或者delete语句时,锁行。 + +## select...for update会锁表还是锁行? + +如果查询条件用了索引/主键,那么`select ... for update`就会加行锁。 + +如果是普通字段(没有索引/主键),那么`select ..... for update`就会加表锁。 + +## MySQL的binlog有几种格式?分别有什么区别? + +有三种格式,statement,row和mixed。 + +- statement:每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。 +- row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。 +- mixed:一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。 + +## 阿里手册为什么禁止使用 count(列名)或 count(常量)来替代 count(*) + +先看下这几种方式的区别。 + +count(主键id):InnoDB引擎会遍历整张表,把每一行id值都取出来,返给server层。server层拿到id后,判断是不可能为空的,就按行累加,不再对每个值进行NULL判断。 + +count(常量):InnoDB引擎会遍历整张表,但不取值。server层对于返回的每一行,放一个常量进去,判断是不可能为空的,按行累加,不再对每个值进行NULL判断。count(常量)比count(主键id)执行的要快,因为从引擎放回id会涉及解析数据行,以及拷贝字段值的操作。 + +count(字段):全表扫描,分情况讨论。 + +1、如果参数字段定义NOT NULL,判断是不可能为空的,按行累加,不再对每个值进行NULL判断。 +2、如果参数字段定义允许为NULL,那么执行的时候,判断可能是NULL,还要把值取出来再判断一下,不是NULL才累加。 + +count(*):统计所有的列,相当于行数,统计结果中会包含字段值为null的列; + +COUNT(`*`)是SQL92定义的标准统计行数的语法,效率高,MySQL对它进行了很多优化,MyISAM中会直接把表的总行数单独记录下来供COUNT(*)查询,而InnoDB则会在扫表的时候选择最小的索引来降低成本。 + +所以,建议使用COUNT(\*)查询表的行数! + +## 存储MD5值应该用VARCHAR还是用CHAR? + +首先说说CHAR和VARCHAR的区别: + +1、存储长度: + +CHAR类型的长度是固定的 + +当我们当定义CHAR(10),输入的值是"abc",但是它占用的空间一样是10个字节,会包含7个空字节。当输入的字符长度超过指定的数时,CHAR会截取超出的字符。而且,当存储为CHAR的时候,MySQL会自动删除输入字符串末尾的空格。 + +VARCHAR的长度是可变的 + +比如VARCHAR(10),然后输入abc三个字符,那么实际存储大小为3个字节。 + +除此之外,VARCHAR还会保留1个或2个额外的字节来记录字符串的实际长度。如果定义的最大长度小于等于255个字节,那么,就会预留1个字节;如果定义的最大长度大于255个字节,那么就会预留2个字节。 + +2、存储效率 + +CHAR类型每次修改后的数据长度不变,效率更高。 + +VARCHAR每次修改的数据要更新数据长度,效率更低。 + +3、存储空间 + +CHAR存储空间是初始的预计长度字符串再加上一个记录字符串长度的字节,可能会存在多余的空间。 + +VARCHAR存储空间的时候是实际字符串再加上一个记录字符串长度的字节,占用空间较小。 + + + +根据以上的分析,由于MD5是一个定长的值,所以MD5值适合使用CHAR存储。对于固定长度的非常短的列,CHAR比VARCHAR效率也更高。 -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/database/sharding-id.md b/docs/database/sharding-id.md new file mode 100644 index 0000000..2b4c95d --- /dev/null +++ b/docs/database/sharding-id.md @@ -0,0 +1,169 @@ +## 分库分表之后,ID主键该如何处理? + +其实这是分库分表之后你必然要面对的一个问题,就是 id 怎么生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那就不对了,需要一个**全局唯一**的 id 来支持。所以这都是实际生产环境中必须考虑的问题。 + +### 基于数据库的实现方案 + +#### 数据库自增 id + +这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个 id。拿到这个 id 之后再往对应的分库分表里去写入。 + +这个方案的好处就是方便简单,谁都会用;**缺点就是单库生成**自增 id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前 id 最大值,然后自己递增几个 id,一次性返回一批 id,然后再把当前最大 id 值修改成递增几个 id 之后的一个值;但是**无论如何都是基于单个数据库**。 + +**适合的场景**:分库分表一般就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你**并发不高,但是数据量太大**导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。 + +#### 设置数据库 sequence 或者表自增字段步长 + +可以通过设置数据库 sequence 或者表的自增字段步长来进行水平伸缩。 + +比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。 + +![](http://img.topjavaer.cn/img/database-id-sequence-step.png) + +**适合的场景**:在用户防止产生的 ID 重复时,这种方案实现起来比较简单,也能达到性能目标。但是服务节点固定,步长也固定,将来如果还要增加服务节点,就不好处理了。 + +### UUID + +好处就是本地生成,不要基于数据库来了;不好之处就是,UUID 太长、占用空间大,**作为主键性能太差**了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。 + +适合的场景:如果你是要随机生成个什么文件名、编号之类的,你可以用 UUID,但是作为主键是不能用 UUID 的。 + +```java +UUID.randomUUID().toString().replace("-", "") -> sfsdf23423rr234sfdafCopy to clipboardErrorCopied +``` + +### 获取系统当前时间 + +这个就是获取当前时间即可,但是问题是,**并发很高的时候**,比如一秒并发几千,**会有重复的情况**,这个是肯定不合适的。基本就不用考虑了。 + +适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。 + +### snowflake 算法 + +snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。 + +- 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。 +- 41 bits:表示的是时间戳,单位是毫秒。41 bits 可以表示的数字多达 `2^41 - 1` ,也就是可以标识 `2^41 - 1` 个毫秒值,换算成年就是表示 69 年的时间。 +- 10 bits:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。但是 10 bits 里 5 个 bits 代表机房 id,5 个 bits 代表机器 id。意思就是最多代表 `2^5` 个机房(32 个机房),每个机房里可以代表 `2^5` 个机器(32 台机器)。 +- 12 bits:这个是用来记录同一个毫秒内产生的不同 id,12 bits 可以代表的最大正整数是 `2^12 - 1 = 4096` ,也就是说可以用这个 12 bits 代表的数字来区分**同一个毫秒内**的 4096 个不同的 id。 + +```java +public class IdWorker { + + private long workerId; + private long datacenterId; + private long sequence; + + public IdWorker(long workerId, long datacenterId, long sequence) { + // sanity check for workerId + // 这儿不就检查了一下,要求就是你传递进来的机房id和机器id不能超过32,不能小于0 + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException( + String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException( + String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + System.out.printf( + "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", + timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId); + + this.workerId = workerId; + this.datacenterId = datacenterId; + this.sequence = sequence; + } + + private long twepoch = 1288834974657L; + + private long workerIdBits = 5L; + private long datacenterIdBits = 5L; + + // 这个是二进制运算,就是 5 bit最多只能有31个数字,也就是说机器id最多只能是32以内 + private long maxWorkerId = -1L ^ (-1L << workerIdBits); + + // 这个是一个意思,就是 5 bit最多只能有31个数字,机房id最多只能是32以内 + private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); + private long sequenceBits = 12L; + + private long workerIdShift = sequenceBits; + private long datacenterIdShift = sequenceBits + workerIdBits; + private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + private long sequenceMask = -1L ^ (-1L << sequenceBits); + + private long lastTimestamp = -1L; + + public long getWorkerId() { + return workerId; + } + + public long getDatacenterId() { + return datacenterId; + } + + public long getTimestamp() { + return System.currentTimeMillis(); + } + + public synchronized long nextId() { + // 这儿就是获取当前时间戳,单位是毫秒 + long timestamp = timeGen(); + + if (timestamp < lastTimestamp) { + System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp); + throw new RuntimeException(String.format( + "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + if (lastTimestamp == timestamp) { + // 这个意思是说一个毫秒内最多只能有4096个数字 + // 无论你传递多少进来,这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围 + sequence = (sequence + 1) & sequenceMask; + if (sequence == 0) { + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0; + } + + // 这儿记录一下最近一次生成id的时间戳,单位是毫秒 + lastTimestamp = timestamp; + + // 这儿就是将时间戳左移,放到 41 bit那儿; + // 将机房 id左移放到 5 bit那儿; + // 将机器id左移放到5 bit那儿;将序号放最后12 bit; + // 最后拼接起来成一个 64 bit的二进制数字,转换成 10 进制就是个 long 型 + return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) + | (workerId << workerIdShift) | sequence; + } + + private long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + + // ---------------测试--------------- + public static void main(String[] args) { + IdWorker worker = new IdWorker(1, 1, 1); + for (int i = 0; i < 30; i++) { + System.out.println(worker.nextId()); + } + } + +} +``` + +就是说 41 bit 是当前毫秒单位的一个时间戳;然后 5 bit 是你传递进来的一个**机房** id(但是最大只能是 32 以内),另外 5 bit 是你传递进来的**机器** id(但是最大只能是 32 以内),剩下的那个 12 bit 序列号,就是如果跟你上次生成 id 的时间还在一个毫秒内,那么会把顺序给你累加,最多在 4096 个序号以内。 + +所以利用这个工具类,搞一个服务,然后对每个机房的每个机器都初始化这么一个东西,刚开始这个机房的这个机器的序号就是 0。然后每次接收到一个请求,说这个机房的这个机器要生成一个 id,你就找到对应的 Worker 生成。 + +利用这个 snowflake 算法,你可以开发自己公司的服务,甚至对于机房 id 和机器 id,反正给你预留了 5 bit + 5 bit,你换成别的有业务含义的东西也可以的。 + +这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。 \ No newline at end of file diff --git a/docs/database/sql-optimize.md b/docs/database/sql-optimize.md new file mode 100644 index 0000000..5adea55 --- /dev/null +++ b/docs/database/sql-optimize.md @@ -0,0 +1,376 @@ +--- +sidebar: heading +title: SQL优化相关面试题 +category: 数据库 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL面试题,SQL优化,慢查询优化,索引失效,MySQL执行计划 + - - meta + - name: description + content: SQL优化常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 聊聊explain执行计划 + +使用 explain 输出 SELECT 语句执行的详细信息,包括以下信息: + +- 表的加载顺序 +- sql 的查询类型 +- 可能用到哪些索引,实际上用到哪些索引 +- 读取的行数 + +`Explain` 执行计划包含字段信息如下:分别是 `id`、`select_type`、`table`、`partitions`、`type`、`possible_keys`、`key`、`key_len`、`ref`、`rows`、`filtered`、`Extra` 12个字段。 + +通过explain extended + show warnings可以在原本explain的基础上额外提供一些查询优化的信息,得到优化以后的可能的查询语句(不一定是最终优化的结果)。 + +先搭建测试环境: + +```sql +CREATE TABLE `blog` ( + `blog_id` int NOT NULL AUTO_INCREMENT COMMENT '唯一博文id--主键', + `blog_title` varchar(255) NOT NULL COMMENT '博文标题', + `blog_body` text NOT NULL COMMENT '博文内容', + `blog_time` datetime NOT NULL COMMENT '博文发布时间', + `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `blog_state` int NOT NULL COMMENT '博文状态--0 删除 1正常', + `user_id` int NOT NULL COMMENT '用户id', + PRIMARY KEY (`blog_id`) +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 + +CREATE TABLE `user` ( + `user_id` int NOT NULL AUTO_INCREMENT COMMENT '用户唯一id--主键', + `user_name` varchar(30) NOT NULL COMMENT '用户名--不能重复', + `user_password` varchar(255) NOT NULL COMMENT '用户密码', + PRIMARY KEY (`user_id`), + KEY `name` (`user_name`) +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 + +CREATE TABLE `discuss` ( + `discuss_id` int NOT NULL AUTO_INCREMENT COMMENT '评论唯一id', + `discuss_body` varchar(255) NOT NULL COMMENT '评论内容', + `discuss_time` datetime NOT NULL COMMENT '评论时间', + `user_id` int NOT NULL COMMENT '用户id', + `blog_id` int NOT NULL COMMENT '博文id', + PRIMARY KEY (`discuss_id`) +) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8 +``` + +**id** + +表示查询中执行select子句或者操作表的顺序,**`id`的值越大,代表优先级越高,越先执行**。 + +```mysql +explain select discuss_body +from discuss +where blog_id = ( + select blog_id from blog where user_id = ( + select user_id from user where user_name = 'admin')); +``` + +三个表依次嵌套,发现最里层的子查询 `id`最大,最先执行。 + +![](http://img.topjavaer.cn/img/explain-id.png) + +**select_type** + +表示 `select` 查询的类型,主要是用于区分各种复杂的查询,例如:`普通查询`、`联合查询`、`子查询`等。 + +1. SIMPLE:表示最简单的 select 查询语句,在查询中不包含子查询或者交并差集等操作。 +2. PRIMARY:查询中最外层的SELECT(存在子查询的外层的表操作为PRIMARY)。 +3. SUBQUERY:子查询中首个SELECT。 +4. DERIVED:被驱动的SELECT子查询(子查询位于FROM子句)。 +5. UNION:在SELECT之后使用了UNION。 + +**table** + +查询的表名,并不一定是真实存在的表,有别名显示别名,也可能为临时表。当from子句中有子查询时,table列是 ``的格式,表示当前查询依赖 id为N的查询,会先执行 id为N的查询。 + +![](http://img.topjavaer.cn/img/image-20210804083523885.png) + +**partitions** + +查询时匹配到的分区信息,对于非分区表值为`NULL`,当查询的是分区表时,`partitions`显示分区表命中的分区情况。 + +![](http://img.topjavaer.cn/img/image-20210802022931773.png) + +**type** + +查询使用了何种类型,它在 `SQL`优化中是一个非常重要的指标。 + +**system** + +当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快。比如,Mysql系统表proxies_priv在Mysql服务启动时候已经加载在内存中,对这个表进行查询不需要进行磁盘 IO。 + +![](http://img.topjavaer.cn/img/image-20210801233419732.png) + +**const** + +单表操作的时候,查询使用了主键或者唯一索引。 + +![](http://img.topjavaer.cn/img/explain-const.png) + +**eq_ref** + +**多表关联**查询的时候,主键和唯一索引作为关联条件。如下图的sql,对于user表(外循环)的每一行,user_role表(内循环)只有一行满足join条件,只要查找到这行记录,就会跳出内循环,继续外循环的下一轮查询。 + +![](http://img.topjavaer.cn/img/image-20210801232638027.png) + +**ref** + +查找条件列使用了索引而且不为主键和唯一索引。虽然使用了索引,但该索引列的值并不唯一,这样即使使用索引查找到了第一条数据,仍然不能停止,要在目标值附近进行小范围扫描。但它的好处是不需要扫全表,因为索引是有序的,即便有重复值,也是在一个非常小的范围内做扫描。 + +![](http://img.topjavaer.cn/img/explain-ref.png) + +**ref_or_null** + +类似 ref,会额外搜索包含`NULL`值的行。 + +**index_merge** + +使用了索引合并优化方法,查询使用了两个以上的索引。新建comment表,id为主键,value_id为非唯一索引,执行`explain select content from comment where value_id = 1181000 and id > 1000;`,执行结果显示查询同时使用了id和value_id索引,type列的值为index_merge。 + +![](http://img.topjavaer.cn/img/image-20210802001215614.png) + +**range** + +有范围的索引扫描,相对于index的全索引扫描,它有范围限制,因此要优于index。像between、and、'>'、'<'、in和or都是范围索引扫描。 + +![](http://img.topjavaer.cn/img/explain-range.png) + +**index** + +index包括select索引列,order by主键两种情况。 + +1. order by主键。这种情况会按照索引顺序全表扫描数据,拿到的数据是按照主键排好序的,不需要额外进行排序。 + + ![](http://img.topjavaer.cn/img/image-20210801225045980.png) + +2. select索引列。type为index,而且extra字段为using index,也称这种情况为索引覆盖。所需要取的数据都在索引列,无需回表查询。 + + ![](http://img.topjavaer.cn/img/image-20210801225942948.png) + +**all** + +全表扫描,查询没有用到索引,性能最差。 + +![](http://img.topjavaer.cn/img/explain-all.png) + +**possible_keys** + +此次查询中可能选用的索引。**但这个索引并不定一会是最终查询数据时所被用到的索引**。 + +**key** + +此次查询中确切使用到的索引。 + +**ref** + +`ref` 列显示使用哪个列或常数与`key`一起从表中选择数据行。常见的值有`const`、`func`、`NULL`、具体字段名。当 `key` 列为 `NULL`,即不使用索引时。如果值是`func`,则使用的值是某个函数的结果。 + +以下SQL的执行计划`ref`为`const`,因为使用了组合索引`(user_id, blog_id)`,`where user_id = 13`中13为常量。 + +```mysql +mysql> explain select blog_id from user_like where user_id = 13; ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +| 1 | SIMPLE | user_like | NULL | ref | ul1,ul2 | ul1 | 4 | const | 2 | 100.00 | Using index | ++----+-------------+-----------+------------+------+---------------+------+---------+-------+------+----------+-------------+ +``` + +而下面这个SQL的执行计划`ref`值为`NULL`,因为`key`为`NULL`,查询没有用到索引。 + +```mysql +mysql> explain select user_id from user_like where status = 1; ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +| 1 | SIMPLE | user_like | NULL | ALL | NULL | NULL | NULL | NULL | 6 | 16.67 | Using where | ++----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ +``` + +**rows** + +估算要找到所需的记录,需要读取的行数。评估`SQL` 性能的一个比较重要的数据,`mysql`需要扫描的行数,很直观的显示 `SQL` 性能的好坏,一般情况下 `rows` 值越小越好。 + +**filtered** + +存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例。 + +**extra** + +表示额外的信息说明。为了方便测试,这里新建两张表。 + +```sql +CREATE TABLE `t_order` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int DEFAULT NULL, + `order_id` int DEFAULT NULL, + `order_status` tinyint DEFAULT NULL, + `create_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_userid_order_id_createdate` (`user_id`,`order_id`,`create_date`) +) ENGINE=InnoDB AUTO_INCREMENT=99 DEFAULT CHARSET=utf8 + +CREATE TABLE `t_orderdetail` ( + `id` int NOT NULL AUTO_INCREMENT, + `order_id` int DEFAULT NULL, + `product_name` varchar(100) DEFAULT NULL, + `cnt` int DEFAULT NULL, + `create_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_orderid_productname` (`order_id`,`product_name`) +) ENGINE=InnoDB AUTO_INCREMENT=152 DEFAULT CHARSET=utf8 +``` + +**1.using where** + +查询的列未被索引覆盖,where筛选条件非索引的前导列。对存储引擎返回的结果进行过滤(Post-filter,后过滤),一般发生在MySQL服务器,而不是存储引擎层。 + +![](http://img.topjavaer.cn/img/image-20210802232729417.png) + +**2.using index** + +查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过**索引查找**就能直接找到符合条件的数据,不需要回表查询数据。 + +![](http://img.topjavaer.cn/img/image-20210802232357282.png) + +**3.Using where&Using index** + +查询的列被索引覆盖,但无法通过索引查找找到符合条件的数据,不过可以通过**索引扫描**找到符合条件的数据,也不需要回表查询数据。 + +包括两种情况(组合索引为(user_id, orde)): + +- where筛选条件不符合最左前缀原则 + + ![](http://img.topjavaer.cn/img/image-20210802233120283.png) + +- where筛选条件是索引列前导列的一个范围 + + ![](http://img.topjavaer.cn/img/image-20210802233455880.png) + +**4.null** + +查询的列未被索引覆盖,并且where筛选条件是索引的前导列,也就是用到了索引,但是部分字段未被索引覆盖,必须回表查询这些字段,Extra中为NULL。 + +![](http://img.topjavaer.cn/img/image-20210802234122321.png) + +**5.using index condition** + +索引下推(index condition pushdown,ICP),先使用where条件过滤索引,过滤完索引后找到所有符合索引条件的数据行,随后用 WHERE 子句中的其他条件去过滤这些数据行。 + +不使用ICP的情况(`set optimizer_switch='index_condition_pushdown=off'`),如下图,在步骤4中,没有使用where条件过滤索引: + +![](http://img.topjavaer.cn/img/no-icp.png) + +使用ICP的情况(`set optimizer_switch='index_condition_pushdown=on'`): + +![](http://img.topjavaer.cn/img/icp.png) + +下面的例子使用了ICP: + +```sql +explain select user_id, order_id, order_status +from t_order where user_id > 1 and user_id < 5\G; +``` + +![](http://img.topjavaer.cn/img/image-20210803084617433.png) + +关掉ICP之后(`set optimizer_switch='index_condition_pushdown=off'`),可以看到extra列为using where,不会使用索引下推。 + +![](http://img.topjavaer.cn/img/image-20210803084815503.png) + +**6.using temporary** + +使用了临时表保存中间结果,常见于 order by 和 group by 中。典型的,当group by和order by同时存在,且作用于不同的字段时,就会建立临时表,以便计算出最终的结果集。 + +**7.filesort** + +文件排序。表示无法利用索引完成排序操作,以下情况会导致filesort: + +- order by 的字段不是索引字段 +- select 查询字段不全是索引字段 +- select 查询字段都是索引字段,但是 order by 字段和索引字段的顺序不一致 + +![](http://img.topjavaer.cn/img/image-20210804084029239.png) + +**8.using join buffer** + +Block Nested Loop,需要进行嵌套循环计算。两个关联表join,关联字段均未建立索引,就会出现这种情况。比如内层和外层的type均为ALL,rows均为4,需要循环进行4*4次计算。常见的优化方案是,在关联字段上添加索引,避免每次嵌套循环计算。 + +## 大表查询慢怎么优化? + +某个表有近千万数据,查询比较慢,如何优化? + +当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: + +* 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描 +* 索引优化,SQL优化。最左匹配原则等,参考:https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95 +* 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能 +* 利用缓存。利用Redis等缓存热点数据,提高查询效率 +* 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内 +* 读写分离。经典的数据库拆分方案,主库负责写,从库负责读 +* 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分 +* 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDERBY命令上涉及的列建立索引 + +7. 数据异构到es +8. 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中 +9. 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、tidb) + +## 深分页怎么优化? + +还是以上面的SQL为空:`select * from xxx order by id limit 500000, 10;` + +**方法一**: + +从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,**拷贝完整数据**相比**只拷贝行数据里的其中一两个列字段**更耗费时间。 + +因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成: + +``` +select * from xxx where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10; +``` + +先执行子查询 `select id from xxx by id limit 500000, 1`, 这个操作,其实也是将在innodb中的主键索引中获取到`500000+1`条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。 + +但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。 + +在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了 + +``` +select * from xxx where id >=500000 order by id limit 10; +``` + +这样innodb再走一次**主键索引**,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。 + +**方法二:** + +将所有的数据**根据id主键进行排序**,然后分批次取,将当前批次的最大id作为下次筛选的条件进行查询。 + +```mysql +select * from xxx where id > start_id order by id limit 10; +``` + +通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。 + +## 导致MySQL慢查询有哪些原因? + +1. 没有索引,或者索引失效。 +2. 单表数据量太大 +3. 查询使用了临时表 +4. join 或者子查询过多 +5. in元素过多。如果使用了in,即使后面的条件加了索引,还是要注意in后面的元素不要过多哈。in元素一般建议不要超过500个,如果超过了,建议分组,每次500一组进行。 + +## 索引什么时候会失效? + +导致索引失效的情况: + +- 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引 +- 以%开头的like查询如`%abc`,无法使用索引;非%开头的like查询如`abc%`,相当于范围查询,会使用索引 +- 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效 +- 判断索引列是否不等于某个值时 +- 对索引列进行运算 +- 查询条件使用`or`连接,也会导致索引失效 diff --git "a/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" new file mode 100644 index 0000000..94d82b5 --- /dev/null +++ "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" @@ -0,0 +1,67 @@ +一条 SQL 查询语句如何执行的 + +在平常的开发中,可能很多人都是 CRUD,对 SQL 语句的语法很熟练,但是说起一条 SQL 语句在 MySQL 中是怎么执行的却浑然不知,今天大彬就由浅入深,带大家一点点剖析一条 SQL 语句在 MySQL 中是怎么执行的。 + +比如你执行下面这个 SQL 语句时,我们看到的只是输入一条语句,返回一个结果,却不知道 MySQL 内部的执行过程: + +```mysql +mysql> select * from T where ID=10; +``` + +在剖析这个语句怎么执行之前,我们先看一下 MySQL 的基本架构示意图,能更清楚的看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。 + +![](http://img.topjavaer.cn/img/202406030841887.png) + +整体来说,MySQL 可以分为 Server 层和存储引擎两部分。 + +Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 + +### 连接器 + +如果要操作 MySQL 数据库,我们必须使用 MySQL 客户端来连接 MySQL 服务器,这时候就是服务器中的连接器来负责根客户端建立连接、获取权限、维持和管理连接。 + +在和服务端完成 TCP 连接后,连接器就要认证身份,需要用到用户名和密码,确保用户有足够的权限执行该SQL语句。 + +### 查询缓存 + +建立完连接后,就可以执行查询语句了,来到第二步:查询缓存。 + +MySQL 拿到第一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中,如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。 + +如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。 + +### 分析器 + +如果没有命中缓存,就要开始真正执行语句了,MySQL 首先会对 SQL 语句做解析。 + +分析器会先做 “词法分析”,MySQL 需要识别出 SQL 里面的字符串分别是什么,代表什么。 + +做完之后就要做“语法分析”,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。若果语句不对,就会收到错误提醒。 + +### 优化器 + +经过了分析器,MySQL 就知道要做什么了,但是在开始执行之前,要先经过优化器的处理。 + +比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据**成本最小原则**来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本。 + +### 执行器 + +执行优化之后的执行计划,在开始执行之前,先判断一下用户对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误;如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。 + +### 存储引擎 + +执行器将查询请求发送给存储引擎组件。 + +存储引擎组件负责具体的数据存储、检索和修改操作。 + +存储引擎根据执行器的请求,从磁盘或内存中读取或写入相关数据。 + +### 返回结果 + +存储引擎将查询结果返回给执行器。 + +执行器将结果返回给连接器。 + +最后,连接器将结果发送回客户端,完成整个执行过程。 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/Apollo\351\205\215\347\275\256\344\270\255\345\277\203.md" "b/docs/framework/Apollo\351\205\215\347\275\256\344\270\255\345\277\203.md" similarity index 100% rename from "\346\241\206\346\236\266/Apollo\351\205\215\347\275\256\344\270\255\345\277\203.md" rename to "docs/framework/Apollo\351\205\215\347\275\256\344\270\255\345\277\203.md" diff --git "a/\346\241\206\346\236\266/Mybatis\351\235\242\350\257\225\351\242\230.md" b/docs/framework/mybatis.md similarity index 69% rename from "\346\241\206\346\236\266/Mybatis\351\235\242\350\257\225\351\242\230.md" rename to docs/framework/mybatis.md index 5dddfd8..b21cb9d 100644 --- "a/\346\241\206\346\236\266/Mybatis\351\235\242\350\257\225\351\242\230.md" +++ b/docs/framework/mybatis.md @@ -1,3 +1,25 @@ +--- +sidebar: heading +title: MyBatis常见面试题总结 +category: 框架 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## Mybatis是什么? - MyBatis框架是一个开源的数据持久层框架。 @@ -5,15 +27,31 @@ - MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。 - 这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。 +## 为什么使用Mybatis代替JDBC? + +MyBatis 是一种优秀的 ORM(Object-Relational Mapping)框架,与 JDBC 相比,有以下几点优势: + +1. 简化了 JDBC 的繁琐操作:使用 JDBC 进行数据库操作需要编写大量的样板代码,如获取连接、创建 Statement/PreparedStatement,设置参数,处理结果集等。而使用 MyBatis 可以将这些操作封装起来,通过简单的配置文件和 SQL 语句就能完成数据库操作,从而大大简化了开发过程。 +2. 提高了 SQL 的可维护性:使用 JDBC 进行数据库操作,SQL 语句通常会散布在代码中的各个位置,当 SQL 语句需要修改时,需要找到所有使用该语句的地方进行修改,这非常不方便,也容易出错。而使用 MyBatis,SQL 语句都可以集中在配置文件中,可以更加方便地修改和维护,同时也提高了 SQL 语句的可读性。 +3. 支持动态 SQL:MyBatis 提供了强大的动态 SQL 功能,可以根据不同的条件生成不同的 SQL 语句,这对于复杂的查询操作非常有用。 +4. 易于集成:MyBatis 可以与 Spring 等流行的框架集成使用,可以通过 XML 或注解配置进行灵活的配置,同时 MyBatis 也提供了非常全面的文档和示例代码,学习和使用 MyBatis 非常方便。 + +综上所述,使用 MyBatis 可以大大简化数据库操作的代码,提高 SQL 语句的可维护性和可读性,同时还提供了强大的动态 SQL 功能,易于集成使用。因此,相比于直接使用 JDBC,使用 MyBatis 更为便捷、高效和方便。 + ## **ORM是什么** ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 -## 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? +## Mybatis和Hibernate的区别? -Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。 +主要有以下几点区别: -而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 +1. Hibernate的**开发难度**大于MyBatis,主要由于Hibernate比较复杂,庞大,学习周期比较长。 +2. Hibernate属于**全自动**ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 +3. **数据库扩展性**的区别。Hibernate与数据库具体的关联在XML中,所以HQL对具体是用什么数据库并不是很关心。MyBatis由于所有sql都是依赖数据库书写的,所以扩展性、迁移性比较差。 +4. **缓存机制**的区别。Hibernate的二级缓存配置在SessionFactory生成配置文件中进行详细配置,然后再在具体的表对象映射中配置那种缓存。MyBatis的二级缓存配置都是在每个具体的表对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓冲机制,并且MyBatis可以在命名空间中共享相同的缓存配置和实例,通过Cache-ref来实现。 +5. **日志系统完善性**的区别。Hibernate日志系统非常健全,涉及广泛,而Mybatis则除了基本记录功能外,功能薄弱很多。 +6. **sql的优化上,Mybatis要比Hibernate方便很多**。由于Mybatis的sql都是写在xml里,因此优化sql比Hibernate方便很多。而Hibernate的sql很多都是自动生成的,无法直接维护sql;总之写sql的灵活度上Hibernate不及Mybatis。 ## MyBatis框架的优缺点及其适用的场合 @@ -80,7 +118,7 @@ Mybatis仅可以编写针对 `ParameterHandler`、`ResultSetHandler`、`Statemen 编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,最后在配置文件中配置你编写的插件。 -## .Mybatis 是否支持延迟加载? +## Mybatis 是否支持延迟加载? Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载`lazyLoadingEnabled=true|false`。 @@ -129,9 +167,13 @@ mybatis底层使用`PreparedStatement`,默认情况下,将对所有的 sql 缓存:合理使用缓存是优化中最常见的方法之一,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,同时提高系统性能。 +Mybatis里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。 + **一级缓存是SqlSession级别的缓存**:Mybatis对缓存提供支持,默认情况下只开启一级缓存,一级缓存作用范围为同一个SqlSession。在SQL和参数相同的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只会执行一次SQL。因为在使用SqlSession第一次查询后,Mybatis会将结果放到缓存中,以后再次查询时,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只会取出当前缓存的数据,不会再次发送SQL到数据库。若使用不同的SqlSession,因为不同的SqlSession是相互隔离的,不会使用一级缓存。 -**二级缓存是mapper级别的缓存**:可以使缓存在各个SqlSession之间共享。二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存: +**二级缓存是mapper级别的缓存**:可以使缓存在各个SqlSession之间共享。当多个用户在查询数据的时候,只要有任何一个SqlSession拿到了数据就会放入到二级缓存里面,其他的SqlSession就可以从二级缓存加载数据。 + +二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存: ```xml @@ -152,4 +194,4 @@ mybatis底层使用`PreparedStatement`,默认情况下,将对所有的 sql -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git "a/\346\241\206\346\236\266/netty\345\256\236\346\210\230.md" b/docs/framework/netty-overview.md similarity index 84% rename from "\346\241\206\346\236\266/netty\345\256\236\346\210\230.md" rename to docs/framework/netty-overview.md index 112aef3..92d7533 100644 --- "a/\346\241\206\346\236\266/netty\345\256\236\346\210\230.md" +++ b/docs/framework/netty-overview.md @@ -1,85 +1,6 @@ - - - - -- [简介](#%E7%AE%80%E4%BB%8B) - - [netty 核心组件](#netty-%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6) - - [NIO](#nio) -- [简单的 netty 应用程序](#%E7%AE%80%E5%8D%95%E7%9A%84-netty-%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F) - - [Echo 服务器](#echo-%E6%9C%8D%E5%8A%A1%E5%99%A8) - - [ChannelHandler 和业务逻辑](#channelhandler-%E5%92%8C%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91) - - [引导服务器](#%E5%BC%95%E5%AF%BC%E6%9C%8D%E5%8A%A1%E5%99%A8) - - [Echo 客户端](#echo-%E5%AE%A2%E6%88%B7%E7%AB%AF) - - [ChannelHandler](#channelhandler) - - [引导客户端](#%E5%BC%95%E5%AF%BC%E5%AE%A2%E6%88%B7%E7%AB%AF) - - [构建和运行 Echo 服务器和客户端](#%E6%9E%84%E5%BB%BA%E5%92%8C%E8%BF%90%E8%A1%8C-echo-%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%92%8C%E5%AE%A2%E6%88%B7%E7%AB%AF) -- [Netty 的组件和设计](#netty-%E7%9A%84%E7%BB%84%E4%BB%B6%E5%92%8C%E8%AE%BE%E8%AE%A1) - - [Channel 接口](#channel-%E6%8E%A5%E5%8F%A3) - - [EventLoop 接口](#eventloop-%E6%8E%A5%E5%8F%A3) - - [ChannelFuture 接口](#channelfuture-%E6%8E%A5%E5%8F%A3) - - [ChannelHandler](#channelhandler-1) - - [ChannelPipeline](#channelpipeline) - - [ChannelInitializer](#channelinitializer) - - [引导](#%E5%BC%95%E5%AF%BC) -- [传输](#%E4%BC%A0%E8%BE%93) - - [传输迁移](#%E4%BC%A0%E8%BE%93%E8%BF%81%E7%A7%BB) - - [传输 API](#%E4%BC%A0%E8%BE%93-api) - - [内置的传输](#%E5%86%85%E7%BD%AE%E7%9A%84%E4%BC%A0%E8%BE%93) - - [Epoll](#epoll) -- [ByteBuf](#bytebuf) - - [Upooled 缓冲区](#upooled-%E7%BC%93%E5%86%B2%E5%8C%BA) -- [ChannelHandler](#channelhandler-2) - - [Channel 的生命周期](#channel-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) - - [ChannelHandler 的生命周期](#channelhandler-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) - - [ChannelInboundHandler 接口](#channelinboundhandler-%E6%8E%A5%E5%8F%A3) - - [ChannelOutboundHandler 接口](#channeloutboundhandler-%E6%8E%A5%E5%8F%A3) - - [ChannelHandlerAdapter](#channelhandleradapter) - - [资源管理](#%E8%B5%84%E6%BA%90%E7%AE%A1%E7%90%86) -- [ChannelPipeline 接口](#channelpipeline-%E6%8E%A5%E5%8F%A3) - - [修改ChannelPipeline](#%E4%BF%AE%E6%94%B9channelpipeline) - - [ChannelHandlerContext 接口](#channelhandlercontext-%E6%8E%A5%E5%8F%A3) - - [异常处理](#%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86) -- [EventLoop 和线程模型](#eventloop-%E5%92%8C%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B) - - [EventLoop 接口](#eventloop-%E6%8E%A5%E5%8F%A3-1) - - [任务调度](#%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6) - - [实现细节](#%E5%AE%9E%E7%8E%B0%E7%BB%86%E8%8A%82) -- [引导](#%E5%BC%95%E5%AF%BC-1) - - [引导客户端](#%E5%BC%95%E5%AF%BC%E5%AE%A2%E6%88%B7%E7%AB%AF-1) - - [引导服务器](#%E5%BC%95%E5%AF%BC%E6%9C%8D%E5%8A%A1%E5%99%A8-1) - - [在引导过程添加多个 ChannelHandler](#%E5%9C%A8%E5%BC%95%E5%AF%BC%E8%BF%87%E7%A8%8B%E6%B7%BB%E5%8A%A0%E5%A4%9A%E4%B8%AA-channelhandler) - - [关闭](#%E5%85%B3%E9%97%AD) -- [编解码器](#%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8) - - [解码器](#%E8%A7%A3%E7%A0%81%E5%99%A8) - - [抽象类 ByteToMessageDecoder](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-bytetomessagedecoder) - - [抽象类 ReplayingDecoder](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-replayingdecoder) - - [抽象类 MessageToMessageDecoder](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-messagetomessagedecoder) - - [TooLongFrameException 类](#toolongframeexception-%E7%B1%BB) - - [编码器](#%E7%BC%96%E7%A0%81%E5%99%A8) - - [抽象类 MessageToByteEncoder](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-messagetobyteencoder) - - [抽象类 MessageToMessageEncoder](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-messagetomessageencoder) - - [编解码器类](#%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8%E7%B1%BB) - - [抽象类 ByteToMessageCodec](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-bytetomessagecodec) - - [抽象类 MessageToMessageCodec](#%E6%8A%BD%E8%B1%A1%E7%B1%BB-messagetomessagecodec) - - [CombinedChannelDuplexHandler 类](#combinedchannelduplexhandler-%E7%B1%BB) -- [预置的 ChannelHandler 和编解码器](#%E9%A2%84%E7%BD%AE%E7%9A%84-channelhandler-%E5%92%8C%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8) - - [SSL/TLS](#ssltls) - - [HTTP/HTTPS 应用程序](#httphttps-%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F) - - [添加 HTTP 支持](#%E6%B7%BB%E5%8A%A0-http-%E6%94%AF%E6%8C%81) - - [聚合 HTTP 消息](#%E8%81%9A%E5%90%88-http-%E6%B6%88%E6%81%AF) - - [HTTP 压缩](#http-%E5%8E%8B%E7%BC%A9) - - [HTTPS](#https) - - [WebSocket](#websocket) - - [空闲的连接和超时](#%E7%A9%BA%E9%97%B2%E7%9A%84%E8%BF%9E%E6%8E%A5%E5%92%8C%E8%B6%85%E6%97%B6) - - [基于分隔符的协议](#%E5%9F%BA%E4%BA%8E%E5%88%86%E9%9A%94%E7%AC%A6%E7%9A%84%E5%8D%8F%E8%AE%AE) - - [基于长度的协议](#%E5%9F%BA%E4%BA%8E%E9%95%BF%E5%BA%A6%E7%9A%84%E5%8D%8F%E8%AE%AE) - - [写大型数据](#%E5%86%99%E5%A4%A7%E5%9E%8B%E6%95%B0%E6%8D%AE) -- [ctx.write() 和 channel().write() 的区别](#ctxwrite-%E5%92%8C-channelwrite-%E7%9A%84%E5%8C%BA%E5%88%AB) - - - -## 简介 - -### netty 核心组件 +# 简介 + +## netty 核心组件 - Channel:传入和传出数据的载体,它可以连接或者断开连接。 @@ -87,25 +8,25 @@ - Future:提供了另一种在操作完成时通知应用程序的方式。 - 事件和 ChannelHandler -### NIO +## NIO 当一个 socket 建立好之后,Thread 会把这个连接请求交给 Selector,Selector 会不断去遍历所有的 Socket,一旦有一个 Socket 建立完成,它就会通知 Thread,然后 Thread 处理完数据在返回给客户端,这个过程是不阻塞的。 -## 简单的 netty 应用程序 +# 简单的 netty 应用程序 Echo 客户端和服务器之间的交互是非常简单的;在客户端建立一个连接之后,它会向服务 器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。 -### Echo 服务器 +## Echo 服务器 所有的 netty 服务器都需要以下两个部分: - 一个 ChannelHandler,实现服务器对接受的客户端的数据的处理 - 引导服务器:配置服务器的启动代码,将服务器绑定到它要监听连接请求的端口上 -#### ChannelHandler 和业务逻辑 +**ChannelHandler 和业务逻辑** Echo 服务器需要实现 ChannelInboundHandler 方法,定义响应入站事件的方法。 @@ -134,7 +55,7 @@ public class EchoServerHandler extends ChannelInboundHandlerAdapter { ChannelHandler 有助于保持业务逻辑与网络处理代码的分离。 -#### 引导服务器 +**引导服务器** 1. 服务器监听端口; 2. 配置 Channel,将有关的入站事件消息通知给 EchoServerHandler。 @@ -180,7 +101,7 @@ public class EchoServer { } ``` -### Echo 客户端 +## Echo 客户端 Echo 客户端的功能: @@ -189,7 +110,7 @@ Echo 客户端的功能: 3. 接收服务器发送的消息; 4. 关闭连接。 -#### ChannelHandler +**ChannelHandler** 客户端也需要实现 ChannelInboundHandler,用于处理数据。 @@ -219,7 +140,7 @@ public class EchoClientHandler extends SimpleChannelInboundHandler { } ``` -#### 引导客户端 +**引导客户端** 客户端使用主机和端口参数来连接远程地址。 @@ -269,7 +190,7 @@ public class EchoClient { } ``` -### 构建和运行 Echo 服务器和客户端 +## 构建和运行 Echo 服务器和客户端 在服务器端,使用`mvn clean package`构建项目,然后在 idea 中配置 Edit Configurations,带参数运行服务器程序。 @@ -279,7 +200,7 @@ public class EchoClient { -## Netty 的组件和设计 +# Netty 的组件和设计 Channel -- Socket; @@ -289,11 +210,11 @@ ChannelFuture -- 异步通知; ChannelHandler -- 处理出站和入站数据; -### Channel 接口 +## Channel 接口 Netty 的Channel 接口所提供的API,大大地降低了直接使用Socket 类的复杂性。 -### EventLoop 接口 +## EventLoop 接口 EventLoop 用于处理连接的生命周期中所发生的事件。 @@ -307,11 +228,11 @@ Channel 和 EventLoop 的关系:Channel 会被注册到 EventLoop 上,在整 一个给定 Channel 的I/O 操作都是由相同的Thread 执行的,实际上消除了对于同步的需要。 -### ChannelFuture 接口 +## ChannelFuture 接口 Netty 中所有 io 操作都是异步的,ChannelFuture 接口用于在操作完成时得到通知。 -### ChannelHandler +## ChannelHandler ChannelHandler 的方法是由网络事件触发的。典型用途: @@ -323,7 +244,7 @@ ChannelHandler 的方法是由网络事件触发的。典型用途: 一些适配器类提供了 ChannelHandler 接口中的所有方法的默认实现。 -### ChannelPipeline +## ChannelPipeline 提供了 ChannelHandler 链的容器。当 Channel 被创建时,会被自动分配到它专属的 ChannelPipeline。 @@ -333,7 +254,7 @@ ChannelHandler 的方法是由网络事件触发的。典型用途: 当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。 -### ChannelInitializer +## ChannelInitializer 作用是给 ChannelPipeline 安装 ChannelHandler。 @@ -343,15 +264,15 @@ ChannelHandler 安装到 ChannelPipeline 的过程: - 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装ChannelHandler; - ChannelInitializer 将它自己从ChannelPipeline 中移除。 -### 引导 +## 引导 Bootstrap 连接远程主机和端口,有一个 EventLoopGroup;ServerBootstrap 绑定到一个本地端口,有两个 EventLoopGroup。 -## 传输 +# 传输 -### 传输迁移 +## 传输迁移 Netty 为所有的传输提供了通用的 API,使得从阻塞传输到非阻塞传输的转换变得更加简单。 @@ -390,13 +311,13 @@ public class NettyNioServer { 只需要改动 SocketChannel 和 EventLoopGroup。 -### 传输 API +## 传输 API 每个 ChannelHandler 都会分配一个 ChannelPipeline 和 ChannelConfig。ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。 可以通过向 ChannelPipeline 添加 ChannelHandler 实例来增加应用程序的功能。 -### 内置的传输 +## 内置的传输 Channel 被注册到选择器 Selector 后,当 Channel 状态发生变化时可以得到通知。可能的状态变化有: @@ -409,29 +330,25 @@ Channel 被注册到选择器 Selector 后,当 Channel 状态发生变化时 > 高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间,其在像FTP 或者 > HTTP 这样的协议中可以显著地提升性能。它只能传输文件的原始内容,不能传输加密或者压缩的文件。 -#### Epoll +**Epoll** 用于 Linux 的本地非阻塞传输。Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并且以一种更加轻量的方式使用中断。 -## ByteBuf +# ByteBuf Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty 的ByteBuffer 替代品是ByteBuf,一个强大的实现,既解决了JDK API 的局限性,又为网络应用程序的开发者提供了更好的API。 - - - - -#### Upooled 缓冲区 +**Upooled 缓冲区** Upooled 工具类提供了静态的辅助方法来创建未池化的 ByteBuf 实例。 -## ChannelHandler +# ChannelHandler -### Channel 的生命周期 +## Channel 的生命周期 | 状态 | 描述 | | ------------------- | -------------------------------------------------- | @@ -444,7 +361,7 @@ Upooled 工具类提供了静态的辅助方法来创建未池化的 ByteBuf 实 ChannelRegistered -> ChannelActive -> ChannelInactive -> ChannelUnregistered -### ChannelHandler 的生命周期 +## ChannelHandler 的生命周期 | 方法 | 描述 | | --------------- | ----------------------------------------------- | @@ -452,7 +369,7 @@ ChannelRegistered -> ChannelActive -> ChannelInactive -> ChannelUnregistered | handlerRemoved | 从ChannelPipeline中移除ChannelHandler时被调用 | | exceptionCaught | 处理过程中在ChannelPipeline中有错误产生时被调用 | -### ChannelInboundHandler 接口 +## ChannelInboundHandler 接口 处理入站数据以及各种状态变化。 @@ -488,7 +405,7 @@ public class SimpleDiscardHandler } ``` -### ChannelOutboundHandler 接口 +## ChannelOutboundHandler 接口 ChannelOutboundHandler 一个强大的功能是可以按需推迟操作或者事件。 @@ -505,25 +422,25 @@ ChannelOutboundHandler 一个强大的功能是可以按需推迟操作或者事 ChannelPromise 是 ChannelFuture 的一个子类,ChannelOutboundHandler 中的大部分方法都需要一个 ChannelPromise参数,以便在操作完成时得到通知。 -### ChannelHandlerAdapter +## ChannelHandlerAdapter ChannelHandlerAdapter 提供了实用方法 isSharable(),如果其对应的实现被标注成 @Sharable,那么这个方法将返回true,表示它可以被添加到多个 ChannelPipeline 中。 共享 ChannelHandler 一个常见的用途是用于收集跨越多个 channel 的统计信息。 -### 资源管理 +## 资源管理 idea 配置 edit configuration -- vm options -- `-Dio.netty.leakDetectionLevel=ADVANCED ` -## ChannelPipeline 接口 +# ChannelPipeline 接口 每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline,这项关联是永久性的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。 ChannelHandlerContext 使得 ChannelHandler 能够和它的 ChannelPipeline 以及其他的 ChannelHandler 交互。 -### 修改ChannelPipeline +## 修改ChannelPipeline ChannelHandler 可以通过添加、删除或者替换其他的ChannelHandler 来实时地修改ChannelPipeline 的布局。 @@ -541,11 +458,11 @@ ChannelPipeline 的用于访问ChannelHandler 的操作: | context | 返回和ChannelHandler绑定的ChannelHandlerContext | | names | 返回所有的ChannelHanlder名称 | -### ChannelHandlerContext 接口 +## ChannelHandlerContext 接口 ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandlerContext。 -![ChannelHandler/ChannelPipeline/ChannelHandlerContext/Channel的关系](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912194315417-1954624274.png) +![](http://img.topjavaer.cn/img/netty1.png) | 方法 | 描述 | | --------------- | ---------------------------------------------------------- | @@ -553,7 +470,7 @@ ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联 | alloc | 返回相关联的Channel所配置的ByteBufAllocator | | bind | 绑定到给定的SocketAddress,并返回ChannelFuture | -### 异常处理 +## 异常处理 入站异常:在 ChannelInboundHandler 实现 exceptionCaught 方法。 @@ -596,9 +513,9 @@ public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter { -## EventLoop 和线程模型 +# EventLoop 和线程模型 -### EventLoop 接口 +## EventLoop 接口 EventLoop 构建在 java.util.concurrent 和 io.netty.channel 之上。EventLoop 继承了 ScheduledExecutorService。EventLoop 由一个永远不会改变的 Thread 驱动,同时任务可以直接提交给 EventLoop 实现。EventLoop 可能服务于多个 Channel。 @@ -606,7 +523,7 @@ Netty4中所有的 io 操作和事件都由 EventLoop 的 Thread 处理。Netty3 Netty 4 中所采用的线程模型,通过在同一个线程中处理某个给定的EventLoop 中所产生的所有事件,解决了这个问题。这提供了一个更加简单的执行体系架构,并且消除了在多个ChannelHandler 中进行同步的需要 -### 任务调度 +## 任务调度 使用 EventLoop 调度任务: @@ -634,27 +551,27 @@ ScheduledFuture future = ch.eventLoop().scheduleAtFixedRate( }, 60, 60, TimeUnit.SECONDES); ``` -### 实现细节 +## 实现细节 -![EventLoop执行逻辑](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912194501807-1452675286.png) +![](http://img.topjavaer.cn/img/netty-eventloop执行逻辑.png) -## 引导 +# 引导 配置 netty 应用程序,使它运行起来。服务器使用一个父 Channel 接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信。客户端只需要一个 Channel 完成所有的网络交互。 引导类是 cloneable 的,在引导类实例上调用 clone() 就可以创建多个具有类似配置或者完全相同配置的 Channel。 -### 引导客户端 +## 引导客户端 BootStrap 类被用于客户端或者使用了无连接协议的应用程序中。 -### 引导服务器 +## 引导服务器 -![ServerBoostrap和ServerChannel](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912194603147-253748270.png) +![](http://img.topjavaer.cn/img/ServerBoostrap和ServerChannel.png) 在基类AbstractBootstrap有handler方法,目的是添加一个handler,监听Bootstrap的动作。 @@ -662,7 +579,7 @@ BootStrap 类被用于客户端或者使用了无连接协议的应用程序中 **handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行。** -### 在引导过程添加多个 ChannelHandler +## 在引导过程添加多个 ChannelHandler 在 handler 传入 ChannelInitializer 的实现类,重写 initChannel 方法,在这个方法中添加多个 ChannelHandler。 @@ -687,7 +604,7 @@ try { } ``` -### 关闭 +## 关闭 关闭 EventLoopGroup,它将处理任何挂起的事件和任务,随后释放所有活动的线程。 @@ -699,17 +616,17 @@ future.syncUninterruptibly(); -## 编解码器 +# 编解码器 数据格式转化。编码器操作出站数据,解码器处理入站数据。继承自 ChannelInboundHandlerAdapter。数据编码或者解码完就会被传入 ChannelPipeline 的下一个 ChannelHandler。 -### 解码器 +## 解码器 ByteToMessageDecoder、ReplayingDecoder:将字节解码为消息。 MessageToMessageDecoder:将消息解码为另一种消息。 -#### 抽象类 ByteToMessageDecoder +**抽象类 ByteToMessageDecoder** ```java public class ToIntegerDecoder extends ByteToMessageDecoder { @@ -725,7 +642,7 @@ public class ToIntegerDecoder extends ByteToMessageDecoder { 调用 readInt() 方法前需要验证输入的 ByteBuf 是否具有足够的数据。 -#### 抽象类 ReplayingDecoder +**抽象类 ReplayingDecoder** 类型参数S 指定了用于状态管理的类型,其中Void 代表不需要状态管理。 @@ -743,7 +660,7 @@ public class ToIntegerDecoder2 extends ReplayingDecoder { 并不是所有的ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException;ReplayingDecoder 稍慢于ByteToMessageDecoder。如果使用ByteToMessageDecoder 不会引入太多的复杂性,那么选用它。 -#### 抽象类 MessageToMessageDecoder +**抽象类 MessageToMessageDecoder** 两种消息格式的转换。 @@ -756,15 +673,15 @@ public class IntegerToStringDecoder extends MessageToMessageDecoder { } ``` -#### TooLongFrameException 类 +**TooLongFrameException 类** 解码器缓冲大量的数据以至于耗尽可用的内存,可以设置一个最大字节数的阈值,如果超出该阈值,则手动抛出一个TooLongFrameException。 -### 编码器 +## 编码器 消息编码为字节;消息编码为消息。 -#### 抽象类 MessageToByteEncoder +**抽象类 MessageToByteEncoder** ```java public class ShortToByteEncoder extends MessageToByteEncoder { @@ -776,7 +693,7 @@ public class ShortToByteEncoder extends MessageToByteEncoder { } ``` -#### 抽象类 MessageToMessageEncoder +**抽象类 MessageToMessageEncoder** ```java public class IntegerToStringEncoder extends MessageToMessageEncoder { @@ -788,19 +705,19 @@ public class IntegerToStringEncoder extends MessageToMessageEncoder { } ``` -### 编解码器类 +## 编解码器类 结合一个解码器和编码器可能会对可重用性造成影响。 -#### 抽象类 ByteToMessageCodec +**抽象类 ByteToMessageCodec** 结合了 ByteToMessageDecoder 和 MessageToByteEncoder。 -#### 抽象类 MessageToMessageCodec +**抽象类 MessageToMessageCodec** 定义:`public abstract class MessageToMessageCodec` -#### CombinedChannelDuplexHandler 类 +**CombinedChannelDuplexHandler 类** 可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。 @@ -845,13 +762,13 @@ public class CombinedByteCharCodec extends CombinedChannelDuplexHandler { } ``` -### HTTP/HTTPS 应用程序 +## HTTP/HTTPS 应用程序 完整的 HTTP 请求(FullHttpRequest)包括请求头信息、若干个 HTTPContent 和 LastHttpContent。 @@ -887,7 +804,7 @@ HTTP 编解码器:HttpRequestEncoder、HttpResponseEncoder、HttpReqeustDecode HttpResponseDecoder:将字节解码为 HttpResponse、HttpContent 和 LastHttpContent。 -#### 添加 HTTP 支持 +**添加 HTTP 支持** ```java public class HttpPipelineInitializer extends ChannelInitializer { @@ -913,7 +830,7 @@ public class HttpPipelineInitializer extends ChannelInitializer { 判断是否是客户端,如果是客户端,则添加 HttpResponseDecoder 对服务器响应进行解码。 -#### 聚合 HTTP 消息 +**聚合 HTTP 消息** 由于 HTTP 请求和响应可能由多个部分组成,需要将它们聚合成完整的消息。Netty 提供了一个聚合器,可以将多个消息部分合并成 FullHttpRequest 或者 FullHttpResponse 消息。 @@ -945,7 +862,7 @@ HttpServerCodec 里面组合了HttpResponseEncoder和HttpRequestDecoder。 HttpClientCodec 里面组合了HttpRequestEncoder和HttpResponseDecoder。 -#### HTTP 压缩 +**HTTP 压缩** 当使用HTTP 时,建议服务器端开启压缩功能以尽可能多地减小传输数据的大小。Netty 为压缩和解压缩提供了ChannelHandler 实现,它们同时支持gzip 和deflate 编码。 @@ -975,7 +892,7 @@ public class HttpCompressionInitializer extends ChannelInitializer { } ``` -#### HTTPS +**HTTPS** 启动 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline。 @@ -1004,11 +921,11 @@ public class HttpsCodecInitializer extends ChannelInitializer { } ``` -#### WebSocket +**WebSocket** WebSocket 在客户端和服务器之间提供了真正的双向数据交换。 -![WebSocket握手](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912194009527-799081544.png) +![](http://img.topjavaer.cn/img/netty-websocket协议.png) WebSocketFrame 类型: @@ -1067,11 +984,11 @@ public class WebSocketServerInitializer extends ChannelInitializer { > 要想为WebSocket 添加安全性,只需要将SslHandler 作为第一个ChannelHandler 添加到ChannelPipeline 中。 -### 空闲的连接和超时 +## 空闲的连接和超时 用于空闲连接以及超时的 ChannelHandler。 -![用于空闲连接以及超时的ChannelHandler](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912194117618-854988800.png) +![](http://img.topjavaer.cn/img/用于空闲连接以及超时的ChannelHandler.png) 发送心跳: @@ -1109,7 +1026,7 @@ public class IdleStateHandlerInitializer extends ChannelInitializer { 使用 IdleStateHandler 测试远程节点是否还活着,失活时关闭连接释放资源。 -### 基于分隔符的协议 +## 基于分隔符的协议 基于分隔符的协议的解码器 @@ -1203,7 +1120,7 @@ public class CmdHandlerInitializer extends ChannelInitializer { } ``` -### 基于长度的协议 +## 基于长度的协议 基于长度的协议的解码器: @@ -1235,7 +1152,7 @@ public class LengthBasedInitializer extends ChannelInitializer { } ``` -### 写大型数据 +## 写大型数据 当写大型数据到远程节点时,如果连接速度比较慢,数据依然不断的往内存写,可能导致内存耗尽。利用 NIO 的零拷贝特性,可以消除将文件内容从文件系统移动到网络栈的复制过程。应用程序需要做的就是实现一个 FileRegion 的接口。 @@ -1255,9 +1172,3 @@ channel.writeAndFlush(region).addListener( - - -## ctx.write() 和 channel().write() 的区别 - -https://blog.csdn.net/lalalahaitang/article/details/81563830 - diff --git a/docs/framework/netty/1-overview.md b/docs/framework/netty/1-overview.md new file mode 100644 index 0000000..d0b180b --- /dev/null +++ b/docs/framework/netty/1-overview.md @@ -0,0 +1,16 @@ +# 简介 + +## netty 核心组件 + +- Channel:传入和传出数据的载体,它可以连接或者断开连接。 + +- 回调:操作完成后通知相关方。 +- Future:提供了另一种在操作完成时通知应用程序的方式。 +- 事件和 ChannelHandler + +## NIO + +当一个 socket 建立好之后,Thread 会把这个连接请求交给 Selector,Selector 会不断去遍历所有的 Socket,一旦有一个 Socket 建立完成,它就会通知 Thread,然后 Thread 处理完数据在返回给客户端,这个过程是不阻塞的。 + + + diff --git a/docs/framework/netty/10-encoder-decoder.md b/docs/framework/netty/10-encoder-decoder.md new file mode 100644 index 0000000..ff7c7cb --- /dev/null +++ b/docs/framework/netty/10-encoder-decoder.md @@ -0,0 +1,146 @@ +# 编解码器 + +数据格式转化。编码器操作出站数据,解码器处理入站数据。继承自 ChannelInboundHandlerAdapter。数据编码或者解码完就会被传入 ChannelPipeline 的下一个 ChannelHandler。 + +## 解码器 + +ByteToMessageDecoder、ReplayingDecoder:将字节解码为消息。 + +MessageToMessageDecoder:将消息解码为另一种消息。 + +**抽象类 ByteToMessageDecoder** + +```java +public class ToIntegerDecoder extends ByteToMessageDecoder { + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, + List list) throws Exception { + if (in.readableBytes() >= 4) { + list.add(in.readInt()); + } + } +} +``` + +调用 readInt() 方法前需要验证输入的 ByteBuf 是否具有足够的数据。 + +**抽象类 ReplayingDecoder** + +类型参数S 指定了用于状态管理的类型,其中Void 代表不需要状态管理。 + +```java +public class ToIntegerDecoder2 extends ReplayingDecoder { + @Override + public void decode(ChannelHandlerContext ctx, ByteBuf in, + List list) throws Exception { + list.add(in.readInt()); + } +} +``` + +如果没有足够的字节可用,这个readInt()方法的实现将会抛出一个Error,将在基类中被捕获并处理。当有更多的数据可供读取时,该decode()方法将会被再次调用。 + +并不是所有的ByteBuf 操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException;ReplayingDecoder 稍慢于ByteToMessageDecoder。如果使用ByteToMessageDecoder 不会引入太多的复杂性,那么选用它。 + +**抽象类 MessageToMessageDecoder** + +两种消息格式的转换。 + +```java +public class IntegerToStringDecoder extends MessageToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, Integer integer, List list) throws Exception { + list.add(String.valueOf(integer)); + } +} +``` + +**TooLongFrameException 类** + +解码器缓冲大量的数据以至于耗尽可用的内存,可以设置一个最大字节数的阈值,如果超出该阈值,则手动抛出一个TooLongFrameException。 + +## 编码器 + +消息编码为字节;消息编码为消息。 + +**抽象类 MessageToByteEncoder** + +```java +public class ShortToByteEncoder extends MessageToByteEncoder { + @Override + public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out) + throws Exception { + out.writeShort(msg); + } +} +``` + +**抽象类 MessageToMessageEncoder** + + ```java +public class IntegerToStringEncoder extends MessageToMessageEncoder { + @Override + public void encode(ChannelHandlerContext ctx, Integer msg + List out) throws Exception { + out.add(String.valueOf(msg)); + } +} + ``` + +## 编解码器类 + +结合一个解码器和编码器可能会对可重用性造成影响。 + +**抽象类 ByteToMessageCodec** + +结合了 ByteToMessageDecoder 和 MessageToByteEncoder。 + +**抽象类 MessageToMessageCodec** + +定义:`public abstract class MessageToMessageCodec` + +**CombinedChannelDuplexHandler 类** + +可以实现一个编解码器,而又不必直接扩展抽象的编解码器类。 + +```java +public class CombinedChannelDuplexHandler +``` + +ByteToCharDecoder 类: + +```java +public class ByteToCharDecoder extends ByteToMessageDecoder { + @Override + protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { + while (byteBuf.readableBytes() >= 2) { + list.add(byteBuf.readChar()); + } + } +} +``` + +CharToByteEncoder 类: + +```java +public class CharToByteEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext channelHandlerContext, Character character, ByteBuf byteBuf) throws Exception { + byteBuf.writeChar(character);//向byteBuf写入基本类型的值 + } +} +``` + +编解码器类: + +```java +public class CombinedByteCharCodec extends CombinedChannelDuplexHandler { + public CombinedByteCharCodec() { + super(new ByteToCharDecoder(), new CharToByteEncoder());//将委托实例传递给父类 + } +} +``` + + + diff --git a/docs/framework/netty/11-preset-channel-handler.md b/docs/framework/netty/11-preset-channel-handler.md new file mode 100644 index 0000000..ffeeb3b --- /dev/null +++ b/docs/framework/netty/11-preset-channel-handler.md @@ -0,0 +1,409 @@ +# 预置的 ChannelHandler 和编解码器 + +## SSL/TLS + +Java 提供了 javax.net.ssl 支持 SSL/TSL,用以实现数据安全。 + +![](http://img.topjavaer.cn/img/sslhandler加解密.png) + +添加 SSL/TLS 支持: + +```java +public class SslChannelInitializer extends ChannelInitializer { + private final SslContext context; + private final boolean startTls; + + public SslChannelInitializer(SslContext context, boolean startTls) { + this.context = context; + this.startTls = startTls; + } + + @Override + protected void initChannel(Channel channel) throws Exception { + SSLEngine engine = context.newEngine(channel.alloc());//alloc返回channel所配置的ByteBufAllocator + channel.pipeline().addFirst("ssl", + new SslHandler(engine, startTls));//大多数情况SslHandler是第一个ChannelHandler + //这确保了所有其他的ChannelHandler处理数据之后,才会进行加密。 + } +} +``` + +## HTTP/HTTPS 应用程序 + +完整的 HTTP 请求(FullHttpRequest)包括请求头信息、若干个 HTTPContent 和 LastHttpContent。 + +完整的 HTTP 响应(FullHttpResponse)包括响应头信息、若干个 HTTPContent 和 LastHttpContent。 + +所有类型的 HTTP 消息都实现了 HttpObject 接口。 + +HTTP 编解码器:HttpRequestEncoder、HttpResponseEncoder、HttpReqeustDecoder 和 HttpResponseDecoder。 + +HttpResponseDecoder:将字节解码为 HttpResponse、HttpContent 和 LastHttpContent。 + +**添加 HTTP 支持** + +```java +public class HttpPipelineInitializer extends ChannelInitializer { + private final boolean client; + + public HttpPipelineInitializer(boolean client) { + this.client = client; + } + + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + if (client) { + pipeline.addLast("decoder", new HttpResponseDecoder()); + pipeline.addLast("encoder", new HttpRequestEncoder()); + } else { + pipeline.addLast("decoder", new HttpRequestDecoder()); + pipeline.addLast("encoder", new HttpResponseEncoder()); + } + } +} +``` + +判断是否是客户端,如果是客户端,则添加 HttpResponseDecoder 对服务器响应进行解码。 + +**聚合 HTTP 消息** + +由于 HTTP 请求和响应可能由多个部分组成,需要将它们聚合成完整的消息。Netty 提供了一个聚合器,可以将多个消息部分合并成 FullHttpRequest 或者 FullHttpResponse 消息。 + +自动聚合 HTTP 的消息片段: + +```java +public class HttpAggregarotInitializer extends ChannelInitializer { + private final boolean isClient; + + public HttpAggregarotInitializer(boolean isClient) { + this.isClient = isClient; + } + + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + if (isClient) { + pipeline.addLast("codec", new HttpClientCodec()); + } else { + pipeline.addLast("codec", new HttpServerCodec()); + } + pipeline.addLast("aggregator", //最大消息大小是512kb + new HttpObjectAggregator(512*1024)); + } +} +``` + +HttpServerCodec 里面组合了HttpResponseEncoder和HttpRequestDecoder。 + +HttpClientCodec 里面组合了HttpRequestEncoder和HttpResponseDecoder。 + +**HTTP 压缩** + +当使用HTTP 时,建议服务器端开启压缩功能以尽可能多地减小传输数据的大小。Netty 为压缩和解压缩提供了ChannelHandler 实现,它们同时支持gzip 和deflate 编码。 + +自动压缩 HTTP 消息: + +```java +public class HttpCompressionInitializer extends ChannelInitializer { + private final boolean isClient; + + public HttpCompressionInitializer(boolean isClient) { + this.isClient = isClient; + } + + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + if (isClient) { + pipeline.addLast("codec", new HttpClientCodec()); + pipeline.addLast("decompressor", + new HttpContentDecompressor());//处理来自服务器的压缩内容 + } else { + pipeline.addLast("codec", new HttpServerCodec()); + pipeline.addLast("compressor", + new HttpContentCompressor());//服务器端压缩数据 + } + } +} +``` + +**HTTPS** + +启动 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline。 + +```java +public class HttpsCodecInitializer extends ChannelInitializer { + private final SslContext context; + private final boolean isClient; + + public HttpsCodecInitializer(SslContext context, boolean isClient) { + this.context = context; + this.isClient = isClient; + } + + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + SSLEngine engine = context.newEngine(channel.alloc()); + pipeline.addFirst("ssl", new SslHandler(engine));//添加SslHandler之后可以使用https + + if (isClient) { + pipeline.addLast("codec", new HttpClientCodec()); + } else { + pipeline.addLast("codec", new HttpServerCodec()); + } + } +} +``` + +**WebSocket** + +WebSocket 在客户端和服务器之间提供了真正的双向数据交换。 + +![](http://img.topjavaer.cn/img/netty-websocket协议.png) + +WebSocketFrame 类型: + +| 名称 | 描述 | +| -------------------------- | ------------------------------------------------- | +| BinaryWebSocketFrame | 二进制数据帧 | +| TextWebSocketFrame | 文本数据帧 | +| ContinuationWebSocketFrame | 二进制和文本数据帧结合体 | +| CloseWebSocketFrame | 控制帧:一个close请求、关闭的状态码以及关闭的原因 | +| PingWebSocketFrame | 控制帧:请求一个PongWebSocketFrame | +| PongWebSocketFrame | 控制帧:对PingWebSocketFrame请求的响应 | + +WebSocketServerProtocolHandler 处理协议升级握手,以及三种控制帧--Close、Ping 和 Pong。Text和Binary数据帧将会被传递给下一个 ChannelHandler 进行处理。 + +```java +public class WebSocketServerInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel channel) throws Exception { + channel.pipeline().addLast( + new HttpServerCodec(), + new HttpObjectAggregator(65536), + new WebSocketServerProtocolHandler("/websocket"),//升级握手 + new TextFrameHandler(), + new BinaryFrameHandler(), + new ContinuationFrameHandler()); + + } + public static final class TextFrameHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception { + //handle text frame + } + } + + public static final class BinaryFrameHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, BinaryWebSocketFrame binaryWebSocketFrame) throws Exception { + //handle binary frame + } + } + + public static final class ContinuationFrameHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, ContinuationWebSocketFrame continuationWebSocketFrame) throws Exception { + //handle continuation frame + } + } +} +``` + +> 要想为WebSocket 添加安全性,只需要将SslHandler 作为第一个ChannelHandler 添加到ChannelPipeline 中。 + +## 空闲的连接和超时 + +用于空闲连接以及超时的 ChannelHandler。 + +![](http://img.topjavaer.cn/img/用于空闲连接以及超时的ChannelHandler.png) + +发送心跳: + +```java +public class IdleStateHandlerInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + //60s没有接受或发送数据,IdelStateHandler会使用IdleStateEvent调用fireUserEventTriggered() + pipeline.addLast(new IdleStateHandler( + 0, 0, 60, TimeUnit.SECONDS)); + pipeline.addLast(new HeartbeatHandler()); + } + + public static final class HeartbeatHandler extends + ChannelInboundHandlerAdapter { + //发送到远程节点的心跳信息 + private static final ByteBuf HEARTBEAT_SEQUENCE = + Unpooled.unreleasableBuffer(Unpooled.copiedBuffer( + "HEARTBEAT", CharsetUtil.ISO_8859_1)); + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + //连接空闲时间太长时,发送心跳消息,并在发送失败时关闭该连接 + ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()) + .addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + } else { + super.userEventTriggered(ctx, evt);//传递给下一个ChannelInboundHandler + } + } + } +} +``` + +使用 IdleStateHandler 测试远程节点是否还活着,失活时关闭连接释放资源。 + +## 基于分隔符的协议 + +基于分隔符的协议的解码器 + +| 名称 | 描述 | +| -------------------------- | ----------------------------------------------------------- | +| DelimiterBasedFrameDecoder | 使用自定义分隔符提取帧的通用解码器 | +| LineBasedFrameDecoder | 提取由行尾符分隔的解码器,速度比DeimiterBasedFrameDecoder快 | + +分隔符提取帧: + +```java +public class LineBasedHandlerInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + //提取帧,并传给下一个ChannelHandler + pipeline.addLast(new LineBasedFrameDecoder(64*1024)); + pipeline.addLast(new FrameHandler());//接收数据帧 + } + + public static final class FrameHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception { + //处理LineBasedFrameDecoder传进的帧 + } + } +``` + +示例:1.每个帧都由换行符(\n)分隔;2.每个帧由一系列的元素组成,每个元素都由的单个空格字符分隔;3.一个帧内容代表一个命令,定义为一个命令名称后面跟着数目可变的参数。 + +```java +public class CmdHandlerInitializer extends ChannelInitializer { + static final byte SPACE = (byte)' '; + + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeline = channel.pipeline(); + pipeline.addLast(new CmdDecoder(64 * 1024)); + pipeline.addLast(new CmdHandler()); + } + + public static final class Cmd { + private final ByteBuf name; + private final ByteBuf args; + + public Cmd(ByteBuf name, ByteBuf args) { + this.name = name; + this.args = args; + } + + public ByteBuf getName() { + return name; + } + + public ByteBuf getArgs() { + return args; + } + } + + public static final class CmdDecoder + extends LineBasedFrameDecoder { + + public CmdDecoder(int maxLength) { + super(maxLength); + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, buffer); + if (frame == null) { + return null; + } + //查找第一个空格字符的索引,空格前是命令名称,后面是参数 + int index = frame.indexOf(frame.readerIndex(), + frame.writerIndex(), SPACE); + return new Cmd(frame.slice(frame.readerIndex(), index), + frame.slice(index + 1, frame.writerIndex())); + } + } + + public static final class CmdHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, Cmd cmd) throws Exception { + //处理cmd + } + } +} +``` + +## 基于长度的协议 + +基于长度的协议的解码器: + +| 名称 | 描述 | +| ---------------------------- | ------------------------------------------------------------ | +| FixedLengthFrameDecoder | 提取固定长度的帧 | +| LengthFieldBasedFrameDecoder | 根据帧头部中的长度值提取帧;该字段的偏移量以及长度在构造函数中指定 | + +变长帧: + +```java +public class LengthBasedInitializer extends ChannelInitializer { + @Override + protected void initChannel(Channel channel) throws Exception { + ChannelPipeline pipeLine = channel.pipeline(); + //帧起始的前8字节是帧长度 + pipeLine.addLast(new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8)); + pipeLine.addLast(new FrameHandler()); + } + + public static class FrameHandler extends + SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception { + //处理帧 + } + } +} +``` + +## 写大型数据 + +当写大型数据到远程节点时,如果连接速度比较慢,数据依然不断的往内存写,可能导致内存耗尽。利用 NIO 的零拷贝特性,可以消除将文件内容从文件系统移动到网络栈的复制过程。应用程序需要做的就是实现一个 FileRegion 的接口。 + +利用零拷贝特性(FileRegion)来传输一个文件的内容。 + +```java +FileInputStream in = new FileInputStream(File); +FileRegion region = new DefaultFileRegion(in.getChannel(), 0, file.length()); +channel.writeAndFlush(region).addListener( + new ChannelFuture(region).addListener( + new ChannelFutureListener() { + + } + ) +); +``` + + diff --git a/docs/framework/netty/2-make-your-app.md b/docs/framework/netty/2-make-your-app.md new file mode 100644 index 0000000..47ba51f --- /dev/null +++ b/docs/framework/netty/2-make-your-app.md @@ -0,0 +1,186 @@ +# 简单的 netty 应用程序 + +Echo 客户端和服务器之间的交互是非常简单的;在客户端建立一个连接之后,它会向服务 +器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。 + +## Echo 服务器 + +所有的 netty 服务器都需要以下两个部分: + +- 一个 ChannelHandler,实现服务器对接受的客户端的数据的处理 +- 引导服务器:配置服务器的启动代码,将服务器绑定到它要监听连接请求的端口上 + +**ChannelHandler 和业务逻辑** + +Echo 服务器需要实现 ChannelInboundHandler 方法,定义响应入站事件的方法。 + +```java +@ChannelHandler.Sharable +public class EchoServerHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + ByteBuf in = (ByteBuf) msg; + System.out.println("server reveived: " + in.toString(CharsetUtil.UTF_8)); + ctx.write(in);//将接受到的消息回传给发送者 + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close();//关闭channel + } +} +``` + +ChannelHandler 有助于保持业务逻辑与网络处理代码的分离。 + +**引导服务器** + +1. 服务器监听端口; +2. 配置 Channel,将有关的入站事件消息通知给 EchoServerHandler。 + +```java +public class EchoServer { + private final int port; + + public EchoServer(int port) { + this.port = port; + } + + public static void main(String[] args) throws Exception { + if (args.length != 1) { + System.err.println( + "Usage: " + EchoServer.class.getSimpleName() + + " "); + } + int port = Integer.parseInt(args[0]); + new EchoServer(port).start(); + } + + public void start() throws Exception { + final EchoServerHandler serverHandler = new EchoServerHandler(); + EventLoopGroup group = new NioEventLoopGroup(); + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(group) + .channel(NioServerSocketChannel.class)//nio传输的channel + .localAddress(new InetSocketAddress(port)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + socketChannel.pipeline().addLast(serverHandler);//将serverHandler添加到自Channel的ChannelPipeline + } + }); + ChannelFuture future = bootstrap.bind().sync();//异步绑定到服务器,阻塞直到绑定成功 + future.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); + } + } +} +``` + +## Echo 客户端 + +Echo 客户端的功能: + +1. 连接到服务器; +2. 发送消息; +3. 接收服务器发送的消息; +4. 关闭连接。 + +**ChannelHandler** + +客户端也需要实现 ChannelInboundHandler,用于处理数据。 + +```java +@ChannelHandler.Sharable +public class EchoClientHandler extends SimpleChannelInboundHandler { + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + //当被通知的channel是活跃的时候发送消息 + ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8)); + } + + /** + * 每当接收数据就会调用此方法,服务器发送的数据可能被分块接收 + */ + @Override + public void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception { + System.out.println("client received: " + byteBuf.toString(CharsetUtil.UTF_8));//接收的消息 + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + ctx.close(); + } +} +``` + +**引导客户端** + +客户端使用主机和端口参数来连接远程地址。 + +```java +public class EchoClient { + private final String host; + private final int port; + + public EchoClient(String host, int port) { + this.host = host; + this.port = port; + } + + public void start() throws Exception { + EventLoopGroup group = new NioEventLoopGroup(); + try { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class)//适用于nio传输的channel类型 + .remoteAddress(new InetSocketAddress(host, port)) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + socketChannel.pipeline().addLast( + new EchoClientHandler()); + } + }); + ChannelFuture future = bootstrap.connect().sync();//连接到远程节点,阻塞等待 + future.channel().closeFuture().sync();//阻塞直到channel关闭 + } finally { + group.shutdownGracefully().sync();//关闭线程池并释放所有的资源 + } + } + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.err.println( + "Usage: " + EchoClient.class.getSimpleName() + + " "); + return; + } + + String host = args[0]; + int port = Integer.parseInt(args[1]); + new EchoClient(host, port).start(); + } +} +``` + +## 构建和运行 Echo 服务器和客户端 + +在服务器端,使用`mvn clean package`构建项目,然后在 idea 中配置 Edit Configurations,带参数运行服务器程序。 + +同理,客户端进行同样的配置,注意带多个参数的运行配置,参数中间使用空格隔开。 + +先运行服务器程序,在运行客户端程序,服务端接收到客户端发出的消息,控制台输出:`server reveived: Netty rocks!`,然后服务端将消息回传客户端,客户端控制台输出:`client received: Netty rocks!`,之后客户端便退出。 + + + diff --git a/docs/framework/netty/3-component.md b/docs/framework/netty/3-component.md new file mode 100644 index 0000000..b7b5b7f --- /dev/null +++ b/docs/framework/netty/3-component.md @@ -0,0 +1,70 @@ +# Netty 的组件和设计 + +Channel -- Socket; + +EventLoop -- 控制流、多线程处理、并发; + +ChannelFuture -- 异步通知; + +ChannelHandler -- 处理出站和入站数据; + +## Channel 接口 + +Netty 的Channel 接口所提供的API,大大地降低了直接使用Socket 类的复杂性。 + +## EventLoop 接口 + +EventLoop 用于处理连接的生命周期中所发生的事件。 + +Channel 和 EventLoop 的关系:Channel 会被注册到 EventLoop 上,在整个生命周期内使用 EventLoop 处理 io 事件。 + +一个EventLoop 在它的生命周期内只和一个Thread 绑定; + +一个Channel 在它的生命周期内只注册于一个EventLoop; + +一个EventLoop 可能会被分配给一个或多个Channel; + +一个给定 Channel 的I/O 操作都是由相同的Thread 执行的,实际上消除了对于同步的需要。 + +## ChannelFuture 接口 + +Netty 中所有 io 操作都是异步的,ChannelFuture 接口用于在操作完成时得到通知。 + +## ChannelHandler + +ChannelHandler 的方法是由网络事件触发的。典型用途: + +- 将数据从一种格式转换为另一种格式 +- 提供异常的通知 +- 提供Channel 变为活动的或者非活动的通知 +- 提供当Channel 注册到EventLoop 或者从EventLoop 注销时的通知 +- 提供有关用户自定义事件的通知 + +一些适配器类提供了 ChannelHandler 接口中的所有方法的默认实现。 + +## ChannelPipeline + +提供了 ChannelHandler 链的容器。当 Channel 被创建时,会被自动分配到它专属的 ChannelPipeline。 + +每一个事件都会流经 ChannelPipeline,被 ChannelHandler链处理,每一个 ChannelHandler 处理完数据会负责把事件传递给下一个 ChannelHandler,它们的顺序即是它们被安装的顺序。 + +从客户端应用程序角度来看,如果事件从客户端传递到服务端,那么称之为出站事件,反之则是入站事件。从服务端角度来看则相反。Netty 能区分 ChannelInboundHandler 和 ChannelOutboundHandler 实现,并确保数据在能在具有相同类型的 ChannelHandler 之间传递。 + +当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。 + +## ChannelInitializer + +作用是给 ChannelPipeline 安装 ChannelHandler。 + +ChannelHandler 安装到 ChannelPipeline 的过程: + +- 一个 ChannelInitializer 的实现被注册到了 ServerBootstrap; +- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装ChannelHandler; +- ChannelInitializer 将它自己从ChannelPipeline 中移除。 + +## 引导 + +Bootstrap 连接远程主机和端口,有一个 EventLoopGroup;ServerBootstrap 绑定到一个本地端口,有两个 EventLoopGroup。 + + + diff --git a/docs/framework/netty/4-transport.md b/docs/framework/netty/4-transport.md new file mode 100644 index 0000000..038459d --- /dev/null +++ b/docs/framework/netty/4-transport.md @@ -0,0 +1,66 @@ +# 传输 + +## 传输迁移 + +Netty 为所有的传输提供了通用的 API,使得从阻塞传输到非阻塞传输的转换变得更加简单。 + +```java +public class NettyNioServer { + public void server(int port) throws Exception { + final ByteBuf buf = Unpooled.copiedBuffer("hi!\r\n", + Charset.forName("UTF-8")); + EventLoopGroup group = new NioEventLoopGroup();//oio-->nio + try { + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(group) + .channel(NioServerSocketChannel.class)//oio-->nio + .localAddress(new InetSocketAddress(port)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.writeAndFlush(buf.duplicate()) + .addListener(ChannelFutureListener.CLOSE); + } + }); + } + }); + //绑定服务器以接受连接 + ChannelFuture future = bootstrap.bind().sync(); + future.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); + } + } +} +``` + +只需要改动 SocketChannel 和 EventLoopGroup。 + +## 传输 API + +每个 ChannelHandler 都会分配一个 ChannelPipeline 和 ChannelConfig。ChannelConfig 包含了该 Channel 的所有配置设置,并且支持热更新。 + +可以通过向 ChannelPipeline 添加 ChannelHandler 实例来增加应用程序的功能。 + +## 内置的传输 + +Channel 被注册到选择器 Selector 后,当 Channel 状态发生变化时可以得到通知。可能的状态变化有: + +- 新的 Channel 已被接受并且就绪; +- Channel 连接已经完成; +- Channel 有已经就绪的可供读取的数据; +- Channel 可用于写数据。 + +> 零拷贝(zero-copy)是一种目前只有在使用NIO 和Epoll 传输时才可使用的特性。它使你可以快速 +> 高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间,其在像FTP 或者 +> HTTP 这样的协议中可以显著地提升性能。它只能传输文件的原始内容,不能传输加密或者压缩的文件。 + +**Epoll** + +用于 Linux 的本地非阻塞传输。Netty为Linux提供了一组NIO API,其以一种和它本身的设计更加一致的方式使用epoll,并且以一种更加轻量的方式使用中断。 + + + diff --git a/docs/framework/netty/6-channel-handler.md b/docs/framework/netty/6-channel-handler.md new file mode 100644 index 0000000..456ab40 --- /dev/null +++ b/docs/framework/netty/6-channel-handler.md @@ -0,0 +1,88 @@ +# ChannelHandler + +## Channel 的生命周期 + +| 状态 | 描述 | +| ------------------- | -------------------------------------------------- | +| ChannelUnregistered | Channel 已创建,但还未注册到 EventLoop | +| ChannelRegistered | Channel 已注册到了 EventLoop | +| ChannelActive | Channel 已经连接到它的远程节点,可以接受和发送数据 | +| ChannelInactive | Channel 没有连接到远程节点 | + +正常的生命周期: + +ChannelRegistered -> ChannelActive -> ChannelInactive -> ChannelUnregistered + +## ChannelHandler 的生命周期 + +| 方法 | 描述 | +| --------------- | ----------------------------------------------- | +| handlerAdded | 把ChanneHandler添加到ChannelPipeline时被调用 | +| handlerRemoved | 从ChannelPipeline中移除ChannelHandler时被调用 | +| exceptionCaught | 处理过程中在ChannelPipeline中有错误产生时被调用 | + +## ChannelInboundHandler 接口 + +处理入站数据以及各种状态变化。 + +| 方法 | 描述 | +| ------------------------ | ------------------------------------------------------------ | +| channelRead | 从Channel读取数据时被调用 | +| channelReadComplete | 从Channel上一个读操作完成时被调用 | +| channelWritablityChanged | Channel 的可写状态发生改变时被调用 | +| userEventTriggered | 当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用,因为一个POJO 被传经了ChannelPipeline | + +ReferenceCountUtil 释放消息资源: + +```java +@Sharable +public class DiscardHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ReferenceCountUtil.release(msg); + } +} +``` + +SimpleChannelInboundHandler 会自动释放资源,所以不应该存储指向任何消息的引用供将来使用,因为这些引用都将会失效。 + +```java +public class SimpleDiscardHandler + extends SimpleChannelInboundHandler { + @Override + public void channelRead0(ChannelHandlerContext ctx, + Object msg) { + // No need to do anything special + } +} +``` + +## ChannelOutboundHandler 接口 + +ChannelOutboundHandler 一个强大的功能是可以按需推迟操作或者事件。 + +| 方法 | 描述 | +| ------------------------------------------------------------ | -------------------------------------------------- | +| bind(ChannelHandlerContext, SocketAddress, ChannelPromise) | 当请求将Channel绑定到本地地址时被调用 | +| connect(ChannelHandlerContext, SocketAddress, ChannelPromise) | 当请求连接到远程节点时被调用 | +| close(ChannelHandlerContext, ChannelPromise) | 当请求关闭Channel时被调用 | +| deregister(ChannelHandlerContext, ChannelPromise) | 当请求将Channel 从它的EventLoop 注销时被调用 | +| read(ChannelHandlerContext) | 当请求从Channel 读取更多的数据时被调用 | +| flush(ChannelHandlerContext) | 当请求通过Channel 将入队数据冲刷到远程节点时被调用 | +| write(ChannelHandlerContext, Object, ChannelPromise) | 当请求通过Channel 将数据写到远程节点时被调用 | + +ChannelPromise 是 ChannelFuture 的一个子类,ChannelOutboundHandler 中的大部分方法都需要一个 ChannelPromise参数,以便在操作完成时得到通知。 + + +## ChannelHandlerAdapter + +ChannelHandlerAdapter 提供了实用方法 isSharable(),如果其对应的实现被标注成 @Sharable,那么这个方法将返回true,表示它可以被添加到多个 ChannelPipeline 中。 + +共享 ChannelHandler 一个常见的用途是用于收集跨越多个 channel 的统计信息。 + +## 资源管理 + +idea 配置 edit configuration -- vm options -- `-Dio.netty.leakDetectionLevel=ADVANCED ` + + + diff --git a/docs/framework/netty/7-channel-pipeline.md b/docs/framework/netty/7-channel-pipeline.md new file mode 100644 index 0000000..ea7d112 --- /dev/null +++ b/docs/framework/netty/7-channel-pipeline.md @@ -0,0 +1,79 @@ +# ChannelPipeline 接口 + +每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline,这项关联是永久性的;Channel 既不能附加另外一个ChannelPipeline,也不能分离其当前的。 + +ChannelHandlerContext 使得 ChannelHandler 能够和它的 ChannelPipeline 以及其他的 ChannelHandler 交互。 + +## 修改ChannelPipeline + +ChannelHandler 可以通过添加、删除或者替换其他的ChannelHandler 来实时地修改ChannelPipeline 的布局。 + +| 方法 | 描述 | +| ----------------------------------- | ---------------------------------------- | +| addFirst/addBefore/addAfter/addLast | 将 ChannelHandler 添加到 ChannelPipeline | +| remove | 移除 | +| replace | 替换 | + +ChannelPipeline 的用于访问ChannelHandler 的操作: + +| 方法 | 描述 | +| ------- | ----------------------------------------------- | +| get | 返回ChannelHandler | +| context | 返回和ChannelHandler绑定的ChannelHandlerContext | +| names | 返回所有的ChannelHanlder名称 | + +## ChannelHandlerContext 接口 + +ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandlerContext。 + +![](http://img.topjavaer.cn/img/netty1.png) + +| 方法 | 描述 | +| --------------- | ---------------------------------------------------------- | +| fireChannelRead | 触发对下一个ChannelInboundHandler的channelRead()方法的调用 | +| alloc | 返回相关联的Channel所配置的ByteBufAllocator | +| bind | 绑定到给定的SocketAddress,并返回ChannelFuture | + +## 异常处理 + +入站异常:在 ChannelInboundHandler 实现 exceptionCaught 方法。 + +出站异常: + +1.添加ChannelFutureListener 到ChannelFuture + +```java + ChannelFuture future = channel.write(someMessage); + future.addListener(new ChannelFutureListener() { + @Override + public void operationComplete (ChannelFuture f){ + if (!f.isSuccess()) { + f.cause().printStackTrace(); + f.channel().close(); + } + } + }); +``` + +2.添加ChannelFutureListener 到ChannelPromise: + +```java +public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter { + @Override + public void write(ChannelHandlerContext ctx, Object msg, + ChannelPromise promise) { + promise.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture f) { + if (!f.isSuccess()) { + f.cause().printStackTrace(); + f.channel().close(); + } + } + }); + } +} +``` + + + diff --git a/docs/framework/netty/8-eventloop-thread-model.md b/docs/framework/netty/8-eventloop-thread-model.md new file mode 100644 index 0000000..4f6ed89 --- /dev/null +++ b/docs/framework/netty/8-eventloop-thread-model.md @@ -0,0 +1,46 @@ +# EventLoop 和线程模型 + +## EventLoop 接口 + +EventLoop 构建在 java.util.concurrent 和 io.netty.channel 之上。EventLoop 继承了 ScheduledExecutorService。EventLoop 由一个永远不会改变的 Thread 驱动,同时任务可以直接提交给 EventLoop 实现。EventLoop 可能服务于多个 Channel。 + +Netty4中所有的 io 操作和事件都由 EventLoop 的 Thread 处理。Netty3只保证入站事件在 EventLoop(io 线程)执行,所有出站事件都由调用线程处理,可能是 io 线程或者别的线程,因此需要在 ChannelHandler 中对出站事件进行同步。 + +Netty 4 中所采用的线程模型,通过在同一个线程中处理某个给定的EventLoop 中所产生的所有事件,解决了这个问题。这提供了一个更加简单的执行体系架构,并且消除了在多个ChannelHandler 中进行同步的需要 + +## 任务调度 + +使用 EventLoop 调度任务: + +```java +Channel ch = ... +ScheduledFuture future = ch.eventLoop().schedule( + new Runnable() { + @Override + public void run() { + //逻辑 + } +}, 60, TimeUnit.SECONDS); +``` + +周期性任务: + +```java +Channel ch = ... +ScheduledFuture future = ch.eventLoop().scheduleAtFixedRate( + new Runnable() { + @Override + public void run() { + //逻辑 + } +}, 60, 60, TimeUnit.SECONDES); +``` + +## 实现细节 + +![](http://img.topjavaer.cn/img/netty-eventloop执行逻辑.png) + + + + + diff --git a/docs/framework/netty/9-guide.md b/docs/framework/netty/9-guide.md new file mode 100644 index 0000000..83d31e4 --- /dev/null +++ b/docs/framework/netty/9-guide.md @@ -0,0 +1,57 @@ +# 引导 + +配置 netty 应用程序,使它运行起来。服务器使用一个父 Channel 接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信。客户端只需要一个 Channel 完成所有的网络交互。 + +引导类是 cloneable 的,在引导类实例上调用 clone() 就可以创建多个具有类似配置或者完全相同配置的 Channel。 + +## 引导客户端 + +BootStrap 类被用于客户端或者使用了无连接协议的应用程序中。 + +## 引导服务器 + +![](http://img.topjavaer.cn/img/ServerBoostrap和ServerChannel.png) + +在基类AbstractBootstrap有handler方法,目的是添加一个handler,监听Bootstrap的动作。 + +在服务端的ServerBootstrap中增加了一个方法childHandler,它的目的是添加handler,用来监听已经连接的客户端的Channel的动作和状态。 + +**handler在初始化时就会执行,而childHandler会在客户端成功connect后才执行。** + +## 在引导过程添加多个 ChannelHandler + +在 handler 传入 ChannelInitializer 的实现类,重写 initChannel 方法,在这个方法中添加多个 ChannelHandler。 + +```java +EventLoopGroup group = new NioEventLoopGroup(); +try { + Bootstrap bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class)//适用于nio传输的channel类型 + .remoteAddress(new InetSocketAddress(host, port)) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel socketChannel) throws Exception { + socketChannel.pipeline().addLast( + new EchoClientHandler()); + } + }); + ChannelFuture future = bootstrap.connect().sync();//连接到远程节点,阻塞等待 + future.channel().closeFuture().sync();//阻塞直到channel关闭 +} finally { + group.shutdownGracefully().sync();//关闭线程池并释放所有的资源 +} +``` + +## 关闭 + +关闭 EventLoopGroup,它将处理任何挂起的事件和任务,随后释放所有活动的线程。 + +```java +Future future = group.shutdownGracefully();//释放所有资源,关闭Channel +// block until the group has shutdown +future.syncUninterruptibly(); +``` + + + diff --git a/docs/framework/netty/README.md b/docs/framework/netty/README.md new file mode 100644 index 0000000..f679043 --- /dev/null +++ b/docs/framework/netty/README.md @@ -0,0 +1,24 @@ +--- +title: Netty基础 +icon: netty +date: 2022-08-06 +category: netty +star: true +--- + +![](http://img.topjavaer.cn/img/netty-img.png) + + + +## Netty总结 + +- [简介](./1-overview.md) +- [简单的netty应用程序](./2-make-your-app.md) +- [Netty的组件和设计](./3-component.md) +- [传输](./4-transport.md) +- [ChannelHandler](./6-channel-handler.md) +- [ChannelPipeline接口](./7-channel-pipeline.md) +- [EventLoop和线程模型](./8-eventloop-thread-model.md) +- [引导](./9-guide.md) +- [编解码器](./10-encoder-decoder.md) +- [预置的ChannelHandler和编解码器](./11-preset-channel-handler.md) diff --git "a/\346\241\206\346\236\266/Spring\351\235\242\350\257\225\351\242\230.md" b/docs/framework/spring.md similarity index 86% rename from "\346\241\206\346\236\266/Spring\351\235\242\350\257\225\351\242\230.md" rename to docs/framework/spring.md index 82c11b5..4ea008b 100644 --- "a/\346\241\206\346\236\266/Spring\351\235\242\350\257\225\351\242\230.md" +++ b/docs/framework/spring.md @@ -1,9 +1,31 @@ +--- +sidebar: heading +title: Spring常见面试题总结 +category: 框架 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring面试题,Spring设计模式,Spring AOP,Spring IOC,Spring 动态代理,Bean生命周期,自动装配,Spring注解,Spring事务,Async注解 + - - meta + - name: description + content: 高质量的Spring常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## Spring的优点 - 通过控制反转和依赖注入实现**松耦合**。 - 支持**面向切面**的编程,并且把应用业务逻辑和系统服务分开。 - 通过切面和模板减少样板式代码。 -- 声明事物的支持。可以从单调繁冗的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。 +- 声明式事务的支持。可以从单调繁冗的事务管理代码中解脱出来,通过声明式方式灵活地进行事务的管理,提高开发效率和质量。 - 方便集成各种优秀框架。内部提供了对各种优秀框架的直接支持(如:Hessian、Quartz、MyBatis等)。 - 方便程序的测试。Spring支持Junit4,添加注解便可以测试Spring程序。 @@ -80,6 +102,18 @@ AOP有两种实现方式:静态代理和动态代理。 动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## Spring AOP的实现原理 `Spring`的`AOP`实现原理其实很简单,就是通过**动态代理**实现的。如果我们为`Spring`的某个`bean`配置了切面,那么`Spring`在创建这个`bean`的时候,实际上创建的是这个`bean`的一个代理对象,我们后续对`bean`中方法的调用,实际上调用的是代理类重写的代理方法。而`Spring`的`AOP`使用了两种动态代理,分别是**JDK的动态代理**,以及**CGLib的动态代理**。 @@ -184,7 +218,7 @@ finishBeanFactoryInitialization(beanFactory); ## Bean的生命周期 -![](http://img.dabin-coder.cn/image/bean生命周期.png) +![](http://img.topjavaer.cn/img/20220709213529.png) 1.调用bean的构造方法创建Bean @@ -194,17 +228,17 @@ finishBeanFactoryInitialization(beanFactory); 4.如果Bean实现了`BeanFactoryAware`接口,Spring将调用`setBeanFactory()`把bean factory设置给Bean -5.如果Bean实现了`ApplicationContextAware`接口,Spring容器将调用`setApplicationContext()`给Bean设置ApplictionContext +5.如果存在`BeanPostProcessor`,Spring将调用它们的`postProcessBeforeInitialization`(预初始化)方法,在Bean初始化前对其进行处理 -6.如果存在`BeanPostProcessor`,Spring将调用它们的`postProcessBeforeInitialization`(预初始化)方法,在Bean初始化前对其进行处理 +6.如果Bean实现了`InitializingBean`接口,Spring将调用它的`afterPropertiesSet`方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行 -7.如果Bean实现了`InitializingBean`接口,Spring将调用它的`afterPropertiesSet`方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行 +7.如果存在`BeanPostProcessor`,Spring将调用它们的`postProcessAfterInitialization`(后初始化)方法,在Bean初始化后对其进行处理 -8.如果存在`BeanPostProcessor`,Spring将调用它们的`postProcessAfterInitialization`(后初始化)方法,在Bean初始化后对其进行处理 +8.Bean初始化完成,供应用使用,这里分两种情况: -9.Bean初始化完成,供应用使用,直到应用被销毁 +8.1 如果Bean为单例的话,那么容器会返回Bean给用户,并存入缓存池。如果Bean实现了`DisposableBean`接口,Spring将调用它的`destory`方法,然后调用在xml中定义的 `destory-method`方法,这两个方法作用类似,都是在Bean实例销毁前执行。 -10.如果Bean实现了`DisposableBean`接口,Spring将调用它的`destory`方法,然后调用在xml中定义的 `destory-method`方法,这两个方法作用类似,都是在Bean实例销毁前执行 +8.2 如果Bean是多例的话,容器将Bean返回给用户,剩下的生命周期由用户控制。 ```java public interface BeanPostProcessor { @@ -270,9 +304,29 @@ public class SqlSessionFactoryBean implements FactoryBean, In Spring 将会在应用启动时创建 `SqlSessionFactory`,并使用 `sqlSessionFactory` 这个名字存储起来。 +## BeanFactory和ApplicationContext有什么区别? + +BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。 + +两者区别如下: + +1、功能上的区别。BeanFactory是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。 + +ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能,如继承MessageSource、支持国际化、统一的资源文件访问方式、同时加载多个配置文件等功能。 + +2、加载方式的区别。BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。 + +而ApplicationContext是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单例Bean,那么在需要的时候,不需要等待创建bean,因为它们已经创建好了。 + +相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。 + +3、创建方式的区别。BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。 + +4、注册方式的区别。BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。 + ## Bean注入容器有哪些方式? -1、@Configuration + @Bean +1、**@Configuration + @Bean** @Configuration用来声明一个配置类,然后使用 @Bean 注解,用于声明一个bean,将其加入到Spring容器中。 @@ -288,7 +342,7 @@ public class MyConfiguration { } ``` -2、通过包扫描特定注解的方式 +2、**通过包扫描特定注解的方式** @ComponentScan放置在我们的配置类上,然后可以指定一个路径,进行扫描带有特定注解的bean,然后加至容器中。 @@ -310,7 +364,7 @@ public class Demo1 { } ``` -3、@Import注解导入 +3、**@Import注解导入** @Import注解平时开发用的不多,但是也是非常重要的,在进行Spring扩展时经常会用到,它经常搭配自定义注解进行使用,然后往容器中导入一个配置文件。 @@ -327,7 +381,7 @@ public class App { } ``` -4、实现BeanDefinitionRegistryPostProcessor进行后置处理。 +4、**实现BeanDefinitionRegistryPostProcessor进行后置处理。** 在Spring容器启动的时候会执行 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法,就是等beanDefinition加载完毕之后,对beanDefinition进行后置处理,可以在此进行调整IOC容器中的beanDefinition,从而干扰到后面进行初始化bean。 @@ -358,7 +412,7 @@ class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPos } ``` -5、使用FactoryBean接口 +5、**使用FactoryBean接口** 如下图代码,使用@Configuration + @Bean的方式将 PersonFactoryBean 加入到容器中,这里没有向容器中直接注入 Person,而是注入 PersonFactoryBean,然后从容器中拿Person这个类型的bean。 @@ -669,11 +723,15 @@ public class OrderServiceImpl implements OrderService { ## Spring怎么解决循环依赖的问题? -构造器注入的循环依赖:Spring处理不了,直接抛出`BeanCurrentlylnCreationException`异常。 +首先,有两种Bean注入的方式。 -单例模式下属性注入的循环依赖:通过三级缓存处理循环依赖。 +构造器注入和属性注入。 -非单例循环依赖:无法处理。 +对于构造器注入的循环依赖,Spring处理不了,会直接抛出`BeanCurrentlylnCreationException`异常。 + +对于属性注入的循环依赖(单例模式下),是通过三级缓存处理来循环依赖的。 + +而非单例对象的循环依赖,则无法处理。 下面分析单例模式下属性注入的循环依赖是怎么处理的: @@ -823,7 +881,7 @@ asyncTest... 我们在主启动类上贴了一个@EnableAsync注解,才能使用@Async生效。@EnableAsync的作用是通过@import导入了AsyncConfigurationSelector。在AsyncConfigurationSelector的selectImports方法将ProxyAsyncConfiguration定义为Bean注入容器。在ProxyAsyncConfiguration中通过@Bean的方式注入AsyncAnnotationBeanPostProcessor类。 -![](http://img.dabin-coder.cn/image/20220628224208.png) +![](http://img.topjavaer.cn/img/20220628224208.png) 代码如下: @@ -911,6 +969,34 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport imple 2.调用本类的异步方法是不会起作用的。这种方式绕过了代理而直接调用了方法,@Async注解会失效。 +## 为什么 Spring和IDEA 都不推荐使用 @Autowired 注解? + +idea 在我们经常使用的`@Autowired` 注解上添加了警告。警告内容是: `Field injection is not recommended`, 译为: **不推荐使用属性注入**。 + +Spring常用的注入方式有:属性注入, 构造方法注入, set 方法注入 + +- 构造器注入:利用构造方法的参数注入依赖 +- set方法注入:调用setter的方法注入依赖 +- 属性注入:在字段上使用@Autowired/Resource注解 + +其中,基于属性注入的方式,容易导致Spring 初始化失败。因为在Spring在初始化的时候,可能由于属性在被注入前就引用而导致空指针异常,进而导致容器初始化失败。 + +如果可能的话,尽量使用构造器注入。Lombok提供了一个注解`@RequiredArgsConstructor`, 可以方便我们快速进行构造注入。 + +@Autowired是属性注入,而且@Autowired默认是按照类型匹配(ByType),因此有可能会出现两个相同的类型bean,进而导致Spring 装配失败。 + +如果要使用属性注入的话,可以使用 `@Resource` 代替 `@Autowired` 注解。@Resource默认是按照名称匹配(ByName),如果找不到则是ByType注入。另外,@Autowired是Spring提供的,@Resource是JSR-250提供的,是Java标准,我们使用的IoC容器会去兼容它,这样即使更换容器,也可以正常工作。 + + +> 最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: +> +> ![](http://img.topjavaer.cn/image/Image.png) +> +> ![](http://img.topjavaer.cn/image/image-20221030094126118.png) +> +> **200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# +> +> 备用链接:https://pan.quark.cn/s/3f1321952a16 -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/framework/spring/transaction-propagation.md b/docs/framework/spring/transaction-propagation.md new file mode 100644 index 0000000..668df6e --- /dev/null +++ b/docs/framework/spring/transaction-propagation.md @@ -0,0 +1,635 @@ +Spring 在 TransactionDefinition 接口中规定了 7 种类型的事务传播行为。事务传播行为是 Spring 框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。 + +这是 Spring 为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。 + +但是人们对他的误解也颇多,你一定也听过“service 方法事务最好不要嵌套”的传言。 + +要想正确的使用工具首先需要了解工具。本文对七种事务传播行为做详细介绍,内容主要代码示例的方式呈现。 + +# 基础概念 + +## 1. 什么是事务传播行为? + +事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。 + +用伪代码说明: + +```java + public void methodA(){ + methodB(); + //doSomething + } + + @Transaction(Propagation=XXX) + public void methodB(){ + //doSomething + } +``` + +代码中`methodA()`方法嵌套调用了`methodB()`方法,`methodB()`的事务传播行为由`@Transaction(Propagation=XXX)`设置决定。这里需要注意的是`methodA()`并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。 + +## 2. Spring 中七种事务传播行为 + +![](http://img.topjavaer.cn/img/spring事务传播行为1.png) + +定义非常简单,也很好理解,下面我们就进入代码测试部分,验证我们的理解是否正确。 + +# 代码验证 + +文中代码以传统三层结构中两层呈现,即 Service 和 Dao 层,由 Spring 负责依赖注入和注解式事务管理,DAO 层由 Mybatis 实现,你也可以使用任何喜欢的方式,例如,Hibernate,JPA,JDBCTemplate 等。数据库使用的是 MySQL 数据库,你也可以使用任何支持事务的数据库,并不会影响验证结果。 + +首先我们在数据库中创建两张表: + +**user1** + +``` +CREATE TABLE `user1` ( + `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(45) NOT NULL DEFAULT '', + PRIMARY KEY(`id`) +) +ENGINE = InnoDB; +``` + +**user2** + +``` +CREATE TABLE `user2` ( + `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(45) NOT NULL DEFAULT '', + PRIMARY KEY(`id`) +) +ENGINE = InnoDB; +``` + +然后编写相应的 Bean 和 DAO 层代码: + +**User1** + +``` +public class User1 { + private Integer id; + private String name; + //get和set方法省略... +} +``` + +**User2** + +``` +public class User2 { + private Integer id; + private String name; + //get和set方法省略... +} +``` + +**User1Mapper** + +``` +public interface User1Mapper { + int insert(User1 record); + User1 selectByPrimaryKey(Integer id); + //其他方法省略... +} +``` + +**User2Mapper** + +``` +public interface User2Mapper { + int insert(User2 record); + User2 selectByPrimaryKey(Integer id); + //其他方法省略... +} +``` + +最后也是具体验证的代码由 service 层实现,下面我们分情况列举。 + +## 1.PROPAGATION_REQUIRED + +我们为 User1Service 和 User2Service 相应方法加上`Propagation.REQUIRED`属性。 + +**User1Service 方法:** + +```java +@Service +public class User1ServiceImpl implements User1Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void addRequired(User1 user){ + user1Mapper.insert(user); + } +} +``` + +**User2Service 方法:** + +```java +@Service +public class User2ServiceImpl implements User2Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void addRequired(User2 user){ + user2Mapper.insert(user); + } + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void addRequiredException(User2 user){ + user2Mapper.insert(user); + throw new RuntimeException(); + } + +} +``` + +### 1.1 场景一 + +此场景外围方法没有开启事务。 + +**验证方法 1:** + +```java + @Override + public void notransaction_exception_required_required(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequired(user2); + + throw new RuntimeException(); + } +``` + +**验证方法 2:** + +```java + @Override + public void notransaction_required_required_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiredException(user2); + } +``` + +分别执行验证方法,结果: + +![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E) + +**结论:通过这两个方法我们证明了在外围方法未开启事务的情况下`Propagation.REQUIRED`修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。** + +### 1.2 场景二 + +外围方法开启事务,这个是使用率比较高的场景。 + +**验证方法 1:** + +```java + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void transaction_exception_required_required(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequired(user2); + + throw new RuntimeException(); + } +``` + +**验证方法 2:** + +```java + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void transaction_required_required_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiredException(user2); + } +``` + +**验证方法 3:** + +```java + @Transactional + @Override + public void transaction_required_required_exception_try(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + try { + user2Service.addRequiredException(user2); + } catch (Exception e) { + System.out.println("方法回滚"); + } + } +``` + +分别执行验证方法,结果: + +![](http://img.topjavaer.cn/img/spring事务传播行为2.png) + +**结论:以上试验结果我们证明在外围方法开启事务的情况下`Propagation.REQUIRED`修饰的内部方法会加入到外围方法的事务中,所有`Propagation.REQUIRED`修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。** + +## 2.PROPAGATION_REQUIRES_NEW + +我们为 User1Service 和 User2Service 相应方法加上`Propagation.REQUIRES_NEW`属性。**User1Service 方法:** + +```java +@Service +public class User1ServiceImpl implements User1Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void addRequiresNew(User1 user){ + user1Mapper.insert(user); + } + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void addRequired(User1 user){ + user1Mapper.insert(user); + } +} +``` + +**User2Service 方法:** + +```java +@Service +public class User2ServiceImpl implements User2Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void addRequiresNew(User2 user){ + user2Mapper.insert(user); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void addRequiresNewException(User2 user){ + user2Mapper.insert(user); + throw new RuntimeException(); + } +} +``` + +### 2.1 场景一 + +外围方法没有开启事务。 + +**验证方法 1:** + +```java + @Override + public void notransaction_exception_requiresNew_requiresNew(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequiresNew(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiresNew(user2); + throw new RuntimeException(); + + } +``` + +**验证方法 2:** + +```java + @Override + public void notransaction_requiresNew_requiresNew_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequiresNew(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiresNewException(user2); + } +``` + +分别执行验证方法,结果: + +![](http://img.topjavaer.cn/img/spring事务传播行为3.png) + +**结论:通过这两个方法我们证明了在外围方法未开启事务的情况下`Propagation.REQUIRES_NEW`修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。** + +### 2.2 场景二 + +外围方法开启事务。 + +**验证方法 1:** + +```java + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void transaction_exception_required_requiresNew_requiresNew(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiresNew(user2); + + User2 user3=new User2(); + user3.setName("王五"); + user2Service.addRequiresNew(user3); + throw new RuntimeException(); + } +``` + +**验证方法 2:** + +```java + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void transaction_required_requiresNew_requiresNew_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiresNew(user2); + + User2 user3=new User2(); + user3.setName("王五"); + user2Service.addRequiresNewException(user3); + } +``` + +**验证方法 3:** + +```java + @Override + @Transactional(propagation = Propagation.REQUIRED) + public void transaction_required_requiresNew_requiresNew_exception_try(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addRequired(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addRequiresNew(user2); + User2 user3=new User2(); + user3.setName("王五"); + try { + user2Service.addRequiresNewException(user3); + } catch (Exception e) { + System.out.println("回滚"); + } + } +``` + +分别执行验证方法,结果: + +![](http://img.topjavaer.cn/img/spring事务传播行为4.png) + +**结论:在外围方法开启事务的情况下`Propagation.REQUIRES_NEW`修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。** + +## 3.PROPAGATION_NESTED + +我们为 User1Service 和 User2Service 相应方法加上`Propagation.NESTED`属性。**User1Service 方法:** + +```java +@Service +public class User1ServiceImpl implements User1Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.NESTED) + public void addNested(User1 user){ + user1Mapper.insert(user); + } +} +``` + +**User2Service 方法:** + +```java +@Service +public class User2ServiceImpl implements User2Service { + //省略其他... + @Override + @Transactional(propagation = Propagation.NESTED) + public void addNested(User2 user){ + user2Mapper.insert(user); + } + + @Override + @Transactional(propagation = Propagation.NESTED) + public void addNestedException(User2 user){ + user2Mapper.insert(user); + throw new RuntimeException(); + } +} +``` + +### 3.1 场景一 + +此场景外围方法没有开启事务。 + +**验证方法 1:** + +```java + @Override + public void notransaction_exception_nested_nested(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addNested(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addNested(user2); + throw new RuntimeException(); + } +``` + +**验证方法 2:** + +```java + @Override + public void notransaction_nested_nested_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addNested(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addNestedException(user2); + } +``` + +分别执行验证方法,结果: + +![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E) + +**结论:通过这两个方法我们证明了在外围方法未开启事务的情况下`Propagation.NESTED`和`Propagation.REQUIRED`作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。** + +### 3.2 场景二 + +外围方法开启事务。 + +**验证方法 1:** + +```java + @Transactional + @Override + public void transaction_exception_nested_nested(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addNested(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addNested(user2); + throw new RuntimeException(); + } +``` + +**验证方法 2:** + +```java + @Transactional + @Override + public void transaction_nested_nested_exception(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addNested(user1); + + User2 user2=new User2(); + user2.setName("李四"); + user2Service.addNestedException(user2); + } +``` + +**验证方法 3:** + +```java + @Transactional + @Override + public void transaction_nested_nested_exception_try(){ + User1 user1=new User1(); + user1.setName("张三"); + user1Service.addNested(user1); + + User2 user2=new User2(); + user2.setName("李四"); + try { + user2Service.addNestedException(user2); + } catch (Exception e) { + System.out.println("方法回滚"); + } + } +``` + +分别执行验证方法,结果: + +![](http://img.topjavaer.cn/img/spring事务传播行为5.png) + +结论:以上试验结果我们证明在外围方法开启事务的情况下`Propagation.NESTED`修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务 + +## 4. REQUIRED,REQUIRES_NEW,NESTED 异同 + +由“1.2 场景二”和“3.2 场景二”对比,我们可知:**NESTED 和 REQUIRED 修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是 REQUIRED 是加入外围方法事务,所以和外围事务同属于一个事务,一旦 REQUIRED 事务抛出异常被回滚,外围方法事务也将被回滚。而 NESTED 是外围方法的子事务,有单独的保存点,所以 NESTED 方法抛出异常被回滚,不会影响到外围方法的事务。** + +由“2.2 场景二”和“3.2 场景二”对比,我们可知:**NESTED 和 REQUIRES_NEW 都可以做到内部方法事务回滚而不影响外围方法事务。但是因为 NESTED 是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而 REQUIRES_NEW 是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。** + +## 5. 其他事务传播行为 + +鉴于文章篇幅问题,其他事务传播行为的测试就不在此一一描述了,感兴趣的读者可以去源码中自己寻找相应测试代码和结果解释。传送门:https://github.com/TmTse/transaction-test + +# 模拟用例 + +介绍了这么多事务传播行为,我们在实际工作中如何应用呢?下面我来举一个示例: + +假设我们有一个注册的方法,方法中调用添加积分的方法,如果我们希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),我们会这样写: + +```jaav + @Service + public class UserServiceImpl implements UserService { + + @Transactional + public void register(User user){ + + try { + membershipPointService.addPoint(Point point); + } catch (Exception e) { + //省略... + } + //省略... + } + //省略... + } +``` + +我们还规定注册失败要影响`addPoint()`方法(注册方法回滚添加积分方法也需要回滚),那么`addPoint()`方法就需要这样实现: + +```java + @Service + public class MembershipPointServiceImpl implements MembershipPointService{ + + @Transactional(propagation = Propagation.NESTED) + public void addPoint(Point point){ + + try { + recordService.addRecord(Record record); + } catch (Exception e) { + //省略... + } + //省略... + } + //省略... + } +``` + +我们注意到了在`addPoint()`中还调用了`addRecord()`方法,这个方法用来记录日志。他的实现如下: + +```java + @Service + public class RecordServiceImpl implements RecordService{ + + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public void addRecord(Record record){ + + + //省略... + } + //省略... + } +``` + +我们注意到`addRecord()`方法中`propagation = Propagation.NOT_SUPPORTED`,因为对于日志无所谓精确,可以多一条也可以少一条,所以`addRecord()`方法本身和外围`addPoint()`方法抛出异常都不会使`addRecord()`方法回滚,并且`addRecord()`方法抛出异常也不会影响外围`addPoint()`方法的执行。 + +通过这个例子相信大家对事务传播行为的使用有了更加直观的认识,通过各种属性的组合确实能让我们的业务实现更加灵活多样。 + +# 结论 + +通过上面的介绍,相信大家对 Spring 事务传播行为有了更加深入的理解,希望大家日常开发工作有所帮助。 + + + +> 作者:handaqiang +> +> 链接:https://segmentfault.com/a/1190000013341344 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/SpringBoot\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" b/docs/framework/springboot.md similarity index 54% rename from "\346\241\206\346\236\266/SpringBoot\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" rename to docs/framework/springboot.md index 7e70a4c..a0b52ad 100644 --- "a/\346\241\206\346\236\266/SpringBoot\351\235\242\350\257\225\351\242\230\346\200\273\347\273\223.md" +++ b/docs/framework/springboot.md @@ -1,3 +1,25 @@ +--- +sidebar: heading +title: Springboot常见面试题总结 +category: 框架 +tag: + - SpringBoot +head: + - - meta + - name: keywords + content: Spring Boot面试题,Spring Boot,自动配置,Spring Boot注解,Spring Boot多数据源 + - - meta + - name: description + content: 高质量的Springboot常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## Springboot的优点 - 内置servlet容器,不需要在服务器部署 tomcat。只需要将项目打成 jar 包,使用 java -jar xxx.jar一键式启动项目 @@ -5,6 +27,18 @@ - 可以快速创建独立运行的spring项目,集成主流框架 - 准生产环境的运行应用监控 +## Javaweb、spring、springmvc和springboot有什么区别,都是做什么用的? + +JavaWeb是 Java 语言的 Web 开发技术,主要用于开发 Web 应用程序,包括基于浏览器的客户端和基于服务器的 Web 服务器。 + +Spring是一个轻量级的开源开发框架,主要用于管理 Java 应用程序中的组件和对象,并提供各种服务,如事务管理、安全控制、面向切面编程和远程访问等。它是一个综合性框架,可应用于所有类型的 Java 应用程序。 + +SpringMVC是 Spring 框架中的一个模块,用于开发 Web 应用程序并实现 MVC(模型-视图-控制器)设计模式,它将请求和响应分离,从而使得应用程序更加模块化、可扩展和易于维护。 + +Spring Boot是基于 Spring 框架开发的用于开发 Web 应用程序的框架,它帮助开发人员快速搭建和配置一个独立的、可执行的、基于 Spring 的应用程序,从而减少了繁琐和重复的配置工作。 + +综上所述,JavaWeb是基于 Java 语言的 Web 开发技术,而 Spring 是一个综合性的开发框架,SpringMVC用于开发 Web 应用程序实现 MVC 设计模式,而 Spring Boot 是基于 Spring 的 Web 应用程序开发框架。 + ## SpringBoot 中的 starter 到底是什么 ? starter提供了一个自动化配置类,一般命名为 XXXAutoConfiguration ,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是 Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配置,然后通过类型安全的属性注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。 @@ -22,13 +56,89 @@ starter提供了一个自动化配置类,一般命名为 XXXAutoConfiguration 3. spring-boot-starter-data-Redis :提供 Redis 。 4. mybatis-spring-boot-starter :提供 MyBatis 。 +## Spring Boot 的核心注解是哪个? + +启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: + +- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 +- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 +- @ComponentScan:Spring组件扫描。 + +## 有哪些常用的SpringBoot注解? + +- @SpringBootApplication。这个注解是Spring Boot最核心的注解,用在 Spring Boot的主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力 + +- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 + +- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 + +- @ComponentScan:Spring组件扫描。 + +- @Repository:用于标注数据访问组件,即DAO组件。 + +- @Service:一般用于修饰service层的组件 + +- **@RestController**。用于标注控制层组件(如struts中的action),表示这是个控制器bean,并且是将函数的返回值直 接填入HTTP响应体中,是REST风格的控制器;它是@Controller和@ResponseBody的合集。 + +- **@ResponseBody**。表示该方法的返回结果直接写入HTTP response body中 + +- **@Component**。泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 + +- **@Bean**,相当于XML中的``,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。 + +- **@AutoWired**,byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 + +- **@Qualifier**。当有多个同一类型的Bean时,可以用@Qualifier("name")来指定。与@Autowired配合使用 + +- **@Resource(name="name",type="type")**。没有括号内内容的话,默认byName。与@Autowired干类似的事。 + +- **@RequestMapping** + + RequestMapping是一个用来处理请求地址映射的注解;提供路由信息,负责URL到Controller中的具体函数的映射,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 + +- **@RequestParam** + + 用在方法的参数前面。 + +- ### @Scope + + 用于声明一个Spring`Bean`实例的作用域 + +- ### @Primary + + 当同一个对象有多个实例时,优先选择该实例。 + +- ### @PostConstruct + + 用于修饰方法,当对象实例被创建并且依赖注入完成后执行,可用于对象实例的初始化操作。 + +- ### @PreDestroy + + 用于修饰方法,当对象实例将被Spring容器移除时执行,可用于对象实例持有资源的释放。 + +- ### @EnableTransactionManagement + + 启用Spring基于注解的事务管理功能,需要和`@Configuration`注解一起使用。 + +- ### @Transactional + + 表示方法和类需要开启事务,当作用与类上时,类中所有方法均会开启事务,当作用于方法上时,方法开启事务,方法上的注解无法被子类所继承。 + +- ### @ControllerAdvice + + 常与`@ExceptionHandler`注解一起使用,用于捕获全局异常,能作用于所有controller中。 + +- ### @ExceptionHandler + + 修饰方法时,表示该方法为处理全局异常的方法。 + ## 自动配置原理 SpringBoot实现自动配置原理图解: > 公众号【程序员大彬】,回复【自动配置】下载高清图片 -![](http://img.dabin-coder.cn/image/SpringBoot的自动配置原理.jpg) +![](http://img.topjavaer.cn/img/SpringBoot的自动配置原理.jpg) 在 application.properties 中设置属性 debug=true,可以在控制台查看已启用和未启用的自动配置。 @@ -291,10 +401,59 @@ public class SpringbootDemoApplication { hello.msg=大彬 ``` -## @Value原理 +## @Value注解的原理 @Value的解析就是在bean初始化阶段。BeanPostProcessor定义了bean初始化前后用户可以对bean进行操作的接口方法,它的一个重要实现类`AutowiredAnnotationBeanPostProcessor`为bean中的@Autowired和@Value注解的注入功能提供支持。 +## Spring Boot 需要独立的容器运行吗? + +不需要,内置了 Tomcat/ Jetty 等容器。 + +## Spring Boot 支持哪些日志框架? + +Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架,但是不管是那种日志框架他都支持将配置文件输出到控制台或者文件中。 + +## YAML 配置的优势在哪里 ? + +YAML 配置和传统的 properties 配置相比之下,有这些优势: + +- 配置有序 +- 简洁明了,支持数组,数组中的元素可以是基本数据类型也可以是对象 + +缺点就是不支持 @PropertySource 注解导入自定义的 YAML 配置。 + +## 什么是 Spring Profiles? + +在项目的开发中,有些配置文件在开发、测试或者生产等不同环境中可能是不同的,例如数据库连接、redis的配置等等。那我们如何在不同环境中自动实现配置的切换呢?Spring给我们提供了profiles机制给我们提供的就是来回切换配置文件的功能 + +Spring Profiles 允许用户根据配置文件(dev,test,prod 等)来注册 bean。因此,当应用程序在开发中运行时,只有某些 bean 可以加载,而在 PRODUCTION中,某些其他 bean 可以加载。假设我们的要求是 Swagger 文档仅适用于 QA 环境,并且禁用所有其他文档。这可以使用配置文件来完成。Spring Boot 使得使用配置文件非常简单。 + +## SpringBoot多数据源事务如何管理 + +第一种方式是在service层的@TransactionManager中使用transactionManager指定DataSourceConfig中配置的事务。 + +第二种是使用jta-atomikos实现分布式事务管理。 + +## spring-boot-starter-parent 有什么用 ? + +新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用: + +1. 定义了 Java 编译版本。 +2. 使用 UTF-8 格式编码。 +3. 执行打包操作的配置。 +4. 自动化的资源过滤。 +5. 自动化的插件配置。 +6. 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。 + +## Spring Boot 打成的 jar 和普通的 jar 有什么区别 ? + +- Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 `java -jar xxx.jar` 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。 +- Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 `\BOOT-INF\classes` 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。 + +## SpringBoot多数据源拆分的思路 + +先在properties配置文件中配置两个数据源,创建分包mapper,使用@ConfigurationProperties读取properties中的配置,使用@MapperScan注册到对应的mapper包中 。 + -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/framework/springboot/springboot-contract.md b/docs/framework/springboot/springboot-contract.md new file mode 100644 index 0000000..10b730a --- /dev/null +++ b/docs/framework/springboot/springboot-contract.md @@ -0,0 +1,453 @@ +# SpringBoot实现电子文件签字+合同系统 + +## **一、前言** + +今天公司领导提出一个功能,说实现一个文件的签字+盖章功能,然后自己进行了简单的学习,对文档进行数字签名与签署纸质文档的原因大致相同,数字签名通过使用计算机加密来验证 (身份验证:验证人员和产品所声明的身份是否属实的过程。例如,通过验证用于签名代码的数字签名来确认软件发行商的代码来源和完整性。)数字信息,如文档、电子邮件和宏。数字签名有助于确保:真实性,完整性,不可否认性。目前市面上的电子签章产品也是多样化,但是不管是哪个厂家的产品,在线签章简单易用,同时也能保证签章的有效性,防篡改,防伪造,稳定,可靠就是好产品。 + +此次开源的系统模拟演示了文件在OA系统中的流转,主要为办公系统跨平台在线处理Office文档提供了完美的解决方案。Word文档在线处理的核心环节,包括:起草文档、领导审批、核稿、领导盖章、正式发文。PageOffice产品支持PC端Word文档在线处理的所有环节;MobOffice产品支持了移动端领导审批和领导盖章的功能。支持PC端和移动端对文档审批和盖章的互认。然后此次博客中使用的卓正软件的电子签章采用自主知识产权的核心智能识别验证技术,确保文档安全可靠。采用 COM、ActiveX嵌入式技术开发,确保软件能够支持多种应用。遵循《中华人民共和国电子签名法》关于电子签名的规范,同时支持国际通用的 RSA算法,符合国家安全标准。 + +PageOffice和MobOffice产品结合使用为跨平台处理Office文件提供了完美的解决方案,主要功能有word在线编辑保存和留痕,word和pdf文件在线盖章(电子印章)。 + +## **二、项目源码及部署** + +### **1、项目结构及使用框架** + +该签字+盖章流程系统使用了SpringBoot+thymeleaf实现的,然后jar包依赖使用了maven + +![](http://img.topjavaer.cn/img/image-20230208082529062.png) + +- 控制层 + +```typescript +@Controller +@RequestMapping("/mobile") +public class MobileOfficeController { + + @Value("${docpath}") + private String docPath; + + @Value("${moblicpath}") + private String moblicpath; + + @Autowired + DocService m_docService; + + /** + * 添加MobOffice的服务器端授权程序Servlet(必须) + * + */ + @RequestMapping("/opendoc") + public void opendoc(HttpServletRequest request, HttpServletResponse response, HttpSession session,String type,String userName)throws Exception { + String fileName = ""; + userName= URLDecoder.decode(userName,"utf-8"); + + Doc doc=m_docService.getDocById(1); + if(type.equals("word")){ + fileName = doc.getDocName(); + }else{ + fileName = doc.getPdfName(); + } + OpenModeType openModeType = OpenModeType.docNormalEdit; + + if (fileName.endsWith(".doc")) { + openModeType = OpenModeType.docNormalEdit; + } else if (fileName.endsWith(".pdf")) { + String mode = request.getParameter("mode"); + if (mode.equals("normal")) { + openModeType = OpenModeType.pdfNormal; + } else { + openModeType = OpenModeType.pdfReadOnly; + } + } + + MobOfficeCtrl mobCtrl = new MobOfficeCtrl(request,response); + mobCtrl.setSysPath(moblicpath); + mobCtrl.setServerPage("/mobserver.zz"); + //mobCtrl.setZoomSealServer("http://xxx.xxx.xxx.xxx:8080/ZoomSealEnt/enserver.zz"); + mobCtrl.setSaveFilePage("/mobile/savedoc?testid="+Math.random()); + mobCtrl.webOpen("file://"+docPath+fileName, openModeType , userName); + } + + @RequestMapping("/savedoc") + public void savedoc(HttpServletRequest request, HttpServletResponse response){ + FileSaver fs = new FileSaver(request, response); + fs.saveToFile(docPath+fs.getFileName()); + fs.close(); + } +} +复制代码 +``` + +- 项目业务层源码 + +```java +@Service +public class DocServiceImpl implements DocService { + @Autowired + DocMapper docMapper; + @Override + public Doc getDocById(int id) throws Exception { + Doc doc=docMapper.getDocById(id); + //如果doc为null的话,页面所有doc.属性都报错 + if(doc==null) { + doc=new Doc(); + } + return doc; + } + + @Override + public Integer addDoc(Doc doc) throws Exception { + int id=docMapper.addDoc(doc); + return id; + } + + @Override + public Integer updateStatusForDocById(Doc doc) throws Exception { + int id=docMapper.updateStatusForDocById(doc); + return id; + } + + @Override + public Integer updateDocNameForDocById(Doc doc) throws Exception { + int id=docMapper.updateDocNameForDocById(doc); + return id; + } + + @Override + public Integer updatePdfNameForDocById(Doc doc) throws Exception { + int id=docMapper.updatePdfNameForDocById(doc); + return id; + } +} +复制代码 +``` + +- 拷贝文件 + +```ini +public class CopyFileUtil { + //拷贝文件 + public static boolean copyFile(String oldPath, String newPath) throws Exception { + boolean copyStatus=false; + + int bytesum = 0; + int byteread = 0; + File oldfile = new File(oldPath); + if (oldfile.exists()) { //文件存在时 + InputStream inStream = new FileInputStream(oldPath); //读入原文件 + FileOutputStream fs = new FileOutputStream(newPath); + + byte[] buffer = new byte[1444]; + int length; + while ((byteread = inStream.read(buffer)) != -1) { + bytesum += byteread; //字节数 文件大小 + //System.out.println(bytesum); + fs.write(buffer, 0, byteread); + } + fs.close(); + inStream.close(); + copyStatus=true; + }else{ + copyStatus=false; + } + return copyStatus; + } +} +复制代码 +``` + +- 二维码源码 + +```ini +public class QRCodeUtil { + private String codeText;//二维码内容 + private BarcodeFormat barcodeFormat;//二维码类型 + private int width;//图片宽度 + private int height;//图片高度 + private String imageformat;//图片格式 + private int backColorRGB;//背景色,颜色RGB的数值既可以用十进制表示,也可以用十六进制表示 + private int codeColorRGB;//二维码颜色 + private ErrorCorrectionLevel errorCorrectionLevel;//二维码纠错能力 + private String encodeType; + + public QRCodeUtil() { + codeText = "www.zhuozhengsoft.com"; + barcodeFormat = BarcodeFormat.PDF_417; + width = 400; + height = 400; + imageformat = "png"; + backColorRGB = 0xFFFFFFFF; + codeColorRGB = 0xFF000000; + errorCorrectionLevel = ErrorCorrectionLevel.H; + encodeType = "UTF-8"; + } + public QRCodeUtil(String text) { + codeText = text; + barcodeFormat = BarcodeFormat.PDF_417; + width = 400; + height = 400; + imageformat = "png"; + backColorRGB = 0xFFFFFFFF; + codeColorRGB = 0xFF000000; + errorCorrectionLevel = ErrorCorrectionLevel.H; + encodeType = "UTF-8"; + } + + public String getCodeText() { + return codeText; + } + + public void setCodeText(String codeText) { + this.codeText = codeText; + } + + public BarcodeFormat getBarcodeFormat() { + return barcodeFormat; + } + + public void setBarcodeFormat(BarcodeFormat barcodeFormat) { + this.barcodeFormat = barcodeFormat; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public String getImageformat() { + return imageformat; + } + + public void setImageformat(String imageformat) { + this.imageformat = imageformat; + } + + public int getBackColorRGB() { + return backColorRGB; + } + + public void setBackColorRGB(int backColorRGB) { + this.backColorRGB = backColorRGB; + } + + public int getCodeColorRGB() { + return codeColorRGB; + } + + public void setCodeColorRGB(int codeColorRGB) { + this.codeColorRGB = codeColorRGB; + } + + public ErrorCorrectionLevel getErrorCorrectionLevel() { + return errorCorrectionLevel; + } + + public void setErrorCorrectionLevel(ErrorCorrectionLevel errorCorrectionLevel) { + this.errorCorrectionLevel = errorCorrectionLevel; + } + + private BufferedImage toBufferedImage(BitMatrix bitMatrix) { + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + image.setRGB(x, y, bitMatrix.get(x, y) ? this.codeColorRGB: this.backColorRGB); + } + } + return image; + } + + private byte[] writeToBytes(BitMatrix bitMatrix) + throws IOException { + + try { + BufferedImage bufferedimage = toBufferedImage(bitMatrix); + + //将图片保存到临时路径中 + File file = java.io.File.createTempFile("~pic","."+ this.imageformat); + //System.out.println("临时图片路径:"+file.getPath()); + ImageIO.write(bufferedimage,this.imageformat,file); + + //获取图片转换成的二进制数组 + FileInputStream fis = new FileInputStream(file); + int fileSize = fis.available(); + byte[] imageBytes = new byte[fileSize]; + fis.read(imageBytes); + fis.close(); + + //删除临时文件 + if (file.exists()) { + file.delete(); + } + + return imageBytes; + } catch (Exception e) { + System.out.println(" Image err :" + e.getMessage()); + return null; + } + + } + + //获取二维码图片的字节数组 + public byte[] getQRCodeBytes() + throws IOException { + + try { + MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); + + //设置二维码参数 + Map hints = new HashMap(); + if (this.errorCorrectionLevel != null) { + //设置二维码的纠错级别 + hints.put(EncodeHintType.ERROR_CORRECTION, this.errorCorrectionLevel); + } + + if (this.encodeType!=null && this.encodeType.trim().length() > 0) { + //设置编码方式 + hints.put(EncodeHintType.CHARACTER_SET, this.encodeType); + } + + BitMatrix bitMatrix = multiFormatWriter.encode(this.codeText, BarcodeFormat.QR_CODE, this.width, this.height, hints); + byte[] bytes = writeToBytes(bitMatrix); + + return bytes; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} +复制代码 +``` + +### **2、项目下载及部署** + +- 项目源码下载地址:[download.csdn.net/download/we…](https://link.juejin.cn?target=https%3A%2F%2Fdownload.csdn.net%2Fdownload%2Fweixin_44385486%2F86427996) +- 下载项目源码后,使用idea导入slndemo项目并运行 + +![](http://img.topjavaer.cn/img/image-20230208082604231.png) + +- 将项目slndemo下的slndemodata.zip压缩包拷贝到本地D盘根目录下并解压 + +- 点击启动项目 + +![](http://img.topjavaer.cn/img/image-20230208082647198.png) + + + +## **三、功能展示** + +### **1、项目启动后登录首页** + +- 项目地址:`http://localhost:8888/pc/login` +- 账户:张三 密码:123456 + +![](http://img.topjavaer.cn/img/image-20230208082700527.png) + +### **2、系统首页功能简介** + +这是一个简单的Demo项目,模拟Word文件在办公系统中的主要流转环节,并不意味着PageOffice产品只能支持这样的文档处理流程。PageOffice产品只提供文档在线处理的功能,包括:打开、编辑、保存、动态填充、文档合并、套红、留痕、盖章等上百项功能(详细请参考PageOffice产品开发包中的示例),不提供流程控制功能,所以不管开发什么样的Web系统,只要是需要在线处理Office文档,都可以根据自己的项目需要,调用PageOffice产品相应的功能即可。**「注意:为了简化代码逻辑,此演示程序只能创建一个文档进行流转。」** + +### **3、点击起草文档** + +- 点击起草文档,点击提交 + +- 点击代办文档,然后点击编辑,当你点击编辑时你没有下载PageOffice,他会提醒你安装,你点击安装之后,关闭浏览器,重新打开浏览器就能编辑了! + +![](http://img.topjavaer.cn/img/image-20230208082720273.png) + +- 我们使用了PageOffice企业版,必须要注册序列化 +- 版 本:PageOffice企业版5(试用) +- 序列号:35N8V-2YUC-LY77-W14XL + +![](http://img.topjavaer.cn/img/image-20230208082731404.png) + +- 当我们注册成功以后,就可以编辑发布的文件或者公告了 + +![](http://img.topjavaer.cn/img/image-20230208082743274.png) + +- 编辑好以后点击保存 + +- 点击审批 + +### **4、审批** + +- 登录李总审批 + +![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ecbb63b692a8445cbf069d349743f77f~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp) + +- 退出系统,然后输入李总 + +![](http://img.topjavaer.cn/img/image-20230208082804115.png) + +- 然后点击批阅,下一步 + +![](http://img.topjavaer.cn/img/image-20230208082813979.png) + +- 登录赵六进行审核稿子 + +### **5、审稿** + +- 审稿 + +![](http://img.topjavaer.cn/img/image-20230208082843220.png) + +- 审核然后到盖章环节 + +![](http://img.topjavaer.cn/img/image-20230208082852422.png) + +- 使用王总登录进行盖章 + +![](http://img.topjavaer.cn/img/image-20230208082903476.png) + +### **6、盖章和签字的实现** + +- 王总登录 + +![](http://img.topjavaer.cn/img/image-20230208082912522.png) + +- 点击盖章 + +- 点击加盖印章 + +- 我们盖章前需要输入姓名+密码,需要输入错误报错 + +![](http://img.topjavaer.cn/img/image-20230208082927539.png) + +- 正确的账户密码是: +- 账户:王五 +- 密码:123456 + +![](http://img.topjavaer.cn/img/image-20230208082941412.png) + +- 登录成功后有选择王五的个人章进行签字 + +- 签字成功 + +- 公司盖章,重复以上步骤 + +- 签字盖章成功 + +![](http://img.topjavaer.cn/img/image-20230208083003966.png) + +### **7、完整签字盖章文件** + +- 保存之后发布文件 + +- 公司文件展示 + +- 盖章签字后的文件 + +![](http://img.topjavaer.cn/img/image-20230208083031956.png) \ No newline at end of file diff --git a/docs/framework/springboot/springboot-cross-domain.md b/docs/framework/springboot/springboot-cross-domain.md new file mode 100644 index 0000000..b93a909 --- /dev/null +++ b/docs/framework/springboot/springboot-cross-domain.md @@ -0,0 +1,93 @@ +# SpringBoot如何解决跨域问题 + +2. 1. 创建一个filter解决跨域 +项目中前后端分离部署,所以需要解决跨域的问题。 我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。 当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。 我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。 + +```java +@Configuration +public class CorsConfig { + @Bean + public CorsFilter corsFilter() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.addAllowedOrigin("*"); + corsConfiguration.addAllowedHeader("*"); + corsConfiguration.addAllowedMethod("*"); + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); + urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); + return new CorsFilter(urlBasedCorsConfigurationSource); + } +} +``` + +2. 基于WebMvcConfigurerAdapter配置加入Cors的跨域 +这种解决方案并非 Spring Boot 特有的,在传统的 SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。 + +```java +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowCredentials(true) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .maxAge(3600); + } +} +``` + + + +3. controller配置CORS +可以向@RequestMapping注解处理程序方法添加一个@CrossOrigin注解,以便启用CORS(默认情况下,@CrossOrigin允许在@RequestMapping注解中指定的所有源和HTTP方法): + +```java +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +@CrossOrigin 表示所有的URL均可访问此资源 @CrossOrigin(origins = “http://127.0.0.1:8080”)//表示只允许这一个url可以跨域访问这个controller 代码说明:@CrossOrigin这个注解用起来很方便,这个可以用在方法上,也可以用在类上。如果你不设置他的value属性,或者是origins属性,就默认是可以允许所有的URL/域访问。 + +- value属性可以设置多个URL。 +- origins属性也可以设置多个URL。 +- maxAge属性指定了准备响应前的缓存持续的最大时间。就是探测请求的有效期。 +- allowCredentials属性表示用户是否可以发送、处理 cookie。默认为false +- allowedHeaders 属性表示允许的请求头部有哪些。 +- methods 属性表示允许请求的方法,默认get,post,head。 + + + +--- + +最后,推荐大家加入我的[**学习圈**](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有130多位小伙伴加入了,文末有**优惠券**,**扫描二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/framework/springboot/springboot-dev-tools.md b/docs/framework/springboot/springboot-dev-tools.md new file mode 100644 index 0000000..f87a182 --- /dev/null +++ b/docs/framework/springboot/springboot-dev-tools.md @@ -0,0 +1,164 @@ +# SpringBoot 三大开发工具,你都用过么? + +## 一、SpringBoot Dedevtools + +他是一个让SpringBoot支持热部署的工具,下面是引用的方法 + +要么在创建项目的时候直接勾选下面的配置: + +![](http://img.topjavaer.cn/img/image-20230211115527377.png) + +要么给springBoot项目添加下面的依赖: + +```xml + + org.springframework.boot + spring-boot-devtools + true + +复制代码 +``` + +- idea修改完代码后再按下 ctrl + f9 使其重新编译一下,即完成了热部署功能 +- eclipse是按ctrl + s保存 即可自动编译 + +如果你想一修改代码就自动重新编译,无需按ctrl+f9。只需要下面的操作: + +### 1.在idea的setting中把下面的勾都打上 + +![](http://img.topjavaer.cn/img/image-20230211115603710.png) + +### 2.进入pom.xml,在build的反标签后给个光标,然后按Alt+Shift+ctrl+/ + +![](http://img.topjavaer.cn/img/image-20230211115616354.png) + +### 3.然后勾选下面的东西,接着重启idea即可 + +![](http://img.topjavaer.cn/img/image-20230211115627263.png) + +## 二、Lombok + +Lombok是简化JavaBean开发的工具,让开发者省去构造器,getter,setter的书写。 + +在项目初始化时勾选下面的配置,即可使用Lombok + +![](http://img.topjavaer.cn/img/image-20230211115645530.png) + +或者在项目中导入下面的依赖: + +```xml + + org.projectlombok + lombok + true + +复制代码 +``` + +使用时,idea还需要下载下面的插件: + +![](http://img.topjavaer.cn/img/image-20230211115658171.png) + +下面的使用的例子 + +```kotlin +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor//全参构造器 +@NoArgsConstructor//无参构造器 +@Data//getter + setter +public class User { + private Long id; + private String name; + private Integer age; + private String email; +} +复制代码 +``` + +### 三、Spring Configuration Processor + +该工具是给实体类的属性注入开启提示,自我感觉该工具意义不是特别大! + +因为SpringBoot存在属性注入,比如下面的实体类: + +```typescript +package org.lzl.HelloWorld.entity; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @author Lenovo + * + */ +@Component +@ConfigurationProperties(prefix = "mypet") +public class Pet { + private String nickName; + private String strain; + public String getNickName() { + return nickName; + } + public void setNickName(String nickName) { + this.nickName = nickName; + } + public String getStrain() { + return strain; + } + public void setStrain(String strain) { + this.strain = strain; + } + @Override + public String toString() { + return "Pet [nickName=" + nickName + ", strain=" + strain + "]"; + } + + +} +复制代码 +``` + +想要在`application.properties`和`application.yml`中给mypet注入属性,却没有任何的提示,为了解决这一问题,我们在创建SpringBoot的时候勾选下面的场景: + +![](http://img.topjavaer.cn/img/image-20230211115712481.png) + +或者直接在项目中添加下面的依赖: + +```xml + + org.springframework.boot + spring-boot-configuration-processor + true + +复制代码 +``` + +并在build的标签中排除对该工具的打包:(减少打成jar包的大小) + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.boot + spring-boot-configuration-processor + + + + + + +``` + + + +> 原文:blog.csdn.net/MoastAll/article/details/108237154 \ No newline at end of file diff --git a/docs/framework/springboot/springboot-websocket-monitor.md b/docs/framework/springboot/springboot-websocket-monitor.md new file mode 100644 index 0000000..c877f4f --- /dev/null +++ b/docs/framework/springboot/springboot-websocket-monitor.md @@ -0,0 +1,320 @@ + + +# Spring Boot + WebSocket 实时监控异常 + +## 写在前面 + +此异常非彼异常,标题所说的异常是业务上的异常。 + +最近做了一个需求,消防的设备巡检,如果巡检发现异常,通过手机端提交,后台的实时监控页面实时获取到该设备的信息及位置,然后安排员工去处理。 + +因为需要服务端主动向客户端发送消息,所以很容易的就想到了用WebSocket来实现这一功能。 + +前端略微复杂,需要在一张位置分布图上进行鼠标描点定位各个设备和根据不同屏幕大小渲染,本文不做介绍,只是简单地用页面样式进行效果呈现。 + +**绿色代表正常,红色代表异常** + +预期效果,未接收到请求前----->id为3的提交了异常,id为3的王五变成了红色 + +![](http://img.topjavaer.cn/img/springboot-websocket异常监控01.png) + +## 实现 + +### **前端:** + +直接贴代码 + +```xml + 1 + 2 + 3 + 4 + 5 实时监控 + 6 + 7 + 29 + 30
+ 31
+ 32 {{item.id}}.{{item.name}} + 33 + 34
+ 35
+ 36 + 37 + 38 +117 +复制代码 +``` + +### **后端:** + +项目结构是这样子的,后面的代码关键注释都有,就不重复描述了 + +![](http://img.topjavaer.cn/img/image-20230202212706870.png) + +1、新建SpringBoot工程,选择web和WebSocket依赖 + +![](http://img.topjavaer.cn/img/image-20230202212716657.png) + +2、配置application.yml + +```makefile +#端口 +server: + port: 18801 + +#密码,因为接口不需要权限,所以加了个密码做校验 +mySocket: + myPwd: jae_123 +复制代码 +``` + +3、WebSocketConfig配置类 + +```typescript + 1 @Configuration + 2 public class WebSocketConfig { + 3 + 4 /** + 5 * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint + 6 */ + 7 @Bean + 8 public ServerEndpointExporter serverEndpointExporter(){ + 9 return new ServerEndpointExporter(); +10 } +11 } +复制代码 +``` + +4、WebSocketServer类,用来进行服务端和客户端之间的交互 + +```scss + 1 /** + 2 * @author jae + 3 * @ServerEndpoint("/webSocket/{uid}") 前端通过此URI与后端建立链接 + 4 */ + 5 + 6 @ServerEndpoint("/webSocket/{uid}") + 7 @Component + 8 public class WebSocketServer { + 9 +10 private static Logger log = LoggerFactory.getLogger(WebSocketServer.class); +11 +12 //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 +13 private static final AtomicInteger onlineNum = new AtomicInteger(0); +14 +15 //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。 +16 private static CopyOnWriteArraySet sessionPools = new CopyOnWriteArraySet(); +17 +18 /** +19 * 有客户端连接成功 +20 */ +21 @OnOpen +22 public void onOpen(Session session, @PathParam(value = "uid") String uid){ +23 sessionPools.add(session); +24 onlineNum.incrementAndGet(); +25 log.info(uid + "加入webSocket!当前人数为" + onlineNum); +26 } +27 +28 /** +29 * 连接关闭调用的方法 +30 */ +31 @OnClose +32 public void onClose(Session session) { +33 sessionPools.remove(session); +34 int cnt = onlineNum.decrementAndGet(); +35 log.info("有连接关闭,当前连接数为:{}", cnt); +36 } +37 +38 /** +39 * 发送消息 +40 */ +41 public void sendMessage(Session session, String message) throws IOException { +42 if(session != null){ +43 synchronized (session) { +44 session.getBasicRemote().sendText(message); +45 } +46 } +47 } +48 +49 /** +50 * 群发消息 +51 */ +52 public void broadCastInfo(String message) throws IOException { +53 for (Session session : sessionPools) { +54 if(session.isOpen()){ +55 sendMessage(session, message); +56 } +57 } +58 } +59 +60 /** +61 * 发生错误 +62 */ +63 @OnError +64 public void onError(Session session, Throwable throwable){ +65 log.error("发生错误"); +66 throwable.printStackTrace(); +67 } +68 +69 } +复制代码 +``` + +5、WebSocketController类,用于进行接口测试 + +```kotlin + 1 @RestController + 2 @RequestMapping("/open/socket") + 3 public class WebSocketController { + 4 + 5 @Value("${mySocket.myPwd}") + 6 public String myPwd; + 7 + 8 @Autowired + 9 private WebSocketServer webSocketServer; +10 +11 /** +12 * 手机客户端请求接口 +13 * @param id 发生异常的设备ID +14 * @param pwd 密码(实际开发记得加密) +15 * @throws IOException +16 */ +17 @PostMapping(value = "/onReceive") +18 public void onReceive(String id,String pwd) throws IOException { +19 if(pwd.equals(myPwd)){ //密码校验一致(这里举例,实际开发还要有个密码加密的校验的),则进行群发 +20 webSocketServer.broadCastInfo(id); +21 } +22 } +23 +24 } +复制代码 +``` + +### 测试 + +1、打开前端页面,进行WebSocket连接 + +控制台输出,连接成功 + +![image-20230202212743994](http://img.topjavaer.cn/img/image-20230202212743994.png) + +2、因为是模拟数据,所以全部显示正常,没有异常提交时的页面呈现 + +![](http://img.topjavaer.cn/img/image-20230202212754519.png) + +3、接下来,我们用接口测试工具Postman提交一个异常 + +![](http://img.topjavaer.cn/img/image-20230202212820587.png) + +**注意id为3的这个数据的状态变化** + +![](http://img.topjavaer.cn/img/image-20230202212828970.png) + +我们可以看到,id为3的王五状态已经变成异常的了,实时通讯成功。 + +## 最后 + +工作中有这方面关于实时监控的需求,可以参考一下哦。 + +> 原文:cnblogs.com/jae-tech/p/15409340.html + diff --git a/docs/framework/springcloud-interview.md b/docs/framework/springcloud-interview.md new file mode 100644 index 0000000..f95f36b --- /dev/null +++ b/docs/framework/springcloud-interview.md @@ -0,0 +1,325 @@ +--- +sidebar: heading +title: Spring Cloud常见面试题总结 +category: 框架 +tag: + - Spring Cloud +head: + - - meta + - name: keywords + content: Spring Cloud面试题,微服务,Spring Cloud优势,服务熔断,Eureka,Ribbon,Feign,Spring Cloud核心组件,Spring Cloud Bus,Spring Cloud Config,Spring Cloud Gateway + - - meta + - name: description + content: Spring Cloud常见知识点和面试题总结,让天下没有难背的八股文! +--- + + +# Spring Cloud核心知识总结 + + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 更新记录 + +- 2024.5.15,完善[Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?](##Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?) + +## 1、什么是Spring Cloud ? + +Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 + +![](http://img.topjavaer.cn/img/202405220918802.png) + +Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 + +## Spring、SpringMVC、Springboot、 Springcloud 的区别是什么? + +### Spring + +Spring是一个生态体系(也可以说是技术体系),是集大成者,它包含了Spring Framework、Spring Boot、Spring Cloud等。**它是一个轻量级控制反转(IOC)和面向切面(AOP)的容器框架**,为开发者提供了一个简易的开发方式。 + +Spring的核心特性思想之一IOC,它实现了容器对Bean对象的管理、降低组件耦合,使各层服务解耦。 + +Spring的另一个核心特性就是AOP,面向切面编程。面向切面编程需要将程序逻辑分解为称为所谓关注点的不同部分。跨越应用程序多个点的功能称为跨领域问题,这些跨领域问题在概念上与应用程序的业务逻辑分离。有许多常见的例子,如日志记录,声明式事务,安全性,缓存等。 + +**如果说IOC依赖注入可以帮助我们将应用程序对象相互分离,那么AOP可以帮助我们将交叉问题与它们所影响的对象分离。二者目的都是使服务解耦,使开发简易。** + +当然,除了Spring 的两大核心功能,还有如下这些,如: + +- Spring JDBC +- Spring MVC +- Spring ORM +- Spring Test + +### SpringMVC + +Spring与MVC可以更好地解释什么是SpringMVC,MVC为现代web项目开发的一种很常见的模式,简言之C(控制器)将V(视图、用户客户端)与M(模块,业务)分开构成了MVC ,业内常见的MVC模式的开发框架有Struts。 + +Spring MVC是Spring的一部分,主要用于开发WEB应用和网络接口,它是Spring的一个模块,通过DispatcherServlet, ModelAndView 和View Resolver,让应用开发变得很容易。 + +### SpringBoot + +SpringBoot是一套整合了框架的框架。 + +它的初衷:解决Spring框架配置文件的繁琐、搭建服务的复杂性。 + +它的设计理念:**约定优于配置**(convention over configuration)。 + +基于此理念实现了**自动配置**,且降低项目搭建的复杂度。 + +搭建一个接口服务,通过SpringBoot几行代码即可实现。基于Spring Boot,不是说原来的配置没有了,而是Spring Boot有一套默认配置,我们可以把它看做比较通用的约定,而Spring Boot遵循的是**约定优于配置**原则,同时,如果你需要使用到Spring以往提供的各种复杂但功能强大的配置功能,Spring Boot一样支持。 + +在Spring Boot中,你会发现引入的所有包都是starter形式,如: + +- spring-boot-starter-web-services,针对SOAP Web Services +- spring-boot-starter-web,针对Web应用与网络接口 +- spring-boot-starter-jdbc,针对JDBC +- spring-boot-starter-cache,针对缓存支持 + +Spring Boot是基于 Spring 框架开发的用于开发 Web 应用程序的框架,它帮助开发人员快速搭建和配置一个独立的、可执行的、基于 Spring 的应用程序,从而减少了繁琐和重复的配置工作。 + +### Spring Cloud + +Spring Cloud事实上是一整套基于Spring Boot的微服务解决方案。它为开发者提供了很多工具,用于快速构建分布式系统的一些通用模式,例如:配置管理、注册中心、服务发现、限流、网关、链路追踪等。Spring Boot是build anything,而Spring Cloud是coordinate anything,Spring Cloud的每一个微服务解决方案都是基于Spring Boot构建的。 + +## 2、什么是微服务? + +微服务架构是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分为一组小的服务,每个服务运行在其独立的自己的进程中,服务之间相互协调、互相配合,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通(通常是基于HTTP的RESTful API),每个服务都围绕着具体的业务进行构建,并且能够被独立的构建在生产环境、类生产环境等。另外,应避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建,可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。 + +通俗地来讲: + +微服务就是一个独立的职责单一的服务应用程序。在 intellij idea 工具里面就是用maven开发的一个个独立的module,具体就是使用springboot 开发的一个小的模块,处理单一专业的业务逻辑,一个模块只做一个事情。 + +微服务强调的是服务大小,关注的是某一个点,具体解决某一个问题/落地对应的一个服务应用,可以看做是idea 里面一个 module。 + +## 3、Spring Cloud有什么优势 + +使用 Spring Boot 开发分布式微服务时,我们面临以下问题 + +- 与分布式系统相关的复杂性-这种开销包括网络问题,延迟开销,带宽问题,安全问题。 +- 服务发现-服务发现工具管理群集中的流程和服务如何查找和互相交谈。它涉及一个服务目录,在该目录中注册服务,然后能够查找并连接到该目录中的服务。 +- 冗余-分布式系统中的冗余问题。 +- 负载平衡 --负载平衡改善跨多个计算资源的工作负荷,诸如计算机,计算机集群,网络链路,中央处理单元,或磁盘驱动器的分布。 +- 性能-问题 由于各种运营开销导致的性能问题。 +- 部署复杂性-Devops 技能的要求。 + +## 4、微服务之间如何独立通讯的? + +同步通信:dobbo通过 RPC 远程过程调用、springcloud通过 REST 接口json调用等。 + +异步:消息队列,如:`RabbitMq`、`ActiveM`、`Kafka`等消息队列。 + +## 5、 **什么是服务熔断?什么是服务降级?** + +熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在Spring Cloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。 + +服务降级,一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。 + +`Hystrix`相关注解`@EnableHystrix`:开启熔断 `@HystrixCommand(fallbackMethod=”XXX”)`,声明一个失败回滚处理函数`XXX`,当被注解的方法执行超时(默认是1000毫秒),就会执行`fallback`函数,返回错误提示。 + +## 6、 请说说Eureka和zookeeper 的区别? + +Zookeeper保证了CP,Eureka保证了AP。 + +> A:高可用 +> +> C:一致性 +> +> P:分区容错性 + +1.当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接down掉不可用。也就是说,服务注册功能对高可用性要求比较高,但zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新选leader。问题在于,选取leader时间过长,30 ~ 120s,且选取期间zk集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。 + +2.Eureka保证了可用性,Eureka各个节点是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而Eureka的客户端向某个Eureka注册或发现时发生连接失败,则会自动切换到其他节点,只要有一台Eureka还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么Eureka就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况: + +①、Eureka不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。 + +②、Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用) + +③、当网络稳定时,当前实例新的注册信息会被同步到其他节点。 + +因此,Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,而不会像Zookeeper那样使整个微服务瘫痪 + +## 7、SpringBoot和SpringCloud的区别? + +SpringBoot专注于快速方便得开发单个个体微服务。 + +SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来, + +为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务 + +SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot ,属于依赖的关系. + +SpringBoot专注于快速、方便得开发单个微服务个体,SpringCloud关注全局的服务治理框架。 + +## 8、负载平衡的意义什么? + +在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源 的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。 + +## 9、什么是Hystrix?它如何实现容错? + +Hystrix是一个延迟和容错库,旨在隔离远程系统,服务和第三方库的访问点,当出现故障是不可避免的故障时,停止级联故障并在复杂的分布式系统中实现弹性。 + +通常对于使用微服务架构开发的系统,涉及到许多微服务。这些微服务彼此协作。 + +思考一下微服务: + +![](http://img.topjavaer.cn/img/springcloud-microservice.png) + +假设如果上图中的微服务9失败了,那么使用传统方法我们将传播一个异常。但这仍然会导致整个系统崩溃。 + +随着微服务数量的增加,这个问题变得更加复杂。微服务的数量可以高达1000.这是hystrix出现的地方 我们将使用Hystrix在这种情况下的Fallback方法功能。我们有两个服务employee-consumer使用由employee-consumer公开的服务。 + +简化图如下所示 + +![](http://img.topjavaer.cn/img/springcloud2.png) + +现在假设由于某种原因,employee-producer公开的服务会抛出异常。我们在这种情况下使用Hystrix定义了一个回退方法。这种后备方法应该具有与公开服务相同的返回类型。如果暴露服务中出现异常,则回退方法将返回一些值。 + +## 10、什么是Hystrix断路器?我们需要它吗? + +由于某些原因,employee-consumer公开服务会引发异常。在这种情况下使用Hystrix我们定义了一个回退方法。如果在公开服务中发生异常,则回退方法返回一些默认值。 + +![](http://img.topjavaer.cn/img/springcloud3.png) + + + +如果firstPage method() 中的异常继续发生,则Hystrix电路将中断,并且员工使用者将一起跳过firtsPage方法,并直接调用回退方法。断路器的目的是给第一页方法或第一页方法可能调用的其他方法留出时间,并导致异常恢复。可能发生的情况是,在负载较小的情况下,导致异常的问题有更好的恢复机会 。 + + + +## 11、说说 RPC 的实现原理 + +首先需要有处理网络连接通讯的模块,负责连接建立、管理和消息的传输。其次需要有编 解码的模块,因为网络通讯都是传输的字节码,需要将我们使用的对象序列化和反序列化。剩下的就是客户端和服务器端的部分,服务器端暴露要开放的服务接口,客户调用服 务接口的一个代理实现,这个代理实现负责收集数据、编码并传输给服务器然后等待结果返回。 + +## 12,eureka自我保护机制是什么? + +当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。 + +## 13,什么是Ribbon? + +ribbon是一个负载均衡客户端,可以很好地控制htt和tcp的一些行为。`feign默认集成了ribbon`。 + +## 14,什么是 Netflix Feign?它的优点是什么? + +Feign 是受到 Retrofit,JAXRS-2.0 和 WebSocket 启发的 java 客户端联编程序。 + +Feign 的第一个目标是将约束分母的复杂性统一到 http apis,而不考虑其稳定性。 + +特点: + +- Feign 采用的是基于接口的注解 +- Feign 整合了ribbon,具有负载均衡的能力 +- 整合了Hystrix,具有熔断的能力 + +使用方式 + +- 添加pom依赖。 +- 启动类添加`@EnableFeignClients` +- 定义一个接口`@FeignClient(name=“xxx”)`指定调用哪个服务 + +## 15, Ribbon和Feign的区别? + +1.**启动类注解不同**,Ribbon是@RibbonClient feign的是@EnableFeignClients;2.**服务指定的位置不同**,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明;3.**调用方式不同**,Ribbon需要自己构建http请求,模拟http请求。 + +## 16、Spring Cloud 的核心组件有哪些? + +- Eureka:服务注册于发现。 +- Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。 +- Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。 +- Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。 +- Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。 + +## 17、说说Spring Boot和Spring Cloud的关系 + +Spring Boot是Spring推出用于解决传统框架配置文件冗余,装配组件繁杂的基于Maven的解决方案,旨在快速搭建单个微服务而Spring Cloud专注于解决各个微服务之间的协调与配置,服务之间的通信,熔断,负载均衡等技术维度并相同,并且Spring Cloud是依赖于Spring Boot的,而Spring Boot并不是依赖与Spring Cloud,甚至还可以和Dubbo进行优秀的整合开发 + +总结 + +- SpringBoot专注于快速方便的开发单个个体的微服务 +- SpringCloud是关注全局的微服务协调整理治理框架,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务 +- Spring Boot不依赖于Spring Cloud,Spring Cloud依赖于Spring Boot,属于依赖关系 +- Spring Boot专注于快速,方便的开发单个的微服务个体,Spring Cloud关注全局的服务治理框架 + +## 18、说说微服务之间是如何独立通讯的? + +**远程过程调用(Remote Procedure Invocation)** + +也就是我们常说的服务的注册与发现,直接通过远程过程调用来访问别的service。 + +**优点**:简单,常见,因为没有中间件代理,系统更简单 + +**缺点**:只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应、发布/订阅、发布/异步响应,降低了可用性,因为客户端和服务端在请求过程中必须都是可用的。 + +**消息** + +使用异步消息来做服务间通信。服务间通过消息管道来交换消息,从而通信。 + +**优点**:把客户端和服务端解耦,更松耦合,提高可用性,因为消息中间件缓存了消息,直到消费者可以消费, 支持很多通信机制比如通知、请求/异步响应、发布/订阅、发布/异步响应。 + +**缺点**:消息中间件有额外的复杂。 + +## 19、Spring Cloud如何实现服务的注册? + +服务发布时,指定对应的服务名,将服务注册到 注册中心(`Eureka 、Zookeeper)`。 + +注册中心加`@EnableEurekaServer`,服务用`@EnableDiscoveryClient`,然后用ribbon或feign进行服务直接的调用发现。 + +## 20、什么是服务熔断? + +在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩 + +为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。 + +所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝"当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。 + +服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值 + +## 21、了解Eureka自我保护机制吗? + +当Eureka Server 节点在短时间内丢失了过多实例的连接时(比如网络故障或频繁启动关闭客户端)节点会进入自我保护模式,保护注册信息,不再删除注册数据,故障恢复时,自动退出自我保护模式。 + +## 22、熟悉 Spring Cloud Bus 吗? + +spring cloud bus 将分布式的节点用轻量的消息代理连接起来,它可以用于广播配置文件的更改或者服务直接的通讯,也可用于监控。如果修改了配置文件,发送一次请求,所有的客户端便会重新读取配置文件。 + +## 23、Spring Cloud 断路器有什么作用? + +当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应,当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)。一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象,这时候断路器完全打开 那么下次请求就不会请求到该服务。 + +半开:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭。关闭:当服务一直处于正常状态 能正常调用。 + +## 24、了解Spring Cloud Config 吗? + +在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件`Spring Cloud Config`,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。 + +在`Spring Cloud Config` 组件中,分两个角色,一是config server,二是config client。 + +使用方式: + +- 添加pom依赖 +- 配置文件添加相关配置 +- 启动类添加注解@EnableConfigServer + +## 25、说说你对Spring Cloud Gateway的理解 + +Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。 + +使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。 + +## Spring Cloud各个微服务之间为什么要用http交互?难道不慢吗? + +Spring Cloud是一个为分布式微服务架构构建应用程序的开发工具箱,是Spring Boot的扩展,通过各种微服务组件的集成,极大地简化了微服务应用程序的构建和开发。在分布式系统中,各个微服务之间的通信是非常重要的,而HTTP作为通信协议具有普遍性和可扩展性,是Spring Cloud微服务架构中主流的通信方式。 + +尽管使用HTTP作为微服务之间的通信协议存在一定的网络开销,但是这种不可避免的网络开销远低于我们所能得到的好处。使用HTTP通信可以实现松耦合和异步通信,微服务之间可以彼此独立地进行开发和测试,单个微服务的故障不会影响整个系统的运行,也可以支持各种不同的技术栈之间的互操作性。 + +另外,使用HTTP作为通信协议还具有优秀的可扩展性。HTTP协议定义了不同的请求方法(例如 GET、POST、DELETE 等),不同请求方法的扩展格式也很灵活,可以用来传递各种类型的数据和格式,同时HTTP协议支持缓存,减少重复性的数据传输和带宽开销。 + +当然,为了提高微服务之间的通信效率,我们也可以通过一些优化手段来减少HTTP协议的网络开销。例如,使用数据压缩和缓存技术来压缩和缓存请求和响应,减少网络数据传输量和响应时间;使用负载均衡技术来合理地分配请求和响应,避免单个微服务出现性能瓶颈;使用高速缓存技术来缓存请求和响应,避免重复的请求和响应等等。 + +因此,Spring Cloud各个微服务之间使用HTTP交互是一个比较成熟的选择。虽然它可能存在一些网络开销,但是在实际应用中,这种开销是可以优化和控制的,甚至可以提高系统的可扩展性和可靠性。 + +> 参考:http://1pgqu.cn/M0NZo diff --git "a/\346\241\206\346\236\266/SpringCloud\345\276\256\346\234\215\345\212\241\345\256\236\346\210\230.md" b/docs/framework/springcloud-overview.md similarity index 96% rename from "\346\241\206\346\236\266/SpringCloud\345\276\256\346\234\215\345\212\241\345\256\236\346\210\230.md" rename to docs/framework/springcloud-overview.md index aedbd7f..36ccf9d 100644 --- "a/\346\241\206\346\236\266/SpringCloud\345\276\256\346\234\215\345\212\241\345\256\236\346\210\230.md" +++ b/docs/framework/springcloud-overview.md @@ -1,4 +1,4 @@ -## 基础知识 +# 基础知识 微服务是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行。 @@ -46,7 +46,7 @@ springcloud是一个基于Spring Boot实现的微服务架构开发工具。spri spring-boot-starter-actuator:该模块能够自动为Spring Boot 构建的应用提供一系列用于监控的端点。 -## 说说对SpringBoot 和SpringCloud的理解 +# 说说对SpringBoot 和SpringCloud的理解 - SpringBoot专注于快速方便的开发单个个体微服务。 - SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务 @@ -55,19 +55,19 @@ spring-boot-starter-actuator:该模块能够自动为Spring Boot 构建的应 Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。 -## Spring Cloud Eureka +# Spring Cloud Eureka Spring Cloud Eureka实现微服务架构中的服务治理功能,使用 Netflix Eureka 实现服务注册与发现,包含客户端组件和服务端组件。服务治理是微服务架构中最为核心和基础的模块。 Eureka 服务端就是服务注册中心。Eureka 客户端用于处理服务的注册和发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中, 在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。 -### 服务注册与发现 +## 服务注册与发现 服务注册:在微服务架构中往往会有一个注册中心,每个微服务都会向注册中心去注册自己的地址及端口信息,注册中心维护着服务名称与服务实例的对应关系。每个微服务都会定时从注册中心获取服务列表,同时汇报自己的运行情况,这样当有的服务需要调用其他服务时,就可以从自己获取到的服务列表中获取实例地址进行调用。 服务发现:服务间的调用不是通过直接调用具体的实例地址,而是通过服务名发起调用。调用方需要向服务注册中心咨询服务,获取服务的实例清单,从而访问具体的服务实例。 -### 代码实例 +## 代码实例 添加依赖: @@ -110,14 +110,14 @@ eureka: 运行EurekaServerApplication,访问地址http://localhost:8001/可以看到Eureka注册中心的界面。 -### 高可用注册中心 +## 高可用注册中心 注册中心互相注册,形成一组互相注册的服务注册中心, 以实现服务清单的互相同步, 达到高可用的效果。 创建application-replica1.yml和 application-replica2.yml: ```yaml -# application-replica1.yml +-- application-replica1.yml server: port: 8002 spring: @@ -133,7 +133,7 @@ eureka: register-with-eureka: true -# application-replica2.yml +-- application-replica2.yml server: port: 8002 spring: @@ -172,7 +172,7 @@ eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/, http://local -## Spring Cloud Ribbon +# Spring Cloud Ribbon Spring Cloud Ribbon 是基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现。Spring Cloud Ribbon 会**将REST请求转换为客户端负载均衡的服务调用**。 @@ -183,11 +183,11 @@ Spring Cloud Ribbon 是基于HTTP和TCP的客户端负载均衡工具,基于Ne 1. 服务提供者启动多个服务实例注册到服务注册中心; 2. 服务消费者直接通过调用被@LoadBalanced 注解修饰过的RestTemplate 来实现面向服务的接口调用。 -### RestTemplate +## RestTemplate RestTemplate是一个HTTP客户端,使用它我们可以方便的调用HTTP接口,支持GET、POST、PUT、DELETE等方法。 -### 代码实例 +## 代码实例 创建hello-service模块,用于给 Ribbon 提供服务调用。 @@ -304,11 +304,11 @@ com.tyson.helloservice.HelloController : /hello, host:DESKTOP-8F30VS1, service -## Spring Cloud Hystrix +# Spring Cloud Hystrix 在微服务架构中,服务与服务之间通过远程调用的方式进行通信,一旦某个被调用的服务发生了故障,其依赖服务也会发生故障,此时就会发生故障的蔓延,最终导致系统瘫痪。Hystrix实现了断路器模式,当某个服务发生故障时,通过断路器的监控,给调用方返回一个错误响应,而不是长时间的等待,这样就不会使得调用方由于**长时间得不到响应而占用线程**,从而防止故障的蔓延。Hystrix具备服务降级、服务熔断、线程隔离、请求缓存、请求合并及服务监控等强大功能。 -### 代码实例 +## 代码实例 创建 hystrix-service 模块。添加相关依赖: @@ -386,11 +386,11 @@ public CommonResult getDefaultUser(@PathVariable Long id) { -## Spring Cloud Feign +# Spring Cloud Feign 基于Netflix Feign 实现,整合了Spring Cloud Ribbon 与Spring Cloud Hystrix, 它提供了一种声明式服务调用的方式。 -### 负载均衡功能 +## 负载均衡功能 添加依赖: @@ -469,7 +469,7 @@ public class HelloFeignController { 启动eureka-service,多个hello-service,feign-service服务,多次调用http://localhost:8701/feign/hello/进行测试。 -### 服务降级功能 +## 服务降级功能 开启Hystrix功能: @@ -506,7 +506,7 @@ public interface HelloService { -## Spring Cloud Zuul +# Spring Cloud Zuul API网关,为微服务架构中的服务提供了统一的访问入口,客户端通过API网关访问相关服务。所有客户端的访问都通过它来进行路由及过滤。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。 @@ -554,7 +554,7 @@ public class ZuulProxyApplication { } ``` -### 路由规则 +## 路由规则 将匹配`/userService/**`的请求路由到user-service服务上去,匹配`/feignService/**`的请求路由到feign-service上去。: @@ -569,7 +569,7 @@ zuul: 访问`http://localhost:8801/userService/user/1`可以发现请求路由到了user-service上了。 -### 访问前缀 +## 访问前缀 给网关路径添加前缀。 @@ -580,7 +580,7 @@ zuul: 需要访问`http://localhost:8801/proxy/user-service/user/1`才能访问到user-service中的接口。 -### header过滤 +## header过滤 默认情况下, Spring Cloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的一些敏感信息, 防止它们被传递到下游服务。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性。这样的话,由于Cookie等信息被过滤掉,会导致应用无法登录和鉴权。为了避免这种情况,可以将敏感头设置为空。 @@ -598,7 +598,7 @@ zuul.routes..customSensitiveHeaders=true zuul.routes..sensitiveHeaders= ``` -### 重定向 +## 重定向 [zuul重定向问题](https://blog.csdn.net/wo18237095579/article/details/83540829) @@ -606,11 +606,11 @@ zuul.routes..sensitiveHeaders= ```yaml zuul: - # 此处解决后端服务重定向导致用户浏览的 host 变成 后端服务的 host 问题 + -- 此处解决后端服务重定向导致用户浏览的 host 变成 后端服务的 host 问题 add-host-header: true ``` -### 查看路由信息 +## 查看路由信息 通过SpringBoot Actuator来查看Zuul中的路由信息。 @@ -660,11 +660,11 @@ management: } ``` -### 过滤器 +## 过滤器 路由与过滤是Zuul的两大核心功能,路由功能负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础,过滤功能负责对请求过程进行额外的处理,是请求校验过滤及服务聚合的基础。 -#### 过滤器类型 +**过滤器类型** Zuul中有以下几种典型的过滤器类型。 @@ -673,7 +673,7 @@ Zuul中有以下几种典型的过滤器类型。 - post:在请求被路由到目标服务后执行,比如给目标服务的响应添加头信息,收集统计数据等功能; - error:请求在其他阶段发生错误时执行。 -#### 自定义过滤器 +**自定义过滤器** 添加PreLogFilter类继承ZuulFilter。在请求被转发到目标服务前打印请求日志。 @@ -728,7 +728,7 @@ public class PreLogFilter extends ZuulFilter { Remote host:0:0:0:0:0:0:0:1,method:GET,uri:/proxy/userService/user/1 ``` -#### 禁用过滤器 +**禁用过滤器** 在application.yml配置: @@ -739,7 +739,7 @@ zuul: disable: true ``` -### Ribbon和Hystrix的支持 +## Ribbon和Hystrix的支持 由于Zuul自动集成了Ribbon和Hystrix,所以Zuul天生就有负载均衡和服务容错能力,我们可以通过Ribbon和Hystrix的配置来配置Zuul中的相应功能。 @@ -763,7 +763,7 @@ zuul: ReadTimeout: 3000 #服务请求处理超时时间(毫秒) ``` -### 常用配置 +## 常用配置 ```yaml zuul: @@ -784,7 +784,7 @@ zuul: -## Spring Cloud Gateway +# Spring Cloud Gateway 为 SpringBoot 应用提供了API网关支持,具有强大的智能路由与过滤器功能。 @@ -797,7 +797,7 @@ zuul: ``` -### 配置路由 +## 配置路由 Gateway 提供了两种不同的方式用于配置路由,一种是通过yml文件来配置,另一种是通过Java Bean来配置。 @@ -840,11 +840,11 @@ Gateway 提供了两种不同的方式用于配置路由,一种是通过yml文 使用http://localhost:9201/user/getByUsername?username=macro测试。 -### Route Predicate +## Route Predicate Spring Cloud Gateway包括许多内置的Route Predicate工厂。 所有这些Predicate都与HTTP请求的不同属性匹配。 多个Route Predicate工厂可以进行组合。 -#### After Route Predicate +**After** Route Predicate 在指定时间之后的请求会匹配该路由。 @@ -859,7 +859,7 @@ spring: - After=2019-09-24T16:30:00+08:00[Asia/Shanghai] ``` -#### Cookie Route Predicate +**Cookie Route Predicate** 带有指定Cookie的请求会匹配该路由。 @@ -880,7 +880,7 @@ spring: curl http://localhost:9201/user/1 --cookie "username=tyson" ``` -#### Header Route Predicate +**Header Route Predicate** 带有指定请求头的请求会匹配该路由。 @@ -901,7 +901,7 @@ spring: curl http://localhost:9201/user/1 -H "X-Request-Id:123" ``` -#### Host Route Predicate +**Host Route Predicate** 带有指定Host的请求会匹配该路由。 @@ -922,11 +922,11 @@ spring: curl http://localhost:9201/user/1 -H "Host:www.macrozheng.com" ``` -### Route Filter +## Route Filter 路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。 -#### AddRequestParameter GatewayFilter +**AddRequestParameter GatewayFilter** 给请求添加参数的过滤器。 @@ -955,7 +955,7 @@ curl http://localhost:9201/user/getByUsername curl http://localhost:8201/user/getByUsername?username=macro ``` -#### StripPrefix GatewayFilter +**StripPrefix GatewayFilter** 对指定数量的路径前缀进行去除的过滤器。 @@ -980,7 +980,7 @@ curl http://localhost:9201/user-service/a/user/1 相当于发起请求:curl http://localhost:8201/user/1 -#### PrefixPate GatewayFilter +**PrefixPate GatewayFilter** 与StripPrefix过滤器恰好相反,会对原有路径进行增加操作的过滤器。 @@ -999,7 +999,7 @@ spring: 以上配置会对所有GET请求添加`/user`路径前缀。 -#### Hystrix GatewayFilter +**Hystrix GatewayFilter** 将断路器功能添加到网关路由中,使服务免受级联故障的影响,并提供服务降级处理。 @@ -1029,7 +1029,7 @@ public class FallbackController { } ``` -#### RequestRateLimiter GatewayFilter +**RequestRateLimiter GatewayFilter** 用于限流,使用RateLimiter实现来确定是否允许当前请求继续进行,如果请求太大默认会返回HTTP 429-太多请求状态。 @@ -1089,7 +1089,7 @@ logging: 多次请求该地址:http://localhost:9201/user/1 ,会返回状态码为429的错误。 -#### Retry GatewayFilter +**Retry GatewayFilter** 对路由请求进行重试的过滤器,可以根据路由请求返回的HTTP状态码来确定是否进行重试。 @@ -1114,9 +1114,9 @@ spring: basedOnPreviousValue: false ``` -### 结合注册中心使用 +## 结合注册中心使用 -#### 使用动态路由 +**使用动态路由** 以服务名为路径创建动态路由。 @@ -1154,7 +1154,7 @@ logging: 使用application-eureka.yml配置文件启动api-gateway服务,访问http://localhost:9201/user-service/user/1 ,可以路由到user-service的http://localhost:8201/user/1 处。 -#### 使用过滤器 +**使用过滤器** 在结合注册中心使用过滤器的时候,需要注意的是uri的协议为`lb`,这样才能启用Gateway的负载均衡功能。 @@ -1191,11 +1191,11 @@ logging: -## Spring Cloud Config +# Spring Cloud Config Spring Cloud Config分为服务端和客户端。服务端也称为分布式配置中心,它是个独立的微服务应用,用于从配置仓库获取配置信息供客户端使用。而客户端则是微服务架构中的各个微服务应用或者基础设施,可以从配置中心获取配置信息,在启动时加载配置。Spring Cloud Config 的配置中心默认采用Git来存储配置信息。 -### 准备配置信息 +## 准备配置信息 在Git仓库中添加好配置文件。 @@ -1206,7 +1206,7 @@ config: info: "config info for dev(master)" ``` -### 配置中心 +## 配置中心 创建config-server模块,在pom.xml中添加依赖: @@ -1256,7 +1256,7 @@ public class ConfigServerApplication { } ``` -### 获取配置信息 +## 获取配置信息 访问http://localhost:8901/master/config-dev来获取master分支上dev环境的配置信息。 @@ -1336,7 +1336,7 @@ public class ConfigClientController { 启动config-client服务,访问http://localhost:9001/configInfo,可以获取到dev分支下dev环境的配置。 -### 刷新配置 +## 刷新配置 当Git仓库中的配置信息更改后,我们可以通过SpringBoot Actuator的refresh端点来刷新客户端配置信息。 @@ -1380,7 +1380,7 @@ public class ConfigClientController { 访问http://localhost:9001/configInfo进行测试,可以发现配置信息已经刷新。 -### 安全认证 +## 安全认证 创建config-security-server模块,在pom.xml中添加相关依赖: @@ -1439,7 +1439,7 @@ spring: 使用application-security.yml启动config-client服务。访问http://localhost:9002/configInfo进行测试。 -### 配置中心集群 +## 配置中心集群 在微服务架构中,所有服务都从配置中心获取配置,配置中心一旦宕机,会发生很严重的问题。通过搭建配置中心集群避免该问题。 @@ -1465,13 +1465,13 @@ spring: -## Spring Cloud Bus +# Spring Cloud Bus 我们通常会使用消息代理来构建一个主题,然后把微服务架构中的所有服务都连接到这个主题上去,当我们向该主题发送消息时,所有订阅该主题的服务都会收到消息并进行消费。使用 Spring Cloud Bus 可以方便地构建起这套机制,所以 Spring Cloud Bus 又被称为消息总线。Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。 -## Spring Cloud Security +# Spring Cloud Security Spring Cloud Security 为构建安全的SpringBoot应用提供了一系列解决方案,结合Oauth2可以实现单点登录、令牌中继、令牌交换等功能。 @@ -1485,7 +1485,7 @@ OAuth在"客户端"(云冲印)与"服务提供商"(谷歌)之间,设 "客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。 -![](http://img.dabin-coder.cn/image/20220530233149.png) +![](http://img.topjavaer.cn/img/20220530233149.png) (A)用户打开客户端以后,客户端要求用户给予授权。 diff --git a/docs/framework/springcloud/1-basic.md b/docs/framework/springcloud/1-basic.md new file mode 100644 index 0000000..5764ebe --- /dev/null +++ b/docs/framework/springcloud/1-basic.md @@ -0,0 +1,48 @@ +# 基础知识 + +微服务是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行。 + +[从单体应用到微服务](https://www.zhihu.com/question/65502802/answer/802678798) + +单体系统的缺点: + +1. 修改一个小功能,就需要将整个系统重新部署上线,影响其他功能的运行; +2. 功能模块互相依赖,强耦合,扩展困难。如果出现性能瓶颈,需要对整体应用进行升级,虽然影响性能的可能只是其中一个小模块; + +单体系统的优点: + +1. 容易部署,程序单一,不存在分布式集群的复杂部署环境; +2. 容易测试,没有复杂的服务调用关系。 + +微服务的优点: + +1. 不同的服务可以使用不同的技术; +2. 隔离性。一个服务不可用不会导致其他服务不可用; +3. 可扩展性。某个服务出现性能瓶颈,只需对此服务进行升级即可; +4. 简化部署。服务的部署是独立的,哪个服务出现问题,只需对此服务进行修改重新部署; + +微服务的缺点: + +1. 网络调用频繁。性能相对函数调用较差。 +2. 运维成本增加。系统由多个独立运行的微服务构成,需要设计一个良好的监控系统对各个微服务的运行状态进行监控。 + +springcloud是一个基于Spring Boot实现的微服务架构开发工具。spring cloud包含多个子项目: + +- Spring Cloud Config:配置管理工具,支持使用Git存储配置内容, 可以使用它实现应用配置的外部化存储, 并支持客户端配置信息刷新、加密/解密配置内容等。 +- Spring Cloud Netflix:核心 组件,对多个Netflix OSS开源套件进行整合。 + - Eureka: 服务治理组件, 包含服务注册中心、服务注册与发现机制的实现。 + - Hystrix: 容错管理组件,实现断路器模式, 帮助服务依赖中出现的延迟和为故障提供强大的容错能力。 + - Ribbon: 客户端负载均衡的服务调用组件。 + - Feign: 基于Ribbon 和Hystrix 的声明式服务调用组件。 + - Zuul: 网关组件, 提供智能路由、访问过滤等功能。 + - Archaius: 外部化配置组件。 +- Spring Cloud Bus: 事件、消息总线, 用于传播集群中的状态变化或事件, 以触发后续的处理, 比如用来动态刷新配置等。 +- Spring Cloud Cluster: 针对ZooKeeper、Redis、Hazelcast、Consul 的选举算法和通用状态模式的实现。 +- Spring Cloud Consul: 服务发现与配置管理工具。 +- Spring Cloud ZooKeeper: 基于ZooKeeper 的服务发现与配置管理组件。 +- Spring Cloud Security:Spring Security组件封装,提供用户验证和权限验证,一般与Spring Security OAuth2 组一起使用,通过搭建授权服务,验证Token或者JWT这种形式对整个微服务系统进行安全验证 +- Spring Cloud Sleuth:分布式链路追踪组件,他分封装了Dapper、Zipkin、Kibana 的组件 +- Spring Cloud Stream:Spring Cloud框架的数据流操作包,可以封装RabbitMq,ActiveMq,Kafka,Redis等消息组件,利用Spring Cloud Stream可以实现消息的接收和发送 + +spring-boot-starter-actuator:该模块能够自动为Spring Boot 构建的应用提供一系列用于监控的端点。 + diff --git a/docs/framework/springcloud/10-bus.md b/docs/framework/springcloud/10-bus.md new file mode 100644 index 0000000..6f658f3 --- /dev/null +++ b/docs/framework/springcloud/10-bus.md @@ -0,0 +1,6 @@ +# Spring Cloud Bus + +我们通常会使用消息代理来构建一个主题,然后把微服务架构中的所有服务都连接到这个主题上去,当我们向该主题发送消息时,所有订阅该主题的服务都会收到消息并进行消费。使用 Spring Cloud Bus 可以方便地构建起这套机制,所以 Spring Cloud Bus 又被称为消息总线。Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新。 + + + diff --git a/docs/framework/springcloud/11-security.md b/docs/framework/springcloud/11-security.md new file mode 100644 index 0000000..5a3f93c --- /dev/null +++ b/docs/framework/springcloud/11-security.md @@ -0,0 +1,28 @@ +# Spring Cloud Security + +Spring Cloud Security 为构建安全的SpringBoot应用提供了一系列解决方案,结合Oauth2可以实现单点登录、令牌中继、令牌交换等功能。 + +OAuth 是一个验证授权(Authorization)的开放标准,所有人都有基于这个标准实现自己的OAuth。在OAuth之前,使用的是`HTTP Basic Authentication`(在浏览网页时候,浏览器会弹出一个登录验证的对话框),需要用户输入用户名和密码的形式进行验证, 这种形式是不安全的。OAuth的出现就是为了解决访问资源的安全性以及灵活性。OAuth使得第三方应用对资源的访问更加安全。 + +适用场景:[OAuth 2.0](https://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) + +有一个"云冲印"的网站,可以将用户储存在Google的照片,冲印出来。用户为了使用该服务,必须让"云冲印"读取自己储存在Google上的照片。传统方法是,用户将自己的Google用户名和密码,告诉"云冲印",后者就可以读取用户的照片了。但是这种方法可能会导致用户密码泄露等问题。 + +OAuth在"客户端"(云冲印)与"服务提供商"(谷歌)之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。 + +"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。 + +![](http://img.topjavaer.cn/img/20220530233149.png) + +(A)用户打开客户端以后,客户端要求用户给予授权。 + +(B)用户同意给予客户端授权。 + +(C)客户端使用上一步获得的授权,向认证服务器申请令牌。 + +(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 + +(E)客户端使用令牌,向资源服务器申请获取资源。 + +(F)资源服务器确认令牌无误,同意向客户端开放资源。 + diff --git a/docs/framework/springcloud/2-springboot-springcloud-diff.md b/docs/framework/springcloud/2-springboot-springcloud-diff.md new file mode 100644 index 0000000..b92ce91 --- /dev/null +++ b/docs/framework/springcloud/2-springboot-springcloud-diff.md @@ -0,0 +1,9 @@ +# 说说对SpringBoot 和SpringCloud的理解 + +- SpringBoot专注于快速方便的开发单个个体微服务。 +- SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务 +- SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,属于依赖的关系。 +- SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。 + +Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。 + diff --git a/docs/framework/springcloud/3-eureka.md b/docs/framework/springcloud/3-eureka.md new file mode 100644 index 0000000..4148784 --- /dev/null +++ b/docs/framework/springcloud/3-eureka.md @@ -0,0 +1,117 @@ +# Spring Cloud Eureka + +Spring Cloud Eureka实现微服务架构中的服务治理功能,使用 Netflix Eureka 实现服务注册与发现,包含客户端组件和服务端组件。服务治理是微服务架构中最为核心和基础的模块。 + +Eureka 服务端就是服务注册中心。Eureka 客户端用于处理服务的注册和发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中, 在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态。 + +## 服务注册与发现 + +服务注册:在微服务架构中往往会有一个注册中心,每个微服务都会向注册中心去注册自己的地址及端口信息,注册中心维护着服务名称与服务实例的对应关系。每个微服务都会定时从注册中心获取服务列表,同时汇报自己的运行情况,这样当有的服务需要调用其他服务时,就可以从自己获取到的服务列表中获取实例地址进行调用。 + +服务发现:服务间的调用不是通过直接调用具体的实例地址,而是通过服务名发起调用。调用方需要向服务注册中心咨询服务,获取服务的实例清单,从而访问具体的服务实例。 + +## 代码实例 + +添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + +``` + +启动服务注册中心功能: + +```java +@EnableEurekaServer +@SpringBootApplication +public class EurekaServerApplication { + public static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } +} +``` + +在配置文件application.yml中添加Eureka注册中心的配置: + +```yaml +server: + port: 8002 +spring: + application: + name: eureka-server +eureka: + instance: + hostname: replica1 + client: + serviceUrl: + defaultZone: http://replica2:8003/eureka/ #注册到另一个Eureka注册中心 + fetch-registry: true + register-with-eureka: true +``` + +运行EurekaServerApplication,访问地址http://localhost:8001/可以看到Eureka注册中心的界面。 + +## 高可用注册中心 + +注册中心互相注册,形成一组互相注册的服务注册中心, 以实现服务清单的互相同步, 达到高可用的效果。 + +创建application-replica1.yml和 application-replica2.yml: + +```yaml +-- application-replica1.yml +server: + port: 8002 +spring: + application: + name: eureka-server +eureka: + instance: + hostname: replica1 + client: + serviceUrl: + defaultZone: http://replica2:8003/eureka/ #注册到另一个Eureka注册中心 + fetch-registry: true + + register-with-eureka: true + +-- application-replica2.yml +server: + port: 8002 +spring: + application: + name: eureka-server +eureka: + instance: + hostname: replica1 + client: + serviceUrl: + defaultZone: http://replica2:8003/eureka/ #注册到另一个Eureka注册中心 + fetch-registry: true + + register-with-eureka: true +``` + +修改hosts文件,让 serviceUrl 能在本地正确访问到: + +```java +127.0.0.1 replica1 +127.0.0.1 replica2 +``` + +通过spring.profiles.active属性来分别启动(或者设置idea -> edit configuration -> active profiles 属性): + +```java +java -jar eureka-server-1.0.0.jar --spring.profiles.active=replica1 +java -jar eureka-server-1.0.0.jar --spring.profiles.active=replica2 +``` + +在设置了多节点的服务注册中心之后, 服务提供方还需要做一些简单的配置才能将服务注册到Eureka Server 集群中: + +```properties +eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/, http://localhost:8003/eureka/ +``` + + + diff --git a/docs/framework/springcloud/4-ribbon.md b/docs/framework/springcloud/4-ribbon.md new file mode 100644 index 0000000..696b40c --- /dev/null +++ b/docs/framework/springcloud/4-ribbon.md @@ -0,0 +1,132 @@ +# Spring Cloud Ribbon + +Spring Cloud Ribbon 是基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现。Spring Cloud Ribbon 会**将REST请求转换为客户端负载均衡的服务调用**。 + +在客户端节点会维护可访问的服务器清单,服务器清单来自服务注册中心,通过心跳维持服务器清单的健康性。 + +开启客户端负载均衡调用: + +1. 服务提供者启动多个服务实例注册到服务注册中心; +2. 服务消费者直接通过调用被@LoadBalanced 注解修饰过的RestTemplate 来实现面向服务的接口调用。 + +## RestTemplate + +RestTemplate是一个HTTP客户端,使用它我们可以方便的调用HTTP接口,支持GET、POST、PUT、DELETE等方法。 + +## 代码实例 + +创建hello-service模块,用于给 Ribbon 提供服务调用。 + +```java +@RestController +public class HelloController { + + @Autowired + private Registration registration; // 服务注册 + + @Autowired + private DiscoveryClient client; + + private static final Logger LOGGER = LoggerFactory.getLogger(HelloController.class); + + @RequestMapping(value = "/hello", method = RequestMethod.GET) + public String hello() throws Exception { + ServiceInstance instance = client.getInstances(registration.getServiceId()).get(0); + LOGGER.info("/hello, host:" + instance.getHost() + ", service_id:" + instance.getServiceId()); + return "Hello World"; + } +} +``` + +添加相关的依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.boot + spring-boot-starter-web + +``` + +配置端口和注册中心地址: + +```properties +spring.application.name=hello-service +eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/, http://localhost:8003/eureka/ +server.port=8081 +``` + +创建ribbon-consumer模块,使用负载均衡调用hello-service服务。 + +添加相关依赖: + +```xml + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + +``` + +配置了端口、注册中心地址。 + +```properties +spring.application.name=ribbon-consumer +server.port=9000 +eureka.client.serviceUrl.defaultZone=http://localhost:8002/eureka/, http://localhost:8002/eureka/ +hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000 +service-url.hello-service = http://HELLO-SERVICE +``` + +使用@LoadBalanced注解赋予RestTemplate负载均衡的能力。 + +```java +@Configuration +public class RibbonConfig { + @Bean + @LoadBalanced + public RestTemplate restTemplate(){ + return new RestTemplate(); + } +} +``` + +创建ConsumerController,注入RestTemplate,使用其调用hello-service中提供的相关接口: + +```java +@RestController +public class ConsumerController { + + @Autowired + RestTemplate restTemplate; + + @Value("${service-url.hello-service}") + private String helloService; + + @RequestMapping(value="/ribbon-consumer", method = RequestMethod.GET) + public String helloConsumer() { + return restTemplate.getForEntity(helloService + "/hello", String.class).getBody(); + } +} +``` + +启动两个 hello-service 实例(8001/8002),调用接口 http://localhost:9000/ribbon-consumer 进行测试。 + +hello-service:8001和hello-service:8002交替输出: + +```java +com.tyson.helloservice.HelloController : /hello, host:DESKTOP-8F30VS1, service_id:HELLO-SERVICE +``` + + + diff --git a/docs/framework/springcloud/5-hystrix.md b/docs/framework/springcloud/5-hystrix.md new file mode 100644 index 0000000..78a8497 --- /dev/null +++ b/docs/framework/springcloud/5-hystrix.md @@ -0,0 +1,82 @@ +# Spring Cloud Hystrix + +在微服务架构中,服务与服务之间通过远程调用的方式进行通信,一旦某个被调用的服务发生了故障,其依赖服务也会发生故障,此时就会发生故障的蔓延,最终导致系统瘫痪。Hystrix实现了断路器模式,当某个服务发生故障时,通过断路器的监控,给调用方返回一个错误响应,而不是长时间的等待,这样就不会使得调用方由于**长时间得不到响应而占用线程**,从而防止故障的蔓延。Hystrix具备服务降级、服务熔断、线程隔离、请求缓存、请求合并及服务监控等强大功能。 + +## 代码实例 + +创建 hystrix-service 模块。添加相关依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + + + org.springframework.boot + spring-boot-starter-web + +``` + +配置 application.yml: + +```yaml +server: + port: 8401 +spring: + application: + name: hystrix-service +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8001/eureka/ +service-url: + user-service: http://user-service +``` + +在启动类上添加@EnableCircuitBreaker来开启Hystrix的断路器功能: + +```java +@EnableCircuitBreaker +@EnableDiscoveryClient +@SpringBootApplication +public class HystrixServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(HystrixServiceApplication.class, args); + } +} +``` + +创建UserHystrixController接口用于调用user-service服务: + +```java +@GetMapping("/testFallback/{id}") +public CommonResult testFallback(@PathVariable Long id) { + return userService.getUser(id); +} +``` + +在UserService中添加调用方法与服务降级方法,方法上需要添加@HystrixCommand注解: + +```java +@HystrixCommand(fallbackMethod = "getDefaultUser") +public CommonResult getUser(Long id) { + return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id); +} + +public CommonResult getDefaultUser(@PathVariable Long id) { + User defaultUser = new User(-1L, "defaultUser", "123456"); + return new CommonResult<>(defaultUser); +} +``` + +关闭user-service服务测试 testFallback 接口,发现已经发生了服务降级。 + + + diff --git a/docs/framework/springcloud/6-feign.md b/docs/framework/springcloud/6-feign.md new file mode 100644 index 0000000..6398544 --- /dev/null +++ b/docs/framework/springcloud/6-feign.md @@ -0,0 +1,120 @@ +# Spring Cloud Feign + +基于Netflix Feign 实现,整合了Spring Cloud Ribbon 与Spring Cloud Hystrix, 它提供了一种声明式服务调用的方式。 + +## 负载均衡功能 + +添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-web + +``` + +配置application.yml: + +```yaml +server: + port: 8701 +spring: + application: + name: feign-service +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8002/eureka/ +``` + +在启动类上添加@EnableFeignClients注解来启用Feign的客户端功能: + +```java +@EnableFeignClients +@EnableDiscoveryClient +@SpringBootApplication +public class FeignServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(FeignServiceApplication.class, args); + } + +} +``` + +添加HelloService接口(与hello-service HelloController定义的接口一致),绑定hello-service服务接口: + +```java +@FeignClient(value="hello-service") +public interface HelloService { + + @RequestMapping(value = "/hello", method = RequestMethod.GET) + String getHello(); +} +``` + +添加HelloFeignController,调用UserService完成服务调用: + +```java +@RestController +@RequestMapping("feign") +public class HelloFeignController { + @Autowired + private HelloService helloService; + + @GetMapping("/hello") + public String getHello() { + return helloService.getHello(); + } +} +``` + +启动eureka-service,多个hello-service,feign-service服务,多次调用http://localhost:8701/feign/hello/进行测试。 + +## 服务降级功能 + +开启Hystrix功能: + +```yaml +feign: + hystrix: + enabled: true #在Feign中开启Hystrix +``` + +添加服务降级实现类: + +```java +@Component +public class HelloFallbackService implements HelloService { + @Override + public String getHello() { + return "hello fallback"; + } +} +``` + +修改HelloService接口,设置服务降级处理类: + +```java +@FeignClient(value="hello-service", fallback = HelloFallbackService.class) +public interface HelloService { + + @GetMapping(value = "/hello") + String getHello(); +} +``` + +关闭hello-service服务,重新启动 feign-service,访问 http://localhost:8701/feign/hello 进行测试。 + + + diff --git a/docs/framework/springcloud/7-zuul.md b/docs/framework/springcloud/7-zuul.md new file mode 100644 index 0000000..5db1afd --- /dev/null +++ b/docs/framework/springcloud/7-zuul.md @@ -0,0 +1,278 @@ +# Spring Cloud Zuul + +API网关,为微服务架构中的服务提供了统一的访问入口,客户端通过API网关访问相关服务。所有客户端的访问都通过它来进行路由及过滤。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。 + +添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + +``` + +配置application.yml: + +```yaml +server: + port: 8801 +spring: + application: + name: zuul-proxy +eureka: + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone: http://localhost:8001/eureka/ +``` + +启动API网关功能: + +```java +@EnableZuulProxy +@EnableDiscoveryClient +@SpringBootApplication +public class ZuulProxyApplication { + + public static void main(String[] args) { + SpringApplication.run(ZuulProxyApplication.class, args); + } + +} +``` + +## 路由规则 + +将匹配`/userService/**`的请求路由到user-service服务上去,匹配`/feignService/**`的请求路由到feign-service上去。: + +```yaml +zuul: + routes: #给服务配置路由 + user-service: + path: /userService/** + feign-service: + path: /feignService/** +``` + +访问`http://localhost:8801/userService/user/1`可以发现请求路由到了user-service上了。 + +## 访问前缀 + +给网关路径添加前缀。 + +```yaml +zuul: + prefix: /proxy #给网关路由添加前缀 +``` + +需要访问`http://localhost:8801/proxy/user-service/user/1`才能访问到user-service中的接口。 + +## header过滤 + +默认情况下, Spring Cloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的一些敏感信息, 防止它们被传递到下游服务。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性。这样的话,由于Cookie等信息被过滤掉,会导致应用无法登录和鉴权。为了避免这种情况,可以将敏感头设置为空。 + +```yaml +zuul: + sensitive-headers: +``` + +也可以通过指定路由的参数来配置,仅对指定路由开启对敏感信息的传递,影响范围小,不至于引起其他服务的信息泄露。 + +```yaml +#方法一:对指定路由开启自定义敏感头 +zuul.routes..customSensitiveHeaders=true +#方法二:将指定路由的敏感头设置为空 +zuul.routes..sensitiveHeaders= +``` + +## 重定向 + +[zuul重定向问题](https://blog.csdn.net/wo18237095579/article/details/83540829) + +客户端通过 Zuul 请求认证服务,认证成功之后重定向到一个欢迎页,但是发现重定向的这个欢迎页的 host 变成了这个认证服务的 host,而不是 Zuul 的 host,如下图所示,直接暴露了认证服务的地址,我们可以在配置里面解决掉这个问题。 + +```yaml +zuul: + -- 此处解决后端服务重定向导致用户浏览的 host 变成 后端服务的 host 问题 + add-host-header: true +``` + +## 查看路由信息 + +通过SpringBoot Actuator来查看Zuul中的路由信息。 + +在pom.xml中添加相关依赖: + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +修改zuul-proxy/application.yml,开启查看路由信息的断点: + +```yaml +management: + endpoints: + web: + exposure: + include: 'routes' +``` + +通过访问http://localhost:8801/actuator/routes查看简单路由信息。访问[http://localhost:8801/actuator/routes/details](http://localhost:8801/actuator/routes)查看详细路由信息。 + +``` +{ + "/proxy/userService/**": { + "id": "user-service", + "fullPath": "/proxy/userService/**", + "location": "user-service", + "path": "/**", + "prefix": "/proxy/userService", + "retryable": false, + "customSensitiveHeaders": false, + "prefixStripped": true + }, + "/proxy/feignService/**": { + "id": "feign-service", + "fullPath": "/proxy/feignService/**", + "location": "feign-service", + "path": "/**", + "prefix": "/proxy/feignService", + "retryable": false, + "customSensitiveHeaders": false, + "prefixStripped": true + } +} +``` + +## 过滤器 + +路由与过滤是Zuul的两大核心功能,路由功能负责将外部请求转发到具体的服务实例上去,是实现统一访问入口的基础,过滤功能负责对请求过程进行额外的处理,是请求校验过滤及服务聚合的基础。 + +**过滤器类型** + +Zuul中有以下几种典型的过滤器类型。 + +- pre:在请求被路由到目标服务前执行,比如权限校验、打印日志等功能; +- routing:在请求被路由到目标服务时执行,这是使用Apache HttpClient或Netflix Ribbon构建和发送原始HTTP请求的地方; +- post:在请求被路由到目标服务后执行,比如给目标服务的响应添加头信息,收集统计数据等功能; +- error:请求在其他阶段发生错误时执行。 + +**自定义过滤器** + +添加PreLogFilter类继承ZuulFilter。在请求被转发到目标服务前打印请求日志。 + +```java +@Component +public class PreLogFilter extends ZuulFilter { + private Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + + /** + * 过滤器类型,有pre、routing、post、error四种。 + */ + @Override + public String filterType() { + return "pre"; + } + + /** + * 过滤器执行顺序,数值越小优先级越高。 + */ + @Override + public int filterOrder() { + return 1; + } + + /** + * 是否进行过滤,返回true会执行过滤。 + */ + @Override + public boolean shouldFilter() { + return true; + } + + /** + * 自定义的过滤器逻辑,当shouldFilter()返回true时会执行。 + */ + @Override + public Object run() throws ZuulException { + RequestContext requestContext = RequestContext.getCurrentContext(); + HttpServletRequest request = requestContext.getRequest(); + String host = request.getRemoteHost(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + LOGGER.info("Remote host:{},method:{},uri:{}", host, method, uri); + return null; + } +} +``` + +添加过滤器后,访问http://localhost:8801/proxy/user-service/user/1,会打印如下日志: + +```java +Remote host:0:0:0:0:0:0:0:1,method:GET,uri:/proxy/userService/user/1 +``` + +**禁用过滤器** + +在application.yml配置: + +```yaml +zuul: + PreLogFilter: + pre: + disable: true +``` + +## Ribbon和Hystrix的支持 + +由于Zuul自动集成了Ribbon和Hystrix,所以Zuul天生就有负载均衡和服务容错能力,我们可以通过Ribbon和Hystrix的配置来配置Zuul中的相应功能。 + +- 可以使用Hystrix的配置来设置路由转发时HystrixCommand的执行超时时间: + + ```yaml + hystrix: + command: #用于控制HystrixCommand的行为 + default: + execution: + isolation: + thread: + timeoutInMilliseconds: 1000 #配置HystrixCommand执行的超时时间,执行超过该时间会进行服务降级处理 + ``` + +- 可以使用Ribbon的配置来设置路由转发时请求连接及处理的超时时间: + + ```yaml + ribbon: #全局配置 + ConnectTimeout: 1000 #服务请求连接超时时间(毫秒) + ReadTimeout: 3000 #服务请求处理超时时间(毫秒) + ``` + +## 常用配置 + +```yaml +zuul: + routes: #给服务配置路由 + user-service: + path: /userService/** + feign-service: + path: /feignService/** + ignored-services: user-service,feign-service #关闭默认路由配置 + prefix: /proxy #给网关路由添加前缀 + sensitive-headers: Cookie,Set-Cookie,Authorization #配置过滤敏感的请求头信息,设置为空就不会过滤 + add-host-header: true #设置为true重定向是会添加host请求头 + retryable: true # 关闭重试机制 + PreLogFilter: + pre: + disable: false #控制是否启用过滤器 +``` + + + diff --git a/docs/framework/springcloud/8-gateway.md b/docs/framework/springcloud/8-gateway.md new file mode 100644 index 0000000..377af66 --- /dev/null +++ b/docs/framework/springcloud/8-gateway.md @@ -0,0 +1,407 @@ +# Spring Cloud Gateway + +为 SpringBoot 应用提供了API网关支持,具有强大的智能路由与过滤器功能。 + +创建api-gateway模块,pom.xml添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-gateway + +``` + +## 配置路由 + +Gateway 提供了两种不同的方式用于配置路由,一种是通过yml文件来配置,另一种是通过Java Bean来配置。 + +1. 使用yml配置。修改application.yml。 + + ```yaml + server: + port: 9201 + service-url: + user-service: http://localhost:8201 + spring: + cloud: + gateway: + routes: + - id: path_route #路由的ID + uri: ${service-url.user-service}/user/{id} #匹配后路由地址 + predicates: # 断言,路径相匹配的进行路由 + - Path=/user/{id} + ``` + + 启动eureka-server,user-service和api-gateway服务,测试http://localhost:9201/user/1 + +2. 使用Java bean配置。 + + 添加相关配置类,并配置一个RouteLocator对象: + + ```java + @Configuration + public class GatewayConfig { + + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("path_route2", r -> r.path("/user/getByUsername") + .uri("http://localhost:8201/user/getByUsername")) + .build(); + } + } + ``` + + 使用http://localhost:9201/user/getByUsername?username=macro测试。 + +## Route Predicate + +Spring Cloud Gateway包括许多内置的Route Predicate工厂。 所有这些Predicate都与HTTP请求的不同属性匹配。 多个Route Predicate工厂可以进行组合。 + +**After** Route Predicate + +在指定时间之后的请求会匹配该路由。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: after_route + uri: ${service-url.user-service} + predicates: + - After=2019-09-24T16:30:00+08:00[Asia/Shanghai] +``` + +**Cookie Route Predicate** + +带有指定Cookie的请求会匹配该路由。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: ${service-url.user-service} + predicates: + - Cookie=username,tyson +``` + +使用curl工具发送带有cookie为`username=macro`的请求可以匹配该路由。 + +``` +curl http://localhost:9201/user/1 --cookie "username=tyson" +``` + +**Header Route Predicate** + +带有指定请求头的请求会匹配该路由。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: header_route + uri: ${service-url.user-service} + predicates: + - Header=X-Request-Id, \d+ +``` + +使用curl工具发送带有请求头为`X-Request-Id:123`的请求可以匹配该路由。 + +``` +curl http://localhost:9201/user/1 -H "X-Request-Id:123" +``` + +**Host Route Predicate** + +带有指定Host的请求会匹配该路由。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: host_route + uri: ${service-url.user-service} + predicates: + - Host=**.macrozheng.com +``` + +使用curl工具发送带有请求头为`Host:www.macrozheng.com`的请求可以匹配该路由。 + +``` +curl http://localhost:9201/user/1 -H "Host:www.macrozheng.com" +``` + +## Route Filter + +路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。 + +**AddRequestParameter GatewayFilter** + +给请求添加参数的过滤器。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: http://localhost:8201 + filters: + - AddRequestParameter=username, macro + predicates: + - Method=GET +``` + +以上配置会对GET请求添加`username=macro`的请求参数,通过curl工具使用以下命令进行测试。 + +``` +curl http://localhost:9201/user/getByUsername +``` + +相当于发起该请求: + +``` +curl http://localhost:8201/user/getByUsername?username=macro +``` + +**StripPrefix GatewayFilter** + +对指定数量的路径前缀进行去除的过滤器。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: strip_prefix_route + uri: http://localhost:8201 + predicates: + - Path=/user-service/** + filters: + - StripPrefix=2 +``` + +以上配置会把以`/user-service/`开头的请求的路径去除两位,通过curl工具使用以下命令进行测试。 + +``` +curl http://localhost:9201/user-service/a/user/1 +``` + +相当于发起请求:curl http://localhost:8201/user/1 + +**PrefixPate GatewayFilter** + +与StripPrefix过滤器恰好相反,会对原有路径进行增加操作的过滤器。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: prefix_path_route + uri: http://localhost:8201 + predicates: + - Method=GET + filters: + - PrefixPath=/user +``` + +以上配置会对所有GET请求添加`/user`路径前缀。 + +**Hystrix GatewayFilter** + +将断路器功能添加到网关路由中,使服务免受级联故障的影响,并提供服务降级处理。 + +要开启断路器功能,我们需要在pom.xml中添加Hystrix的相关依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-hystrix + +``` + +然后添加相关服务降级的处理类: + +```java +@RestController +public class FallbackController { + + @GetMapping("/fallback") + public Object fallback() { + Map result = new HashMap<>(); + result.put("data",null); + result.put("message","Get request fallback!"); + result.put("code",500); + return result; + } +} +``` + +**RequestRateLimiter GatewayFilter** + +用于限流,使用RateLimiter实现来确定是否允许当前请求继续进行,如果请求太大默认会返回HTTP 429-太多请求状态。 + +在pom.xml中添加相关依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-redis-reactive + +``` + +添加限流策略的配置类,这里有两种策略一种是根据请求参数中的username进行限流,另一种是根据访问IP进行限流。 + +```java +@Configuration +public class RedisRateLimiterConfig { + @Bean + KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("username")); + } + + @Bean + public KeyResolver ipKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); + } +} +``` + +我们使用Redis来进行限流,所以需要添加Redis和RequestRateLimiter的配置,这里对所有的GET请求都进行了按IP来限流的操作。 + +```yaml +server: + port: 9201 +spring: + redis: + host: localhost + password: 123456 + port: 6379 + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: http://localhost:8201 + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 1 #每秒允许处理的请求数量 + redis-rate-limiter.burstCapacity: 2 #每秒最大处理的请求数量 + key-resolver: "#{@ipKeyResolver}" #限流策略,对应策略的Bean + predicates: + - Method=GET +logging: + level: + org.springframework.cloud.gateway: debug +``` + +多次请求该地址:http://localhost:9201/user/1 ,会返回状态码为429的错误。 + +**Retry GatewayFilter** + +对路由请求进行重试的过滤器,可以根据路由请求返回的HTTP状态码来确定是否进行重试。 + +```yaml +spring: + cloud: + gateway: + routes: + - id: retry_route + uri: http://localhost:8201 + predicates: + - Method=GET + filters: + - name: Retry + args: + retries: 1 #需要进行重试的次数 + statuses: BAD_GATEWAY #返回哪个状态码需要进行重试,返回状态码为5XX进行重试 + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false +``` + +## 结合注册中心使用 + +**使用动态路由** + +以服务名为路径创建动态路由。 + +在pom.xml中添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +添加application-eureka.yml配置文件: + +```yaml +server: + port: 9201 +spring: + application: + name: api-gateway + cloud: + gateway: + discovery: + locator: + enabled: true #开启从注册中心动态创建路由的功能 + lower-case-service-id: true #使用小写服务名,默认是大写 +eureka: + client: + service-url: + defaultZone: http://localhost:8001/eureka/ +logging: + level: + org.springframework.cloud.gateway: debug +``` + +使用application-eureka.yml配置文件启动api-gateway服务,访问http://localhost:9201/user-service/user/1 ,可以路由到user-service的http://localhost:8201/user/1 处。 + +**使用过滤器** + +在结合注册中心使用过滤器的时候,需要注意的是uri的协议为`lb`,这样才能启用Gateway的负载均衡功能。 + +修改application-eureka.yml文件,使用了PrefixPath过滤器,会为所有GET请求路径添加`/user`路径并路由。 + +```yaml +server: + port: 9201 +spring: + application: + name: api-gateway + cloud: + gateway: + routes: + - id: prefixpath_route + uri: lb://user-service #此处需要使用lb协议 + predicates: + - Method=GET + filters: + - PrefixPath=/user + discovery: + locator: + enabled: true +eureka: + client: + service-url: + defaultZone: http://localhost:8001/eureka/ +logging: + level: + org.springframework.cloud.gateway: debug +``` + +使用application-eureka.yml配置文件启动api-gateway服务,访问http://localhost:9201/1 ,可以路由到user-service的http://localhost:8201/user/1 处。 + + + diff --git a/docs/framework/springcloud/9-config.md b/docs/framework/springcloud/9-config.md new file mode 100644 index 0000000..ec5839a --- /dev/null +++ b/docs/framework/springcloud/9-config.md @@ -0,0 +1,274 @@ +# Spring Cloud Config + +Spring Cloud Config分为服务端和客户端。服务端也称为分布式配置中心,它是个独立的微服务应用,用于从配置仓库获取配置信息供客户端使用。而客户端则是微服务架构中的各个微服务应用或者基础设施,可以从配置中心获取配置信息,在启动时加载配置。Spring Cloud Config 的配置中心默认采用Git来存储配置信息。 + +## 准备配置信息 + +在Git仓库中添加好配置文件。 + +master分支下的配置信息: + +```yaml +config: + info: "config info for dev(master)" +``` + +## 配置中心 + +创建config-server模块,在pom.xml中添加依赖: + +```xml + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +在application.yml中进行配置: + +```yaml +server: + port: 8901 +spring: + application: + name: config-server + cloud: + config: + server: + git: #配置存储配置信息的Git仓库 + uri: https://gitee.com/macrozheng/springcloud-config.git + username: macro + password: 123456 + clone-on-start: true #开启启动时直接从git获取配置 +eureka: + client: + service-url: + defaultZone: http://localhost:8001/eureka/ +``` + +在启动类添加@EnableConfigServer启动配置中心的功能。 + +```java +@EnableConfigServer +@EnableDiscoveryClient +@SpringBootApplication +public class ConfigServerApplication { + public static void main(String[] args) { + SpringApplication.run(ConfigServerApplication.class, args); + } +} +``` + +## 获取配置信息 + +访问http://localhost:8901/master/config-dev来获取master分支上dev环境的配置信息。 + +```json +{ + "name":"master", + "profiles":[ + "config-dev" + ], + "label":null, + "version":"caf6549c807a6dde1fbbe399ffca18dc886ac03d", + "state":null, + "propertySources":[ + ] +} +``` + +访问http://localhost:8901/master/config-dev.yml来获取master分支上dev环境的配置文件信息。 + +```yaml +config: + info: config info for dev(master) +``` + +创建config-client模块,在 pom.xml 中添加相关依赖。 + +```xml + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.boot + spring-boot-starter-web + +``` + +配置application.yml。 + +```yaml +server: + port: 9001 +spring: + application: + name: config-client + cloud: + config: #Config客户端配置 + profile: dev #启用配置后缀名称 + label: dev #分支名称 + uri: http://localhost:8901 #配置中心地址 + name: config #配置文件名称 +eureka: + client: + service-url: + defaultZone: http://localhost:8001/eureka/ +``` + +添加ConfigClientController用于获取配置。 + +```java +@RestController +public class ConfigClientController { + + @Value("${config.info}") + private String configInfo; + + @GetMapping("/configInfo") + public String getConfigInfo() { + return configInfo; + } +} +``` + +启动config-client服务,访问http://localhost:9001/configInfo,可以获取到dev分支下dev环境的配置。 + +## 刷新配置 + +当Git仓库中的配置信息更改后,我们可以通过SpringBoot Actuator的refresh端点来刷新客户端配置信息。 + +config-client增加依赖: + +```xml + + org.springframework.boot + spring-boot-starter-actuator + +``` + +application.yml开启refresh端点: + +```yaml +management: + endpoints: + web: + exposure: + include: 'refresh' +``` + +在ConfigClientController类添加@RefreshScope注解用于刷新配置: + +```java +@RestController +@RefreshScope +public class ConfigClientController { + + @Value("${config.info}") + private String configInfo; + + @GetMapping("/configInfo") + public String getConfigInfo() { + return configInfo; + } +} +``` + +重新启动config-client后,调用refresh端点进行配置刷新。 + +访问http://localhost:9001/configInfo进行测试,可以发现配置信息已经刷新。 + +## 安全认证 + +创建config-security-server模块,在pom.xml中添加相关依赖: + +```xml + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.boot + spring-boot-starter-security + +``` + +在application.yml中进行配置: + +```yaml +server: + port: 8905 +spring: + application: + name: config-security-server + cloud: + config: + server: + git: + uri: https://gitee.com/macrozheng/springcloud-config.git + username: + password: + clone-on-start: true #开启启动时直接从git获取配置 + security: #配置用户名和密码 + user: + name: + password: +``` + +然后启动config-security-server服务。 + +修改config-client的配置(添加application-security.yml配置文件,主要是配置了配置中心的用户名和密码): + +```yaml +server: + port: 9002 +spring: + application: + name: config-client + cloud: + config: + profile: dev #启用配置后缀名称 + label: dev #分支名称 + uri: http://localhost:8905 #配置中心地址 + name: config #配置文件名称 + username: + password: +``` + +使用application-security.yml启动config-client服务。访问http://localhost:9002/configInfo进行测试。 + +## 配置中心集群 + +在微服务架构中,所有服务都从配置中心获取配置,配置中心一旦宕机,会发生很严重的问题。通过搭建配置中心集群避免该问题。 + +1. 启动两个config-server分别运行在8902和8903端口上; + +2. 添加config-client的配置文件application-cluster.yml,主要是添加了从注册中心获取配置中心地址的配置并去除了配置中心uri的配置: + + ```yaml + spring: + cloud: + config: + profile: dev #启用环境名称 + label: dev #分支名称 + name: config #配置文件名称 + discovery: + enabled: true + service-id: config-server + eureka: + client: + service-url: + defaultZone: http://localhost:8001/eureka/ + ``` + + + diff --git a/docs/framework/springcloud/README.md b/docs/framework/springcloud/README.md new file mode 100644 index 0000000..e73d735 --- /dev/null +++ b/docs/framework/springcloud/README.md @@ -0,0 +1,22 @@ +--- +title: SpringCloud基础 +icon: cloud +date: 2022-08-06 +category: springcloud +star: true +--- + + +## SpringCloud总结 + +- [基础知识](./1-basic.md) +- [说说对SpringBoot和SpringCloud的理解](./2-springboot-springcloud-diff.md) +- [SpringCloudEureka](./3-eureka.md) +- [SpringCloudRibbon](./4-ribbon.md) +- [SpringCloudHystrix](./5-hystrix.md) +- [SpringCloudFeign](./6-feign.md) +- [SpringCloudZuul](./7-zuul.md) +- [SpringCloudGateway](./8-gateway.md) +- [SpringCloudConfig](./9-config.md) +- [SpringCloudBus](./10-bus.md) +- [SpringCloudSecurity](./11-security.md) diff --git a/docs/framework/springmvc.md b/docs/framework/springmvc.md new file mode 100644 index 0000000..6bf2369 --- /dev/null +++ b/docs/framework/springmvc.md @@ -0,0 +1,50 @@ +--- +sidebar: heading +title: Spring MVC常见面试题总结 +category: 框架 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + + + +**Spring MVC高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到**Java面试手册完整版**。 + +![](http://img.topjavaer.cn/img/202311152201457.png) + +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 + +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +另外星球也提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) + +![](http://img.topjavaer.cn/img/image-20230102210715391.png) + +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/interview/concurrent/1-forbid-default-executor.md b/docs/interview/concurrent/1-forbid-default-executor.md new file mode 100644 index 0000000..c7b313d --- /dev/null +++ b/docs/interview/concurrent/1-forbid-default-executor.md @@ -0,0 +1,58 @@ +--- +sidebar: heading +title: 为什么阿里禁止使用Java内置线程池? +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: Java并发,多线程,线程池,为什么禁止使用Java内置线程池 + - - meta + - name: description + content: 高质量的Java并发常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 为什么阿里禁止使用Java内置线程池? + +首先要了解一下线程池 ThreadPoolExecutor 的参数及其作用。 + +ThreadPoolExecutor有以下这些参数。 + +1. **corePoolSize** 指定了线程池里的线程数量,核心线程池大小 + +2. maximumPoolSize 指定了线程池里的最大线程数量 + +3. **keepAliveTime** 当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁。 + +4. **TimeUnit**时间单位。 + +5. workQueue 任务队列,用于存放提交但是尚未被执行的任务。 + +6. **threadFactory** 线程工厂,用于创建线程,一般可以用默认的 + +7. **RejectedExecutionHandler** 拒绝策略,所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。 + +阿里规约之所以强制要求手动创建线程池,也是和这些参数有关。 + +阿里规约上指出,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的**运行规则**,规避资源耗尽的风险。 + +虽然Executor提供的四个静态方法创建线程池,但是不建议去使用这些静态方法创建线程池,因为这些方法创建的线程池,如果使用不当,可能会有OOM的风险。 + +比如**newFixedThreadPool**和**newSingleThreadExecutor**的问题是堆积的请求处理队列可能会耗费非常大的内存,甚至**OOM**。 + +**newCachedThreadPool**和**newScheduledThreadPool**的问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至**OOM**。 + + + +--end-- + +最后分享一份大彬精心整理的**大厂面试手册**,包含**操作系统、计算机网络、Java基础、JVM、分布式**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ + +![](http://img.topjavaer.cn/img/面试手册1.png) + +![](http://img.topjavaer.cn/img/面试手册.png) + +**手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**手册**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/image-20221207225029295.png) diff --git a/docs/interview/java/1-create-object.md b/docs/interview/java/1-create-object.md new file mode 100644 index 0000000..144141b --- /dev/null +++ b/docs/interview/java/1-create-object.md @@ -0,0 +1,24 @@ +--- +sidebar: heading +title: Java创建对象有几种方式? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: java创建对象的方式 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## Java创建对象有几种方式? + +Java创建对象有以下几种方式: + +- 1、用new语句创建对象。 +- 2、使用反射机制创建对象,用Class类或Constructor类的newInstance()方法。 +- 3、调用对象的clone()方法。需要实现Cloneable接口,重写object类的clone方法。当调用一个对象的clone方法,JVM就会创建一个新的对象,将前面对象的内容全部拷贝进去。 +- 4、运用反序列化手段。需要让类实现Serializable接口,通过ObjectInputStream的readObject()方法反序列化类。当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象。 + diff --git a/docs/interview/java/2-3rd-interface.md b/docs/interview/java/2-3rd-interface.md new file mode 100644 index 0000000..978273d --- /dev/null +++ b/docs/interview/java/2-3rd-interface.md @@ -0,0 +1,13 @@ +## 对接第三方接口要考虑什么? + +嗯,需要考虑以下几点: + +1. 确认接口对接的网络协议,是https/http或者自定义的私有协议等。 +2. 约定好数据传参、响应格式(如application/json),弱类型对接强类型语言时要特别注意 +3. 接口安全方面,要确定身份校验方式,使用token、证书校验等 +4. 确认是否需要接口调用失败后的重试机制,保证数据传输的最终一致性。 +5. 日志记录要全面。接口出入参数,以及解析之后的参数值,都要用日志记录下来,方便定位问题(甩锅)。 + + + +参考:https://blog.csdn.net/gzt19881123/article/details/108791034 \ No newline at end of file diff --git a/docs/interview/java/3-design-interface.md b/docs/interview/java/3-design-interface.md new file mode 100644 index 0000000..7fb9f92 --- /dev/null +++ b/docs/interview/java/3-design-interface.md @@ -0,0 +1,11 @@ +## 设计接口要注意什么? + +1. 接口参数校验。接口必须校验参数,比如入参是否允许为空,入参长度是否符合预期。 +2. 设计接口时,充分考虑接口的可扩展性。思考接口是否可以复用,怎样保持接口的可扩展性。 +3. 串行调用考虑改并行调用。比如设计一个商城首页接口,需要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比较大了。这种场景是可以改为并行调用的,降低接口耗时。 +4. 接口是否需要防重处理。涉及到数据库修改的,要考虑防重处理,可以使用数据库防重表,以唯一流水号作为唯一索引。 +5. 日志打印全面,入参出参,接口耗时,记录好日志,方便甩锅。 +6. 修改旧接口时,注意兼容性设计。 +7. 异常处理得当。使用finally关闭流资源、使用log打印而不是e.printStackTrace()、不要吞异常等等 +8. 是否需要考虑限流。限流为了保护系统,防止流量洪峰超过系统的承载能力。 + diff --git a/docs/interview/java/4-interface-slow.md b/docs/interview/java/4-interface-slow.md new file mode 100644 index 0000000..7823dc4 --- /dev/null +++ b/docs/interview/java/4-interface-slow.md @@ -0,0 +1,19 @@ +## 线上接口很慢怎么办? + +使用调用链跟踪,如zipkin,查看每一条路线的耗时情况,甚至包括此时 CPU 性能,然后再针对性能差的服务接口进行分析。 + +针对数据库性能优化问题,从设计上来说,设计表之前考虑好引擎,数据量,字段格式,索引等。 + +从 SQL 层面要利用好索引覆盖,尽量不回表,优化好 SQL。 + +从架构上来说,采用高可用的架构,如 MySQL 扩展的 MHA 架构 + +从业务代码层面来说,分析是不是代码问题,比如查询 SQL 过慢,返回超时,或者限流不合理等情况 + +如果数据量比较大的话,千万以及上亿级别,做好分表分库或者选用分布式数据库等 + +如果下游处理能力薄弱或者调用链路很深,考虑是否能够使用消息队列方式处理。 + + + +> 参考:https://youle.zhipin.com/questions/d260cce58d0333c9tnVy2tW8GFQ~.html \ No newline at end of file diff --git a/docs/interview/java/5-comparable-vs-comparator.md b/docs/interview/java/5-comparable-vs-comparator.md new file mode 100644 index 0000000..059a5b0 --- /dev/null +++ b/docs/interview/java/5-comparable-vs-comparator.md @@ -0,0 +1,14 @@ +## Comparable和Comparator有什么区别? + +Comparable 和 Comparator 都是用来进行元素排序的。它们之间的区别如下: + +1、字面含义不同。Comparable 是“比较”的意思,而 Comparator 是“比较器”的意思。 + +2、Comparable 是通过重写 compareTo 方法实现排序的,而 Comparator 是通过重写compare 方法实现排序的。 + +3、Comparable 必须由自定义类内部实现排序方法,而 Comparator 是外部定义并实现排序的。 + + + +总结一下:Comparable 可以看作是“对内”进行排序接口,而 Comparator 是“对外”进行排序的接口。 + diff --git a/docs/interview/java/README.md b/docs/interview/java/README.md new file mode 100644 index 0000000..ca6227c --- /dev/null +++ b/docs/interview/java/README.md @@ -0,0 +1,7 @@ + +## 目录 + +- [Java创建对象有几种方式?](./1-create-object.md) +- [对接第三方接口要考虑什么?](./2-3rd-interface.md) +- [设计接口要注意什么?](./3-design-interface.md) +- [线上接口很慢怎么办?](./4-interface-slow.md) diff --git a/docs/interview/javaweb/1-interceptor-filter.md b/docs/interview/javaweb/1-interceptor-filter.md new file mode 100644 index 0000000..3733c81 --- /dev/null +++ b/docs/interview/javaweb/1-interceptor-filter.md @@ -0,0 +1,27 @@ +## 过滤器和拦截器有什么区别? + +1、**实现原理不同**。 + +过滤器和拦截器底层实现不同。过滤器是基于函数回调的,拦截器是基于Java的反射机制(动态代理)实现的。一般自定义的过滤器中都会实现一个doFilter()方法,这个方法有一个FilterChain参数,而实际上它是一个回调接口。 + +2、**使用范围不同**。 + +过滤器实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。而拦截器是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。拦截器不仅能应用在web程序中,也可以用于Application、Swing等程序中。 + +3、**使用的场景不同**。 + +因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 + +4、**触发时机不同**。 + +过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 + +拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 + +5、**拦截的请求范围不同**。 + +请求的执行顺序是:请求进入容器 -> 进入过滤器 -> 进入 Servlet -> 进入拦截器 -> 执行控制器。可以看到过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。 + + + +> 参考链接:https://segmentfault.com/a/1190000022833940 \ No newline at end of file diff --git a/docs/interview/mq/1-why-to-use-mq.md b/docs/interview/mq/1-why-to-use-mq.md new file mode 100644 index 0000000..f525539 --- /dev/null +++ b/docs/interview/mq/1-why-to-use-mq.md @@ -0,0 +1,32 @@ +## 为什么要使用消息队列? + +主要有以下几点原因: + +1、解**耦**。比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单操作失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。 + +2、**异步**。将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务。 + +3、**削峰**。消费端慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。比如秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉。这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,如果消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。 + +总结一下,主要三点原因:**解耦、异步、削峰**。 + +## 那使用了消息队列会有什么缺点 + +- **系统可用性降低**。引入消息队列之后,如果消息队列挂了,可能会影响到业务系统的可用性。 +- **系统复杂性增加**。加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。 + + + + + +--- + + + +[整理了200本计算机经典书籍,含下载方式](https://github.com/Tyson0314/java-books) + +[Github疯传!谷歌师兄的LeetCode刷题笔记](https://topjavaer.cn/learning-resources/leetcode-note.html) + +[大彬花了一周收集的简历模板](https://mp.weixin.qq.com/s/JEG11UnWdO_Wi8-Qib89VQ) + +[学弟靠这本大厂面试手册,拿了字节跳动offer!](https://mp.weixin.qq.com/s/LVa4H5feXrjZL-DUtQQ0Yg) \ No newline at end of file diff --git a/docs/interview/network/1-input-url-return-page.md b/docs/interview/network/1-input-url-return-page.md new file mode 100644 index 0000000..1c5ac25 --- /dev/null +++ b/docs/interview/network/1-input-url-return-page.md @@ -0,0 +1,25 @@ +## 说说浏览器中输入URL返回页面过程 + +1. **解析域名**,找到主机 IP。 +2. 浏览器利用 IP 直接与网站主机通信,**三次握手**,建立 TCP 连接。浏览器会以一个随机端口向服务端的 web 程序 80 端口发起 TCP 的连接。 +3. 建立 TCP 连接后,浏览器向主机发起一个HTTP请求。 +4. 参数从客户端传递到服务器端。 +5. 服务器端得到客户端参数之后,进行相应的业务处理,再将结果封装成 HTTP 包,返回给客户端。 +6. 服务器端和客户端的交互完成,断开 TCP 连接(4 次挥手)。 +7. 浏览器**解析响应内容,进行渲染**,呈现给用户。 + +![](http://img.topjavaer.cn/img/输入url返回页面过程1.png) + +## DNS 域名解析的过程详细讲讲 + +在网络中定位是依靠 IP 进行身份定位的,所以 URL 访问的第一步便是先要得到服务器端的 IP 地址。而得到服务器的 IP 地址需要使用 DNS(Domain Name System,域名系统)域名解析,DNS 域名解析就是通过 URL 找到与之相对应的 IP 地址。 + +DNS 域名解析的大致流程如下: + +1. 先检**查浏览器中的 DNS 缓存**,如果浏览器中有对应的记录会直接使用,并完成解析; +2. 如果浏览器没有缓存,那就去**查询操作系统的缓存**,如果查询到记录就可以直接返回 IP 地址,完成解析; +3. 如果操作系统没有 DNS 缓存,就会去**查看本地 host 文件**,Windows 操作系统下,host 文件一般位于 "C:\Windows\System32\drivers\etc\hosts",如果 host 文件有记录则直接使用; +4. 如果本地 host 文件没有相应的记录,会**请求本地 DNS 服务器**,本地 DNS 服务器一般是由本地网络服务商如移动、联通等提供。通常情况下可通过 DHCP 自动分配,当然也可以自己手动配置。目前用的比较多的是谷歌提供的公用 DNS 是 8.8.8.8 和国内的公用 DNS 是 114.114.114.114。 +5. 如果本地 DNS 服务器没有相应的记录,就会**去根域名服务器查询**了。为了能更高效完成全球所有域名的解析请求,根域名服务器本身并不会直接去解析域名,而是会把不同的解析请求分配给下面的其他服务器去完成。 + +![](http://img.topjavaer.cn/img/image-20221123001836666.png) \ No newline at end of file diff --git a/docs/interview/network/2-http-status-code.md b/docs/interview/network/2-http-status-code.md new file mode 100644 index 0000000..d309c3f --- /dev/null +++ b/docs/interview/network/2-http-status-code.md @@ -0,0 +1,86 @@ +## 常见的 HTTP 状态码有哪些? + +HTTP 状态码是服务器端返回给客户端的响应状态码,根据状态码我们就能知道服务器端想要给客户端表达的具体含义,比如 200 就表示请求访问成功,500 就表示服务器端程序出错等。HTTP 状态码可分为 5 大类: + +1. 1XX:消息状态码。 +2. 2XX:成功状态码。 +3. 3XX:重定向状态码。 +4. 4XX:客户端错误状态码。 +5. 5XX:服务端错误状态码。 + +这 5 大类中又包含了很多具体的状态码。 + +1XX为**消息状态码**,其中: + +- 100:Continue 继续。客户端应继续其请求。 +- 101:Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到 HTTP 的新版本协议。 + +2XX为**成功状态码**,其中: + +- **200:OK 请求成功。一般用于 GET 与 POST 请求。** +- 201:Created 已创建。成功请求并创建了新的资源。 +- 202:Accepted 已接受。已经接受请求,但未处理完成。 +- 203:Non-Authoritative Information 非授权信息。请求成功。但返回的 meta 信息不在原始的服务器,而是一个副本。 +- 204:No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档。 +- 205:Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域。 +- 206:Partial Content 部分内容。服务器成功处理了部分 GET 请求。 + +3XX为**重定向状态码**,其中: + +- 300:Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择。 +- **301:Moved Permanently 永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI 代替。** +- **302:Found 临时移动,与 301 类似。但资源只是临时被移动。客户端应继续使用原有URI。** +- 303:See Other 查看其它地址。与 301 类似。使用 GET 和 POST 请求查看。 +- 304:Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源。 +- 305:Use Proxy 使用代理。所请求的资源必须通过代理访问。 +- 306:Unused 已经被废弃的 HTTP 状态码。 +- 307:Temporary Redirect 临时重定向。与 302 类似。使用 GET 请求重定向。 + +4XX为客户端**错误状态码**,其中: + +- 400:Bad Request 客户端请求的语法错误,服务器无法理解。 +- 401:Unauthorized 请求要求用户的身份认证。 +- 402:Payment Required 保留,将来使用。 +- 403:Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求。 +- **404:Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面。** +- **405:Method Not Allowed 客户端请求中的方法被禁止。** +- 406:Not Acceptable 服务器无法根据客户端请求的内容特性完成请求。 +- 407:Proxy Authentication Required 请求要求代理的身份认证,与 401 类似,但请求者应当使用代理进行授权。 +- 408:Request Time-out 服务器等待客户端发送的请求时间过长,超时。 +- 409:Conflict 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突。 +- 410:Gone 客户端请求的资源已经不存在。410 不同于 404,如果资源以前有现在被永久删除了可使用 410 代码,网站设计人员可通过 301 代码指定资源的新位置。 +- 411:Length Required 服务器无法处理客户端发送的不带 Content-Length 的请求信息。 +- 412:Precondition Failed 客户端请求信息的先决条件错误。 +- 413:Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个 Retry-After 的响应信息。 +- 414:Request-URI Too Large 请求的 URI 过长(URI通常为网址),服务器无法处理。 +- 415:Unsupported Media Type 服务器无法处理请求附带的媒体格式。 +- 416:Requested range not satisfiable 客户端请求的范围无效。 +- 417:Expectation Failed 服务器无法满足 Expect 的请求头信息。 + +5XX为**服务端错误状态码**,其中: + +- **500:Internal Server Error 服务器内部错误,无法完成请求。** +- 501:Not Implemented 服务器不支持请求的功能,无法完成请求。 +- 502:Bad Gateway 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应。 +- 503:Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中。 +- 504:Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求。 +- 505:HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理。 + +**总结一下**: + +HTTP 状态码分为 5 大类:1XX:表示消息状态码;2XX:表示成功状态码;3XX:表示重定向状态码;4XX:表示客户端错误状态码;5XX:表示服务端错误状态码。其中常见的具体状态码有:200:请求成功;301:永久重定向;302:临时重定向;404:无法找到此页面;405:请求的方法类型不支持;500:服务器内部出错。 + + + +--end-- + +最后**推荐几份非常有用的资料**! + +[整理了200本计算机经典书籍,含下载方式](https://mp.weixin.qq.com/s/ynxpvDkTOdk0pY3S5nStZQ) + +[Github疯传!LeetCode刷题笔记(c/c++、Java、Go三个版本)](https://mp.weixin.qq.com/s/3QX1L9fUPAzGVTPrn6tMJQ) + +[大彬花了一周收集的简历模板](https://mp.weixin.qq.com/s/JEG11UnWdO_Wi8-Qib89VQ) + +[学弟靠这本大厂面试手册,拿了字节跳动offer!](https://mp.weixin.qq.com/s/LVa4H5feXrjZL-DUtQQ0Yg) + diff --git a/docs/java/basic/reflect-affect-permance.md b/docs/java/basic/reflect-affect-permance.md new file mode 100644 index 0000000..1ef95a1 --- /dev/null +++ b/docs/java/basic/reflect-affect-permance.md @@ -0,0 +1,282 @@ +--- +sidebar: heading +title: 反射是怎么影响性能的? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: java反射 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +今天来聊聊反射的性能问题。反射具体是怎么影响性能的? + +# 01 反射真的存在性能问题吗? + +为了放大问题,找到共性,采用逐渐扩大测试次数、每次测试多次取平均值的方式,针对同一个方法分别就直接调用该方法、反射调用该方法、直接调用该方法对应的实例、反射调用该方法对应的实例分别从 1-1000000,每隔一个数量级测试一次: + +测试代码如下: + +```java +public class ReflectionPerformanceActivity extends Activity{ + private TextView mExecuteResultTxtView = null; + private EditText mExecuteCountEditTxt = null; + private Executor mPerformanceExecutor = Executors.newSingleThreadExecutor(); + private static final int AVERAGE_COUNT = 10; + + @Override + protected void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_reflection_performance_layout); + mExecuteResultTxtView = (TextView)findViewById(R.id.executeResultTxtId); + mExecuteCountEditTxt = (EditText)findViewById(R.id.executeCountEditTxtId); + } + + public void onClick(View v){ + switch(v.getId()){ + case R.id.executeBtnId:{ + execute(); + } + break; + default:{ + + + } + break; + } + } + + private void execute(){ + mExecuteResultTxtView.setText(""); + mPerformanceExecutor.execute(new Runnable(){ + @Override + public void run(){ + long costTime = 0; + int executeCount = Integer.parseInt(mExecuteCountEditTxt.getText().toString()); + long reflectMethodCostTime=0,normalMethodCostTime=0,reflectFieldCostTime=0,normalFieldCostTime=0; + updateResultTextView(executeCount + "毫秒耗时情况测试"); + for(int index = 0; index < AVERAGE_COUNT; index++){ + updateResultTextView("第 " + (index+1) + " 次"); + costTime = getNormalCallCostTime(executeCount); + reflectMethodCostTime += costTime; + updateResultTextView("执行直接调用方法耗时:" + costTime + " 毫秒"); + costTime = getReflectCallMethodCostTime(executeCount); + normalMethodCostTime += costTime; + updateResultTextView("执行反射调用方法耗时:" + costTime + " 毫秒"); + costTime = getNormalFieldCostTime(executeCount); + reflectFieldCostTime += costTime; + updateResultTextView("执行普通调用实例耗时:" + costTime + " 毫秒"); + costTime = getReflectCallFieldCostTime(executeCount); + normalFieldCostTime += costTime; + updateResultTextView("执行反射调用实例耗时:" + costTime + " 毫秒"); + } + + updateResultTextView("执行直接调用方法平均耗时:" + reflectMethodCostTime/AVERAGE_COUNT + " 毫秒"); + updateResultTextView("执行反射调用方法平均耗时:" + normalMethodCostTime/AVERAGE_COUNT + " 毫秒"); + updateResultTextView("执行普通调用实例平均耗时:" + reflectFieldCostTime/AVERAGE_COUNT + " 毫秒"); + updateResultTextView("执行反射调用实例平均耗时:" + normalFieldCostTime/AVERAGE_COUNT + " 毫秒"); + } + }); + } + + private long getReflectCallMethodCostTime(int count){ + long startTime = System.currentTimeMillis(); + for(int index = 0 ; index < count; index++){ + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + try{ + Method setmLanguageMethod = programMonkey.getClass().getMethod("setmLanguage", String.class); + setmLanguageMethod.setAccessible(true); + setmLanguageMethod.invoke(programMonkey, "Java"); + }catch(IllegalAccessException e){ + e.printStackTrace(); + }catch(InvocationTargetException e){ + e.printStackTrace(); + }catch(NoSuchMethodException e){ + e.printStackTrace(); + } + } + + return System.currentTimeMillis()-startTime; + } + + private long getReflectCallFieldCostTime(int count){ + long startTime = System.currentTimeMillis(); + for(int index = 0 ; index < count; index++){ + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + try{ + Field ageField = programMonkey.getClass().getDeclaredField("mLanguage"); + ageField.set(programMonkey, "Java"); + }catch(NoSuchFieldException e){ + e.printStackTrace(); + }catch(IllegalAccessException e){ + e.printStackTrace(); + } + } + + return System.currentTimeMillis()-startTime; + } + + private long getNormalCallCostTime(int count){ + long startTime = System.currentTimeMillis(); + for(int index = 0 ; index < count; index++){ + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + programMonkey.setmLanguage("Java"); + } + + return System.currentTimeMillis()-startTime; + } + + private long getNormalFieldCostTime(int count){ + long startTime = System.currentTimeMillis(); + for(int index = 0 ; index < count; index++){ + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + programMonkey.mLanguage = "Java"; + } + + return System.currentTimeMillis()-startTime; + } + + private void updateResultTextView(final String content){ + ReflectionPerformanceActivity.this.runOnUiThread(new Runnable(){ + @Override + public void run(){ + mExecuteResultTxtView.append(content); + mExecuteResultTxtView.append("\n"); + } + }); + } +} +``` + +测试结果如下: + +![](http://img.topjavaer.cn/img/反射影响性能1.png) + +测试结论: + +**反射的确会导致性能问题。反射导致的性能问题是否严重跟使用的次数有关系,如果控制在100次以内,基本上没什么差别,如果调用次数超过了100次,性能差异会很明显。** + +四种访问方式,直接访问实例的方式效率最高;其次是直接调用方法的方式,耗时约为直接调用实例的 1.4 倍;接着是通过反射访问实例的方式,耗时约为直接访问实例的 3.75 倍;最慢的是通过反射访问方法的方式,耗时约为直接访问实例的 6.2 倍。 + +# 02 反射到底慢在哪? + +跟踪源码可以发现,四个方法中都存在实例化`ProgramMonkey`的代码,所以可以排除是这句话导致的不同调用方式产生的性能差异;通过反射调用方法中调用了`setAccessible`方法,但该方法纯粹只是设置属性值,不会产生明显的性能差异;所以最有可能产生性能差异的只有`getMethod和getDeclaredField、invoke`和`set`方法了,下面分别就这两组方法进行测试,找到具体慢在哪? + +首先测试`invoke`和`set`方法,修改`getReflectCallMethodCostTime`和`getReflectCallFieldCostTime`方法的代码如下: + +```java +private long getReflectCallMethodCostTime(int count) { + long startTime = System.currentTimeMillis(); + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + Method setmLanguageMethod = null; + try { + setmLanguageMethod = programMonkey.getClass().getMethod("setmLanguage", String.class); + setmLanguageMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + + for (int index = 0; index < count; index++) { + try { + setmLanguageMethod.invoke(programMonkey, "Java"); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + + return System.currentTimeMillis() - startTime; +} + +private long getReflectCallFieldCostTime(int count) { + long startTime = System.currentTimeMillis(); + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + Field ageField = null; + try { + ageField = programMonkey.getClass().getDeclaredField("mLanguage"); + + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + + for (int index = 0; index < count; index++) { + try { + ageField.set(programMonkey, "Java"); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + return System.currentTimeMillis() - startTime; +} +``` + +沿用上面的测试方法,测试结果如下: + +![](http://img.topjavaer.cn/img/反射影响性能2.png) + +修改`getReflectCallMethodCostTime`和`getReflectCallFieldCostTime`方法的代码如下,对`getMethod`和`getDeclaredField`进行测试: + +```java +private long getReflectCallMethodCostTime(int count){ + long startTime = System.currentTimeMillis(); + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + + for(int index = 0 ; index < count; index++){ + try{ + Method setmLanguageMethod = programMonkey.getClass().getMethod("setmLanguage", String.class); + }catch(NoSuchMethodException e){ + e.printStackTrace(); + } + } + + return System.currentTimeMillis()-startTime; +} + +private long getReflectCallFieldCostTime(int count){ + long startTime = System.currentTimeMillis(); + ProgramMonkey programMonkey = new ProgramMonkey("小明", "男", 12); + for(int index = 0 ; index < count; index++){ + try{ + Field ageField = programMonkey.getClass().getDeclaredField("mLanguage"); + }catch(NoSuchFieldException e){ + e.printStackTrace(); + } + } + return System.currentTimeMillis()-startTime; +} +``` + +沿用上面的测试方法,测试结果如下: + +![](http://img.topjavaer.cn/img/反射影响性能3.png) + +测试结论: + +- `getMethod`和`getDeclaredField`方法会比`invoke`和`set`方法耗时; +- 随着测试数量级越大,性能差异的比例越趋于稳定; + +由于测试的这四个方法最终调用的都是`native`方法,无法进一步跟踪。个人猜测应该是和在程序运行时操作`class`有关。 + +比如需要判断是否安全?是否允许这样操作?入参是否正确?是否能够在虚拟机中找到需要反射的类?主要是这一系列判断条件导致了反射耗时;也有可能是因为调用`natvie`方法,需要使用`JNI`接口,导致了性能问题(参照`Log.java、System.out.println`,都是调用`native`方法,重复调用多次耗时很明显)。 + +# 03 如果避免反射导致的性能问题? + +通过上面的测试可以看出,过多地使用反射,的确会存在性能问题,但如果使用得当,所谓反射导致性能问题也就不是问题了,关于反射对性能的影响,参照下面的使用原则,并不会有什么明显的问题: + +- 不要过于频繁地使用反射,大量地使用反射会带来性能问题; +- 通过反射直接访问实例会比访问方法快很多,所以应该优先采用访问实例的方式。 + +# 04 总结 + +上面的测试并不全面,但在一定程度上能够反映出反射的确会导致性能问题,也能够大概知道是哪个地方导致的问题。如果后面有必要进一步测试,我会从下面几个方面作进一步测试: + +- 测试频繁调用`native`方法是否会有明显的性能问题; +- 测试同一个方法内,过多的条件判断是否会有明显的性能问题; +- 测试类的复杂程度是否会对反射的性能有明显影响。 + diff --git a/docs/java/basic/serialization.md b/docs/java/basic/serialization.md new file mode 100644 index 0000000..9f3381f --- /dev/null +++ b/docs/java/basic/serialization.md @@ -0,0 +1,153 @@ +--- +sidebar: heading +title: 为什么要序列化? +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: 序列化 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +> 本文转自爱笑的架构师 + +凡事都要问为什么,在讲解序列化概念和原理前,我们先来了解一下为什么需要序列化。 + +# 为什么要序列化? + +如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来感受一下什么是序列化。 + +都玩过游戏么?玩过的同学应该知道游戏里有一个叫『存档』的功能,每次不想玩的时候可以把当前进度存档,下次有时间想玩的时候,直接载入存档就可以接着玩了,这样的好处是之前的游戏进度不会丢失,要是每次打开都重新玩估计大家也没什么耐心了。 + +如果把面向对象的思想带到游戏的世界,那在我们眼中不管是游戏角色还是游戏中的怪兽、装备等等都可以看成是一个个对象: + +- 角色对象(包含性别、等级、经验值、血量、伤害值、护甲值等属性) +- 怪兽对象(包含类型、血量、等级等等属性) +- 装备对象(包含类型、伤害值、附加值等等属性) + +在玩游戏的过程中创建一个游戏角色就好像是创建了一个角色对象,拿到一套装备就好像创建了一个装备对象,路上遇到的怪兽等等也都是对象了。 + +我们再用计算机的思维去思考,创建的这些对象都是保存在内存中的,大家都知道内存的数据是短暂保留的,断电之后是会消失的,但是游戏经过手动存档之后就算你关机几天了,再次进入游戏读取存档,你会发现之前在游戏中创建的角色和装备都还在呢,这就很奇怪了,明明内存的数据断电就消失了,这是为什么? + +稍加思考就知道,我们在存档的过程中就是将内存中的数据存储到电脑的硬盘中,硬盘的数据在关机断电后是不会丢失的(别杠,硬盘损坏数据丢失先不考虑)。这个过程就是对象的持久化,也就是我们今天要讲的**对象序列化**。对象的序列化逆过程就叫做**反序列化**,反序列化也很好理解就是将硬盘中的信息读取出来形成对象。 + +# 什么是序列化? + +前面引入游戏的例子是为了让大家生动地理解什么是序列化和反序列化。简单总结一下就是: + +- **序列化**是指将对象实例的状态存到存储媒体的过程 +- **反序列化**是指将存储在存储媒体中的对象状态装换成对象的过程 + +用更为抽象的概念来讲: + +**序列化:**把对象转化为可传输的字节序列过程 + +![](http://img.topjavaer.cn/img/序列化的理解1.png) + +**反序列化:把字节序列还原为对象的过程** + +![](http://img.topjavaer.cn/img/序列化的理解2.png) + + + +# 序列化的机制 + +序列化最终的目的是为了对象可以**跨平台存储**和**进行网络传输**,而我们进行跨平台存储和网络传输的方式就是 **IO**,而 IO 支持的数据格式就是**字节数组**。 + +那现在的问题就是如何把对象转换成字节数组?这个很好办,一般的编程语言都有这个能力,可以很容易将对象转成字节数组。 + +仔细一想,我们单方面的把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,简单说就是将对象转成字节数组容易但是将字节数组还原成对象就难了,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化)**,那么我们从 IO 流里面读出数据的时候再以这种规则把对象还原回来**(反序列化)。 + +还是拿上面游戏那个例子,我们将正在玩的游戏存档到硬盘,**序列化**就是将一个个角色对象和装备对象存储到硬盘,然后留下一张原来对象的结构图纸,**反序列化**就是将硬盘里一个个对象读出来照着图纸逐个还原恢复。 + +# 常见序列化的方式 + +序列化只是定义了拆解对象的具体规则,那这种规则肯定也是多种多样的,比如现在常见的序列化方式有:JDK 原生、JSON、ProtoBuf、Hessian、Kryo等。 + +**(1)JDK 原生** + +作为一个成熟的编程语言,JDK自带了序列化方法。只需要类实现了`Serializable`接口,就可以通过`ObjectOutputStream`类将对象变成byte[]字节数组。 + +JDK 序列化会把对象类的描述信息和所有的属性以及继承的元数据都序列化为字节流,所以会导致生成的字节流相对比较大。 + +另外,这种序列化方式是 JDK 自带的,因此不支持跨语言。 + +简单总结一下:JDK 原生的序列化方式生成的字节流比较大,也不支持跨语言,因此在实际项目和框架中用的都比较少。 + +**(2)ProtoBuf** + +谷歌推出的,是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于通信协议、数据存储等。序列化后体积小,一般用于对传输性能有较高要求的系统。 + +**(4)Hessian** + +Hessian 是一个轻量级的二进制 web service 协议,主要用于传输二进制数据。 + +在传输数据前 Hessian 支持将对象序列化成二进制流,相对于 JDK 原生序列化,Hessian序列化之后体积更小,性能更优。 + +**(5)Kryo** + +Kryo 是一个 Java 序列化框架,号称 Java 最快的序列化框架。Kryo 在序列化速度上很有优势,底层依赖于字节码生成机制。 + +由于只能限定在 JVM 语言上,所以 Kryo 不支持跨语言使用。 + +**(6)JSON** + +上面讲的几种序列化方式都是直接将对象变成二进制,也就是byte[]字节数组,这些方式都可以叫二进制方式。 + +JSON 序列化方式生成的是一串有规则的字符串,在可读性上要优于上面几种方式,但是在体积上就没什么优势了。 + +另外 JSON 是有规则的字符串,不跟任何编程语言绑定,天然上就具备了跨平台。 + +**总结一下:JSON 可读性强,支持跨平台,体积稍微逊色。** + +JSON 序列化常见的框架有: + +`fastJSON`、`Jackson`、`Gson` 等。 + +# 序列化技术的选型 + +上面列举的这些序列化技术各有优缺点,不能简单地说哪一种就是最好的,不然也不会有这么多序列化技术共存了。 + +既然有这么多序列化技术可供选择,那在实际项目中如何选型呢? + +我认为需要结合具体的项目来看,比较技术是服务于业务的。你可以从下面这几个因素来考虑: + +**(1)协议是否支持跨平台** + +如果一个大的系统有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,比如 JDK 原生、Kryo 这些只能用在 Java 语言范围下,你用 JDK 原生方式进行序列化,用其他语言是无法反序列化的。 + +**(2)序列化的速度** + +如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。 + +**(3)序列化生成的体积** + +如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能,因此序列化生成的体积就很关键了。 + +# 小结 + +(1)为什么我们要序列化? + +因为我们需要将内存中的对象存储到媒介中,或者我们需要将一个对象通过网络传输到另外一个系统中。 + +(2)什么是序列化? + +**序列化**就是把对象转化为可传输的字节序列过程;**反序列化**就是把字节序列还原为对象的过程。 + +(3)序列化的机制 + +序列化最终的目的是为了对象可以**跨平台存储**和**进行网络传输**,而我们进行跨平台存储和网络传输的方式就是 **IO**,而 IO 支持的数据格式就是**字节数组**。 + +将对象转成字节数组的时候需要制定一种规则,这种规则就是序列化机制。 + +(4)常见序列化的方式 + +现在常见的序列化方式有:JDK 原生、JSON、ProtoBuf、Hessian、Kryo等。 + +(5)序列化技术的选型 + +选型最重要的就是要考虑这三个方面:**协议是否支持跨平台**、**序列化的速度**、**序列化生成的体积**。 diff --git "a/Java/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" b/docs/java/java-basic.md similarity index 54% rename from "Java/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" rename to docs/java/java-basic.md index 04f0e86..ad1b6a4 100644 --- "a/Java/Java\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ b/docs/java/java-basic.md @@ -1,16 +1,48 @@ +--- +sidebar: heading +title: Java基础常见面试题总结 +category: Java +tag: + - Java基础 +head: + - - meta + - name: keywords + content: JVM,JDK,JRE,面向对象,Java 基本数据类型,装箱和拆箱,Java数组,值传递和引用传递,String,深拷贝,final关键字,同步和异步,Java8新特性,序列化和反序列化,反射,泛型 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## Java的特点 -**Java是一门面向对象的编程语言。**面向对象和面向过程的区别参考下一个问题。 +**Java是一门面向对象的编程语言**。面向对象和面向过程的区别参考下一个问题。 -**Java具有平台独立性和移植性。** +**Java具有平台独立性和移植性**。 - Java有一句口号:`Write once, run anywhere`,一次编写、到处运行。这也是Java的魅力所在。而实现这种特性的正是Java虚拟机JVM。已编译的Java程序可以在任何带有JVM的平台上运行。你可以在windows平台编写代码,然后拿到linux上运行。只要你在编写完代码后,将代码编译成.class文件,再把class文件打成Java包,这个jar包就可以在不同的平台上运行了。 -**Java具有稳健性。** +**Java具有稳健性**。 - Java是一个强类型语言,它允许扩展编译时检查潜在类型不匹配问题的功能。Java要求显式的方法声明,它不支持C风格的隐式声明。这些严格的要求保证编译程序能捕捉调用错误,这就导致更可靠的程序。 - 异常处理是Java中使得程序更稳健的另一个特征。异常是某种类似于错误的异常条件出现的信号。使用`try/catch/finally`语句,程序员可以找到出错的处理代码,这就简化了出错处理和恢复的任务。 +## Java是如何实现跨平台的? + +Java是通过JVM(Java虚拟机)实现跨平台的。 + +JVM可以理解成一个软件,不同的平台有不同的版本。我们编写的Java代码,编译后会生成.class 文件(字节码文件)。Java虚拟机就是负责将字节码文件翻译成特定平台下的机器码,通过JVM翻译成机器码之后才能运行。不同平台下编译生成的字节码是一样的,但是由JVM翻译成的机器码却不一样。 + +只要在不同平台上安装对应的JVM,就可以运行字节码文件,运行我们编写的Java程序。 + +因此,运行Java程序必须有JVM的支持,因为编译的结果不是机器码,必须要经过JVM的翻译才能执行。 + ## Java 与 C++ 的区别 - Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 兼容 C ,不但支持面向对象也支持面向过程。 @@ -19,74 +51,116 @@ - Java 支持自动垃圾回收,而 C++ 需要手动回收。 - Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。 -## 面向对象和面向过程的区别? - -面向对象和面向过程是一种软件开发思想。 - -- 面向过程就是分析出解决问题所需要的步骤,然后用函数按这些步骤实现,使用的时候依次调用就可以了。 +## JDK/JRE/JVM三者的关系 -- 面向对象是把构成问题事务分解成各个对象,分别设计这些对象,然后将他们组装成有完整功能的系统。面向过程只用函数实现,面向对象是用类实现各个功能模块。 +**JVM** -以五子棋为例,面向过程的设计思路就是首先分析问题的步骤: +英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。Java 能够跨平台运行的核心在于 JVM 。 -1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。 -把上面每个步骤用分别的函数来实现,问题就解决了。 - -而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为: - -1. 黑白双方 -2. 棋盘系统,负责绘制画面 -3. 规则系统,负责判定诸如犯规、输赢等。 - -黑白双方负责接受用户的输入,并告知棋盘系统棋子布局发生变化,棋盘系统接收到了棋子的变化的信息就负责在屏幕上面显示出这种变化,同时利用规则系统来对棋局进行判定。 - -## JKD/JRE/JVM三者的关系 - -### JVM - -**JVM** :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。Java 能够跨平台运行的核心在于 JVM 。 - -![](http://img.dabin-coder.cn/image/20220402230447.png) +![](http://img.topjavaer.cn/img/20220402230447.png) 所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。也就是说class文件并不直接与机器的操作系统交互,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。 针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。这就是Java能够跨平台,实现一次编写,多处运行的原因所在。 -### JRE +**JRE** 英文名称(Java Runtime Environment),就是Java 运行时环境。我们编写的Java程序必须要在JRE才能运行。它主要包含两个部分,JVM 和 Java 核心类库。 -![](http://img.dabin-coder.cn/image/20220401234008.png) +![](http://img.topjavaer.cn/img/20220401234008.png) JRE是Java的运行环境,并不是一个开发环境,所以没有包含任何开发工具,如编译器和调试器等。 如果你只是想运行Java程序,而不是开发Java程序的话,那么你只需要安装JRE即可。 -### JDK +**JDK** 英文名称(Java Development Kit),就是 Java 开发工具包 学过Java的同学,都应该安装过JDK。当我们安装完JDK之后,目录结构是这样的 -![](https://cdn.jsdelivr.net/gh/Tyson0314/img/20220404120509.png) +![](http://img.topjavaer.cn/img/20220404120509.png) 可以看到,JDK目录下有个JRE,也就是JDK中已经集成了 JRE,不用单独安装JRE。 另外,JDK中还有一些好用的工具,如jinfo,jps,jstack等。 -![](https://cdn.jsdelivr.net/gh/Tyson0314/img/20220404120507.png) +![](http://img.topjavaer.cn/img/20220404120507.png) +最后,总结一下JDK/JRE/JVM,他们三者的关系 +**JRE = JVM + Java 核心类库** -### 总结 +**JDK = JRE + Java工具 + 编译器 + 调试器** -最后,总结一下JDK/JRE/JVM,他们三者的关系 +![](http://img.topjavaer.cn/img/20220402230613.png) + + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + +## Java程序是编译执行还是解释执行? + +先看看什么是编译型语言和解释型语言。 + +**编译型语言** + +在程序运行之前,通过编译器将源程序编译成机器码可运行的二进制,以后执行这个程序时,就不用再进行编译了。 + +优点:编译器一般会有预编译的过程对代码进行优化。因为编译只做一次,运行时不需要编译,所以编译型语言的程序执行效率高,可以脱离语言环境独立运行。 + +缺点:编译之后如果需要修改就需要整个模块重新编译。编译的时候根据对应的运行环境生成机器码,不同的操作系统之间移植就会有问题,需要根据运行的操作系统环境编译不同的可执行文件。 + +**总结**:执行速度快、效率高;依靠编译器、跨平台性差些。 + +**代表语言**:C、C++、Pascal、Object-C以及Swift。 -JRE = JVM + Java 核心类库 +**解释型语言** -JDK = JRE + Java工具 + 编译器 + 调试器 +定义:解释型语言的源代码不是直接翻译成机器码,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。在运行的时候才将源程序翻译成机器码,翻译一句,然后执行一句,直至结束。 -![](http://img.dabin-coder.cn/image/20220402230613.png) +优点: + +1. 有良好的平台兼容性,在任何环境中都可以运行,前提是安装了解释器(如虚拟机)。 +2. 灵活,修改代码的时候直接修改就可以,可以快速部署,不用停机维护。 + +缺点:每次运行的时候都要解释一遍,性能上不如编译型语言。 + +总结:解释型语言执行速度慢、效率低;依靠解释器、跨平台性好。 + +代表语言:JavaScript、Python、Erlang、PHP、Perl、Ruby。 + +对于Java这种语言,它的**源代码**会先通过javac编译成**字节码**,再通过jvm将字节码转换成**机器码**执行,即解释运行 和编译运行配合使用,所以可以称为混合型或者半编译型。 + +## 面向对象和面向过程的区别? + +面向对象和面向过程是一种软件开发思想。 + +- 面向过程就是分析出解决问题所需要的步骤,然后用函数按这些步骤实现,使用的时候依次调用就可以了。 + +- 面向对象是把构成问题事务分解成各个对象,分别设计这些对象,然后将他们组装成有完整功能的系统。面向过程只用函数实现,面向对象是用类实现各个功能模块。 + +以五子棋为例,面向过程的设计思路就是首先分析问题的步骤: + +1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。 +把上面每个步骤用分别的函数来实现,问题就解决了。 + +而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为: + +1. 黑白双方 +2. 棋盘系统,负责绘制画面 +3. 规则系统,负责判定诸如犯规、输赢等。 + +黑白双方负责接受用户的输入,并告知棋盘系统棋子布局发生变化,棋盘系统接收到了棋子的变化的信息就负责在屏幕上面显示出这种变化,同时利用规则系统来对棋局进行判定。 ## 面向对象有哪些特性? @@ -103,6 +177,45 @@ JDK = JRE + Java工具 + 编译器 + 调试器 4、抽象。把客观事物用代码抽象出来。 +## 面向对象编程的六大原则? + +- **对象单一职责**:我们设计创建的对象,必须职责明确,比如商品类,里面相关的属性和方法都必须跟商品相关,不能出现订单等不相关的内容。这里的类可以是模块、类库、程序集,而不单单指类。 +- **里式替换原则**:子类能够完全替代父类,反之则不行。通常用于实现接口时运用。因为子类能够完全替代基(父)类,那么这样父类就拥有很多子类,在后续的程序扩展中就很容易进行扩展,程序完全不需要进行修改即可进行扩展。比如IA的实现为A,因为项目需求变更,现在需要新的实现,直接在容器注入处更换接口即可. +- **迪米特法则**,也叫最小原则,或者说最小耦合。通常在设计程序或开发程序的时候,尽量要高内聚,低耦合。当两个类进行交互的时候,会产生依赖。而迪米特法则就是建议这种依赖越少越好。就像构造函数注入父类对象时一样,当需要依赖某个对象时,并不在意其内部是怎么实现的,而是在容器中注入相应的实现,既符合里式替换原则,又起到了解耦的作用。 +- 开闭原则:开放扩展,封闭修改。当项目需求发生变更时,要尽可能的不去对原有的代码进行修改,而在原有的基础上进行扩展。 +- **依赖倒置原则**:高层模块不应该直接依赖于底层模块的具体实现,而应该依赖于底层的抽象。接口和抽象类不应该依赖于实现类,而实现类依赖接口或抽象类。 +- **接口隔离原则**:一个对象和另外一个对象交互的过程中,依赖的内容最小。也就是说在接口设计的时候,在遵循对象单一职责的情况下,尽量减少接口的内容。 + +**简洁版**: + +- 单一职责:对象设计要求独立,不能设计万能对象。 +- 开闭原则:对象修改最小化。 +- 里式替换:程序扩展中抽象被具体可以替换(接口、父类、可以被实现类对象、子类替换对象) +- 迪米特:高内聚,低耦合。尽量不要依赖细节。 +- 依赖倒置:面向抽象编程。也就是参数传递,或者返回值,可以使用父类类型或者接口类型。从广义上讲:基于接口编程,提前设计好接口框架。 +- 接口隔离:接口设计大小要适中。过大导致污染,过小,导致调用麻烦。 + +## 数组到底是不是对象? + +先说说对象的概念。对象是根据某个类创建出来的一个实例,表示某类事物中一个具体的个体。 + +对象具有各种属性,并且具有一些特定的行为。站在计算机的角度,对象就是内存中的一个内存块,在这个内存块封装了一些数据,也就是类中定义的各个属性。 + +所以,对象是用来封装数据的。 + +java中的数组具有java中其他对象的一些基本特点。比如封装了一些数据,可以访问属性,也可以调用方法。 + +因此,可以说,数组是对象。 + +也可以通过代码验证数组是对象的事实。比如以下的代码,输出结果为java.lang.Object。 + +```java +Class clz = int[].class; +System.out.println(clz.getSuperclass().getName()); +``` + +由此,可以看出,数组类的父类就是Object类,那么可以推断出数组就是对象。 + ## Java的基本数据类型有哪些? - byte,8bit @@ -119,6 +232,8 @@ JDK = JRE + Java工具 + 编译器 + 调试器 | 二进制位数 | 1 | 8 | 16 | 16 | 32 | 64 | 32 | 64 | | 包装类 | Boolean | Byte | Character | Short | Integer | Long | Float | Double | +在Java规范中,没有明确指出boolean的大小。在《Java虚拟机规范》给出了单个boolean占4个字节,和boolean数组1个字节的定义,具体 **还要看虚拟机实现是否按照规范来**,因此boolean占用1个字节或者4个字节都是有可能的。 + ## 为什么不能用浮点型表示金额? 由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,千万不要在代码中使用浮点数来表示金额等重要的指标。 @@ -127,8 +242,10 @@ JDK = JRE + Java工具 + 编译器 + 调试器 ## 什么是值传递和引用传递? -- 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。 -- 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。所以对引用对象进行操作会同时改变原对象。 +- 值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。 +- 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,两者指向同一片内存空间。所以对引用对象进行操作会同时改变原对象。 + +**java中不存在引用传递,只有值传递**。即不存在变量a指向变量b,变量b指向对象的这种情况。 ## 了解Java的包装类型吗?为什么需要包装类? @@ -170,6 +287,8 @@ Integer x = 1; // 装箱 调⽤ Integer.valueOf(1) int y = x; // 拆箱 调⽤了 X.intValue() ``` +## 两个Integer 用== 比较不相等的原因 + 下面看一道常见的面试题: ```java @@ -189,7 +308,7 @@ true false ``` -为什么第三个输出是false?看看 Integer 类的源码就知道啦。 +为什么第二个输出是false?看看 Integer 类的源码就知道啦。 ```java public static Integer valueOf(int i) { @@ -199,7 +318,7 @@ public static Integer valueOf(int i) { } ``` -`Integer c = 200;` 会调用 调⽤`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 +`Integer c = 200;` 会调用`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 ```java private static class IntegerCache { @@ -228,7 +347,7 @@ private static class IntegerCache { } ``` -这是IntegerCache静态代码块中的一段,默认Integer cache 的下限是-128,上限默认127。当赋值100给Integer时,刚好在这个范围内,所以从cache中取对应的Integer并返回,所以a和b返回的是同一个对象,所以==比较是相等的,当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然==比较的结果是不相等的。 +这是IntegerCache静态代码块中的一段,默认Integer cache 的下限是-128,上限默认127。当赋值100给Integer时,刚好在这个范围内,所以从cache中取对应的Integer并返回,所以a和b返回的是同一个对象,所以`==`比较是相等的,当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然`==`比较的结果是不相等的。 ## String 为什么不可变? @@ -262,14 +381,26 @@ String类内部所有的字段都是私有的,也就是被private修饰。而 主要有以下几点原因: 1. **线程安全**。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。 -2. **支持hash映射和缓存。**因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。 +2. **支持hash映射和缓存**。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。 3. **出于安全考虑**。网络地址URL、文件路径path、密码通常情况下都是以String类型保存,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String的类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。 -3. **字符串常量池优化**。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。 +4. **字符串常量池优化**。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。 既然我们的String是不可变的,它内部还有很多substring, replace, replaceAll这些操作的方法。这些方法好像会改变String对象?怎么解释呢? 其实不是的,我们每次调用replace等方法,其实会在堆内存中创建了一个新的对象。然后其value数组引用指向不同的对象。 +## 为何JDK9要将String的底层实现由char[]改成byte[]? + +主要是为了**节约String占用的内存**。 + +在大部分Java程序的堆内存中,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。 + +而在JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节,它也要按照2字节进行分配,浪费了一半的内存空间。 + +到了JDK9之后,对于每个字符串,会先判断它是不是只有Latin-1字符,如果是,就按照1字节的规格进行分配内存,如果不是,就按照2字节的规格进行分配,这样便提高了内存使用率,同时GC次数也会减少,提升效率。 + +不过Latin-1编码集支持的字符有限,比如不支持中文字符,因此对于中文字符串,用的是UTF16编码(两个字节),所以用byte[]和char[]实现没什么区别。 + ## String, StringBuffer 和 StringBuilder区别 **1. 可变性** @@ -283,6 +414,62 @@ String类内部所有的字段都是私有的,也就是被private修饰。而 - StringBuilder 不是线程安全的 - StringBuffer 是线程安全的,内部使用 synchronized 进行同步 +## 什么是StringJoiner? + +StringJoiner是 Java 8 新增的一个 API,它基于 StringBuilder 实现,用于实现对字符串之间通过分隔符拼接的场景。 + +StringJoiner 有两个构造方法,第一个构造要求依次传入分隔符、前缀和后缀。第二个构造则只要求传入分隔符即可(前缀和后缀默认为空字符串)。 + +```java +StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) +StringJoiner(CharSequence delimiter) +``` + +有些字符串拼接场景,使用 StringBuffer 或 StringBuilder 则显得比较繁琐。 + +比如下面的例子: + +```java +List values = Arrays.asList(1, 3, 5); +StringBuilder sb = new StringBuilder("("); + +for (int i = 0; i < values.size(); i++) { + sb.append(values.get(i)); + if (i != values.size() -1) { + sb.append(","); + } +} + +sb.append(")"); +``` + +而通过StringJoiner来实现拼接List的各个元素,代码看起来更加简洁。 + +```java +List values = Arrays.asList(1, 3, 5); +StringJoiner sj = new StringJoiner(",", "(", ")"); + +for (Integer value : values) { + sj.add(value.toString()); +} +``` + +另外,像平时经常使用的Collectors.joining(","),底层就是通过StringJoiner实现的。 + +源码如下: + +```java +public static Collector joining( + CharSequence delimiter,CharSequence prefix,CharSequence suffix) { + return new CollectorImpl<>( + () -> new StringJoiner(delimiter, prefix, suffix), + StringJoiner::add, StringJoiner::merge, + StringJoiner::toString, CH_NOID); +} +``` + + + ## String 类的常用方法有哪些? - indexOf():返回指定字符的索引。 @@ -308,6 +495,49 @@ String类内部所有的字段都是私有的,也就是被private修饰。而 字符串常量池(String Pool)保存着所有字符串字面量,这些字面量在编译时期就确定。字符串常量池位于堆内存中,专门用来存储字符串常量。在创建字符串时,JVM首先会检查字符串常量池,如果该字符串已经存在池中,则返回其引用,如果不存在,则创建此字符串并放入池中,并返回其引用。 +## String最大长度是多少? + +String类提供了一个length方法,返回值为int类型,而int的取值上限为2^31 -1。 + +所以理论上String的最大长度为2^31 -1。 + +**达到这个长度的话需要多大的内存吗**? + +String内部是使用一个char数组来维护字符序列的,一个char占用两个字节。如果说String最大长度是2^31 -1的话,那么最大的字符串占用内存空间约等于4GB。 + +也就是说,我们需要有大于4GB的JVM运行内存才行。 + +**那String一般都存储在JVM的哪块区域呢**? + +字符串在JVM中的存储分两种情况,一种是String对象,存储在JVM的堆栈中。一种是字符串常量,存储在常量池里面。 + +**什么情况下字符串会存储在常量池呢**? + +当通过字面量进行字符串声明时,比如String s = "程序新大彬";,这个字符串在编译之后会以常量的形式进入到常量池。 + +**那常量池中的字符串最大长度是2^31-1吗**? + +不是的,常量池对String的长度是有另外限制的。。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。 + +```java +CONSTANT_Utf8_info { + u1 tag; + u2 length; + u1 bytes[length]; +} +``` + +length在这里就是代表字符串的长度,length的类型是u2,u2是无符号的16位整数,也就是说最大长度可以做到2^16-1 即 65535。 + +不过javac编译器做了限制,需要length < 65535。所以字符串常量在常量池中的最大长度是65535 - 1 = 65534。 + +最后总结一下: + +String在不同的状态下,具有不同的长度限制。 + +- 字符串常量长度不能超过65534 +- 堆内字符串的长度不超过2^31-1 + ## Object常用方法有哪些? Java面试经常会出现的一道题目,Object的常用方法。下面给大家整理一下。 @@ -574,6 +804,8 @@ equals与hashcode的关系: hashcode方法主要是用来**提升对象比较的效率**,先进行hashcode()的比较,如果不相同,那就不必在进行equals的比较,这样就大大减少了equals比较的次数,当比较对象的数量很大的时候能提升效率。 +## 为什么重写 equals 时一定要重写 hashCode? + 之所以重写`equals()`要重写`hashcode()`,是为了保证`equals()`方法返回true的情况下hashcode值也要一致,如果重写了`equals()`没有重写`hashcode()`,就会出现两个对象相等但`hashcode()`不相等的情况。这样,当用其中的一个对象作为键保存到hashMap、hashTable或hashSet中,再以另一个对象作为键值去查找他们的时候,则会查找不到。 ## Java创建对象有几种方式? @@ -836,6 +1068,15 @@ public class B extends A { - finally 是异常处理语句结构的一部分,一般以`try-catch-finally`出现,`finally`代码块表示总是被执行。 - finalize 是Object类的一个方法,该方法一般由垃圾回收器来调用,当我们调用`System.gc()`方法的时候,由垃圾回收器调用`finalize()`方法,回收垃圾,JVM并不保证此方法总被调用。 +## Java中的finally一定会被执行吗? + +答案是不一定。 + +有以下两种情况finally不会被执行: + +- 程序未执行到try代码块 +- 如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。 + ## final关键字的作用? - final 修饰的类不能被继承。 @@ -844,7 +1085,7 @@ public class B extends A { ## 方法重载和重写的区别? -**同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载。**参数列表又叫参数签名,包括参数的类型、参数的个数、参数的顺序,只要有一个不同就叫做参数列表不同。 +**同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载**。参数列表又叫参数签名,包括参数的类型、参数的个数、参数的顺序,只要有一个不同就叫做参数列表不同。 重载是面向对象的一个基本特性。 @@ -862,7 +1103,7 @@ public class OverrideTest { } ``` -**方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写。**方法重写时, 方法名与形参列表必须一致。 +**方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写**。方法重写时, 方法名与形参列表必须一致。 如下代码,Person为父类,Student为子类,在Student中重写了dailyTask方法。 @@ -888,9 +1129,9 @@ public class Student extends Person { 1、**语法层面**上的区别 -- 抽象类可以有方法实现,而接口的方法中只能是抽象方法(Java 8 开始接口方法可以有默认实现); +- 抽象类可以有方法实现,而接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默认实现); - 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型; -- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法; +- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法(Java 8之后接口可以有静态方法); - 一个类只能继承一个抽象类,而一个类却可以实现多个接口。 2、**设计层面**上的区别 @@ -921,7 +1162,7 @@ class BMWCar extends Car implements Alarm { 5. `NumberFormatException` //数字格式化异常 6. `ArithmeticException` //数学运算异常 -unchecked Exception: +checked Exception: 1. `NoSuchFieldException` //反射异常,没有对应的字段 2. `ClassNotFoundException` //类没有找到异常 @@ -994,7 +1235,7 @@ unchecked Exception: 不同的线程干专业的事情,最终每个线程都没空着,系统的吞吐量自然就上去了。 -![](http://img.dabin-coder.cn/image/20220423154450.png) +![](http://img.topjavaer.cn/img/20220423154450.png) @@ -1071,17 +1312,60 @@ Java不支持多继承的原因: - Optional 类 :Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。 - Date Time API :加强对日期与时间的处理。 -> [Java8 新特性总结]([Java-learning/Java8.md at master · Tyson0314/Java-learning (github.com)](https://github.com/Tyson0314/Java-learning/blob/master/Java/Java8.md)) +> [Java8 新特性总结](https://github.com/Tyson0314/Java-learning/blob/master/Java/Java8新特性.md) + +## 序列化和反序列化 + +- 序列化:把对象转换为字节序列的过程称为对象的序列化. +- 反序列化:把字节序列恢复为对象的过程称为对象的反序列化. -## 什么是序列化和反序列化? +## 什么时候需要用到序列化和反序列化呢? -序列化:把内存中的对象转换为字节序列的过程。 +当我们只在本地 JVM 里运行下 Java 实例,这个时候是不需要什么序列化和反序列化的,但当我们需要将内存中的对象持久化到磁盘,数据库中时,当我们需要与浏览器进行交互时,当我们需要实现 RPC 时,这个时候就需要序列化和反序列化了. -反序列化:把字节序列恢复为Java对象的过程。 +前两个需要用到序列化和反序列化的场景,是不是让我们有一个很大的疑问? 我们在与浏览器交互时,还有将内存中的对象持久化到数据库中时,好像都没有去进行序列化和反序列化,因为我们都没有实现 Serializable 接口,但一直正常运行. -## 如何实现序列化 +下面先给出结论: -实现`Serializable`接口即可。序列化的时候(如`objectOutputStream.writeObject(user)`),会判断user是否实现了`Serializable`,如果对象没有实现`Serializable`接口,在序列化的时候会抛出`NotSerializableException`异常。源码如下: +**只要我们对内存中的对象进行持久化或网络传输,这个时候都需要序列化和反序列化.** + +理由: + +服务器与浏览器交互时真的没有用到 Serializable 接口吗? JSON 格式实际上就是将一个对象转化为字符串,所以服务器与浏览器交互时的数据格式其实是字符串,我们来看来 String 类型的源码: + +``` +public final class String + implements java.io.Serializable,Comparable,CharSequence { + /\*\* The value is used for character storage. \*/ + private final char value\[\]; + + /\*\* Cache the hash code for the string \*/ + private int hash; // Default to 0 + + /\*\* use serialVersionUID from JDK 1.0.2 for interoperability \*/ + private static final long serialVersionUID = -6849794470754667710L; + + ...... +} +``` + +String 类型实现了 Serializable 接口,并显示指定 serialVersionUID 的值. + +然后我们再来看对象持久化到数据库中时的情况,Mybatis 数据库映射文件里的 insert 代码: + +``` + + INSERT INTO t\_user(name,age) VALUES (#{name},#{age}) + +``` + +实际上我们并不是将整个对象持久化到数据库中,而是将对象中的属性持久化到数据库中,而这些属性(如Date/String)都实现了 Serializable 接口。 + +## 实现序列化和反序列化为什么要实现 Serializable 接口? + +在 Java 中实现了 Serializable 接口后, JVM 在类加载的时候就会发现我们实现了这个接口,然后在初始化实例对象的时候就会在底层帮我们实现序列化和反序列化。 + +如果被写对象类型不是String、数组、Enum,并且没有实现Serializable接口,那么在进行序列化的时候,将抛出NotSerializableException。源码如下: ```java // remaining cases @@ -1103,6 +1387,20 @@ if (obj instanceof String) { } ``` +## 实现 Serializable 接口之后,为什么还要显示指定 serialVersionUID 的值? + +如果不显示指定 serialVersionUID,JVM 在序列化时会根据属性自动生成一个 serialVersionUID,然后与属性一起序列化,再进行持久化或网络传输. 在反序列化时,JVM 会再根据属性自动生成一个新版 serialVersionUID,然后将这个新版 serialVersionUID 与序列化时生成的旧版 serialVersionUID 进行比较,如果相同则反序列化成功,否则报错. + +如果显示指定了 serialVersionUID,JVM 在序列化和反序列化时仍然都会生成一个 serialVersionUID,但值为我们显示指定的值,这样在反序列化时新旧版本的 serialVersionUID 就一致了. + +如果我们的类写完后不再修改,那么不指定serialVersionUID,不会有问题,但这在实际开发中是不可能的,我们的类会不断迭代,一旦类被修改了,那旧对象反序列化就会报错。 所以在实际开发中,我们都会显示指定一个 serialVersionUID。 + +## static 属性为什么不会被序列化? + +因为序列化是针对对象而言的,而 static 属性优先于对象存在,随着类的加载而加载,所以不会被序列化. + +看到这个结论,是不是有人会问,serialVersionUID 也被 static 修饰,为什么 serialVersionUID 会被序列化? 其实 serialVersionUID 属性并没有被序列化,JVM 在序列化对象时会自动生成一个 serialVersionUID,然后将我们显示指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID. + ## transient关键字的作用? Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。 @@ -1128,8 +1426,232 @@ Java泛型是JDK 5中引⼊的⼀个新特性, 允许在定义类和接口的 泛型最⼤的好处是可以提⾼代码的复⽤性。以List接口为例,我们可以将String、 Integer等类型放⼊List中, 如不⽤泛型, 存放String类型要写⼀个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。 -## String为什么不可变? +## 如何停止一个正在运行的线程? + +有几种方式。 + +1、**使用线程的stop方法**。 + +使用stop()方法可以强制终止线程。不过stop是一个被废弃掉的方法,不推荐使用。 + +使用Stop方法,会一直向上传播ThreadDeath异常,从而使得目标线程解锁所有锁住的监视器,即释放掉所有的对象锁。使得之前被锁住的对象得不到同步的处理,因此可能会造成数据不一致的问题。 + +2、**使用interrupt方法中断线程**,该方法只是告诉线程要终止,但最终何时终止取决于计算机。调用interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。 + +接着调用 Thread.currentThread().isInterrupted()方法,可以用来判断当前线程是否被终止,通过这个判断我们可以做一些业务逻辑处理,通常如果isInterrupted返回true的话,会抛一个中断异常,然后通过try-catch捕获。 + +3、**设置标志位** + +设置标志位,当标识位为某个值时,使线程正常退出。设置标志位是用到了共享变量的方式,为了保证共享变量在内存中的可见性,可以使用volatile修饰它,这样的话,变量取值始终会从主存中获取最新值。 + +但是这种volatile标记共享变量的方式,在线程发生阻塞时是无法完成响应的。比如调用Thread.sleep() 方法之后,线程处于不可运行状态,即便是主线程修改了共享变量的值,该线程此时根本无法检查循环标志,所以也就无法实现线程中断。 + +因此,interrupt() 加上手动抛异常的方式是目前中断一个正在运行的线程**最为正确**的方式了。 + +## 什么是跨域? + +简单来讲,跨域是指从一个域名的网页去请求另一个域名的资源。由于有**同源策略**的关系,一般是不允许这么直接访问的。但是,很多场景经常会有跨域访问的需求,比如,在前后端分离的模式下,前后端的域名是不一致的,此时就会发生跨域问题。 + +**那什么是同源策略呢**? + +所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 + +同源策略限制以下几种行为: + +```mipsasm +1. Cookie、LocalStorage 和 IndexDB 无法读取 +2. DOM 和 Js对象无法获得 +3. AJAX 请求不能发送 +``` + +**为什么要有同源策略**? + +举个例子,假如你刚刚在网银输入账号密码,查看了自己的余额,然后再去访问其他带颜色的网站,这个网站可以访问刚刚的网银站点,并且获取账号密码,那后果可想而知。因此,从安全的角度来讲,同源策略是有利于保护网站信息的。 + +## 跨域问题怎么解决呢? + +嗯,有以下几种方法: + +**CORS**,跨域资源共享 + +CORS(Cross-origin resource sharing),跨域资源共享。CORS 其实是浏览器制定的一个规范,浏览器会自动进行 CORS 通信,它的实现主要在服务端,通过一些 HTTP Header 来限制可以访问的域,例如页面 A 需要访问 B 服务器上的数据,如果 B 服务器 上声明了允许 A 的域名访问,那么从 A 到 B 的跨域请求就可以完成。 + +**@CrossOrigin注解** + +如果项目使用的是Springboot,可以在Controller类上添加一个 @CrossOrigin(origins ="*") 注解就可以实现对当前controller 的跨域访问了,当然这个标签也可以加到方法上,或者直接加到入口类上对所有接口进行跨域处理。注意SpringMVC的版本要在4.2或以上版本才支持@CrossOrigin。 + +**nginx反向代理接口跨域** + +nginx反向代理跨域原理如下: 首先同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。 + +nginx反向代理接口跨域实现思路如下:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。 + +```js +// proxy服务器 +server { + listen 81; + server_name www.domain1.com; + location / { + proxy_pass http://www.domain2.com:8080; #反向代理 + proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 + index index.html index.htm; + + add_header Access-Control-Allow-Origin http://www.domain1.com; + } +} +``` + +这样我们的前端代理只要访问 http:www.domain1.com:81/*就可以了。 + +**通过jsonp跨域** + +通常为了减轻web服务器的负载,我们把js、css,img等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,这是浏览器允许的操作,基于此原理,我们可以通过动态创建script,再请求一个带参网址实现跨域通信。 + +## 设计接口要注意什么? + +1. **接口参数校验**。接口必须校验参数,比如入参是否允许为空,入参长度是否符合预期。 +2. 设计接口时,充分考虑接口的**可扩展性**。思考接口是否可以复用,怎样保持接口的可扩展性。 +3. **串行调用考虑改并行调用**。比如设计一个商城首页接口,需要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比较大了。这种场景是可以改为并行调用的,降低接口耗时。 +4. 接口是否需要**防重**处理。涉及到数据库修改的,要考虑防重处理,可以使用数据库防重表,以唯一流水号作为唯一索引。 +5. **日志打印全面**,入参出参,接口耗时,记录好日志,方便甩锅。 +6. 修改旧接口时,注意**兼容性设计**。 +7. **异常处理得当**。使用finally关闭流资源、使用log打印而不是e.printStackTrace()、不要吞异常等等 +8. 是否需要考虑**限流**。限流为了保护系统,防止流量洪峰超过系统的承载能力。 + +## 过滤器和拦截器有什么区别? + +1、**实现原理不同**。 + +过滤器和拦截器底层实现不同。过滤器是基于函数回调的,拦截器是基于Java的反射机制(动态代理)实现的。一般自定义的过滤器中都会实现一个doFilter()方法,这个方法有一个FilterChain参数,而实际上它是一个回调接口。 + +2、**触发时机不同**。 + +过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 + +拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 + +![](http://img.topjavaer.cn/img/202405100905076.png) + +3、**使用的场景不同**。 + +因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 + +4、**拦截的请求范围不同**。 + +请求的执行顺序是:请求进入容器 -> 进入过滤器 -> 进入 Servlet -> 进入拦截器 -> 执行控制器。可以看到过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。 + +> 参考链接:https://segmentfault.com/a/1190000022833940 + +## 对接第三方接口要考虑什么? + +嗯,需要考虑以下几点: + +1. 确认接口对接的**网络协议**,是https/http或者自定义的私有协议等。 +2. 约定好**数据传参、响应格式**(如application/json),弱类型对接强类型语言时要特别注意 +3. **接口安全**方面,要确定身份校验方式,使用token、证书校验等 +4. 确认是否需要接口调用失败后的**重试**机制,保证数据传输的最终一致性。 +5. **日志记录要全面**。接口出入参数,以及解析之后的参数值,都要用日志记录下来,方便定位问题(甩锅)。 + +参考:https://blog.csdn.net/gzt19881123/article/details/108791034 + +## 后端接口性能优化有哪些方法? + +有以下这些方法: + +1、**优化索引**。给where条件的关键字段,或者`order by`后面的排序字段,加索引。 + +2、**优化sql语句**。比如避免使用select *、批量操作、避免深分页、提升group by的效率等 + +3、**避免大事务**。使用@Transactional注解这种声明式事务的方式提供事务功能,容易造成大事务,引发其他的问题。应该避免在事务中一次性处理太多数据,将一些跟事务无关的逻辑放到事务外面执行。 + +4、**异步处理**。剥离主逻辑和副逻辑,副逻辑可以异步执行,异步写库。比如用户购买的商品发货了,需要发短信通知,短信通知是副流程,可以异步执行,以免影响主流程的执行。 + +5、**降低锁粒度**。在并发场景下,多个线程同时修改数据,造成数据不一致的情况。这种情况下,一般会加锁解决。但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。 + +6、**加缓存**。如果表数据量非常大的话,直接从数据库查询数据,性能会非常差。可以使用Redis`和`memcached提升查询性能,从而提高接口性能。 + +7、**分库分表**。当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。或者数据库表数据非常大,SQL查询即使走了索引,也很耗时。这时,可以通过分库分表解决。分库用于解决数据库连接资源不足问题,和磁盘IO的性能瓶颈问题。分表用于解决单表数据量太大,sql语句查询数据时,即使走了索引也非常耗时问题。 + +8、**避免在循环中查询数据库**。循环查询数据库,非常耗时,最好能在一次查询中获取所有需要的数据。 + +## 为什么在阿里巴巴Java开发手册中强制要求使用包装类型定义属性呢? + +嗯,以布尔字段为例,当我们没有设置对象的字段的值的时候,Boolean类型的变量会设置默认值为`null`,而boolean类型的变量会设置默认值为`false`。 + +也就是说,包装类型的默认值都是null,而基本数据类型的默认值是一个固定值,如boolean是false,byte、short、int、long是0,float是0.0f等。 + +举一个例子,比如有一个扣费系统,扣费时需要从外部的定价系统中读取一个费率的值,我们预期该接口的返回值中会包含一个浮点型的费率字段。当我们取到这个值得时候就使用公式:金额*费率=费用 进行计算,计算结果进行划扣。 + +如果由于计费系统异常,他可能会返回个默认值,如果这个字段是Double类型的话,该默认值为null,如果该字段是double类型的话,该默认值为0.0。 + +如果扣费系统对于该费率返回值没做特殊处理的话,拿到null值进行计算会直接报错,阻断程序。拿到0.0可能就直接进行计算,得出接口为0后进行扣费了。这种异常情况就无法被感知。 + +**那我可以对0.0做特殊判断,如果是0就阻断报错,这样是否可以呢?** + +不对,这时候就会产生一个问题,如果允许费率是0的场景又怎么处理呢? + +使用基本数据类型只会让方案越来越复杂,坑越来越多。 + +这种使用包装类型定义变量的方式,通过异常来阻断程序,进而可以被识别到这种线上问题。如果使用基本数据类型的话,系统可能不会报错,进而认为无异常。 + +因此,建议在POJO和RPC的返回值中使用包装类型。 + +> 参考链接:https://mp.weixin.qq.com/s/O_jCxZWtTTkFZ9FlaZgOCg + +## 8招让接口性能提升100倍 + +**池化思想** + +如果你每次需要用到线程,都去创建,就会有增加一定的耗时,而线程池可以重复利用线程,避免不必要的耗时。 + +比如`TCP`三次握手,它为了减少性能损耗,引入了`Keep-Alive长连接`,避免频繁的创建和销毁连接。 + +**拒绝阻塞等待** + +如果你调用一个系统`B`的接口,但是它处理业务逻辑,耗时需要`10s`甚至更多。然后你是一直**阻塞等待,直到系统B的下游接口返回**,再继续你的下一步操作吗?这样**显然不合理**。 + +参考**IO多路复用模型**。即我们不用阻塞等待系统`B`的接口,而是先去做别的操作。等系统`B`的接口处理完,通过**事件回调**通知,我们接口收到通知再进行对应的业务操作即可。 + +**远程调用由串行改为并行** + +比如设计一个商城首页接口,需要查商品信息、营销信息、用户信息等等。如果是串行一个一个查,那耗时就比较大了。这种场景是可以改为并行调用的,降低接口耗时。 + +**锁粒度避免过粗** + +在高并发场景,为了防止**超卖等情况**,我们经常需要**加锁来保护共享资源**。但是,如果加锁的粒度过粗,是很影响接口性能的。 + +不管你是`synchronized`加锁还是`redis`分布式锁,只需要在共享临界资源加锁即可,不涉及共享资源的,就不必要加锁。 + +**耗时操作,考虑放到异步执行** + +耗时操作,考虑用**异步处理**,这样可以降低接口耗时。比如用户注册成功后,短信邮件通知,是可以异步处理的。 + +**使用缓存** + +把要查的数据,提前放好到缓存里面,需要时,**直接查缓存,而避免去查数据库或者计算的过程**。 + +**提前初始化到缓存** + +预取思想很容易理解,就是**提前把要计算查询的数据,初始化到缓存**。如果你在未来某个时间需要用到某个经过复杂计算的数据,**才实时去计算的话,可能耗时比较大**。这时候,我们可以采取预取思想,**提前把将来可能需要的数据计算好,放到缓存中**,等需要的时候,去缓存取就行。这将大幅度提高接口性能。 + +**压缩传输内容** + +压缩传输内容,传输报文变得更小,因此传输会更快。 + +> 参考:https://juejin.cn/post/7167153109158854687 + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 + -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git "a/Java/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" b/docs/java/java-collection.md similarity index 88% rename from "Java/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" rename to docs/java/java-collection.md index 5af404d..df13bad 100644 --- "a/Java/Java\351\233\206\345\220\210\351\235\242\350\257\225\351\242\230.md" +++ b/docs/java/java-collection.md @@ -1,12 +1,34 @@ +--- +sidebar: heading +title: Java集合常见面试题总结 +category: Java +tag: + - Java集合 +head: + - - meta + - name: keywords + content: Java集合,ArrayList,LinkedList,HashMap扩容,HashMap线程不安全,LinkedHashMap底层原理,TreeMap介绍,HashSet,fail fast,fail safe,Iterator,并发容器 + - - meta + - name: description + content: 高质量的Java集合常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## 常见的集合有哪些? Java集合类主要由两个接口**Collection**和**Map**派生出来的,Collection有三个子接口:List、Set、Queue。 Java集合框架图如下: -![](http://img.dabin-coder.cn/image/collections2.png) +![](http://img.topjavaer.cn/img/collections.drawio.png) -![](http://img.dabin-coder.cn/image/map.png) +![](http://img.topjavaer.cn/img/map.png) List代表了有序可重复集合,可直接根据元素的索引来访问;Set代表无序不可重复集合,只能根据元素本身来访问;Queue是队列集合。Map代表的是存储key-value对的集合,可根据元素的key来访问value。 @@ -124,13 +146,13 @@ return h&(length-1); //第三步 取模运算 在JDK1.8的实现中,优化了高位运算的算法,通过`hashCode()`的高16位异或低16位实现的:这么做可以在数组比较小的时候,也能保证考虑到高低位都参与到Hash的计算中,可以减少冲突,同时不会有太大的开销。 -## 为什么建议设置HashMap的容量? +### 为什么建议设置HashMap的容量? HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容条件就是当HashMap中的元素个数超过临界值时就会自动扩容(threshold = loadFactor * capacity)。 如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容。而HashMap每次扩容都需要重建hash表,非常影响性能。所以建议开发者在创建HashMap的时候指定初始化容量。 -### 扩容过程? +### HashMap扩容过程是怎样的? 1.8扩容机制:当元素个数大于`threshold`时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。 @@ -138,7 +160,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据`e.hash & oldCap == 0`判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。 -### put方法流程? +### 说说HashMapput方法的流程? 1. 如果table没有初始化就先进行初始化过程 2. 使用hash算法计算key的索引 @@ -147,7 +169,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 5. 链表的数量大于阈值8,就要转换成红黑树的结构 6. 添加成功后会检查是否需要扩容 -![图片来源网络](http://img.dabin-coder.cn/image/hashmap-put.png) +![](http://img.topjavaer.cn/img/map_put.png) ### 红黑树的特点? @@ -158,7 +180,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 ### 在解决 hash 冲突的时候,为什么选择先用链表,再转红黑树? -因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。所以,当元素个数小于8个的时候,采用链表结构可以保证查询性能。而当元素个数大于8个的时候并且数组容量大于64,会采用红黑树结构。因为红黑树搜索时间复杂度是 `O(logn)`,而链表是 `O(n)`,在n比较大的时候,使用红黑树可以加快查询速度。 +因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。所以,当元素个数小于8个的时候,采用链表结构可以保证查询性能。而当元素个数大于8个的时候并且数组容量大于等于64,会采用红黑树结构。因为红黑树搜索时间复杂度是 `O(logn)`,而链表是 `O(n)`,在n比较大的时候,使用红黑树可以加快查询速度。 ### HashMap 的长度为什么是 2 的幂次方? @@ -221,7 +243,7 @@ public class TreeMap TreeMap 的继承结构: -![](http://img.dabin-coder.cn/image/image-20210905215046510.png) +![](http://img.topjavaer.cn/img/image-20210905215046510.png) **TreeMap的特点:** @@ -322,6 +344,28 @@ ListIterator 是 Iterator的增强版。 - ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。 - ListIterator只能用于遍历List及其子类,Iterator可用来遍历所有集合。 +## 如何让一个集合不能被修改? + +可以采用Collections包下的unmodifiableMap/unmodifiableList/unmodifiableSet方法,通过这个方法返回的集合,是不可以修改的。如果修改的话,会抛出 java.lang.UnsupportedOperationException异常。 + +```java +List list = new ArrayList<>(); +list.add("x"); +Collection clist = Collections.unmodifiableCollection(list); +clist.add("y"); // 运行时此行报错 +System.out.println(list. size()); +``` + +对于List/Set/Map集合,Collections包都有相应的支持。 + +**那使用final关键字进行修饰可以实现吗?** + +答案是不可以。 + +final关键字修饰的成员变量如果是是引用类型的话,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。 + +而集合类都是引用类型,用final修饰的话,集合里面的内容还是可以修改的。 + ## 并发容器 JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。 @@ -400,12 +444,22 @@ Copy-On-Write,写时复制。当我们往容器添加元素时,不直接往 ### CopyOnWriteArrayList -CopyOnWriteArrayList相当于线程安全的ArrayList,CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素add到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。 +**CopyOnWriteArrayList**是Java并发包中提供的一个并发容器。CopyOnWriteArrayList相当于线程安全的ArrayList,CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素add到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。 `CopyOnWriteArrayList`中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向`CopyOnWriteArrayList`添加数据,还是可以读到旧的数据。 CopyOnWrite并发容器用于读多写少的并发场景。 +**优点**: + +读操作性能很高,因为无需任何同步措施,比较适用于**读多写少**的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出**ConcurrentModificationException**异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。 + +**缺点**: + +**一是内存占用问题**,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC; + +**二是无法保证实时性**,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。 + ### ConcurrentLinkedQueue 非阻塞队列。高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,通过 CAS 操作实现。 @@ -548,4 +602,4 @@ https://coolshell.cn/articles/9606.htm(HashMap 死循环) -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git "a/Java/Java\345\271\266\345\217\221\351\235\242\350\257\225\351\242\230.md" b/docs/java/java-concurrent.md similarity index 68% rename from "Java/Java\345\271\266\345\217\221\351\235\242\350\257\225\351\242\230.md" rename to docs/java/java-concurrent.md index 022a2fc..09c408f 100644 --- "a/Java/Java\345\271\266\345\217\221\351\235\242\350\257\225\351\242\230.md" +++ b/docs/java/java-concurrent.md @@ -1,6 +1,62 @@ +--- +sidebar: heading +title: Java并发常见面试题总结 +category: Java +tag: + - Java并发 +head: + - - meta + - name: keywords + content: Java并发,多线程,线程池执行原理,线程池参数,线程和进程,死锁,volatile,synchronized,ThreadLocal,Java锁,Java并发工具,原子类,AQS + - - meta + - name: description + content: 高质量的Java并发常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + ## 线程池 -线程池:一个管理线程的池子。 +### 什么是线程池,如何使用?为什么要使用线程池? + +线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高了代码执行效率。 + +### 为什么平时都是使用线程池创建线程,直接new一个线程不好吗? + +嗯,手动创建线程有两个缺点 + +1. 不受控风险 +2. 频繁创建开销大 + +**为什么不受控**? + +系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建线程没有统一标准,比如创建的线程有没有名字等。当系统运行起来,所有线程都在抢占资源,毫无规则,混乱场面可想而知,不好管控。 + +### 频繁手动创建线程为什么开销会大?跟new Object() 有什么差别? + +虽然Java中万物皆对象,但是new Thread() 创建一个线程和 new Object()还是有区别的。 + +new Object()过程如下: + +1. JVM分配一块内存 M +2. 在内存 M 上初始化该对象 +3. 将内存 M 的地址赋值给引用变量 obj + +创建线程的过程如下: + +1. JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧 +2. 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成 +3. 每个线程获得一个程序计数器,用于记录当前虚拟机正在执行的线程指令地址 +4. 系统创建一个与Java线程对应的本机线程 +5. 将与线程相关的描述符添加到JVM内部数据结构中 +6. 线程共享堆和方法区域 + +创建一个线程大概需要1M左右的空间(Java8,机器规格2c8G)。可见,频繁手动创建/销毁线程的代价是非常大的。 ### 为什么使用线程池? @@ -10,7 +66,7 @@ ### 线程池执行原理? -![线程池执行流程](http://img.dabin-coder.cn/image/线程池执行流程.png) +![线程池执行流程](http://img.topjavaer.cn/img/线程池执行流程.png) 1. 当线程池里存活的线程数小于核心线程数`corePoolSize`时,这时对于一个新提交的任务,线程池会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数`corePoolSize`时,线程池里面的线程会一直存活着,就算空闲时间超过了`keepAliveTime`,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。 2. 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。 @@ -145,11 +201,45 @@ public static ExecutorService newCachedThreadPool() { 3. 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间; 4. 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。 -![](http://img.dabin-coder.cn/image/scheduled-task.jpg) +![](http://img.topjavaer.cn/img/scheduled-task.jpg) 适用场景:周期性执行任务的场景,需要限制线程数量的场景。 +### 怎么判断线程池的任务是不是执行完了? + +有几种方法: + +1、使用线程池的原生函数**isTerminated()**; + +executor提供一个原生函数isTerminated()来判断线程池中的任务是否全部完成。如果全部完成返回true,否则返回false。 + +2、**使用重入锁,维持一个公共计数**。 + +所有的普通任务维持一个计数器,当任务完成时计数器加一(这里要加锁),当计数器的值等于任务数时,这时所有的任务已经执行完毕了。 + +3、**使用CountDownLatch**。 +它的原理跟第二种方法类似,给CountDownLatch一个计数值,任务执行完毕后,调用countDown()执行计数值减一。最后执行的任务在调用方法的开始调用await()方法,这样整个任务会阻塞,直到这个计数值为零,才会继续执行。 + +这种方式的**缺点**就是需要提前知道任务的数量。 + +4、**submit向线程池提交任务,使用Future判断任务执行状态**。 + +使用submit向线程池提交任务与execute提交不同,submit会有Future类型的返回值。通过future.isDone()方法可以知道任务是否执行完成。 + +### 为什么要使用Executor线程池框架呢? + +- 每次执行任务都通过new Thread()去创建线程,比较消耗性能,创建一个线程是比较耗时、耗资源的 +- 调用new Thread()创建的线程缺乏管理,可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪 +- 直接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不好实现 + +## execute和submit的区别 + +execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。 + +execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常 + +execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。 ## 进程线程 @@ -171,7 +261,7 @@ public static ExecutorService newCachedThreadPool() { **终止(TERMINATED)**:表示该线程已经执行完毕。 -![](http://img.dabin-coder.cn/image/image-20210909235618175.png) +![](http://img.topjavaer.cn/img/image-20210909235618175.png) > 图片来源:Java并发编程的艺术 @@ -347,7 +437,7 @@ class RunnableDemo implements Runnable { 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方持有的资源,所以这两个线程就会互相等待而进入死锁状态。 -![死锁](http://img.dabin-coder.cn/image/死锁.png) +![死锁](http://img.topjavaer.cn/img/死锁.png) 下面通过例子说明线程死锁,代码来自并发编程之美。 @@ -427,33 +517,46 @@ Thread[线程 2,5,main]waiting get resource1 ### 线程都有哪些方法? -**join** +**start** -Thread.join(),在main中创建了thread线程,在main中调用了thread.join()/thread.join(long millis),main线程放弃cpu控制权,线程进入WAITING/TIMED_WAITING状态,等到thread线程执行完才继续执行main线程。 +用于启动线程。 -```java -public final void join() throws InterruptedException { - join(0); -} -``` +**getPriority** -**yield** +获取线程优先级,默认是5,线程默认优先级为5,如果不手动指定,那么线程优先级具有继承性,比如线程A启动线程B,那么线程B的优先级和线程A的优先级相同 -Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。 +**setPriority** -```java -public static native void yield(); //static方法 -``` +设置线程优先级。CPU会尽量将执行资源让给优先级比较高的线程。 -**sleep** +**interrupt** -Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,让出cpu资源,但不释放对象锁,指定时间到后又恢复运行。作用:给其它线程执行机会的最佳方式。 +告诉线程,你应该中断了,具体到底中断还是继续运行,由被通知的线程自己处理。 -```java -public static native void sleep(long millis) throws InterruptedException;//static方法 -``` +当对一个线程调用 interrupt() 时,有两种情况: + +1. 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。 + +2. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。不过,被设置中断标志的线程可以继续正常运行,不受影响。 + +interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。 + +**join** + +等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。 + +**yield** + +暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 + +**sleep** + +使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,线程自动转为Runnable状态。 +### 如何停止一个正在运行的线程? +1. 使用共享变量的方式。共享变量可以被多个执行相同任务的线程用来作为是否停止的信号,通知停止线程的执行。 +2. 使用interrupt方法终止线程。当一个线程被阻塞,处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这时候可以使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。 ## volatile底层原理 @@ -473,6 +576,16 @@ public static native void sleep(long millis) throws InterruptedException;//stati > 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入`内存屏障`指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。 +## volatile为什么不能保证原子性? + +volatile可以保证可见性和顺序性,但是它不能保证原子性。 + +举个例子。一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。 + +假如i的初始值为100。线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也去取i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。 + +那么问题来了,线程A之前已经读取到了i的值为100,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存。这样i经过两次自增之后,结果值只加了1,明显是有问题的。所以说即便volatile具有可见性,也不能保证对它修饰的变量具有原子性。 + ## synchronized的用法有哪些? 1. **修饰普通方法**:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁 @@ -612,27 +725,37 @@ class SeasonThreadTask implements Runnable{ ## 线程间通信方式 -### volatile - -**volatile** 使用共享内存实现线程间相互通信。多个线程同时监听一个变量,当这个变量被某一个线程修改的时候,其他线程可以感知到这个变化。 +1、使用 Object 类的 **wait()/notify()**。Object 类提供了线程间通信的方法:`wait()`、`notify()`、`notifyAll()`,它们是多线程通信的基础。其中,`wait/notify` 必须配合 `synchronized` 使用,wait 方法释放锁,notify 方法不释放锁。wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了`notify()`,notify并不释放锁,只是告诉调用过`wait()`的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放,调用 `wait()` 的一个或多个线程就会解除 wait 状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。 -### wait 和 notify +2、使用 **volatile** 关键字。基于volatile关键字实现线程间相互通信,其底层使用了共享内存。简单来说,就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。 -`wait/notify`为Object对象的方法,调用`wait/notify`需要先获得对象的锁。对象调用`wait()`之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的`notify()`方法后,等待线程并不会立即从`wait()`返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从`wait()`方法返回,即从`wait()`方法返回前提是线程获得锁。 +3、使用JUC工具类 **CountDownLatch**。jdk1.5 之后在`java.util.concurrent`包下提供了很多并发编程相关的工具类,简化了并发编程开发,`CountDownLatch` 基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。 -### join - -当在一个线程调用另一个线程的`join()`方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行。`join()`是基于等待通知机制实现的。 +4、基于 **LockSupport** 实现线程间的阻塞和唤醒。`LockSupport` 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。 ## ThreadLocal +### ThreadLocal是什么 + 线程本地变量。当使用`ThreadLocal`维护变量时,`ThreadLocal`为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。 -### ThreadLocal原理 +### 为什么要使用ThreadLocal? + +并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现**线性安全问题**。 + +为了解决线性安全问题,可以用加锁的方式,比如使用`synchronized` 或者`Lock`。但是加锁的方式,可能会导致系统变慢。 + +还有另外一种方案,就是使用空间换时间的方式,即使用`ThreadLocal`。使用`ThreadLocal`类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。 + +### Thread和ThreadLocal有什么联系呢? + +Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中,来实现多线程处理。 + +### 说说ThreadLocal的原理? 每个线程都有一个`ThreadLocalMap`(`ThreadLocal`内部类),Map中元素的键为`ThreadLocal`,而值对应线程的变量副本。 -![](http://img.dabin-coder.cn/image/threadlocal.png) +![](http://img.topjavaer.cn/img/threadlocal.png) 调用`threadLocal.set()`-->调用`getMap(Thread)`-->返回当前线程的`ThreadLocalMap`-->`map.set(this, value)`,this是`threadLocal`本身。源码如下: @@ -715,7 +838,36 @@ public class ThreadLocalDemo { ### ThreadLocal使用场景有哪些? -`ThreadLocal`适用场景:每个线程需要有自己单独的实例,且需要在多个方法中共享实例,即同时满足实例在线程间的隔离与方法间的共享,这种情况适合使用`ThreadLocal`。比如Java web应用中,每个线程有自己单独的`Session`实例,就可以使用`ThreadLocal`来实现。 +**场景1** + +ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。 + +这种场景通常用于保存线程不安全的工具类,典型的使用的类就是 SimpleDateFormat。 + +假如需求为500个线程都要用到 SimpleDateFormat,使用线程池来实现线程的复用,否则会消耗过多的内存等资源,如果我们每个任务都创建了一个 simpleDateFormat 对象,也就是说,500个任务对应500个 simpleDateFormat 对象。但是这么多对象的创建是有开销的,而且这么多对象同时存在在内存中也是一种内存的浪费。可以将simpleDateFormat 对象给提取了出来,变成静态变量,但是这样一来就会有线程不安全的问题。我们想要的效果是,既不浪费过多的内存,同时又想保证线程安全。此时,可以使用 ThreadLocal来达到这个目的,每个线程都拥有一个自己的 simpleDateFormat 对象。 + +**场景2** + +ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过 ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。 + +比如Java web应用中,每个线程有自己单独的`Session`实例,就可以使用`ThreadLocal`来实现。 + +## 什么是AQS? + +AQS(AbstractQueuedSynchronizer)是java.util.concurrent包下的核心类,我们经常使用的ReentrantLock、CountDownLatch,都是基于AQS抽象同步式队列实现的。 + +AQS作为一个抽象类,通常是通过继承来使用的。它本身是没有同步接口的,只是定义了同步状态和同步获取和同步释放的方法。 + +JUC包下面大部分同步类,都是基于AQS的同步状态的获取与释放来实现的,然后AQS是个双向链表。 + +## 为什么AQS是双向链表而不是单向的? + +双向链表有两个指针,一个指针指向前置节点,一个指针指向后继节点。所以,双向链表可以支持常量 O(1) 时间复杂度的情况下找到前驱节点。因此,双向链表在插入和删除操作的时候,要比单向链表简单、高效。 + +从双向链表的特性来看,AQS 使用双向链表有2个方面的原因: + +1. 没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 Head 节点开始遍历,性能非常低。 +2. 在 Lock 接口里面有一个lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。而被标记为 CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。这就意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从 Head 节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。 ## AQS原理 @@ -729,7 +881,7 @@ private volatile int state;//共享变量,使用volatile修饰保证线程可 同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享 )构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。 -![](http://img.dabin-coder.cn/image/aqs.png) +![](http://img.topjavaer.cn/img/aqs.png) ## ReentrantLock 是如何实现可重入性的? @@ -1067,17 +1219,6 @@ public final void lazySet(int i, int newValue)//最终 将index=i 位置的元 - AtomicStampedReference:带有版本号的引用类型原子类。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来 -## 为什么要使用Executor线程池框架呢? - -- 每次执行任务都通过new Thread()去创建线程,比较消耗性能,创建一个线程是比较耗时、耗资源的 -- 调用new Thread()创建的线程缺乏管理,可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪 -- 直接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不好实现 - -## 如何停止一个正在运行的线程? - -1. 使用共享变量的方式。共享变量可以被多个执行相同任务的线程用来作为是否停止的信号,通知停止线程的执行。 -2. 使用interrupt方法终止线程。当一个线程被阻塞,处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这时候可以使用Thread提供的interrupt()方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。 - ## 什么是Daemon线程? 后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这个线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台线程。反过来说,只要有任何非后台线程还在运行,程序就不会终止。必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。 @@ -1093,6 +1234,143 @@ SynchronizedMap一次锁住整张表来保证线程安全,所以每次只能 JDK1.8 ConcurrentHashMap采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,支持并发访问、修改。 另外ConcurrentHashMap使用了一种不同的迭代方式。当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。 +## 什么是Future? + +在并发编程中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。 + +Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。 + +举个例子:比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟,但是因为你在等包子的时候,可以同时准备凉菜,所以在准备凉菜的过程中,可以同时准备包子,这样只需要等待3分钟。Future就是后面这种执行模式。 + +Future接口主要包括5个方法: + +1. get()方法可以当任务结束后返回一个结果,如果调用时,工作还没有结束,则会阻塞线程,直到任务执行完毕 +2. get(long timeout,TimeUnit unit)做多等待timeout的时间就会返回结果 +3. cancel(boolean mayInterruptIfRunning)方法可以用来停止一个任务,如果任务可以停止(通过mayInterruptIfRunning来进行判断),则可以返回true,如果任务已经完成或者已经停止,或者这个任务无法停止,则会返回false。 +4. isDone()方法判断当前方法是否完成 +5. isCancel()方法判断当前方法是否取消 + +## select、poll、epoll之间的区别 + +select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。 + +select的时间复杂度O(n)。它仅仅知道有I/O事件发生了,却并不知道是哪那几个流,只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的时间复杂度,同时处理的流越多,轮询时间就越长。 + +poll的时间复杂度O(n)。poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的. + +epoll的时间复杂度O(1)。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动的。 + +> 参考链接:https://blog.csdn.net/u014209205/article/details/80598209 + +## ReadWriteLock 和 StampedLock 的区别 + +在多线程编程中,对于共享资源的访问控制是一个非常重要的问题。在并发环境下,多个线程同时访问共享资源可能会导致数据不一致的问题,因此需要一种机制来保证数据的一致性和并发性。 + +Java提供了多种机制来实现并发控制,其中 ReadWriteLock 和 StampedLock 是两个常用的锁类。本文将分别介绍这两个类的特性、使用场景以及示例代码。 + +**ReadWriteLock** + +ReadWriteLock 是Java提供的一个接口,全类名:`java.util.concurrent.locks.ReentrantLock`。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制可以提高读取操作的并发性,但写入操作需要独占资源。 + +**特性** + +- 多个线程可以同时获取读锁,但只有一个线程可以获取写锁。 +- 当一个线程持有写锁时,其他线程无法获取读锁和写锁,读写互斥。 +- 当一个线程持有读锁时,其他线程可以同时获取读锁,读读共享。 + +**使用场景** + +**ReadWriteLock** 适用于读多写少的场景,例如缓存系统、数据库连接池等。在这些场景中,读取操作占据大部分时间,而写入操作较少。 + +**示例代码** + +下面是一个使用 ReadWriteLock 的示例,实现了一个简单的缓存系统: + +```java +public class Cache { + private Map data = new HashMap<>(); + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + public Object get(String key) { + lock.readLock().lock(); + try { + return data.get(key); + } finally { + lock.readLock().unlock(); + } + } + + public void put(String key, Object value) { + lock.writeLock().lock(); + try { + data.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +在上述示例中,Cache 类使用 ReadWriteLock 来实现对 data 的并发访问控制。get 方法获取读锁并读取数据,put 方法获取写锁并写入数据。 + +**StampedLock** + +StampedLock 是Java 8 中引入的一种新的锁机制,全类名:`java.util.concurrent.locks.StampedLock`,它提供了一种乐观读的机制,可以进一步提升读取操作的并发性能。 + +**特性** + +- 与 ReadWriteLock 类似,StampedLock 也支持多个线程同时获取读锁,但只允许一个线程获取写锁。 +- 与 ReadWriteLock 不同的是,StampedLock 还提供了一个乐观读锁(Optimistic Read Lock),即不阻塞其他线程的写操作,但在读取完成后需要验证数据的一致性。 + +**使用场景** + +StampedLock 适用于读远远大于写的场景,并且对数据的一致性要求不高,例如统计数据、监控系统等。 + +**示例代码** + +下面是一个使用 StampedLock 的示例,实现了一个计数器: + +```java +public class Counter { + private int count = 0; + private StampedLock lock = new StampedLock(); + + public int getCount() { + long stamp = lock.tryOptimisticRead(); + int value = count; + if (!lock.validate(stamp)) { + stamp = lock.readLock(); + try { + value = count; + } finally { + lock.unlockRead(stamp); + } + } + return value; + } + + public void increment() { + long stamp = lock.writeLock(); + try { + count++; + } finally { + lock.unlockWrite(stamp); + } + } +} +``` + +在上述示例中,Counter 类使用 StampedLock 来实现对计数器的并发访问控制。getCount 方法首先尝试获取乐观读锁,并读取计数器的值,然后通过 validate 方法验证数据的一致性。如果验证失败,则获取悲观读锁,并重新读取计数器的值。increment 方法获取写锁,并对计数器进行递增操作。 + +**总结** + +**ReadWriteLock** 和 **StampedLock** 都是Java中用于并发控制的重要机制。 + +- **ReadWriteLock** 适用于读多写少的场景; +- **StampedLock** 则适用于读远远大于写的场景,并且对数据的一致性要求不高; + +在实际应用中,我们需要根据具体场景来选择合适的锁机制。通过合理使用这些锁机制,我们可以提高并发程序的性能和可靠性。 + -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git "a/Java/Java8\346\226\260\347\211\271\346\200\247.md" b/docs/java/java8-all.md similarity index 87% rename from "Java/Java8\346\226\260\347\211\271\346\200\247.md" rename to docs/java/java8-all.md index efd2c16..86db8d6 100644 --- "a/Java/Java8\346\226\260\347\211\271\346\200\247.md" +++ b/docs/java/java8-all.md @@ -1,37 +1,8 @@ - - - - -- [函数式编程](#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B) -- [Lambda 表达式](#lambda-%E8%A1%A8%E8%BE%BE%E5%BC%8F) -- [函数式接口](#%E5%87%BD%E6%95%B0%E5%BC%8F%E6%8E%A5%E5%8F%A3) -- [内置的函数式接口](#%E5%86%85%E7%BD%AE%E7%9A%84%E5%87%BD%E6%95%B0%E5%BC%8F%E6%8E%A5%E5%8F%A3) - - [Predicate 断言](#predicate-%E6%96%AD%E8%A8%80) - - [Comparator](#comparator) - - [Consumer](#consumer) -- [Stream](#stream) - - [Filter 过滤](#filter-%E8%BF%87%E6%BB%A4) - - [Sorted 排序](#sorted-%E6%8E%92%E5%BA%8F) - - [Map 转换](#map-%E8%BD%AC%E6%8D%A2) - - [Match 匹配](#match-%E5%8C%B9%E9%85%8D) - - [Count 计数](#count-%E8%AE%A1%E6%95%B0) - - [Reduce](#reduce) - - [flatMap](#flatmap) -- [Parallel-Streams](#parallel-streams) -- [Map 集合](#map-%E9%9B%86%E5%90%88) -- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - - -> 本文已经收录到github仓库,此仓库用于分享Java相关知识总结,包括Java基础、MySQL、Spring Boot、MyBatis、Redis、RabbitMQ等等,欢迎大家提pr和star! -> -> github地址:https://github.com/Tyson0314/Java-learning -> -> 如果github访问不了,可以访问gitee仓库。 -> -> gitee地址:https://gitee.com/tysondai/Java-learning - -## 函数式编程 +--- +sidebar: heading +--- + +# 函数式编程 面向对象编程:面向对象的语言,一切皆对象,如果想要调用一个函数,函数必须属于一个类或对象,然后在使用类或对象进行调用。面向对象编程可能需要多写很多重复的代码行。 @@ -47,9 +18,7 @@ 函数式编程:在某些编程语言中,如js、c++,我们可以直接写一个函数,然后在需要的时候进行调用,即函数式编程。 - - -## Lambda 表达式 +# Lambda 表达式 在Java8以前,使用`Collections`的sort方法对字符串排序的写法: @@ -75,7 +44,7 @@ names.sort((a, b) -> b.compareTo(a)); //简化写法二,省略入参类型,J 可以看到使用lambda表示式之后,代码变得很简短并且易于阅读。 -## 函数式接口 +# 函数式接口 Functional Interface:函数式接口,只包含一个抽象方法的接口。只有函数式接口才能缩写成 Lambda 表达式。@FunctionalInterface 定义类为一个函数式接口,如果添加了第二个抽象方法,编译器会立刻抛出错误提示。 @@ -100,11 +69,11 @@ public class FunctionalInterfaceTest { -## 内置的函数式接口 +# 内置的函数式接口 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。 -### Predicate 断言 +## Predicate 断言 指定入参类型,并返回 boolean 值的函数式接口。用来组合一个复杂的逻辑判断。 @@ -114,7 +83,7 @@ Predicate predicate = (s) -> s.length() > 0; predicate.test("dabin"); // true ``` -### Comparator +## Comparator Java8 将 Comparator 升级成函数式接口,可以使用lambda表示式简化代码。 @@ -151,7 +120,7 @@ class Person { ``` -### Consumer +## Consumer Consumer 接口接收一个泛型参数,然后调用 accept,对这个参数做一系列消费操作。 @@ -249,11 +218,11 @@ public class ConsumersTest { } ``` -## Stream +# Stream 使用 `java.util.Stream` 对一个包含一个或多个元素的集合做各种操作,原集合不变,返回新集合。只能对实现了 `java.util.Collection` 接口的类做流的操作。`Map` 不支持 `Stream` 流。`Stream` 流支持同步执行,也支持并发执行。 -### Filter 过滤 +## Filter 过滤 Filter` 的入参是一个 `Predicate,用于筛选出我们需要的集合元素。原集合不变。filter 会过滤掉不符合特定条件的,下面的代码会过滤掉`nameList`中不以大彬开头的字符串。 @@ -284,7 +253,7 @@ public class StreamTest { } ``` -### Sorted 排序 +## Sorted 排序 自然排序,不改变原集合,返回排序后的集合。 @@ -333,7 +302,7 @@ list.stream().sorted(Comparator.comparing(Student::getAge).reversed()); list.stream().sorted(Comparator.comparing(Student::getAge)); ``` -### Map 转换 +## Map 转换 将每个字符串转为大写。 @@ -362,7 +331,7 @@ public class StreamTest2 { } ``` -### Match 匹配 +## Match 匹配 验证 nameList 中的字符串是否有以`大彬`开头的。 @@ -393,7 +362,7 @@ public class StreamTest3 { } ``` -### Count 计数 +## Count 计数 统计 `stream` 流中的元素总数,返回值是 `long` 类型。 @@ -426,7 +395,7 @@ public class StreamTest4 { } ``` -### Reduce +## Reduce 类似拼接。可以实现将 `list` 归约成一个值。它的返回类型是 `Optional` 类型。 @@ -458,7 +427,7 @@ public class StreamTest5 { ``` -### flatMap +## flatMap flatMap 用于将多个Stream连接成一个Stream。 @@ -521,7 +490,7 @@ public class StreamTest7 { -## Parallel-Streams +# Parallel-Streams 并行流。`stream` 流是支持**顺序**和**并行**的。顺序流操作是单线程操作,串行化的流无法带来性能上的提升,通常我们会使用多线程来并行执行任务,处理速度更快。 @@ -552,7 +521,7 @@ public class StreamTest7 { -## Map 集合 +# Map 集合 Java8 针对 map 操作增加了一些方法,非常方便 @@ -670,16 +639,8 @@ public class MapTest3 { -## 参考资料 +# 参考资料 `https://juejin.im/post/5c3d7c8a51882525dd591ac7#heading-16` - -最后给大家分享一个github仓库,上面放了**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ - -github地址:https://github.com/Tyson0314/java-books - -如果github访问不了,可以访问gitee仓库。 - -gitee地址:https://gitee.com/tysondai/java-books \ No newline at end of file diff --git a/docs/java/java8/1-functional-program.md b/docs/java/java8/1-functional-program.md new file mode 100644 index 0000000..70bfa84 --- /dev/null +++ b/docs/java/java8/1-functional-program.md @@ -0,0 +1,31 @@ +--- +sidebar: heading +title: 函数式编程 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式编程 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 函数式编程 + +面向对象编程:面向对象的语言,一切皆对象,如果想要调用一个函数,函数必须属于一个类或对象,然后在使用类或对象进行调用。面向对象编程可能需要多写很多重复的代码行。 + +```java + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("do something..."); + } + }; + +``` + +函数式编程:在某些编程语言中,如js、c++,我们可以直接写一个函数,然后在需要的时候进行调用,即函数式编程。 + diff --git a/docs/java/java8/2-lambda.md b/docs/java/java8/2-lambda.md new file mode 100644 index 0000000..99576a6 --- /dev/null +++ b/docs/java/java8/2-lambda.md @@ -0,0 +1,41 @@ +--- +sidebar: heading +title: Lambda表达式 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: Lambda表达式 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# Lambda 表达式 + +在Java8以前,使用`Collections`的sort方法对字符串排序的写法: + +```java +List names = Arrays.asList("dabin", "tyson", "sophia"); + +Collections.sort(names, new Comparator() { + @Override + public int compare(String a, String b) { + return b.compareTo(a); + } +}); +``` + +Java8 推荐使用lambda表达式,简化这种写法。 + +```java +List names = Arrays.asList("dabin", "tyson", "sophia"); + +Collections.sort(names, (String a, String b) -> b.compareTo(a)); //简化写法一 +names.sort((a, b) -> b.compareTo(a)); //简化写法二,省略入参类型,Java 编译器能够根据类型推断机制判断出参数类型 +``` + +可以看到使用lambda表示式之后,代码变得很简短并且易于阅读。 + diff --git a/docs/java/java8/3-functional-interface.md b/docs/java/java8/3-functional-interface.md new file mode 100644 index 0000000..8cc60bd --- /dev/null +++ b/docs/java/java8/3-functional-interface.md @@ -0,0 +1,40 @@ +--- +sidebar: heading +title: 函数式接口 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式接口 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 函数式接口 + +Functional Interface:函数式接口,只包含一个抽象方法的接口。只有函数式接口才能缩写成 Lambda 表达式。@FunctionalInterface 定义类为一个函数式接口,如果添加了第二个抽象方法,编译器会立刻抛出错误提示。 + +```java +@FunctionalInterface +interface Converter { + T convert(F from); +} + +public class FunctionalInterfaceTest { + public static void main(String[] args) { + Converter converter = (from) -> Integer.valueOf(from); + Integer converted = converter.convert("666"); + System.out.println(converted); + } + /** + * output + * 666 + */ +} +``` + + + diff --git a/docs/java/java8/4-inner-functional-interface.md b/docs/java/java8/4-inner-functional-interface.md new file mode 100644 index 0000000..6075c36 --- /dev/null +++ b/docs/java/java8/4-inner-functional-interface.md @@ -0,0 +1,164 @@ +--- +sidebar: heading +title: 内置的函数式接口 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: 函数式接口,内置的函数式接口 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 内置的函数式接口 + +Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。 + +## Predicate 断言 + +指定入参类型,并返回 boolean 值的函数式接口。用来组合一个复杂的逻辑判断。 + +```java +Predicate predicate = (s) -> s.length() > 0; + +predicate.test("dabin"); // true +``` + +## Comparator + +Java8 将 Comparator 升级成函数式接口,可以使用lambda表示式简化代码。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-05 23:24 + */ +public class ComparatorTest { + public static void main(String[] args) { + Comparator comparator = Comparator.comparing(p -> p.firstName); + + Person p1 = new Person("dabin", "wang"); + Person p2 = new Person("xiaobin", "wang"); + + // 打印-20 + System.out.println(comparator.compare(p1, p2)); + // 打印20 + System.out.println(comparator.reversed().compare(p1, p2)); + } + +} + +class Person { + public String firstName; + public String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } +} +``` + + +## Consumer + +Consumer 接口接收一个泛型参数,然后调用 accept,对这个参数做一系列消费操作。 + +Consumer 源码: + +```java +@FunctionalInterface +public interface Consumer { + + void accept(T t); + + default Consumer andThen(Consumer after) { + Objects.requireNonNull(after); + return (T t) -> { accept(t); after.accept(t); }; + } +} +``` + +示例1: + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-05 23:41 + */ +public class ConsumerTest { + public static void main(String[] args) { + Consumer consumer = x -> { + int a = x + 6; + System.out.println(a); + System.out.println("大彬" + a); + }; + consumer.accept(660); + } + /** + * output + * 666 + * 大彬666 + */ +} +``` + +示例2:在stream里,对入参做一些操作,主要是用于forEach,对传入的参数,做一系列的业务操作。 + +```java +// CopyOnWriteArrayList +public void forEach(Consumer action) { + if (action == null) throw new NullPointerException(); + Object[] elements = getArray(); + int len = elements.length; + for (int i = 0; i < len; ++i) { + @SuppressWarnings("unchecked") E e = (E) elements[i]; + action.accept(e); + } +} + +CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); +list.add(1); +list.add(2); +//forEach需要传入Consumer参数 +list + .stream() + .forEach(System.out::println); +list.forEach(System.out::println); +``` + +示例3:addThen方法使用。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-05 23:59 + */ +public class ConsumersTest { + public static void main(String[] args) { + Consumer consumer1 = x -> System.out.println("first x : " + x); + Consumer consumer2 = x -> { + System.out.println("second x : " + x); + throw new NullPointerException("throw exception second"); + }; + Consumer consumer3 = x -> System.out.println("third x : " + x); + + consumer1.andThen(consumer2).andThen(consumer3).accept(1); + } + /** + * output + * first x : 1 + * second x : 1 + * Exception in thread "main" java.lang.NullPointerException: throw exception second + * at com.dabin.java8.ConsumersTest.lambda$main$1(ConsumersTest.java:15) + * ... + */ +} +``` + diff --git a/docs/java/java8/5-stream.md b/docs/java/java8/5-stream.md new file mode 100644 index 0000000..784f5ea --- /dev/null +++ b/docs/java/java8/5-stream.md @@ -0,0 +1,287 @@ +--- +sidebar: heading +title: Stream +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: java stream,java流操作 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# Stream + +使用 `java.util.Stream` 对一个包含一个或多个元素的集合做各种操作,原集合不变,返回新集合。只能对实现了 `java.util.Collection` 接口的类做流的操作。`Map` 不支持 `Stream` 流。`Stream` 流支持同步执行,也支持并发执行。 + +## Filter 过滤 + +Filter` 的入参是一个 `Predicate,用于筛选出我们需要的集合元素。原集合不变。filter 会过滤掉不符合特定条件的,下面的代码会过滤掉`nameList`中不以大彬开头的字符串。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("大彬1"); + nameList.add("大彬2"); + nameList.add("aaa"); + nameList.add("bbb"); + + nameList + .stream() + .filter((s) -> s.startsWith("大彬")) + .forEach(System.out::println); + } + /** + * output + * 大彬1 + * 大彬2 + */ +} +``` + +## Sorted 排序 + +自然排序,不改变原集合,返回排序后的集合。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest1 { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("大彬3"); + nameList.add("大彬1"); + nameList.add("大彬2"); + nameList.add("aaa"); + nameList.add("bbb"); + + nameList + .stream() + .filter((s) -> s.startsWith("大彬")) + .sorted() + .forEach(System.out::println); + } + /** + * output + * 大彬1 + * 大彬2 + * 大彬3 + */ +} +``` + +逆序排序: + +```java +nameList + .stream() + .sorted(Comparator.reverseOrder()); +``` + +对元素某个字段排序: + +```java +list.stream().sorted(Comparator.comparing(Student::getAge).reversed()); +list.stream().sorted(Comparator.comparing(Student::getAge)); +``` + +## Map 转换 + +将每个字符串转为大写。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest2 { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("aaa"); + nameList.add("bbb"); + + nameList + .stream() + .map(String::toUpperCase) + .forEach(System.out::println); + } + /** + * output + * AAA + * BBB + */ +} +``` + +## Match 匹配 + +验证 nameList 中的字符串是否有以`大彬`开头的。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest3 { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("大彬1"); + nameList.add("大彬2"); + + boolean startWithDabin = + nameList + .stream() + .map(String::toUpperCase) + .anyMatch((s) -> s.startsWith("大彬")); + + System.out.println(startWithDabin); + } + /** + * output + * true + */ +} +``` + +## Count 计数 + +统计 `stream` 流中的元素总数,返回值是 `long` 类型。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest4 { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("大彬1"); + nameList.add("大彬2"); + nameList.add("aaa"); + + long count = + nameList + .stream() + .map(String::toUpperCase) + .filter((s) -> s.startsWith("大彬")) + .count(); + + System.out.println(count); + } + /** + * output + * 2 + */ +} +``` + +## Reduce + +类似拼接。可以实现将 `list` 归约成一个值。它的返回类型是 `Optional` 类型。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:22 + */ +public class StreamTest5 { + public static void main(String[] args) { + List nameList = new ArrayList<>(); + nameList.add("大彬1"); + nameList.add("大彬2"); + + Optional reduced = + nameList + .stream() + .sorted() + .reduce((s1, s2) -> s1 + "#" + s2); + + reduced.ifPresent(System.out::println); + } + /** + * output + * 大彬1#大彬2 + */ +} +``` + + +## flatMap + +flatMap 用于将多个Stream连接成一个Stream。 + +下面的例子,把几个小的list转换到一个大的list。 + +```java +/** + * @description: 把几个小的list转换到一个大的list。 + * @author: 程序员大彬 + * @time: 2021-09-06 00:28 + */ +public class StreamTest6 { + public static void main(String[] args) { + List team1 = Arrays.asList("大彬1", "大彬2", "大彬3"); + List team2 = Arrays.asList("大彬4", "大彬5"); + + List> players = new ArrayList<>(); + players.add(team1); + players.add(team2); + + List flatMapList = players.stream() + .flatMap(pList -> pList.stream()) + .collect(Collectors.toList()); + + System.out.println(flatMapList); + } + /** + * output + * [大彬1, 大彬2, 大彬3, 大彬4, 大彬5] + */ +} +``` +下面的例子中,将words数组中的元素按照字符拆分,然后对字符去重。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:35 + */ +public class StreamTest7 { + public static void main(String[] args) { + List words = new ArrayList(); + words.add("大彬最强"); + words.add("大彬666"); + + //将words数组中的元素按照字符拆分,然后对字符去重 + List stringList = words.stream() + .flatMap(word -> Arrays.stream(word.split(""))) + .distinct() + .collect(Collectors.toList()); + stringList.forEach(e -> System.out.print(e + ", ")); + } + /** + * output + * 大, 彬, 最, 强, 6, + */ +} +``` + + + diff --git a/docs/java/java8/6-parallel-stream.md b/docs/java/java8/6-parallel-stream.md new file mode 100644 index 0000000..37368b8 --- /dev/null +++ b/docs/java/java8/6-parallel-stream.md @@ -0,0 +1,46 @@ +--- +sidebar: heading +title: Parallel-Streams +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: Parallel-Streams,并行流 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# Parallel-Streams + +并行流。`stream` 流是支持**顺序**和**并行**的。顺序流操作是单线程操作,串行化的流无法带来性能上的提升,通常我们会使用多线程来并行执行任务,处理速度更快。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-06 00:05 + */ +public class StreamTest7 { + public static void main(String[] args) { + int max = 100; + List strs = new ArrayList<>(max); + for (int i = 0; i < max; i++) { + UUID uuid = UUID.randomUUID(); + strs.add(uuid.toString()); + } + + List sortedStrs = strs.stream().sorted().collect(Collectors.toList()); + System.out.println(sortedStrs); + } + /** + * output + * [029be6d0-e77e-4188-b511-f1571cdbf299, 02d97425-b696-483a-80c6-e2ef51c05d83, 0632f1e9-e749-4bce-8bac-1cf6c9e93afa, ...] + */ +} +``` + + + diff --git a/docs/java/java8/7-map.md b/docs/java/java8/7-map.md new file mode 100644 index 0000000..4cd983b --- /dev/null +++ b/docs/java/java8/7-map.md @@ -0,0 +1,133 @@ +--- +sidebar: heading +title: Map集合 +category: Java +tag: + - Java8 +head: + - - meta + - name: keywords + content: map集合,java map + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# Map 集合 + +Java8 针对 map 操作增加了一些方法,非常方便 + +1、删除元素使用`removeIf()`方法。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-07 00:03 + */ +public class MapTest { + public static void main(String[] args) { + + Map map = new HashMap<>(); + map.put(1, "dabin1"); + map.put(2, "dabin2"); + + //删除value没有含有1的键值对 + map.values().removeIf(value -> !value.contains("1")); + + System.out.println(map); + } + /** + * output + * {1=dabin1} + */ +} +``` + +2、`putIfAbsent(key, value) ` 如果指定的 key 不存在,则 put 进去。 + +```java +/** + * @description: + * @author: 程序员大彬 + * @time: 2021-09-07 00:08 + */ +public class MapTest1 { + public static void main(String[] args) { + + Map map = new HashMap<>(); + map.put(1, "大彬1"); + + for (int i = 0; i < 3; i++) { + map.putIfAbsent(i, "大彬" + i); + } + map.forEach((id, val) -> System.out.print(val + ", ")); + } + /** + * output + * 大彬0, 大彬1, 大彬2 + */ +} +``` + +3、map 转换。 + +```java +/** + * @author: 程序员大彬 + * @time: 2021-09-07 08:15 + */ +public class MapTest2 { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put("1", 1); + map.put("2", 2); + + Map newMap = map.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> "大彬" + String.valueOf(e.getValue()))); + + newMap.forEach((key, val) -> System.out.print(val + ", ")); + } + /** + * output + * 大彬1, 大彬2, + */ +} +``` + +4、map遍历。 + +```java +/** + * @author: 程序员大彬 + * @time: 2021-09-07 08:31 + */ +public class MapTest3 { + public static void main(String[] args) { + Map map = new HashMap<>(); + map.put(1, "大彬1"); + map.put(2, "大彬2"); + + //方式1 + map.keySet().forEach(k -> { + System.out.print(map.get(k) + ", "); + }); + + //方式2 + map.entrySet().iterator().forEachRemaining(e -> System.out.print(e.getValue() + ", ")); + + //方式3 + map.entrySet().forEach(entry -> { + System.out.print(entry.getValue() + ", "); + }); + + //方式4 + map.values().forEach(v -> { + System.out.print(v + ", "); + }); + } +} +``` + + + diff --git a/docs/java/java8/README.md b/docs/java/java8/README.md new file mode 100644 index 0000000..0644930 --- /dev/null +++ b/docs/java/java8/README.md @@ -0,0 +1,11 @@ + +## Java8总结 + +- [函数式编程](./1-functional-program.md) +- [Lambda表达式](./2-lambda.md) +- [函数式接口](./3-functional-interface.md) +- [内置的函数式接口](./4-inner-functional-interface.md) +- [Stream](./5-stream.md) +- [Parallel-Streams](./6-parallel-stream.md) +- [Map集合](./7-map.md) + diff --git a/docs/java/jvm.md b/docs/java/jvm.md new file mode 100644 index 0000000..5400aac --- /dev/null +++ b/docs/java/jvm.md @@ -0,0 +1,33 @@ +**JVM重要知识点&高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到Java面试手册**完整版**。 + +![](http://img.topjavaer.cn/img/image-20230102194032026.png) + +除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) + +![](http://img.topjavaer.cn/img/image-20230102210715391.png) + +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 + +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/java/jvm/jvm-heap-memory-share.md b/docs/java/jvm/jvm-heap-memory-share.md new file mode 100644 index 0000000..6efdde7 --- /dev/null +++ b/docs/java/jvm/jvm-heap-memory-share.md @@ -0,0 +1,127 @@ +--- +sidebar: heading +title: Java堆内存是线程共享的? +category: Java +tag: + - JVM +head: + - - meta + - name: keywords + content: 堆内存内存共享,内存共享 + - - meta + - name: description + content: 高质量的Java常见知识点和面试题总结,让天下没有难背的八股文! +--- + +> 本文转自Hollis + +Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解。可以说,关于JVM的相关知识,基本是每个Java开发者必学的知识点,也是面试的时候必考的知识点。 + +在JVM的内存结构中,比较常见的两个区域就是堆内存和栈内存(如无特指,本文提到的栈均指的是虚拟机栈),关于堆和栈的区别,很多开发者也是如数家珍,有很多书籍,或者网上的文章大概都是这样介绍的: + +> 1、堆是线程共享的内存区域,栈是线程独享的内存区域。 +> +> 2、堆中主要存放对象实例,栈中主要存放各种基本数据类型、对象的引用。 + +但是,作者可以很负责任的告诉大家,以上两个结论均不是完全正确的。 + +在开始进入正题之前,请允许我问一个和这个问题看似没有任何关系的问题:Java对象的内存分配过程是如何保证线程安全的? + +### **Java对象的内存分配过程是如何保证线程安全的?** + +我们知道,Java是一门面向对象的语言,我们在Java中使用的对象都需要被创建出来,在Java中,创建一个对象的方法有很多种,但是无论如何,对象在创建过程中,都需要进行内存分配。 + +对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作。 + +但是,因为堆是全局共享的,因此在同一时间,可能有多个线程在堆上申请空间,那么,在并发场景中,如果两个线程先后把对象引用指向了同一个内存区域,怎么办。 + +![](http://img.topjavaer.cn/img/Java对象的内存分配过程是如何保证线程安全的.png) + +为了解决这个并发问题,对象的内存分配过程就必须进行同步控制。但是我们都知道,无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。 + +而Java对象的分配是Java中的高频操作,所有,人们想到另外一个办法来提升效率。这里我们重点说一个HotSpot虚拟机的方案: + +> 每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。 + +这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。 + +### **什么是TLAB** + +TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的。在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。 + +注意到上面的描述中"线程专属"、"只给当前线程使用"、"每个线程单独拥有"的描述了吗? + +所以说,因为有了TLAB技术,堆内存并不是完完全全的线程共享,其eden区域中还是有一部分空间是分配给线程独享的。 + +这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独享的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。 + +![](http://img.topjavaer.cn/img/什么是TLAB.png) + +也就是说,虽然每个线程在初始化时都会去堆内存中申请一块TLAB,并不是说这个TLAB区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。 + +并且,在TLAB分配之后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能通过TLAB分配内存,存放在Eden区,但是还是会被垃圾回收或者被移到Survivor Space、Old Gen等。 + +![](http://img.topjavaer.cn/img/什么是TLAB1.png) + +还有一点需要注意的是,我们说TLAB是在eden区分配的,因为eden区域本身就不太大,而且TLAB空间的内存也非常小,默认情况下仅占有整个Eden空间的1%。所以,必然存在一些大对象是无法在TLAB直接分配。 + +遇到TLAB中无法分配的大对象,对象还是可能在eden区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。 + +### **TLAB带来的问题** + +虽然在一定程度上,TLAB大大的提升了对象的分配速度,但是TLAB并不是就没有任何问题的。 + +前面我们说过,因为TLAB内存区域并不是很大,所以,有可能会经常出现不够的情况。在《实战Java虚拟机》中有这样一个例子: + +比如一个线程的TLAB空间有100KB,其中已经使用了80KB,当需要再分配一个30KB的对象时,就无法直接在TLAB中分配,遇到这种情况时,有两种处理方案: + +1、如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则直接在堆内存中对该对象进行内存分配。 + +2、如果一个对象需要的空间大小超过TLAB中剩余的空间大小,则废弃当前TLAB,重新申请TLAB空间再次进行内存分配。 + +以上两个方案各有利弊,如果采用方案1,那么就可能存在着一种极端情况,就是TLAB只剩下1KB,就会导致后续需要分配的大多数对象都需要在堆内存直接分配。 + +如果采用方案2,也有可能存在频繁废弃TLAB,频繁申请TLAB的情况,而我们知道,虽然在TLAB上分配内存是线程独享的,但是TLAB内存自己从堆中划分出来的过程确实可能存在冲突的,所以,TLAB的分配过程其实也是需要并发控制的。而频繁的TLAB分配就失去了使用TLAB的意义。 + +为了解决这两个方案存在的问题,虚拟机定义了一个refill_waste的值,这个值可以翻译为“最大浪费空间”。 + +当请求分配的内存大于refill_waste的时候,会选择在堆内存中分配。若小于refill_waste值,则会废弃当前TLAB,重新创建TLAB进行对象内存分配。 + +前面的例子中,TLAB总空间100KB,使用了80KB,剩余20KB,如果设置的refill_waste的值为25KB,那么如果新对象的内存大于25KB,则直接堆内存分配,如果小于25KB,则会废弃掉之前的那个TLAB,重新分配一个TLAB空间,给新对象分配内存。 + +### **TLAB使用的相关参数** + +TLAB功能是可以选择开启或者关闭的,可以通过设置-XX:+/-UseTLAB参数来指定是否开启TLAB分配。 + +TLAB默认是eden区的1%,可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。 + +默认情况下,TLAB的空间会在运行时不断调整,使系统达到最佳的运行状态。如果需要禁用自动调整TLAB的大小,可以使用-XX:-ResizeTLAB来禁用,并且使用-XX:TLABSize来手工指定TLAB的大小。 + +TLAB的refill_waste也是可以调整的,默认值为64,即表示使用约为1/64空间大小作为refill_waste,使用参数:-XX:TLABRefillWasteFraction来调整。 + +如果想要观察TLAB的使用情况,可以使用参数-XX+PringTLAB 进行跟踪。 + +### **总结** + +为了保证对象的内存分配过程中的线程安全性,HotSpot虚拟机提供了一种叫做TLAB(Thread Local Allocation Buffer)的技术。 + +在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,当需要分配内存时,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。 + +所以,“堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在读取上确实是线程共享的,但是在内存分配上,是线程独享的。 + +TLAB的空间其实并不大,所以大对象还是可能需要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是再eden分配还是在老年代分配。 + +![](http://img.topjavaer.cn/img/TLAB空间.png) + +### **多说几句** + +相信一部分看完这篇文章之后,可能会觉得作者有点过于“咬文嚼字”、“吹毛求疵”了。可能不乏有些性子急的人只看了开头就直接翻到文末准备开怼了。 + +不管你认不认同作者说的:“堆是线程共享的内存区域这句话并不完全正确”。这其实都不重要,重要的是当提到堆内存、提到线程共享、提到对象内存分配的时候,你可以想到还有个TLAB是比较特殊的,就可以了。 + +有些时候,最可怕的不是自己不知道,而是,不知道自己不知道。 + +还有就是,TLAB只是HotSpot虚拟机的一个优化方案,Java虚拟机规范中也没有关于TLAB的任何规定。所以,不代表所有的虚拟机都有这个特性。 + +本文的概述都是基于HotSpot虚拟机的,作者也不是故意“以偏概全”,而是因为HotSpot虚拟机是目前最流行的虚拟机了,大多数默认情况下,我们讨论的时候也都是基于HotSpot的。 + diff --git a/docs/learn/ghelper.md b/docs/learn/ghelper.md new file mode 100644 index 0000000..f5de885 --- /dev/null +++ b/docs/learn/ghelper.md @@ -0,0 +1,92 @@ +--- +sidebar: heading +title: 科学上网教程 +category: 工具 +tag: + - 工具 +head: + - - meta + - name: keywords + content: ghelper + - - meta + - name: description + content: 提高工作效率的工具 +--- + + + +## Ghelper插件 + +**Ghelper是一款谷歌浏览器插件,用于访问google。** + +第一步:下载并安装**Chrome浏览器**。 + +Chrome下载链接:[点击下载](https://links.jianshu.com/go?to=https%3A%2F%2Fdl.softmgr.qq.com%2Foriginal%2FBrowser%2F87.0.4280.88_chrome_installer_32.exe) + +安装完成后我们可以准备Ghelper插件 + + + +第二步:下载Ghelper.crx插件 + +Ghelper可以在google play商店进行下载,需要访问google商店,无法直接下载的小伙伴,可以微信搜索「**程序员大彬**」或者扫描下面的二维码,发送关键字「**ghelper**」下载。 + +![](http://img.topjavaer.cn/img/公众号.jpg) + + + +第三步:在浏览器中安装插件 + + ①. 首先需要【打开浏览器】,在右上角找到【设置】按钮 + + ②. 找到【更多工具】选项 + + ③. 找到【扩展程序】 + +![](http://img.topjavaer.cn/img/image-20221029164558200.png) + + ④. 打开【开发者选项】 + +![](http://img.topjavaer.cn/img/image-20221029164617082.png) + +步骤④ + + ⑤. 将插件文件【拖拽】至界面中,点击【添加扩展程序】即可完成。 + +![](http://img.topjavaer.cn/img/image-20221029164638825.png) + +拖拽 + +![](http://img.topjavaer.cn/img/image-20221029164652442.png) + +确认添加 + +**至此添加完成** + +**最后可以通过右上角插件按钮管理已添加的插件** + +![image-20221029164706044](http://img.topjavaer.cn/img/image-20221029164706044.png) + +图钉按钮可固定 + +**至此安装全部结束。** + +## lantern + +具体下载方式和操作指引见:https://github.com/getlantern/lantern + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/learn/leetcode.md b/docs/learn/leetcode.md new file mode 100644 index 0000000..32f99bd --- /dev/null +++ b/docs/learn/leetcode.md @@ -0,0 +1,25 @@ +在学习数据结构和算法,或者准备面试的小伙伴们,千万不要错过这份宝藏算法笔记! + +![](http://img.topjavaer.cn/img/image-20210828120937311.png) + +![](http://img.topjavaer.cn/img/image-20210828121035926.png) + +**手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**谷歌**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) + + + + + +--- + + + +> 最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/img/Image.png) + +![](http://img.topjavaer.cn/img/image-20221030094126118.png) + +**Github地址**:https://github.com/Tyson0314/java-books \ No newline at end of file diff --git a/docs/learn/manual.md b/docs/learn/manual.md new file mode 100644 index 0000000..a1d862d --- /dev/null +++ b/docs/learn/manual.md @@ -0,0 +1,22 @@ +大家好,我是大彬~ + +最近抽空将小破站的**面试题汇总成PDF手册**了,方便小伙伴打印出来看。手册内容基本跟网站上的一致,包含**Java基础、MySQL、SpringBoot、MyBatis、Redis、RabbitMQ、计算机网络、数据结构与算法、微服务、场景题**等内容。 + +这份面试手册非常**详细**,并且有相应的**配图讲解**: + +![](http://img.topjavaer.cn/img/image-20211127163043770.png) + +![](http://img.topjavaer.cn/img/image-20211127151141076.png) + +![](http://img.topjavaer.cn/img/image-20220316234337881.png) + +![](http://img.topjavaer.cn/img/image-20211127150136157.png) + +![](http://img.topjavaer.cn/img/image-20211127150322032.png) + +**手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**手册**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img//公众号.jpg) + +祝大家学习愉快! + diff --git a/docs/learning-resources/chang-gou-mall.md b/docs/learning-resources/chang-gou-mall.md new file mode 100644 index 0000000..42f8f00 --- /dev/null +++ b/docs/learning-resources/chang-gou-mall.md @@ -0,0 +1,15 @@ +畅购商城项目是一个B2C的电商网站,采用了微服务架构,并且使用了前后端分离的方式进行开发。 + +整个微服务的开发是基于SpringBoot的,OAuth2.0是用来进行授权操作的,JWT用来封装用户的授权信息,Spring AMQP是消息队列协议。然后就是一套Spring Cloud的微服务框架。 + +持久化技术栈选用了MyBatis。数据库采用了MySQL,消息队列选用了RabbitMQ,还实现了MySQL读写分离。支付接口就选择了微信支付。 + +项目架构图如下: + +![](http://img.topjavaer.cn/img/畅购商城.jpg) + +这个项目比较适合在校生学习,推荐给大家。 + +**获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**畅购**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) \ No newline at end of file diff --git a/docs/learning-resources/cs-learn-guide.md b/docs/learning-resources/cs-learn-guide.md new file mode 100644 index 0000000..dae9863 --- /dev/null +++ b/docs/learning-resources/cs-learn-guide.md @@ -0,0 +1,284 @@ +--- +sidebar: heading +title: CS自学路线 +category: 学习路线 +tag: + - 计算机 +head: + - - meta + - name: keywords + content: CS学习路线,经验分享,数据库,数据结构与算法,操作系统,计算机网络 + - - meta + - name: description + content: CS自学路线分享 +--- + +大家好,我是大彬~今天给大家分享CS自学路线。 + +首先看看计算机专业的培养体系,基本上每个学校都差不多: + +![](http://img.topjavaer.cn/img/20220703110718.png) + +像计算机组成原理、操作系统等课程让我们了解计算机体系结构的基本知识和概念,计算机是怎么运行的,学习计算机的基本组成原理和内部运行机制,并探索硬、软件之间相互作用的关系,以及如何有效利用硬件提高系统性能。 + +计算机网络则是让你明白计算机网络的概念、原理和体系结构,知道计算机分层结构,物理层、数据链路层、介质访问子层、网络层、传输层和应用层的基本原理和协议,掌握以 TCP/IP 协议族为主的网络协议结构,并且了解网络新技术的最新发展。 + +编译原理则是介绍编译程序构造的原理与实践,让你明白高级语言都是如何被转换为另外一种语言的。学完编译原理,可以尝试自己去实现一个完整的小型面向对象语言的编译程序。 + +汇编语言则是高级语言在机器层面的表示,掌握这两种语言的对应可以将程序的执行与计算机的工作过程紧密联系起来,直接体现汇编语言本身固有的特点,即它是最易于将“程序”和“机器” 统一起来的一个结合点。 + +计算机专业课程里边,**计算机核心课程无非以下几个:** + +1. **计算机组成原理** +2. **操作系统** +3. **编译原理** +4. **计算机网络** +5. **数据结构与算法** +6. **数据库基础** + +![](http://img.topjavaer.cn/img/20220703162734.png) + +## 操作系统 + +无论学习什么编程语言,和需要和操作系统打交道。如果对操作系统不熟悉,那么你在未来的学习路上将会遇到很多障碍,比如线程进程调度、内存分配、Java的虚拟机等知识,都会一头雾水。因此,只有把操作系统搞明白了,才能够更好地学习计算机的其他知识。 + +**书籍推荐** + +入门级别书籍:**《现代操作系统》、《操作系统导论》**,进阶:**《深入理解计算机系统》** + +强推《深入理解计算机系统》 这本书。 + +![](http://img.topjavaer.cn/img/20220703132845.jpg) + +CSAPP是一本很好的书,糅合了计算机组成原理、操作系统、网络编程、并行程序设计原理等课程的基础知识。对于刚接触编程,或者像大彬这种非科班出身的人来说,这是一本指导性的书,它会告诉你,要想成为一个优秀的程序员,应当重点理解哪些计算机底层原理,告诉你应该在以后的自学过程中,应该重点学习哪些课程,比如操作系统和体系结构等。 + +**视频教程推荐** + +Udacity的Advanced OS公开课:https://www.classcentral.com/course/udacity-advanced-operating-systems-1016 + +还有国内不错的操作系统的课程,清华大学的公开课:https://www.xuetangx.com/course/THU08091000267/5883104?channel=search_result + +![](http://img.topjavaer.cn/img/20220703132955.png) + +由清华大学两位老师向勇、陈渝讲授,同时配有一套完整的实验,实验内容是从无到有地建立起一个小却五脏俱全的操作系统,以主流操作系统为实例,以教学操作系统ucore为实验环境,讲授操作系统的概念、基本原理和实现技术,为学生从事操作系统软件研究和开发,以及充分利用操作系统功能进行应用软件研究和开发打下扎实的基础。 + +另外,推荐另一门MIT操作系统课程:**MIT6.268** + +课程地址:https://pdos.csail.mit.edu/6.828/2018/schedule.html + +![](http://img.topjavaer.cn/img/20220626115851.png) + +MIT6.828 是一门非常值得学习的课程,广受好评,是**理论与实践相结合的经典**。 + +只要你跟着项目一步一步走,做完 6 个实验,就能实现一个简单的操作系统内核。 + +每个实验都有对应的知识点,学完理论知识后会有相应的练习,学习体验非常棒! + +**建议**在开始学习这门课之前先熟悉C和汇编,对计算机组成有一定了解。 + +**操作系统主要知识点**: + +- 操作系统的基础特征 +- 进程与线程的本质区别、以及各自的使用场景 +- 进程的几种状态 +- 进程通信方法的特点以及使用场景 +- 进程任务调度算法的特点以及使用场景 +- 死锁的原因、必要条件、死锁处理。手写死锁代码、Java是如何解决死锁的。 +- 线程实现的方式 +- 协程的作用 +- 内存管理的方式 +- 虚拟内存的作用,分页系统实现虚拟内存原理 +- 页面置换算法的原理 +- 静态链接和动态链接 + +## 计算机组成原理 + +计算机组成原理,主要学习计算机的基本组成原理和内部运行机制,并探索硬、软件之间相互作用的关系,以及如何有效利用硬件提高系统性能。 + +**书籍推荐** + +《计算机是怎样跑起来的》这本书相对比较基础,描述计算机各个方面。从单片机电路开始,汇编,结构化程序,数据结构于算法,面向对象,数据库,TCP/IP原理,加密解密,XML,软件工程统统有清晰描述,易于理解。在知识的整体理解基础上再阅读文档,学习编程会事半功倍。所以而推荐本书。 + +![](http://img.topjavaer.cn/img/20220703123249.jpg) + +另外还有两本书也不错,推荐给大家《计算机组成与设计:硬件 / 软件接口》 、《深入理解计算机系统》 + +**视频推荐** + +计算机组成原理(哈工大刘宏伟): https://www.bilibili.com/video/BV1WW411Q7PF + +刘宏伟老师主讲,他的课不仅适合考研人,也非常适合初学者,初学者也听得懂。 + +![](http://img.topjavaer.cn/img/20220703131541.png) + +【麻省理工学院-中文字幕版】计算机组成原理:https://www.bilibili.com/video/BV1kU4y177x9 + +课程为 MIT 6.004 Computation Structures, Spring 2017,如果英文不错,可以跟着学学,课程质量很高。 + +![](http://img.topjavaer.cn/img/20220703131900.png) + + + +## 编译原理 + +编译原理介绍了编译程序构造的原理与实践,让你明白高级语言都是如何被转换为另外一种语言的。学完编译原理,可以尝试自己去实现一个完整的小型面向对象语言的编译程序。 + +**书籍推荐** + +**《编译原理》**和**《编译器设计 第二版》**。 + +特别是《编译器设计 第二版》这本书,对于对编译有兴趣的同学,这本书绝对是值得推荐的。 + +![](http://img.topjavaer.cn/img/20220703133449.jpg) + +这本书覆盖了编译器从前端到后端的全部主题,很多算法都可以在 JVM C2 compiler 中找到对应实现。整体翻译很流畅,不愧是经典! + +哈工大的编译原理视频:https://www.bilibili.com/video/BV1zW411t7YE?p=1&vd_source=2b77c4a826e636ae19a4f75a4b2ca146 + +![](http://img.topjavaer.cn/img/20220703132211.png) + +比起很多砖头书和博客,强太多!陈鄞老师的 PPT 做的很好,讲得也很通俗易懂,课程评价也很高。推荐! + +编译原理-国防科技大学:https://www.bilibili.com/video/BV12741147J3 + +课程前置知识:具备计算机程序设计语言和程序设计知识,对数据结构与算法、计算机原理、离散数学等相关知识有一定了解更好。视频简洁明了,适合多刷几遍。 + +![](http://img.topjavaer.cn/img/20220703132547.png) + +## 数据结构和算法 + +为什么学习数据结构与算法?对于计算机专业的同学来说,这门课程是必修的,考研基本也是必考科目。对于程序员来说,数据结构与算法也是面试、笔试必备的非常重要的考察点。 + +数据结构与算法是程序员内功体现的重要标准之一,且数据结构也应用在各个方面。数据结构也蕴含一些面向对象的思想,故学好掌握数据结构对逻辑思维处理抽象能力有很大提升。 + +**书籍推荐** + +**《大话数据结构》和《算法图解》** + +《大话数据结构》 这本书最大的特点是,通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。这本书特别适合初学者。 + +![](http://img.topjavaer.cn/img/20220703150022.jpg) + +《算法图解》是非常好的入门算法书,示例丰富,图文并茂,以让人容易理解的方式阐释了算法,旨在帮助程序员在日常项目中更好地发挥算法的能量。 + +很多学Java的同学,可能会问有没有Java版本的数据结构和算法书籍? + +当然有的,可以看看**《数据结构与算法分析 java语言描述》**这本书,用Java语言描述各种数据结构和算法,对于Java开发者来说,更容易理解。 + +**视频推荐** + +**UCSanDiego的数据结构与算法专项课程**:https://www.coursera.org/specializations/algorithms + +**浙大陈越姥姥的数据结构课程**: + +https://www.bilibili.com/video/BV1H4411N7oD + +![](http://img.topjavaer.cn/img/20220703150635.png) + +浙江大学陈越姥姥和何钦铭教授联合授课,非常经典的课程。姥姥我的偶像! + +**小甲鱼的数据结构和算法课程**:https://www.bilibili.com/video/BV1jW411K7yg + +数据结构与算法主要学习以下内容: + +- 基本数据结构(数组、链表、栈、队列等) +- 树(二叉树、avl树、b树、红黑树等) +- 堆结构 +- 排序算法(冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等及时间空间复杂度) +- 动态规划、回溯、贪心算法(多刷刷leetcode) +- 递归 +- 位运算 + +学完感觉还很吃力?可以借助一些刷题网站巩固下。下面推荐几个刷题网站。 + +### [牛客网](https://www.nowcoder.com/) + +![](http://img.topjavaer.cn/img/20220619223253.png) + +作为牛客红名大佬,来给牛客宣传一波!(牛客打钱!) + +牛客网拥有超级丰富的 IT 题库,题库+面试+学习+求职+讨论,基本涵盖所有面试笔试题型,堪称"互联网求职神器"。在这里不仅可以刷题,还可以跟其他牛友讨论交流,一起成长。牛客上还会各种的内推机会,对于求职的同学也是极其不错的。 + +### [LeetCode](https://leetcode.cn/) + +![](http://img.topjavaer.cn/img/20220619231232.png) + +**力扣,强推**!力扣虐我千百遍,我待力扣如初恋! + +从现在开始,每天一道力扣算法题,坚持几个月的时间,你会感谢我的(傲娇脸) + +我刚开始刷算法题的时候,就选择在力扣上刷。最初刷easy级别题目的时候,都感觉有点吃力,坚持半年之后,遇到中等题目甚至hard级别的题目都不慌了。 + +不过是熟能生巧罢了。 + +### [LintCode](https://www.lintcode.com/) + +![](http://img.topjavaer.cn/img/20220619231320.png) + +与Leetcode类似的刷题网站。 + +LeetCode/LintCode的题目量差不多。LeetCode的**test case比较完备**,并且LeetCode有**讨论区**,看别人的代码还是比较有意义的。 + +LintCode的UI、tagging、filter更加灵活,更有优点,大家选择其中一个进行刷题即可。 + +## 计算机网络 + +计算机网络这门课需要学习计算机网络的概念、原理和体系结构,知道计算机分层结构,物理层、数据链路层、介质访问子层、网络层、传输层和应用层的基本原理和协议,掌握以 TCP/IP 协议族为主的网络协议结构,并且了解网络新技术的最新发展。 + +**书籍推荐** + +《计算机网络自顶向下方法》 + +![](http://img.topjavaer.cn/img/20220703144400.jpg) + +这本书是经典的计算机网络教材,采用作者独创的自顶向下方法来讲授计算机网络的原理及其协议,自第1版出版以来已经被数百所大学和学院选作教材。书中从应用层讲起,然后展开,摆脱了从物理层开始的枯燥,直接接触应用实例,更能吸引读者的兴趣。而且,书上很多例子举的很好,生动形象。 + +**视频推荐** + +视频推荐中科大郑烇、杨坚全套《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》课程。这门课是2020年秋科大自动化系本科课程录制版,可与中科大学生一起完成专业知识的学习。 + +https://www.bilibili.com/video/BV1JV411t7ow?p=7&vd_source=2b77c4a826e636ae19a4f75a4b2ca146 + +![](http://img.topjavaer.cn/img/20220703143955.png) + +另外还可以看看哈尔滨工业大学李全龙老师的计算机网络课程:https://www.bilibili.com/video/BV1Up411Z7hC + +![](http://img.topjavaer.cn/img/20220703144500.png) + +**计算机网络核心知识点**: + +- 网络分层结构 +- TCP/IP +- 三次握手四次挥手 +- 滑动窗口、拥塞控制 +- HTTP/HTTPS +- 网络安全问题(CSRF、XSS、SQL注入等) + +## 数据库 + +互联网应用大多属于数据密集型应用,对于真实世界的数据密集型应用而言,除非你准备从基础组件的轮子造起,不然根本没那么多机会去摆弄花哨的数据结构和算法。 + +实际生产中,数据表就是数据结构,索引与查询就是算法。而应用代码往往扮演的是胶水的角色,处理IO与业务逻辑,其他大部分工作都是在数据系统之间搬运数据。在最宽泛的意义上,有状态的地方就有数据库。它无所不在,网站的背后、应用的内部,单机软件,区块链里,甚至在离数据库最远的Web浏览器中。 + +**书籍推荐** + +- 《MySQL必知必会》 +- 《高性能mysql》 + +《MySQL必知必会》主要是Mysql的基础语法,很好理解。后面有了基础再看《高性能mysql》,这本书主要讲解索引、SQL优化、高级特性等,很多Mysql相关面试题出自《高性能MySQL》这本书,值得一看。 + +**视频推荐** + +伯克利的 CS168 课程:https://archive.org/details/UCBerkeley_Course_Computer_Science_186 + +![](http://img.topjavaer.cn/img/20220703152850.png) + +国内中国人民大学王珊老师的《数据库系统概论》:https://www.bilibili.com/video/BV1pW411W7Do + +![](http://img.topjavaer.cn/img/20220703153133.png) + + + +码字不易,如果觉得对你有帮助,可以**点个赞**鼓励一下! + +我是 [@程序员大彬](https://zhuanlan.zhihu.com/people/dai-shu-bin-13) ,专注Java后端硬核知识分享,欢迎大家关注~ diff --git a/docs/learning-resources/java-learn-guide.md b/docs/learning-resources/java-learn-guide.md new file mode 100644 index 0000000..f17c203 --- /dev/null +++ b/docs/learning-resources/java-learn-guide.md @@ -0,0 +1,858 @@ +--- +sidebar: heading +title: Java自学路线 +category: 学习路线 +tag: + - Java +head: + - - meta + - name: keywords + content: java学习路线,经验分享,java,Spring,mysql,redis,springboot + - - meta + - name: description + content: 绝对全面的Java自学路线 +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +大家好,我是大彬~ + +我本科学的不是计算机,大四开始自学Java,并且找到了中大厂的offer。自学路上遇到不少问题,每天晚上都是坚持到一两点才睡觉,**最终也拿到了30w的offer**。 + +![](http://img.topjavaer.cn/img/image-20211206000941636.png) + +在这里也提醒学弟学妹们,要尽早确定以后的方向,读研还是工作,找工作的话,也要尽快确定工作岗位,想转行的,需要花更多的时间准备。很多同学到了大四快毕业的时候,才思考自己未来要做什么,这个时候已经有点晚了。如果错过了校招,走社招渠道去找工作,难度将会提升一个等级,到时后悔也来不及! + +下面来说说自己的经历吧(附自学路线)。 + +## 接触编程 + +大学以前基本没碰过电脑,家里没电脑,也没去过网吧。高中的计算机课程,期末作业要完成一个自我介绍的PPT,也不会做,最后直接抄同桌的作业(复制粘贴都不会。。还得同桌教,捂脸)。 + +高考完一个月后,买了电脑,真正开始使用上了电脑。 + +大一上学期的时候,系里开了一门C语言的课程,这也是我第一次接触编程。教材是英文的,刚开始学还是挺头大的。每次课程作业,周围的同学都是一顿复制粘贴,我也一样嘿嘿。 + +记得在讲指针那一章的时候,听的一头雾水。稍微走神,回过头来,已经不知道讲的是啥了。 + +后面系里开设了兴趣小组,因为平时比较闲,也想着去捣鼓点东西,就去参加了。刚开始的时候,什么都不懂,老师推荐我学一下51单片机,拿了一本厚厚的51单片机的书籍,跟着书里的demo敲了一遍,发现了新天地!原来编程这么有意思! + +![](https://pic2.zhimg.com/80/v2-2ac0759cc48ed0d17b9ce46a13bb0f1e_720w.jpg) + +记得第一次跑出流水灯的时候,那叫一个激动啊,满满的都是成就感!后面也写了一些电机、红外遥控等demo。从那以后,激发了我学习编程的兴趣。 + +到了大二,辅导员在群里发布全国电子设计大赛的信息,参赛题跟四轴飞行器相关,那段时间对四轴飞行器比较感兴趣,于是约了两个小伙伴一块参加。距离比赛时间只有一个月,在那一个月的时间里,每天都是早出晚归,吃饭的时候还在想着哪一块代码出了bug。虽然最后没能获奖,但是在这个过程中,学到很多知识,编程能力也有了很大的提升。 + +## 决定转码 + +转眼间,大三开学,开始纠结考研还是工作,思考了一周时间,也进了系里的实验室体验了一把研究生生活,最后还是听从内心的想法,决定直接找工作。 + +我咨询了本专业的师兄师姐们往年的就业情况,他们大部分人还是找了互联网方向的工作。有一个在传统行业的师兄,也劝我投互联网公司的岗位,因为在传统行业加班也不少,但是工资贼低。。最后决定转行程序员,找后端相关的工作。 + +那么学习哪一种语言呢?当时有三个选择:c++,Java,python。 + +那段时间python比较火,但是经过一番深思熟虑之后,还是选择了Java。为什么选择Java呢? + +很简单,市场需求大,学习难度适中。相比科班同学来说,我缺乏系统的计算机基础知识,而距离秋招也只有不到一年时间,所以还是选择学习难度低一点的Java。 + +## 闭关自学 + +确定方向后,便开始制定学习路线。不得不说,Java要学的东西是真的多。。 + +自学期间遇到挺多问题,比如一些环境配置问题,有时候搞上好几天,很打击积极性。中途也有很多次怀疑自己的水平,是不是不适合干编程,差点就放弃了。幸好最后还是坚持了下来。 + +半年多的时间,除了平时上课,其他时间就是在图书馆。周末或者节假日,每天都是7点起床,八点到图书馆开始学习,到了晚上十点,图书馆闭馆,才回宿舍,每天都是图书馆最后走的一批。回到宿舍,洗完澡,继续肝到十二点多(卷王!)。 + +![img](https://pic3.zhimg.com/80/v2-25f6523ffc0ea6982c576b75458695dc_720w.jpg) + +> 很多人在问,大三才开始自学Java,来的及吗? 我觉得,还是看个人的投入程度和学习能力。有些人自学能力强一点,每天可以投入10小时及以上的时间去学习,那完全没问题。 + +自学过程还是挺辛苦的,要耐得住寂寞,最最重要的还是得坚持! + +我根据自己的自学经历,整理了一些学习过程中踩坑总结的经验,希望自学的小伙伴可以少走弯路: + +- **注重实践**,不要只是埋头看书,一定要多动手写代码。 +- 刚开始自学的时候,可以不用太深究细节,不然可能会怀疑自己的学习能力。等到后面有了一定的基础,回过头来重新回顾,可能会恍然大悟,没有当初想的那么难。 +- 可以适当加一些交流群,遇到不懂的知识点,多与其他人交流。 + +好了,下面给大家分享一下我的自学经验。 + +## 自学路线 + +首先看一下Java学习路线图: + +![](http://img.topjavaer.cn/img/Java学习路线-gitmind-清晰.png) + +在这里也给大家分享一份精心整理的**大厂高频面试题PDF**,小伙伴靠着这份手册拿过阿里offer,需要的小伙伴可以自行下载: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## Java + +![](http://img.topjavaer.cn/img/Java基础.jpg) + +推荐书籍: + +- 《head first java》 +- 《JAVA核心技术卷》 + +这本书我看了两遍,是一本非常棒的书。不得不说,head first系列的书籍质量是真的高,清晰的条理,生动的图示,偶尔来点老外的幽默,阅读体验非常舒畅。 + +![](http://img.topjavaer.cn/img/202305172229964.png) + +《JAVA核心技术卷》难度相对适中,内容也比较全面,部分章节(如Swing)可以跳过。 + +![](http://img.topjavaer.cn/img/202305172238052.png) + +> 这些书籍,我已经整理了电子版,放到github上了,总共**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~(花了一个多月的时间整理的,希望对大家有帮助,欢迎star~) +> +> 仓库持续更新中~ +> +> ![](http://img.topjavaer.cn/img/书单new.png) +> +> 有需要的自取: +> +> github仓库:https://github.com/Tyson0314/java-books + +视频推荐动力节点老杜的视频教程,1000w的播放量!视频总体上质量很不错,讲解挺详细,适合新手。跟着老杜的视频学下来,可以学到很多知识! + +https://www.bilibili.com/video/BV1Rx411876f + +再次强调:**多敲代码!多敲代码!多敲代码!** + +学习编程就是看书加实践,要多动手,不然看过的知识点很快就会忘,而且多实践也会遇到很多坑,丰富经验。 可以到github上找一些项目练练手,通过做项目巩固知识,而且每实现一个功能之后,会有满满的成就感,也会激励你不断去学习。 + +**Java基础知识主要有:** + +- 面向对象特性 +- Java语言基础、循环、数组 ; 了解类和对象 + + - 掌握强制数据类型转换和自动类型提升规则; + - 常量如何声明及赋值; + - 循环的语法及作用; + - 数组的声明及定义; + - 掌握类的概念以及什么是对象。 +- 抽象类和接口 +- 数据类型、重写重载、封装继承多态 +- 容器类Map/List/Set等 +- 异常处理 +- 反射机制 +- 泛型 +- 常用类:String、时间类 +- 函数式编程 +- Stream API +- Lambda 表达式 +- IO流操作,多线程及Socket + - 掌握IO读写流相关的类,了解字节流,字符流和字符流缓冲区; + - 掌握线程的概念,多线程的创建、启动方式,锁和同步的概念及运用; + - 掌握Socket通信的概念,如何声明客户端服务端,如何完成双端数据通信。 + +## Java Web + +Java Web是一系列技术的综合,也是大多数Java开发者的技术方向。有必要学习一下。这部分可以看看视频教程。 + +视频推荐尚硅谷的JavaWeb全套教程,HTML/CSS/JavaScript等跟前端相关的可以倍速观看。 + +https://www.bilibili.com/video/BV1Y7411K7zz + +黑马程序员的Java web教程总体也不错 + +https://www.bilibili.com/video/BV1qv4y1o79t + +下面列举Java web需要掌握的知识点。 + +HTML: + +- 掌握网页的基本构成; +- 掌握HTML的基本语法; +- 表格的作用以及合并行、合并列; +- 表单标签的使用,提交方式get/post的区别; +- 框架布局的使用 + +CSS: + +- 掌握CSS的语法及作用,在html中的声明方式; +- 掌握CSS布局的函数使用; +- 掌握CSS外部样式的引入。 + +JavaScript: + +- 掌握JS的语法及作用,在HTML中的声明方式; +- 掌握JS的运行方式; +- 掌握JS中的变量声明、函数声明、参数传递等; +- 掌握HTML中的标签事件使用; +- 掌握JS中的DOM原型 + +jQuery: + +- 了解如何使用jQuery,下载最新版或者老版本的jQuery.js +- 掌握选择器、文档处理、属性、事件等语法及使用; +- 能够灵活使用选择器查找到想要查找的元素并操作他们的属性; +- 动态声明事件; +- 动态创建元素。 + +Servlet + +- 掌握Java中的Web项目目录结构; +- 掌握Java Web项目的重要中间件Tomcat; +- 掌握Servlet中的Request和Response; +- 掌握Servlet的基本运行过程。 +- 掌握Servlet的声明周期 + +Ajax + +- 掌握Ajax的基本概念; +- 掌握jQuery中的Ajax请求; +- 掌握JSON + +Filter、Listener: + +- 掌握Filter和Listener +- 掌握Session过滤器和编码过滤器 + +JSP数据交互 + +- JSP中如何编写Java代码,如何使用Java中的类; +- JSP中的参数传递。 + +状态管理Session和Cookie + +- 掌握Session、Cookie的作用及作用域; +- 掌握Session及Cookie的区别,存储位置,声明周期等; +- 掌握Session及Cookie分别在JSP和Cookie中的使用 + +## 框架 + +主流框架主要有: + +- spring:面向切面、依赖注入。 +- springboot:习惯优于配置、自动配置。目前很多公司内部都是使用Spring Boot。 +- springmvc:基于MVC架构模式的轻量级Web框架 +- Mybatis:orm框架。 +- springcloud + +### Spring + +![](http://img.topjavaer.cn/img/Spring总结.jpg) + +大部分公司都会用到 Spring框架,必学!。主要理解 Spring 面向切面、依赖注入的特性,学会使用 Spring 构建应用程序。推荐书籍《Spring实战》,通过demo的方式带你一步步搭建Spring应用 + +视频推荐尚硅谷王泽老师的Spring5框架最新版教程,视频刚出不久,内容也是与时俱进,值得学习! + +https://www.bilibili.com/video/BV1Vf4y127N5 + +### SpringMVC + +SpringMVC是基于**MVC架构模式**的轻量级Web框架,对于初学者,需要掌握Web请求从发出到相应的这个过程,SpringMVC做了什么,还有MVC模式的思想。 + +视频推荐狂神说Java的SpringMVC最新教程。 + +【狂神说Java】SpringMVC最新教程IDEA版通俗易懂:https://www.bilibili.com/video/BV1aE41167Tu + +### Mybatis + +MyBatis 是一款优秀的持久层框架,MyBatis 帮助我们做了很多事情:建立连接、操作 Statment、ResultSet、处理 JDBC 相关异常等,简化了开发流程。推荐书籍《深入浅出Mybatis》。 + +视频推荐狂神说的Mybatis最新完整教程,b站播放量最高,获得了很多小伙伴的一致好评。 + +https://www.bilibili.com/video/BV1NE411Q7Nx + +### SpringBoot + +学完 SSM,就要进一步学习 SpringBoot 了,相信很多人在学了 Spring 之后,面对各种各样的配置,想必都会头疼。而 SpringBoot 的出现解决了这个问题,SpringBoot 去除了大量的 XML 配置文件,简化了复杂的依赖管理。书籍推荐《Spring Boot实战》。 + +视频推荐尚硅谷雷神的2021版最新SpringBoot2权威教程。 + +https://www.bilibili.com/video/BV1Et411Y7tQ + +### SpringCloud + +现在面试基本都会问到微服务相关的内容,最好了解下微服务相关的知识。服务注册与发现、负载均衡、服务降级、API网关等。推荐书籍《spring cloud微服务实战》 + +视频教程可以看看尚硅谷周阳老师的: + +https://www.bilibili.com/video/BV18E411x7eT + +## 并发 + +![](http://img.topjavaer.cn/img/并发与多线程知识点总结.jpg) + +什么是并发编程,简单来说就是为了充分利用cpu,多个任务同时执行,快速完成任务。 + +并发编程的相关内容可以看看《JAVA并发编程实战》这本书。 + +豆瓣评分9.0,本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则等。 + +![](http://img.topjavaer.cn/img/202305172256703.png) + +本书关于并发编程的细节介绍得非常详细,看得出有很多实践功底,而不是一个理论派,建议每一个学并发的同学看看。 + +视频推荐狂神说Java,很不错的视频: + +https://www.bilibili.com/video/BV1B7411L7tE + +主要知识点有: + +- 线程的概念以及案例 +- 线程池原理 +- 线程间通信方式 +- 锁(synchronized、ReentrantLock) +- 并发工具类(CountDownLatch/CyclicBarrier/Semaphore) +- 原子类 +- AQS +- Thread生命周期状态 +- Java内存模型 + +## Redis + +![](http://img.topjavaer.cn/img/Redis知识点.jpg) + +用来缓存热点数据,加快读写速度,从而提高性能。现在Java后端的面试基本都会问到Redis。 + +书籍推荐《redis实战》和《redis设计与实现》。 + +视频推荐狂神说Java的Redis最新超详细版教程,不仅教你学Redis,还会教你学习的方式。 + +https://www.bilibili.com/video/BV1S54y1R7SB + +## 消息队列 + +消息队列是基础数据结构中`FIFO`的一种数据结构,用来解决应用解耦、异步消息、流量削锋等问题,可以实现高性能、高可用、可伸缩和最终一致性。 + +视频推荐黑马的RocketMQ教程和百知教育的RabbitMQ教程,两者挑一个学习就可以! + +【编程不良人】MQ消息中间件之RabbitMQ: + +https://www.bilibili.com/video/BV1dE411K7MG + +黑马程序员Java教程RocketMQ系统精讲: + +https://www.bilibili.com/video/BV1L4411y7mn + +## JVM + +![](http://img.topjavaer.cn/img/JVM知识点总结.jpg) + +JVM也是面试经常会问的内容。Java开发者不用自己进行内存管理、垃圾回收,JVM帮我们做了,但是还是有必要了解下JVM的工作原理,这样在出现oom等问题的时候,才有思路去排查和解决问题。书籍推荐周老师的《深入理解Java虚拟机》。 + +关于Java虚拟机的书很少,这本书算是我心目中写的最好的关于JVM的书籍了。这本书不光在理论方面具有相当深度,在实操方面也极具实用性,特别是书的第二部分,2--5章全部是讲GC的。真正做到了:理论与实践相结合。叙述方面:深入浅出,行文流畅,言辞优美,读起来非常舒服。 + +![](http://img.topjavaer.cn/img/202305172253765.png) + +视频推荐尚硅谷宋红康的全套课程,全套课程分为三个篇章:《内存与垃圾回收篇》、《字节码与类的加载篇》和《性能监控与调优篇》。 + +尚硅谷JVM全套教程: + +https://www.bilibili.com/video/BV1PJ411n7xZ + +JVM的基础知识: + +- jvm内存结构(程序计数器、虚拟机栈、本地方法栈、堆、方法区、运行时常量池、直接内存) +- 类加载过程 +- 双亲委派 +- 垃圾回收算法 +- 垃圾回收器 +- 调优工具(jsp/jstack/jstat/jmap,了解即可) + +## 计算机基础知识 + +学编程一定要打好计算机基础! + +对于非科班同学来说,与科班同学最大的差距在于**基本理论知识**。如果你是非科班自学编程的,想要进入大厂,那么计算机基础知识一定不能落下。 + +每一个合格的程序员,应该要知道计算机体系的结构,内在的逻辑是什么,要有自己的思考。 + +**总之,基本功非常重要!** + +### 操作系统 + +无论学习什么编程语言,和需要和操作系统打交道。如果对操作系统不熟悉,那么你在未来的学习路上将会遇到很多障碍,比如线程进程调度、内存分配、Java的虚拟机等知识,都会一头雾水。因此,只有把操作系统搞明白了,才能够更好地学习计算机的其他知识。 + +**书籍推荐** + +入门级别书籍:**《现代操作系统》、《操作系统导论》**,进阶:**《深入理解计算机系统》** + +强推《深入理解计算机系统》 这本书。 + +![](http://img.topjavaer.cn/img/20220703132845.jpg) + +CSAPP是一本很好的书,糅合了计算机组成原理、操作系统、网络编程、并行程序设计原理等课程的基础知识。对于刚接触编程,或者像大彬这种非科班出身的人来说,这是一本指导性的书,它会告诉你,要想成为一个优秀的程序员,应当重点理解哪些计算机底层原理,告诉你应该在以后的自学过程中,应该重点学习哪些课程,比如操作系统和体系结构等。 + +**视频教程推荐** + +Udacity的Advanced OS公开课:https://www.classcentral.com/course/udacity-advanced-operating-systems-1016 + +还有国内不错的操作系统的课程,清华大学的公开课:https://www.xuetangx.com/course/THU08091000267/5883104?channel=search_result + +![](http://img.topjavaer.cn/img/20220703132955.png) + +由清华大学两位老师向勇、陈渝讲授,同时配有一套完整的实验,实验内容是从无到有地建立起一个小却五脏俱全的操作系统,以主流操作系统为实例,以教学操作系统ucore为实验环境,讲授操作系统的概念、基本原理和实现技术,为学生从事操作系统软件研究和开发,以及充分利用操作系统功能进行应用软件研究和开发打下扎实的基础。 + +另外,推荐另一门MIT操作系统课程:**MIT6.268** + +课程地址:https://pdos.csail.mit.edu/6.828/2018/schedule.html + +![](http://img.topjavaer.cn/img/20220626115851.png) + +MIT6.828 是一门非常值得学习的课程,广受好评,是**理论与实践相结合的经典**。 + +只要你跟着项目一步一步走,做完 6 个实验,就能实现一个简单的操作系统内核。 + +每个实验都有对应的知识点,学完理论知识后会有相应的练习,学习体验非常棒! + +**建议**在开始学习这门课之前先熟悉C和汇编,对计算机组成有一定了解。 + +**操作系统主要知识点**: + +- 操作系统的基础特征 +- 进程与线程的本质区别、以及各自的使用场景 +- 进程的几种状态 +- 进程通信方法的特点以及使用场景 +- 进程任务调度算法的特点以及使用场景 +- 死锁的原因、必要条件、死锁处理。手写死锁代码、Java是如何解决死锁的。 +- 线程实现的方式 +- 协程的作用 +- 内存管理的方式 +- 虚拟内存的作用,分页系统实现虚拟内存原理 +- 页面置换算法的原理 +- 静态链接和动态链接 + +### 数据结构和算法 + +![](http://img.topjavaer.cn/img/数据结构与算法.jpg) + +为什么学习数据结构与算法?对于计算机专业的同学来说,这门课程是必修的,考研基本也是必考科目。对于程序员来说,数据结构与算法也是面试、笔试必备的非常重要的考察点。 + +数据结构与算法是程序员内功体现的重要标准之一,且数据结构也应用在各个方面。数据结构也蕴含一些面向对象的思想,故学好掌握数据结构对逻辑思维处理抽象能力有很大提升。 + +**书籍推荐** + +**《大话数据结构》和《算法图解》** + +《大话数据结构》 这本书最大的特点是,通篇以一种趣味方式来叙述,大量引用了各种各样的生活知识来类比,并充分运用图形语言来体现抽象内容,对数据结构所涉及到的一些经典算法做到逐行分析、多算法比较。这本书特别适合初学者。 + +![](http://img.topjavaer.cn/img/20220703150022.jpg) + +《算法图解》是非常好的入门算法书,示例丰富,图文并茂,以让人容易理解的方式阐释了算法,旨在帮助程序员在日常项目中更好地发挥算法的能量。 + +很多学Java的同学,可能会问有没有Java版本的数据结构和算法书籍? + +当然有的,可以看看**《数据结构与算法分析 java语言描述》**这本书,用Java语言描述各种数据结构和算法,对于Java开发者来说,更容易理解。 + +**视频推荐** + +**UCSanDiego的数据结构与算法专项课程**:https://www.coursera.org/specializations/algorithms + +**浙大陈越姥姥的数据结构课程**: + +https://www.bilibili.com/video/BV1H4411N7oD + +![](http://img.topjavaer.cn/img/20220703150635.png) + +浙江大学陈越姥姥和何钦铭教授联合授课,非常经典的课程。姥姥我的偶像! + +**小甲鱼的数据结构和算法课程**:https://www.bilibili.com/video/BV1jW411K7yg + +数据结构与算法主要学习以下内容: + +- 基本数据结构(数组、链表、栈、队列等) +- 树(二叉树、avl树、b树、红黑树等) +- 堆结构 +- 排序算法(冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序等及时间空间复杂度) +- 动态规划、回溯、贪心算法(多刷刷leetcode) +- 递归 +- 位运算 + +学完感觉还很吃力?可以借助一些刷题网站巩固下。下面推荐几个刷题网站。 + +### [牛客网](https://www.nowcoder.com/) + +![](http://img.topjavaer.cn/img/20220619223253.png) + +作为牛客红名大佬,来给牛客宣传一波!(牛客打钱!) + +牛客网拥有超级丰富的 IT 题库,题库+面试+学习+求职+讨论,基本涵盖所有面试笔试题型,堪称"互联网求职神器"。在这里不仅可以刷题,还可以跟其他牛友讨论交流,一起成长。牛客上还会各种的内推机会,对于求职的同学也是极其不错的。 + +### [LeetCode](https://leetcode.cn/) + +![](http://img.topjavaer.cn/img/20220619231232.png) + +**力扣,强推**!力扣虐我千百遍,我待力扣如初恋! + +从现在开始,每天一道力扣算法题,坚持几个月的时间,你会感谢我的(傲娇脸) + +我刚开始刷算法题的时候,就选择在力扣上刷。最初刷easy级别题目的时候,都感觉有点吃力,坚持半年之后,遇到中等题目甚至hard级别的题目都不慌了。 + +不过是熟能生巧罢了。 + +### [LintCode](https://www.lintcode.com/) + +![](http://img.topjavaer.cn/img/20220619231320.png) + +与Leetcode类似的刷题网站。 + +LeetCode/LintCode的题目量差不多。LeetCode的**test case比较完备**,并且LeetCode有**讨论区**,看别人的代码还是比较有意义的。 + +LintCode的UI、tagging、filter更加灵活,更有优点,大家选择其中一个进行刷题即可。 + +### 数据库 + +![](http://img.topjavaer.cn/img/MySQL知识点总结.jpg) + +互联网应用大多属于数据密集型应用,对于真实世界的数据密集型应用而言,除非你准备从基础组件的轮子造起,不然根本没那么多机会去摆弄花哨的数据结构和算法。 + +实际生产中,数据表就是数据结构,索引与查询就是算法。而应用代码往往扮演的是胶水的角色,处理IO与业务逻辑,其他大部分工作都是在数据系统之间搬运数据。在最宽泛的意义上,有状态的地方就有数据库。它无所不在,网站的背后、应用的内部,单机软件,区块链里,甚至在离数据库最远的Web浏览器中。 + +**书籍推荐** + +- 《MySQL必知必会》 +- 《高性能mysql》 + +《MySQL必知必会》主要是Mysql的基础语法,很好理解。后面有了基础再看《高性能mysql》,这本书主要讲解索引、SQL优化、高级特性等。 + +![](http://img.topjavaer.cn/img/202305172259165.png) + +非常好的一本书,五星力荐,即使你不是DBA也值得一读。 + +**视频推荐** + +伯克利的 CS168 课程:https://archive.org/details/UCBerkeley_Course_Computer_Science_186 + +![](http://img.topjavaer.cn/img/20220703152850.png) + +国内中国人民大学王珊老师的《数据库系统概论》:https://www.bilibili.com/video/BV1pW411W7Do + +![](http://img.topjavaer.cn/img/20220703153133.png) + +MySQL基础知识: + +- 增删改查 +- 事务特性、隔离级别 +- 索引原理、优化 +- b+树 +- 最左匹配原则 +- 存储引擎 +- MVCC +- 执行计划 +- 分库分表 +- 日志,bin log/undo log/redo log +- ... + +### 计算机网络 + +![](http://img.topjavaer.cn/img/计算机网络知识.jpg) + +计算机网络这门课需要学习计算机网络的概念、原理和体系结构,知道计算机分层结构,物理层、数据链路层、介质访问子层、网络层、传输层和应用层的基本原理和协议,掌握以 TCP/IP 协议族为主的网络协议结构,并且了解网络新技术的最新发展。 + +**书籍推荐** + +《计算机网络自顶向下方法》 + +![](http://img.topjavaer.cn/img/20220703144400.jpg) + +这本书是经典的计算机网络教材,采用作者独创的自顶向下方法来讲授计算机网络的原理及其协议,自第1版出版以来已经被数百所大学和学院选作教材。书中从应用层讲起,然后展开,摆脱了从物理层开始的枯燥,直接接触应用实例,更能吸引读者的兴趣。而且,书上很多例子举的很好,生动形象。 + +**视频推荐** + +视频推荐中科大郑烇、杨坚全套《计算机网络(自顶向下方法 第7版,James F.Kurose,Keith W.Ross)》课程。这门课是2020年秋科大自动化系本科课程录制版,可与中科大学生一起完成专业知识的学习。 + +https://www.bilibili.com/video/BV1JV411t7ow?p=7&vd_source=2b77c4a826e636ae19a4f75a4b2ca146 + +![](http://img.topjavaer.cn/img/20220703143955.png) + +另外还可以看看哈尔滨工业大学李全龙老师的计算机网络课程:https://www.bilibili.com/video/BV1Up411Z7hC + +![](http://img.topjavaer.cn/img/20220703144500.png) + +**计算机网络核心知识点**: + +- 网络分层结构 +- TCP/IP +- 三次握手四次挥手 +- 滑动窗口、拥塞控制 +- HTTP/HTTPS +- 网络安全问题(CSRF、XSS、SQL注入等) + +### linux + +Linux 系统已经渗透到 IT 领域的各个角落,作为一名 IT 从业人员,不管你是专注于编程,还是专注于运维,都应该对 Linux 有所了解,甚至还要深入学习,掌握核心原理。 + +至少要熟悉常用的Linux命令。书籍推荐**《鸟哥的linux私房菜》**。 + +视频推荐: + +https://www.bilibili.com/video/BV1dW411M7xL + +## 设计模式 + +![](http://img.topjavaer.cn/img/设计模式.jpg) + +设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。对于具有丰富的开发经验的开发人员,学习设计模式有助于了解在软件开发过程中所面临的问题的最佳解决方案;对于那些经验不足的开发人员,学习设计模式有助于通过一种简单快捷的方式来学习软件设计。 + +**为什么要学习设计模式**: + +- 设计模式是从许多优秀的软件系统中总结出能够实现可维护性、复用的设计方案,使用这些方案可以避免做一些重复性的工作 +- 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统 +- 学习设计模式将有助于初学者更加深入地理解面向对象思想 + +**设计模式分类**: + +**1.1 创建型模式** + +创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将模块中对象的创建和对象的使用分离。 + +创建型模式包括工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式 + +**1.2 结构型模式** + +结构型模式(Structural Pattern)描述如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。 + +结构型模式包括适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式 + +**1.3 行为型模式** + +行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。 + + 行为型模式包括策略模式、模板模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式等。 + +推荐秦疆老师**基于Java讲解的23种设计模式**视频教程。 + +https://www.bilibili.com/video/BV1mc411h719 + +## 工具 + +### Git + +Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 + +视频推荐:https://www.bilibili.com/video/BV1BE411g7SV + +### Maven + +Maven 是一个软件项目管理工具,可以对 Java 项目进行全自动构建,管理项目所需要的依赖。Maven 也可被用于构建和管理各种项目,例如 C#,Ruby,Scala 和其他语言编写的项目。 + +视频推荐: + +https://www.bilibili.com/video/BV1Ah411S7ZE + +### docker + +Docker 是一个开源的应用容器引擎。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。 + +Docker 是一个用于开发,交付和运行应用程序的开放平台。Docker 使您能够将应用程序与基础架构分开,从而可以快速交付软件。 + +**Docker的应用场景** + +- Web 应用的自动化打包和发布。 +- 自动化测试和持续集成、发布。 +- 在服务型环境中部署和调整数据库或其他的后台应用。 + +视频推荐广州云科的docker入门教程,非常详细。 + +https://www.bilibili.com/video/BV11L411g7U1 + +## 项目 + +很多同学初学Java都会遇到一个问题,不知道去哪里找Java的项目练手。以前我也遇到这个问题,现在在这里分享下一些比较值得学习的项目。 + +首先给大家推荐几个Java项目的**视频教程**,都是B站上的视频,风评很好,讲解也非常详细,有兴趣的可以看一下~ + +尚硅谷尚筹网Java项目实战开发教程: + +https://www.bilibili.com/video/BV1bE411T7oZ + +尚硅谷Java微服务+分布式+全栈项目【尚医通】 + +https://www.bilibili.com/video/BV1V5411K7rT + +Java Web项目实战-畅购商城: + +https://www.bilibili.com/video/BV13J411k7aQ + +下面也推荐几个Github上比较优质的开源项目。 + +### newbee-mall + +star:7.8k + +https://github.com/newbee-ltd/newbee-mall + +newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。 后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。 + +![](http://img.topjavaer.cn/img/image-20210827001950033.png) + +### litemall + +star:16.2k + +https://github.com/linlinjava/litemall + +又一个小商城。litemall = Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端。 + +小商城功能: + +- 首页 +- 专题列表、专题详情 +- 分类列表、分类详情 +- 品牌列表、品牌详情 +- 新品首发、人气推荐 +- 优惠券列表、优惠券选择 +- ... + +![](http://img.topjavaer.cn/img/litemall.png) + +![](http://img.topjavaer.cn/img/litemall01.png) + +在这里也分享一份非常棒的Java学习笔记,**Github标星137k+**!这份笔记主要Java基础、容器、Java IO、并发和虚拟机等内容,排版精良,内容更是无可挑剔。 + +![](https://pic3.zhimg.com/80/v2-cdfb49d3d6562191415ae9771055807a_720w.jpg) + +需要的小伙伴可自行下载: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=100000392&idx=1&sn=f6c8e84651ce48f6ef5b0d496f0f6adf&chksm=4e98ffce79ef76d8dcebdc4787ae8b37760ec193574da9036e46954ae8954ebd56c78792726f#rd + +### eladmin + +star:16.2k + +https://github.com/elunez/eladmin + +一个基于 Spring Boot 2.1.0 、 Spring Boot Jpa、 JWT、Spring Security、Redis、Vue的前后端分离的后台管理系统。项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。 + +项目提供了非常详细的文档,地址是[https://el-admin.vip](https://el-admin.vip/) + +项目体验地址:[https://el-admin.xin](https://el-admin.xin/) + +使用的技术栈也比较新,给作者点赞! + +![](http://img.topjavaer.cn/img/image-20210826235913747.png) + +![](http://img.topjavaer.cn/img/image-20210826235952292.png) + +### vhr + +star:22.2k + +https://github.com/lenve/vhr + +微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。项目加入常见的企业级应用所涉及到的技术点,例如 Redis、RabbitMQ 等。 + +![](http://img.topjavaer.cn/img/vhr01.png) + + + +### Blog + +star1.2k + +https://github.com/zhisheng17/blog + +`My-Blog` 使用的是 Docker + SpringBoot + Mybatis + thymeleaf 打造的一个个人博客模板。此项目在 [Tale](https://github.com/otale/tale) 博客系统基础上进行修改的。 + +![](http://img.topjavaer.cn/img/blog.png) + +### community + +star:1.8k + +https://github.com/codedrinker/community + +码问社区。开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap。 + +![](http://img.topjavaer.cn/img/image-20210826234936711.png) + + + +### vblog + +star:6.5k + +https://github.com/lenve/VBlog + +V部落,Vue+SpringBoot实现的多用户博客管理平台! + +后端主要采用了: + +1.SpringBoot +2.SpringSecurity +3.MyBatis +4.部分接口遵循Restful风格 +5.MySQL + +前端主要采用了: + +1.Vue +2.axios +3.ElementUI +4.vue-echarts +5.mavon-editor +6.vue-router + +![](http://img.topjavaer.cn/img/20220505162337.png) + +### gpmall + +star:4.3k + +https://github.com/2227324689/gpmall + +【咕泡学院实战项目】基于SpringBoot+Dubbo构建的电商平台。业务模块划分,尽量贴合互联网公司的架构体系。所以,除了业务本身的复杂度不是很高之外,整体的架构基本和实际架构相差无几。 + +后端的主要架构是基于springboot+dubbo+mybatis。 + +![](http://img.topjavaer.cn/img/20220505162427.png) + +![](http://img.topjavaer.cn/img/20220505162154.png) + +### guns + +star:3.4k + +https://github.com/stylefeng/Guns + +Guns是一个现代化的Java应用开发框架,基于主流技术Spring Boot2,Guns的核心理念是提高开发人员开发效率,降低企业信息化系统的开发成本,提高企业整体开发水平。 + +Guns基于**插件化架构**,在建设系统时,可以自由组合细粒度模块依赖,实现不同功能的组合和剔除,让项目体积灵活控制,从而更方便地搭建不同的业务系统。 + +使用Guns可以快速开发出各类信息化管理系统,例如OA办公系统、项目管理系统、商城系统、供应链系统、客户关系管理系统等。 + +![](http://img.topjavaer.cn/img/20220505162031.png) + +### music-website + +star:2.3k + +https://github.com/Yin-Hongwei/music-website + +音乐网站。客户端和管理端使用 **Vue** 框架来实现,服务端使用 **Spring Boot + MyBatis** 来实现,数据库使用了 **MySQL**。 + +前端技术栈:Vue3.0 + TypeScript + Vue-Router + Vuex + Axios + ElementPlus + Echarts。 + +![](http://img.topjavaer.cn/img/20220505161944.jpg) + + + +以上就是Java自学的学习路线,内容不少,转行的小伙伴们加油! + + + +另外,上面提到的书籍,我已经整理了电子版,放到github上了,总共**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~(花了一个多月的时间整理的,希望对大家有帮助,欢迎star~) + +仓库持续更新中~ + +![](http://img.topjavaer.cn/img/书单new.png) + +有需要的自取: + +github仓库:https://github.com/Tyson0314/java-books + + + +码字不易,小伙伴们觉得有帮助的话,点个赞呗 你的赞就是我创作的动力! + +我是 [@程序员大彬](https://zhuanlan.zhihu.com/people/a19ae109d127ec8dacde6bdaa3e83c7a) ,定期会分享Java后台硬核知识,欢迎大家关注~ diff --git a/docs/learning-resources/leetcode-note.md b/docs/learning-resources/leetcode-note.md new file mode 100644 index 0000000..2303882 --- /dev/null +++ b/docs/learning-resources/leetcode-note.md @@ -0,0 +1,26 @@ +分享三份LeetCode刷题笔记,分别是**c/c++、Java、Go**版本的,每份笔记都挺详细,内容分别有**字符串、栈队列、树、排序、查找、BFS、DFS、贪心、动态规划**等等,每道题都包含解题思路、最佳解法、拓展题型等。 + +![](http://img.topjavaer.cn/img/image-20221209234446942.png) + +**c/c++版本**: + +![](http://img.topjavaer.cn/img/image-20221208235757286.png) + +**Java版本**: + +![](http://img.topjavaer.cn/img/image-20221208235458520.png) + +**Go版本**: + +![](http://img.topjavaer.cn/img/image-20221209000002484.png) + +正在学习数据结构和算法,或者准备面试的小伙伴们,千万不要错过这几份**宝藏笔记**! + +**手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**力扣**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) + +祝大家学习愉快! + + + diff --git a/docs/learning-resources/mysql-top.md b/docs/learning-resources/mysql-top.md new file mode 100644 index 0000000..35d81fd --- /dev/null +++ b/docs/learning-resources/mysql-top.md @@ -0,0 +1,213 @@ +另外给大家推荐一套MySQL的教程,堪称 MySQL 教程的天花板(附**下载地址**) + +**此教程包含**:6 大范式讲解、7 大日志剖析、7 大 SQL 性能分析工具、9 大存储引擎剖析、10 大类 30 小类优化场景15 个不同锁的应用讲解、18 种创建索引的规则、300+张高清技术剖析图...... + +在学习MySQL的小伙伴千万不要错过! + +## 目录 + +``` +01-MySQL教程简介 +02-为什么使用数据库及数据库常用概念 +03-常见的DBMS的对比 +04-RDBMS和非RDBMS的对比 +05-ER模型与表记录的4种关系 +06-MySQL8.0的卸载 +07-MySQL8.0与5.7版本的下载、安装与配置 +08-MySQL安装常见问题_服务启动与用户登录 +09-MySQL的使用演示_MySQL5.7字符集的设置 +10-Navicat_SQLyog_dbeaver等工具的使用 +11-MySQL目录结构及前2章课后练习 +12-SQL概述与SQL分类 +13-SQL使用规范与数据的导入 +14-最基本的SELECT...FROM结构 +15-列的别名_去重_NULL_DESC等操作 +16-使用WHERE过滤数据 +17-第3章基本SELECT查询课后练习 +18-算术运算符的使用 +19-比较运算符的使用 +20-逻辑运算符与位运算符的使用 +21-第4章运算符课后练习 +22-ORDER BY实现排序操作 +23-LIMIT实现分页操作 +24-第5章排序与分页课后练习 +25-为什么需要多表的查询 +26-笛卡尔积的错误与正确的多表查询 +27-等值连接vs非等值连接、自连接vs非自连接 +28-SQL92与99语法如何实现内连接和外连接 +29-使用SQL99实现7种JOIN操作 +30-NATURAL JOIN与USING的使用 +31-第6章多表查询课后练习 +32-函数的分类 +33-数值类型的函数讲解 +34-字符串类型的函数讲解 +35-日期时间类型的函数讲解 +36-流程控制函数讲解 +37-加密解密_MySQL信息函数等讲解 +38-第7章单行函数课后练习 +39-5大常用的聚合函数 +40-GROUP BY的使用 +41-HAVING的使用与SQL语句执行过程 +42-第8章聚合函数课后练习 +43-子查询举例与子查询的分类 +44-单行子查询案例分析 +45-多行子查询案例分析 +46-相关子查询案例分析 +47-第9章子查询课后练习1 +48-第9章子查询课后练习2 +49-数据库的创建、修改与删除 +50-常见的数据类型_创建表的两种方式 +51-修改表_重命名表_删除表_清空表 +52-DCL中COMMIT与ROLLBACK的使用 +53-阿里MySQL命名规范及MySQL8DDL的原子化 +54-第10章创建管理表课后练习 +55-DML之添加数据 +56-DML之更新删除操作_MySQL8新特性之计算列 +57-DDL和DML的综合案例 +58-第11章增删改课后练习 +59-MySQL数据类型概述_字符集设置 +60-整型数据类型讲解 +61-浮点数、定点数与位类型讲解 +62-日期时间类型讲解 +63-文本字符串类型(含ENUM、SET)讲解 +64-二进制类型与JSON类型讲解 +65-小结及类型使用建议 +66-数据完整性与约束的分类 +67-非空约束的使用 +68-唯一性约束的使用 +69-主键约束的使用 +70-AUTO_INCREMENT +71-外键约束的使用 +72-检查约束与默认值约束 +73-第13章约束课后练习 +74-数据库对象与视图的理解 +75-视图的创建与查看 +76-更新视图数据与视图的删除 +77-第14章视图课后练习 +78-存储过程使用说明 +79-存储过程的创建与调用 +80-存储函数的创建与调用 +81-存储过程与函数的查看修改和删除 +82-第15章存储过程函数课后练习 +83-GLOBAL与SESSION系统变量的使用 +84-会话用户变量与局部变量的使用 +85-程序出错的处理机制 +86-分支结构IF的使用 +87-分支结构CASE的使用 +88-LOOP_WHILE_REPEAT三种循环结构 +89-LEAVE和ITERATE的使用 +90-游标的使用 +91-第16章课后练习 +92-创建触发器 +93-查看删除触发器_触发器课后练习 +94-MySQL8.0新特性_窗口函数的使用 +95-公用表表达式_课后练习_最后寄语 +96-MySQL高级特性篇章节概览 +97-CentOS环境的准备 +98-MySQL的卸载 +99-Linux下安装MySQL8.0与5.7版本 +100-SQLyog实现MySQL8.0和5.7的远程连接 +101-字符集的修改与底层原理说明 +102-比较规则_请求到响应过程中的编码与解码过程 +103-SQL大小写规范与sql_mode的设置 +104-MySQL目录结构与表在文件系统中的表示 +105-用户的创建_修改_删除 +106-用户密码的设置和管理 +107-权限管理与访问控制 +108-角色的使用 +109-配置文件、系统变量与MySQL逻辑架构 +110-SQL执行流程 +111-MySQL8.0和5.7中SQL执行流程的演示 +112-Oracle中SQL执行流程_缓冲池的使用 +113-设置表的存储引擎、InnoDB与MyISAM的对比 +114-Archive、CSV、Memory等存储引擎的使用 +115-为什么使用索引及索引的优缺点 +116-一个简单的索引设计方案 +117-索引的迭代设计方案 +118-聚簇索引、二级索引与联合索引的概念 +119-InnoDB中B+树注意事项_MyISAM的索引方案 +120-Hash索引、AVL树、B树与B+树对比 +121-InnoDB数据存储结构概述 +122-页结构之文件头部与文件尾部 +123-页结构之最小最大记录_行格式之记录头信息 +124-页结构之页目录与页头 +125-设置行格式与ibd文件剖析Compact行格式 +126-行溢出与Dynamic、Compressed、Redundant行格式 +127-区、段、碎片区与表空间结构 +128-索引的分类 +129-表中添加索引的三种方式 +130-删除索引与索引新特性:降序索引、隐藏索引 +131-适合创建索引的11种情况1 +132-适合创建索引的11种情况2 +133-不适合创建索引的7种情况 +134-数据库优化步骤_查看系统性能参数 +135-慢查询日志分析、SHOW PROFILE查看SQL执行成本 +136-EXPLAIN的概述与table、id字段剖析 +137-EXPLAIN中select_type、partitions、type、possible_keys、key、key_len剖析 +138-EXPLAIN中ref、rows、filtered、extra剖析 +139-EXPLAIN的4种格式与查看优化器重写SQL +140-trace分析优化器执行计划与Sys schema视图的使用 +141-数据准备与索引失效的11种情况1 +142-索引失效的11种情况2 +143-外连接与内连接的查询优化 +144-JOIN的底层原理 +145-子查询优化与排序优化 +146-GROUP BY优化、分页查询优化 +147-覆盖索引、字符串的前缀索引 +148-索引条件下推(ICP) +149-普通索引和唯一索引的选择、其它5个优化策略 +150-淘宝数据库的主键如何设计 +151-范式概述与第一范式 +152-第二范式与第三范式 +153-反范式化的应用 +154-巴斯范式、第四范式、第五范式和域键范式 +155-范式的实战案例 +156-ER建模与转换数据表的过程 +157-数据库的设计原则和日常SQL编写规范 +158-PowerDesigner创建概念、物理数据模型 +159-数据库调优整体步骤、优化MySQL服务器硬件和参数 +160-数据库结构优化、大表优化、其它3个策略 +161-事务的ACID特性与事务的状态 +162-显式事务与隐式事务 +163-事务的使用举例 +164-数据并发问题与4种隔离级别 +165-MySQL隔离级别的查看和设置 +166-读未提交隔离性下的演示 +167-读已提交和可重复读的隔离性下的演示 +168-幻读的演示与解决方案 +169-Redo日志和Undo日志的理解、为什么需要Redo日志 +170-Redo日志的刷盘策略与过程剖析 +171-写入Redo Log Buffer和Redo Log File的写入策略 +172-Undo日志的概述与写入过程 +173-锁的概述_读写的并发问题 +174-数据操作类型的角度理解S锁与X锁 +175-表锁之S锁、X锁、意向锁 +176-表锁之自增锁、元数据锁 +177-行锁之记录锁、间隙锁 +178-行锁之临键锁与插入意向锁 +179-页锁的理解、乐观锁与悲观锁的使用 +180-加锁方式划分:隐式锁与显式锁 +181-全局锁与死锁的理解 +182-锁的内存结构与监控策略 +183-MVCC解决读写问题 +184-MVCC三剑客:隐藏字段、UndoLog版本链、ReadView规则 +185-MVCC在读已提交和可重复读隔离级别下的操作流程 +186-MVCC在可重复读下解决幻读的流程 +187-六大日志文件的概述 +188-通用查询日志、错误日志 +189-binlog日志的参数设置与实现数据恢复演示 +190-binlog的删除、binlog的写入机制与两阶段提交 +191-中继日志、主从复制的步骤与原理剖析 +192-一主一从架构搭建与主从同步的实现 +193-binlog的format设置说明 +194-主从延迟问题与数据同步一致性问题解决 +195-数据备份概述与mysqldump实现逻辑备份数据 +196-演示mysql实现逻辑恢复数据 +197-物理备份和物理恢复的演示、表数据的导出与导入 +198-数据库迁移与如何删库不跑路 +199-最后寄语 +``` + +**获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**MySQL**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) \ No newline at end of file diff --git a/docs/learning-resources/mysql45-section.md b/docs/learning-resources/mysql45-section.md new file mode 100644 index 0000000..226a283 --- /dev/null +++ b/docs/learning-resources/mysql45-section.md @@ -0,0 +1,19 @@ +MySQL 使用和面试中遇到的问题,很多人会通过搜索别人的经验来解决 ,零散不成体系。实际上只要理解了 MySQL 的底层工作原理,就能很快地直戳问题的本质。 + +MySQL45讲专栏通过探讨 MySQL 实战中最常见的 **36 个** 痛点问题,串起各个零散的知识点,配合 **100+** 手绘详解图,由线到面带你构建 MySQL 系统的学习路径。 + +专栏共包括两大模块。 + +**模块一,基础篇**。为你深入浅出地讲述 MySQL 核心知识,涵盖 MySQL 基础架构、日志系统、事务隔离、锁等内容。 + +**模块二,实践篇**。将从一个个关键的数据库问题出发,分析数据库原理,并给出实践指导。每个问题,都不只是简单地给出答案,而是从为什么要这么想、到底该怎样做出发,让你能够知其所以然,都将能够解决你平时工作中的一个疑惑点。 + +建议大家都去听听这个专栏,干货满满! + +![](http://img.topjavaer.cn/img/mysql45.jpg) + + + +**手册获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**1003**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) \ No newline at end of file diff --git a/docs/learning-resources/sgg-java-learn.md b/docs/learning-resources/sgg-java-learn.md new file mode 100644 index 0000000..a19a23f --- /dev/null +++ b/docs/learning-resources/sgg-java-learn.md @@ -0,0 +1,33 @@ +尚硅谷的 java 教程,不用多说,在行业内是排前面的,java 相关的所有技术,尚硅谷基本上都有对应的视频教程,这里给大家分享尚硅谷 java 全套教程,200多G,**文末直接获取**。 + +## 全套视频共5部分 + +![](http://img.topjavaer.cn/img/image-20220319142501022.png) + +## 基础必备 + +![](http://img.topjavaer.cn/img/image-20220319142551879.png) + +## 微服务核心 + +![](http://img.topjavaer.cn/img/image-20220319142612142.png) + +## 微服务生态 + +![](http://img.topjavaer.cn/img/image-20220319142638848.png) + +![](http://img.topjavaer.cn/img/image-20220319142651513.png) + +## 项目实战 + +![](http://img.topjavaer.cn/img/image-20220319142705479.png) + +## 选学技术 + +![](http://img.topjavaer.cn/img/image-20220319142723454.png) + +![](http://img.topjavaer.cn/img/image-20220319142734177.png) + +## 获取方式 + +百度云链接: https://pan.baidu.com/s/1F5HJKF9o455_1_oqMhMOEw?pwd=dv9s 提取码: dv9s \ No newline at end of file diff --git a/docs/learning-resources/shang-chou.md b/docs/learning-resources/shang-chou.md new file mode 100644 index 0000000..71c29ed --- /dev/null +++ b/docs/learning-resources/shang-chou.md @@ -0,0 +1,13 @@ +尚筹网项目是一个众筹项目,整个项目分为后台管理与前台会员两大部分。其整体框架图如下: + +![](http://img.topjavaer.cn/img/尚筹网.jpg) + +1、后台管理系统为单一架构,由SSM框架进行实现,同时整合SpringSecurity来对用户权限进行控制。 + +2、前台会员系统 前台会员系统为分布式微服务架构,由SpringBoot+SpringCloud进行实现,同时为了上传项目图片整合了oss服务,以及为了支付整合了阿里的支付API。 + +![](http://img.topjavaer.cn/img/尚筹网1.jpg) + +**获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**尚筹网**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/公众号.jpg) \ No newline at end of file diff --git a/docs/learning-resources/springboot-guide.md b/docs/learning-resources/springboot-guide.md new file mode 100644 index 0000000..3331879 --- /dev/null +++ b/docs/learning-resources/springboot-guide.md @@ -0,0 +1,21 @@ +# Alibaba官方上线!SpringBoot+SpringCloud全彩指南(终极版) + +**Alibaba**作为国内一线互联网大厂,其中**SpringCloudAlibaba**更是阿里微服务最具代表性的技术之一,很多人只知道SpringCloudAlibaba其实面向微服务技术基本上都有的下面就给大家**推荐一份**Alibaba官网最新版:Spring+SpringBoot+SpringCloud微服务全栈开发小册,带你全面掌握Spring全家桶的知识。 + +下面直接给大家展示**目录**: + +**SpringBoot** + +![](http://img.topjavaer.cn/img/springboot-guide1.jpg) + +![](http://img.topjavaer.cn/img/springboot-guide.jpg) + +**SpringCloudAlibaba** + +![](http://img.topjavaer.cn/img/springboot-guide3.jpg) + +![](http://img.topjavaer.cn/img/springboot-guide4.jpg) + +**高清pdf版获取方式**:微信搜索「**程序员大彬**」或者扫描下面的二维码,关注后发送关键字「**1001**」就可以找到下载链接了(**无套路,无解压密码**)。 + +![](http://img.topjavaer.cn/img/image-20221207225029295.png) \ No newline at end of file diff --git a/docs/leetcode/README.md b/docs/leetcode/README.md new file mode 100644 index 0000000..c5a7b72 --- /dev/null +++ b/docs/leetcode/README.md @@ -0,0 +1,40 @@ +## 目录 + +- [1.两数之和-梦开始的地方!!](./hot120/1-two-sum.md) +- [3.无重复字符的最长子串](./hot120/3-longest-substring-without-repeating-characters.md) +- [5.最长回文子串](./hot120/5-longest-palindromic-substring.md) +- [7.整数反转](./hot120/7-reverse-integer.md) +- [15.三数之和](./hot120/15-3sum.md) +- [19.删除链表的倒数第N个结点](./hot120/19-remove-nth-node-from-end-of-list.md) +- [22.括号生成](./hot120/22-generate-parentheses.md) +- [28.对称的二叉树](./hot120/28-mirror-binary-tree.md) +- [40.组合总和II](./hot120/40-combination-sum-ii.md) +- [46.全排列](./hot120/46-permutations.md) +- [47.全排列II](./hot120/47-permutations-ii.md) +- [53.最大子数组和](./hot120/53-maximum-subarray.md) +- [55.跳跃游戏](./hot120/55-jump-game.md) +- [56.合并区间](./hot120/56-merge-intervals.md) +- [62.不同路径](./hot120/62-unique-paths.md) +- [92.反转链表II](./hot120/92-reverse-linked-list-ii.md) +- [98.验证二叉搜索树](./hot120/98-validate-binary-search-tree.md) +- [103.二叉树的锯齿形层序遍历](./hot120/103-binary-tree-zigzag-level-order-traversal.md) +- [104.二叉树的最大深度](./hot120/104-maximum-depth-of-binary-tree.md) +- [120.三角形最小路径和](./hot120/120-triangle.md) +- [121.买卖股票的最佳时机](./hot120/121-best-time-to-buy-and-sell-stock.md) +- [122.买卖股票的最佳时机II](./hot120/122-best-time-to-buy-and-sell-stock-ii.md) +- [131.分割回文串](./hot120/131-palindrome-partion.md) +- [133.克隆图](./hot120/133-clone-graph.md) +- [134.加油站](./hot120/134-gas-station.md) +- [141.环形链表II](./hot120/141-linked-list-cycle.md) +- [152.乘积最大子数组](./hot120/152-maximum-product-subarray.md) +- [160.相交链表](./hot120/160-intersection-of-two-linked-lists.md) +- [169.多数元素](./hot120/169-majority-element.md) +- [199.二叉树的右视图](./hot120/199-binary-tree-right-side-view.md) +- [200.岛屿数量](./hot120/200-number-of-islands.md) +- [206.反转链表](./hot120/206-reverse-linked-list.md) +- [215.数组中的第K个最大元素](./hot120/215-kth-largest-element-in-an-array.md) +- [234.回文链表](./hot120/234-palindrome-linked-list.md) +- [236.二叉树的最近公共祖先](./hot120/236-lowest-common-ancestor-of-a-binary-tree.md) +- [415.字符串相加](./hot120/415-add-strings.md) +- [543.二叉树的直径](./hot120/543-diameter-of-binary-tree.md) +- [1143.最长公共子序列](./hot120/1143-longest-common-subquence.md) diff --git a/docs/leetcode/hot120/1-two-sum.md b/docs/leetcode/hot120/1-two-sum.md new file mode 100644 index 0000000..c1fb3cc --- /dev/null +++ b/docs/leetcode/hot120/1-two-sum.md @@ -0,0 +1,49 @@ +# 1. 两数之和 + +[1. 两数之和](https://leetcode.cn/problems/two-sum/) + +**题目描述** + +给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 + +你可以按任意顺序返回答案。 + +**示例** + +```java +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 +``` + +**解题思路** + +- 遍历数组 nums,i 为当前下标,每个值都判断map中是否存在 `nums[i]` 的 key 值。 + +- 如果存在则找到了两个值,如果不存在则将当前的 `(target - nums[i],i)` 存入 map 中,继续遍历直到找到为止。 + +**参考代码**: + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int[] result = new int[2]; + Map map = new HashMap<>(); + + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])) { + result[0] = map.get(nums[i]); + result[1] = i; + return result; + } + map.put(target - nums[i], i); + } + + return result; + } +} +``` + + diff --git a/docs/leetcode/hot120/103-binary-tree-zigzag-level-order-traversal.md b/docs/leetcode/hot120/103-binary-tree-zigzag-level-order-traversal.md new file mode 100644 index 0000000..103469b --- /dev/null +++ b/docs/leetcode/hot120/103-binary-tree-zigzag-level-order-traversal.md @@ -0,0 +1,69 @@ +# 103. 二叉树的锯齿形层序遍历 + +[103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) + +**题目描述** + +给你二叉树的根节点 `root` ,返回其节点值的 **锯齿形层序遍历** 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 + +**示例** + +``` +输入:root = [3,9,20,null,null,15,7] +输出:[[3],[20,9],[15,7]] +``` + +**解题思路** + +使用两个栈实现。 + +**代码实现** + +```java +class Solution { + public List> zigzagLevelOrder(TreeNode root) { + List> res = new ArrayList<>(); + + //判空 + if (root == null) { + return res; + } + Stack s1 = new Stack<>(); + Stack s2 = new Stack<>(); + + s1.push(root); + + while (!s1.isEmpty() || !s2.isEmpty()) { + List list = new ArrayList<>(); + if (!s1.isEmpty()) { + while (!s1.isEmpty()) { + TreeNode node = s1.pop(); + list.add(node.val); + if (node.left != null) { + s2.push(node.left); + } + if (node.right != null) { + s2.push(node.right); + } + } + } else { + while (!s2.isEmpty()) { + TreeNode node = s2.pop(); + list.add(node.val); + if (node.right != null) { + s1.push(node.right); + } + if (node.left != null) { + s1.push(node.left); + } + } + } + res.add(list); + } + + return res; + } +} +``` + + diff --git a/docs/leetcode/hot120/104-maximum-depth-of-binary-tree.md b/docs/leetcode/hot120/104-maximum-depth-of-binary-tree.md new file mode 100644 index 0000000..d2f3ef0 --- /dev/null +++ b/docs/leetcode/hot120/104-maximum-depth-of-binary-tree.md @@ -0,0 +1,52 @@ +# 104. 二叉树的最大深度 + +[题目链接](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) + +**题目描述** + +给定一个二叉树,找出其最大深度。 + +二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 + +**说明:** 叶子节点是指没有子节点的节点。 + +**示例** + +```java +给定二叉树 [3,9,20,null,null,15,7], + 3 + / \ + 9 20 + / \ + 15 7 +返回它的最大深度 3 。 +``` + +**解题思路** + +找出终止条件:当前节点为空 +找出返回值:节点为空时说明高度为 0,所以返回 0;节点不为空时则分别求左右子树的高度的最大值,同时加1表示当前节点的高度,返回该数值 + +**参考代码**: + +```java +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +class Solution { + public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; + } +} +``` + + diff --git a/docs/leetcode/hot120/11-container-with-most-water.md b/docs/leetcode/hot120/11-container-with-most-water.md new file mode 100644 index 0000000..9c5ed4e --- /dev/null +++ b/docs/leetcode/hot120/11-container-with-most-water.md @@ -0,0 +1,52 @@ +# 11. 盛最多水的容器 + +[11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) + +**题目描述** + +给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 + +找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +返回容器可以储存的最大水量。 + +说明:你不能倾斜容器。 + +![](http://img.topjavaer.cn/img/1586272990587.png) + +**示例** + +```java +输入:[1,8,6,2,5,4,8,3,7] +输出:49 +解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 + +输入:height = [1,1] +输出:1 +``` + +**解题思路** + +左右指针,数字小的指针往数字大的指针移动,面积才有可能变大。注意左右指针数字相同的情况。 + +```java +class Solution { + public int maxArea(int[] height) { + int maxArea = 0; + int left = 0; + int right = height.length - 1; + + while(left < right) { + maxArea = Math.max(maxArea, Math.min(height[left], height[right]) * (right - left)); + if(height[left] > height[right]) { + right--; + } else { + left++; + } + } + + return maxArea; + } +} +``` + diff --git a/docs/leetcode/hot120/1143-longest-common-subquence.md b/docs/leetcode/hot120/1143-longest-common-subquence.md new file mode 100644 index 0000000..d33282c --- /dev/null +++ b/docs/leetcode/hot120/1143-longest-common-subquence.md @@ -0,0 +1,102 @@ +# 1143. 最长公共子序列 + +[原题链接](https://leetcode.cn/problems/longest-common-subsequence/) + +## 题目描述 + +给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 + +一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 + +例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 +两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。 + +示例: + +```java +输入:text1 = "abcde", text2 = "ace" +输出:3 +解释:最长公共子序列是 "ace" ,它的长度为 3 。 +``` + +## 解题思路 + +动态规划。 + +解法一:`dp[i][j]`表示text1以i-1结尾的子串和text2以j-1结尾的子串的最长公共子序列的长度。dp横坐标或纵坐标为0表示空字符串,`dp[0][j] = dp[i][0] = 0`,无需额外处理base case。 + +![](http://img.topjavaer.cn/img/longestCommonSubsequence.png) + +```java +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + char[] arr1 = text1.toCharArray(); + char[] arr2 = text2.toCharArray(); + //dp[0][x]和dp[x][0]表示有一个为空字符串 + //dp[1][1]为text1第一个字符和text2第一个字符的最长公共子序列的长度 + //dp[i][j]表示text1以i-1结尾的子串和text2以j-1结尾的子串的最长公共子序列的长度 + int len1 = arr1.length; + int len2 = arr2.length; + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 1; i < len1 + 1; i++) { + for (int j = 1; j < len2 + 1; j++) { + if (arr1[i - 1] == arr2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[len1][len2]; + } +} +``` + +解法二:`dp[i][j]`表示text1以i结尾的子串和text2以j结尾的子串的最长公共子序列的长度。需要处理base case。 + +```java +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + char[] arr1 = text1.toCharArray(); + char[] arr2 = text2.toCharArray(); + + int len1 = arr1.length; + int len2 = arr2.length; + //`dp[i][j]`表示text1以i结尾的子串和text2以j结尾的子串的最长公共子序列的长度。 + int[][] dp = new int[len1][len2]; + + if (arr1[0] == arr2[0]) { + dp[0][0] = 1; + } + for (int i = 1; i < len1; i++) { + if (arr1[i] == arr2[0]) { + dp[i][0] = 1; + } else { + dp[i][0] = dp[i - 1][0]; + } + } + for (int i = 1; i < len2; i++) { + if (arr1[0] == arr2[i]) { + dp[0][i] = 1; + } else { + dp[0][i] = dp[0][i - 1]; + } + } + + for (int i = 1; i < len1; i++) { + for (int j = 1; j < len2; j++) { + if (arr1[i] == arr2[j]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[len1 - 1][len2 - 1]; + } +} +``` + diff --git a/docs/leetcode/hot120/120-triangle.md b/docs/leetcode/hot120/120-triangle.md new file mode 100644 index 0000000..afea3eb --- /dev/null +++ b/docs/leetcode/hot120/120-triangle.md @@ -0,0 +1,47 @@ +# 120. 三角形最小路径和 + +[题目链接](https://leetcode.cn/problems/triangle/) + +**题目描述** + +给定一个三角形 triangle ,找出自顶向下的最小路径和。 + +每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。 + +**示例** + +```java +输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] +输出:11 +解释:如下面简图所示: + 2 + 3 4 + 6 5 7 +4 1 8 3 +自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 + +来源:力扣(LeetCode) +链接:https://leetcode.cn/problems/triangle +著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 +``` + +**解题思路** + +动态规划。 + +```java +class Solution { + public int minimumTotal(List> triangle) { + int size = triangle.size(); + int[] minLen = new int[size + 1];//+1 + for (int i = size - 1; i >= 0 ; i--) { + for (int j = 0; j <= i; j++) { + minLen[j] = Math.min(minLen[j], minLen[j + 1]) + triangle.get(i).get(j); + } + } + + return minLen[0]; + } +} +``` + diff --git a/docs/leetcode/hot120/121-best-time-to-buy-and-sell-stock.md b/docs/leetcode/hot120/121-best-time-to-buy-and-sell-stock.md new file mode 100644 index 0000000..2042f2f --- /dev/null +++ b/docs/leetcode/hot120/121-best-time-to-buy-and-sell-stock.md @@ -0,0 +1,41 @@ +# 121. 买卖股票的最佳时机 + +[原题链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) + +## 题目描述 + +给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 + +你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 + +返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。 + +**示例**: + +```java +输入:[7,1,5,3,6,4] +输出:5 +解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 + 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 +``` + +## 解题思路 + +动态规划。前i天的最大收益 = max{前i-1天的最大收益,第i天的价格-前i-1天中的最小价格} + +```java +class Solution { + public int maxProfit(int[] prices) { + int minPrice = Integer.MAX_VALUE; + int maxProfit = 0; + for (int i = 0; i < prices.length; i++) { + if (prices[i] < minPrice) { + minPrice = prices[i]; + } + maxProfit = Math.max(prices[i] - minPrice, maxProfit); + } + return maxProfit; + } +} +``` + diff --git a/docs/leetcode/hot120/122-best-time-to-buy-and-sell-stock-ii.md b/docs/leetcode/hot120/122-best-time-to-buy-and-sell-stock-ii.md new file mode 100644 index 0000000..0d5cca4 --- /dev/null +++ b/docs/leetcode/hot120/122-best-time-to-buy-and-sell-stock-ii.md @@ -0,0 +1,43 @@ +# 122. 买卖股票的最佳时机 II + +[题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) + +**题目描述**: + +给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。 + +在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。 + +返回 你能获得的 最大 利润 。 + +**示例**: + +```java +输入:prices = [1,2,3,4,5] +输出:4 +解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 +  总利润为 4 。 +``` + +**解题思路** + +可以尽可能地完成更多的交易,但不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +```java +//输入: [7,1,5,3,6,4] +//输出: 7 +class Solution { + public int maxProfit(int[] prices) { + int profit = 0; + for (int i = 1; i < prices.length; i++) { + int tmp = prices[i] - prices[i - 1]; + if (tmp > 0) { + profit += tmp; + } + } + + return profit; + } +} +``` + diff --git a/docs/leetcode/hot120/128-longest-consecutive-sequence.md b/docs/leetcode/hot120/128-longest-consecutive-sequence.md new file mode 100644 index 0000000..00e2aa2 --- /dev/null +++ b/docs/leetcode/hot120/128-longest-consecutive-sequence.md @@ -0,0 +1,54 @@ +# 128. 最长连续序列 + +[128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/) + +**题目描述**: + +给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 + +请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 + +**示例**: + +```java +输入:nums = [100,4,200,1,3,2] +输出:4 +解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 +``` + +**解题思路** + +将数组存入set。遍历数组,对`num[i]`前后连续的数字进行计数,并将set中的这些数字移除,避免重复计算。 + +比如`[10, 9, 8, 7, 11, 14]`,存入set。遍历第一个元素10,set移除10,10前面有7-8-9,后面有11,连续序列长度为5,将set中的7-8-9-11移除,后续遍历到7-8-9-11,直接跳过。 + +```java +class Solution { + public int longestConsecutive(int[] nums) { + Set set = new HashSet<>(); + for (int num : nums) { + set.add(num); + } + + int len = 0; + int maxLen = 0; + for (int num : nums) { + len = 1; + if (set.remove(num)) { + int cur = num; + while (set.remove(--cur)) { + len++; + } + cur = num; + while (set.remove(++cur)) { + len++; + } + maxLen = maxLen > len ? maxLen : len; + } + } + + return maxLen; + } +} +``` + diff --git a/docs/leetcode/hot120/131-palindrome-partion.md b/docs/leetcode/hot120/131-palindrome-partion.md new file mode 100644 index 0000000..20d92a7 --- /dev/null +++ b/docs/leetcode/hot120/131-palindrome-partion.md @@ -0,0 +1,69 @@ +# 131. 分割回文串 + +[题目链接](https://leetcode.cn/problems/palindrome-partitioning/) + +**题目描述** + +给你一个字符串 `s`,请你将 `s` 分割成一些子串,使每个子串都是 **回文串** 。返回 `s` 所有可能的分割方案。 + +**回文串** 是正着读和反着读都一样的字符串。 + +**示例** + +``` +输入:s = "aab" +输出:[["a","a","b"],["aa","b"]] +``` + +**解题思路** + +[参考题解](https://leetcode-cn.com/problems/palindrome-partitioning/solution/hui-su-you-hua-jia-liao-dong-tai-gui-hua-by-liweiw/) + +使用动态规划预标记出哪一段属于回文串。 + +```java +class Solution { + public List> partition(String s) { + List> res = new ArrayList<>(); + + int len = s.length(); + if (s == null || len == 0) { + return res; + } + + boolean dp[][] = new boolean[len][len]; + char[] charArr = s.toCharArray(); + + for (int i = 0; i < len; i++) { + dp[i][i] = true; + } + + for (int right = 1; right < len; right++) { + for (int left = 0; left < right; left++) { + dp[left][right] = charArr[left] == charArr[right] && (right - left <= 2 || dp[left + 1][right - 1]); + } + } + + LinkedList path = new LinkedList<>(); + dfs(s, 0, res, path, dp); + + return res; + } + + private void dfs(String s, int start, List> res, LinkedList path, boolean[][] dp) { + if (start == s.length()) { + res.add(new ArrayList<>(path)); + return; + } + + for (int i = start; i < s.length(); i++) { + if (dp[start][i]) { + path.addLast(s.substring(start, i + 1)); + dfs(s, i + 1, res, path, dp); + path.removeLast(); + } + } + } +} +``` + diff --git a/docs/leetcode/hot120/133-clone-graph.md b/docs/leetcode/hot120/133-clone-graph.md new file mode 100644 index 0000000..8873b3b --- /dev/null +++ b/docs/leetcode/hot120/133-clone-graph.md @@ -0,0 +1,89 @@ +# 133.克隆图 + +[题目链接](https://leetcode.cn/problems/clone-graph/) + +**题目描述** + +给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。 + +图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。 + +```java +class Node { + public int val; + public List neighbors; +} +``` + +示例: + +```java +输入:adjList = [[2,4],[1,3],[2,4],[1,3]] +输出:[[2,4],[1,3],[2,4],[1,3]] +解释: +图中有 4 个节点。 +节点 1 的值是 1,它有两个邻居:节点 2 和 4 。 +节点 2 的值是 2,它有两个邻居:节点 1 和 3 。 +节点 3 的值是 3,它有两个邻居:节点 2 和 4 。 +节点 4 的值是 4,它有两个邻居:节点 1 和 3 。 +``` + +**解题思路** + +深度优先搜索。 + +```java +class Solution { + public Node cloneGraph(Node node) { + Map lookup = new HashMap<>(); + return dfs(node, lookup); + } + + private Node dfs(Node node, Map lookup) { + if (node == null) return null; + if (lookup.containsKey(node)) return lookup.get(node); + Node clone = new Node(node.val, new ArrayList<>()); + lookup.put(node, clone); + for (Node n : node.neighbors)clone.neighbors.add(dfs(n,lookup)); + return clone; + } +} +``` + +广度优先搜索。遍历图的过程,未访问过的节点放入队列。 + +```java +class Solution { + public Node cloneGraph(Node node) { + return bfs(node, new HashMap()); + } + + private Node bfs(Node root, HashMap visitedMap) { + if (root == null) { + return root; + } + + Queue queue = new ArrayDeque<>(); + queue.offer(root); + Node cloneNode = new Node(root.val, new ArrayList<>()); + visitedMap.put(root, cloneNode); + + while (queue.size() != 0) { + Node node = queue.poll(); + + for (Node n : node.neighbors) { + Node tmp = visitedMap.get(n); + if (tmp == null) { + queue.offer(n); //未访问过的放入队列 + tmp = new Node(n.val, new ArrayList<>()); + visitedMap.put(n, tmp); //标记访问过 + } + visitedMap.get(node).neighbors.add(tmp); + } + } + + return visitedMap.get(root); + } +} +``` + diff --git a/docs/leetcode/hot120/134-gas-station.md b/docs/leetcode/hot120/134-gas-station.md new file mode 100644 index 0000000..7d149db --- /dev/null +++ b/docs/leetcode/hot120/134-gas-station.md @@ -0,0 +1,50 @@ +# 134. 加油站 + +[134. 加油站](https://leetcode.cn/problems/gas-station/) + +**题目描述** + +在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 + +你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 + +给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。 + +**示例** + +```java +输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] +输出: 3 +解释: +从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 +开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 +开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 +开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 +开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 +开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 +因此,3 可为起始索引。 +``` + +**解题思路**: + +1. 遍历一周,总获得的油量少于要花掉的油量必然没有结果; +2. 先苦后甜,记录遍历时所存的油量最少的站点,由于题目有解只有唯一解,所以从当前站点的下一个站点开始是唯一可能成功开完全程的。 + +```java +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int minIdx=0; + int sum=Integer.MAX_VALUE; + int num=0; + for (int i = 0; i < gas.length; i++) { + num+=gas[i]-cost[i]; + if(num 0) { + fast = fast.next; + } + while (fast != slow) { + fast = fast.next; + slow = slow.next; + } + + return fast; + } +} +``` + diff --git a/docs/leetcode/hot120/148-sort-list.md b/docs/leetcode/hot120/148-sort-list.md new file mode 100644 index 0000000..3f13a87 --- /dev/null +++ b/docs/leetcode/hot120/148-sort-list.md @@ -0,0 +1,112 @@ +# 148. 排序链表 + +[148. 排序链表](https://leetcode.cn/problems/sort-list/) + +**题目描述** + +给你链表的头结点 `head` ,请将其按 **升序** 排列并返回 **排序后的链表** 。 + +**示例** + +```java +输入:head = [4,2,1,3] +输出:[1,2,3,4] +``` + +**解题思路** + +1、归并排序。 + +```java +//输入: 4->2->1->3 +//输出: 1->2->3->4 +class Solution { + public ListNode sortList(ListNode head) { + return mergeSort(head); + } + + public ListNode mergeSort(ListNode head) { + if (head == null || head.next == null) { + return head; + } + + ListNode fast = head, slow = head; + while (fast != null && fast.next != null && fast.next.next != null) { + slow = slow.next; + fast = fast.next.next; + } + ListNode rHead = slow.next; + slow.next = null; //断开链表 + + ListNode l = mergeSort(head); + ListNode r = mergeSort(rHead); + + return merge(l, r); + } + + public ListNode merge(ListNode l, ListNode r) { + ListNode tmp = new ListNode(0); + ListNode cur = tmp; + + while (l != null && r != null) { + if (l.val > r.val) { + cur.next = r; + r = r.next; + } else { + cur.next = l; + l = l.next; + } + cur = cur.next; + } + + cur.next = l == null ? r : l; + + return tmp.next; + } +} +``` + +[排序链表快速排序](https://www.cnblogs.com/morethink/p/8452914.html) + +![](http://img.topjavaer.cn/image/sort-list.png) + +```java +//快速排序 +//链表头结点作为基准值key。使用两个指针p1和p2,这两个指针均往next方向移动,移动的过程中保持p1之前的key都小于选定的key,p1和p2之间的key都大于选定的key,那么当p2走到末尾时交换p1与key值便完成了一次切分。 +class Solution { + public ListNode sortList(ListNode head) { + return quickSort(head, null); + } + + private ListNode quickSort(ListNode head, ListNode tail) { + if (head != tail) { + ListNode node = quickSortHelper(head, tail); + quickSort(head, node); + quickSort(node.next, tail); + } + return head; + } + + private ListNode quickSortHelper(ListNode head, ListNode tail) { + ListNode p1 = head, p2 = head.next; + while (p2 != tail) { + if (p2.val < head.val) { + p1 = p1.next; + int tmp = p1.val; + p1.val = p2.val; + p2.val = tmp; + } + p2 = p2.next; + } + + if (p1 != head) { + int tmp = p1.val; + p1.val = head.val; + head.val = tmp; + } + + return p1; + } +} +``` + diff --git a/docs/leetcode/hot120/15-3sum.md b/docs/leetcode/hot120/15-3sum.md new file mode 100644 index 0000000..e24767f --- /dev/null +++ b/docs/leetcode/hot120/15-3sum.md @@ -0,0 +1,62 @@ +# 15. 三数之和 + +[题目链接](https://leetcode.cn/problems/3sum/) + +**题目描述** + +给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。 + +注意:答案中不可以包含重复的三元组。 + +**示例** + +```java +输入:nums = [-1,0,1,2,-1,-4] +输出:[[-1,-1,2],[-1,0,1]] +``` + +**解题思路** + +固定 3 个指针中最左(最小)数字的指针 k,双指针 i,j 分设在数组索引 k 两端,通过双指针交替向中间移动,记录对于每个固定指针 k 的所有满足 nums[k] + nums[i] + nums[j] == 0 的 i,j 组合: + +- 当 nums[k] > 0 时直接break跳出:因为 nums[j] >= nums[i] >= nums[k] > 0,即 3 个数字都大于 0 ,在此固定指针 k 之后不可能再找到结果了。 +- 当 k > 0且nums[k] == nums[k - 1]时即跳过此元素nums[k]:因为已经将 nums[k - 1] 的所有组合加入到结果中,本次双指针搜索只会得到重复组合。 +- i,j 分设在数组索引 (k, len(nums))(k,len(nums)) 两端,当i < j时循环计算s = nums[k] + nums[i] + nums[j],并按照以下规则执行双指针移动: + - 当s < 0时,i += 1并跳过所有重复的nums[i]; + - 当s > 0时,j -= 1并跳过所有重复的nums[j]; + - 当s == 0时,记录组合[k, i, j]至res,执行i += 1和j -= 1并跳过所有重复的nums[i]和nums[j],防止记录到重复组合。 + +**参考代码**: + +```java +class Solution { + public List> threeSum(int[] nums) { + Arrays.sort(nums); + List> res = new ArrayList<>(); + for(int k = 0; k < nums.length - 2; k++){ + if(nums[k] > 0) break; + if(k > 0 && nums[k] == nums[k - 1]) continue; + int i = k + 1, j = nums.length - 1; + while(i < j){ + int sum = nums[k] + nums[i] + nums[j]; + if(sum < 0){ + while(i < j && nums[i] == nums[++i]); + } else if (sum > 0) { + while(i < j && nums[j] == nums[--j]); + } else { + res.add(new ArrayList(Arrays.asList(nums[k], nums[i], nums[j]))); + while(i < j && nums[i] == nums[++i]); + while(i < j && nums[j] == nums[--j]); + } + } + } + return res; + } +} +``` + + + +> 作者:jyd +> 链接:https://leetcode.cn/problems/3sum/solution/3sumpai-xu-shuang-zhi-zhen-yi-dong-by-jyd/ +> 来源:力扣(LeetCode) \ No newline at end of file diff --git a/docs/leetcode/hot120/152-maximum-product-subarray.md b/docs/leetcode/hot120/152-maximum-product-subarray.md new file mode 100644 index 0000000..3d4c41c --- /dev/null +++ b/docs/leetcode/hot120/152-maximum-product-subarray.md @@ -0,0 +1,45 @@ +# 152. 乘积最大子数组 + +[题目链接](https://leetcode.cn/problems/maximum-product-subarray/) + +**题目描述** + +给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。 + +测试用例的答案是一个 32-位 整数。 + +子数组 是数组的连续子序列。 + +**示例** + +```java +输入: nums = [2,3,-2,4] +输出: 6 +解释: 子数组 [2,3] 有最大乘积 6。 +``` + +**解题思路** + +动态规划。 + +```java +class Solution { + public int maxProduct(int[] nums) { + int curMax = 1, curMin = 1;//保证i=1时,结果正确 + int max = Integer.MIN_VALUE; + for (int i = 0; i < nums.length; i++) { + if (nums[i] < 0) { + int tmp = curMax; + curMax = curMin; + curMin = tmp; + } + curMax = Math.max(curMax, curMax * nums[i]); + curMin = Math.min(curMin, curMin * nums[i]); + + max = Math.max(max, curMax);//[2,-2],当i=1时,curMax与curMin交换,curMax=1,max=2,故取max与curMax中最大值 + } + return max; + } +} +``` + diff --git a/docs/leetcode/hot120/160-intersection-of-two-linked-lists.md b/docs/leetcode/hot120/160-intersection-of-two-linked-lists.md new file mode 100644 index 0000000..e613359 --- /dev/null +++ b/docs/leetcode/hot120/160-intersection-of-two-linked-lists.md @@ -0,0 +1,39 @@ +# 160. 相交链表 + +[题目链接](https://leetcode.cn/problems/intersection-of-two-linked-lists/) + +**题目描述** + +给你两个单链表的头节点 `headA` 和 `headB` ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 `null` 。 + +**示例** + +```java +输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3 +输出:Intersected at '8' +解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。 +从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。 +在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 +— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。 +``` + +**解题思路** + +两个指针a和b,a指向headA,b指向headB,两个指针同时出发。假如a先走到尽头,则a重新指向headB。然后b走到尽头,b重新指向headA。如果是相交链表,则a和b会相遇,否则a/b最终会指向null。 + +```java +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode a = headA; + ListNode b = headB; + + while (a != b) { + a = a == null ? headB : a.next; + b = b == null ? headA : b.next; + } + + return a; + } +} +``` + diff --git a/docs/leetcode/hot120/169-majority-element.md b/docs/leetcode/hot120/169-majority-element.md new file mode 100644 index 0000000..2390101 --- /dev/null +++ b/docs/leetcode/hot120/169-majority-element.md @@ -0,0 +1,47 @@ +# 169. 多数元素 + +[169. 多数元素](https://leetcode.cn/problems/majority-element/) + +**题目描述** + +给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 + +你可以假设数组是非空的,并且给定的数组总是存在多数元素。 + +**示例** + +``` +输入:nums = [3,2,3] +输出:3 +``` + +**解题思路** + +**摩尔投票法** + +从第一个数开始count=1,遇到相同的就加1,遇到不同的就减1,减到0就重新换个数开始计数,总能找到最多的那个。 + +**代码实现** + +```java +class Solution { + public int majorityElement(int[] nums) { + int ret = nums[0]; + int count = 1; + for(int num : nums) { + if(num != ret) { + count--; + if(count == 0) { + count = 1; + ret = num; + } + } + else + count++; + } + return ret; + } +} +``` + + diff --git a/docs/leetcode/hot120/18-4sum .md b/docs/leetcode/hot120/18-4sum .md new file mode 100644 index 0000000..92ccdf0 --- /dev/null +++ b/docs/leetcode/hot120/18-4sum .md @@ -0,0 +1,76 @@ +# 18. 四数之和 + +[18. 四数之和](https://leetcode.cn/problems/4sum/) + +**题目描述** + +给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复): + +- 0 <= a, b, c, d < n +- a、b、c 和 d 互不相同 +- nums[a] + nums[b] + nums[c] + nums[d] == target + +你可以按 任意顺序 返回答案 。 + +**示例** + +```java +输入:nums = [1,0,-1,0,-2,2], target = 0 +输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]] + +输入:nums = [2,2,2,2,2], target = 8 +输出:[[2,2,2,2]] +``` + +**解题思路** + +**参考代码**: + +```java +class Solution { + public List> fourSum(int[] nums, int target) { + List> ans = new ArrayList<>(); + if (nums == null || nums.length < 4) { + return ans; + } + int len = nums.length; + + Arrays.sort(nums); + + for (int i = 0; i < len; i++) { + if (i > 0 && nums[i] == nums[i - 1]) { // forgot, 去重复 + continue; + } + for (int j = i + 1; j < len; j++) { + if (j > i + 1 && nums[j] == nums[j - 1]) {//j大于i+1才去重复 + continue; + } + int left = j + 1; + int right = len - 1; + while (left < right) { + int sum = nums[i] + nums[j] + nums[left] + nums[right]; + if (sum == target) { + ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); + while (left < right && nums[left] == nums[left + 1]) { + left++;//去重复 + } + while (left < right && nums[right] == nums[right - 1]) { + right--;//去重复 + } + left++;//forgot + right--;//forgot + } else if (sum < target) { + left++; + } else { + right--; + } + } + } + } + + return ans; + } +} +``` + + diff --git a/docs/leetcode/hot120/19-remove-nth-node-from-end-of-list.md b/docs/leetcode/hot120/19-remove-nth-node-from-end-of-list.md new file mode 100644 index 0000000..aad2a7b --- /dev/null +++ b/docs/leetcode/hot120/19-remove-nth-node-from-end-of-list.md @@ -0,0 +1,55 @@ +# 19. 删除链表的倒数第 N 个结点 + +[原题链接](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) + +**题目描述** + +给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。 + +**示例** + +```java +输入:head = [1,2,3,4,5], n = 2 +输出:[1,2,3,5] +``` + +**解题思路** + +我们可以设想假设设定了双指针 p 和 q 的话,当 q 指向末尾的 NULL,p 与 q 之间相隔的元素个数为 n 时,那么删除掉 p 的下一个指针就完成了要求。 + +- 设置虚拟节点 dummyHead 指向 head +- 设定双指针 p 和 q,初始都指向虚拟节点 dummyHead +- 移动 q,直到 p 与 q 之间相隔的元素个数为 n +- 同时移动 p 与 q,直到 q 指向的为 NULL +- 将 p 的下一个节点指向下下个节点 + +![](http://img.topjavaer.cn/img/删除链表nth.gif) + +> 作者:cxywushixiong +> 链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/solution/dong-hua-tu-jie-leetcode-di-19-hao-wen-ti-shan-chu/ +> 来源:力扣(LeetCode) + +**代码实现** + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode dummy = new ListNode(-1); + dummy.next = head; + ListNode pre = dummy; + ListNode slow = head; + ListNode fast = head; + for(int i=0;i rightSideView(TreeNode root) { + List res = new ArrayList<>(); + if (root == null) { + return res; + } + + Queue queue = new ArrayDeque<>(); + queue.offer(root); + + while (!queue.isEmpty()) { + int size = queue.size(); + while (size-- > 0) { //先取size的值,再自减 + TreeNode node = queue.poll(); + if (node.left != null) { + queue.offer(node.left); + } + if (node.right != null) { + queue.offer(node.right); + } + if (size == 0) { //最右边的节点 + res.add(node.val); + } + } + } + + return res; + } +} +``` + diff --git a/docs/leetcode/hot120/20-valid-parentheses.md b/docs/leetcode/hot120/20-valid-parentheses.md new file mode 100644 index 0000000..62b4db6 --- /dev/null +++ b/docs/leetcode/hot120/20-valid-parentheses.md @@ -0,0 +1,56 @@ +# 20. 有效的括号 + +[20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) + +**题目描述** + +给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。 + +有效字符串需满足: + +- 左括号必须用相同类型的右括号闭合。 +- 左括号必须以正确的顺序闭合。 +- 每个右括号都有一个对应的相同类型的左括号。 + +**示例** + +```java +输入:s = "()" +输出:true + +输入:s = "()[]{}" +输出:true +``` + +**解题思路** + +使用栈实现。 + +```java +class Solution { + public boolean isValid(String s) { + Stack stack = new Stack<>(); + + for (int i = 0; i < s.length(); i++) { + switch (s.charAt(i)) { + case '(': + stack.push(')');//存进相反符号 + break; + case '[': + stack.push(']'); + break; + case '{': + stack.push('}'); + break; + default: + if (stack.isEmpty() || s.charAt(i) != stack.pop()) { + return false; + } + break; + } + } + return stack.isEmpty(); + } +} +``` + diff --git a/docs/leetcode/hot120/200-number-of-islands.md b/docs/leetcode/hot120/200-number-of-islands.md new file mode 100644 index 0000000..d44c3a1 --- /dev/null +++ b/docs/leetcode/hot120/200-number-of-islands.md @@ -0,0 +1,65 @@ +# 200. 岛屿数量 + +[题目链接](https://leetcode.cn/problems/number-of-islands/) + +**题目描述** + +给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 + +此外,你可以假设该网格的四条边均被水包围。 + +**示例** + +```java +输入:grid = [ + ["1","1","1","1","0"], + ["1","1","0","1","0"], + ["1","1","0","0","0"], + ["0","0","0","0","0"] +] +输出:1 +``` + +**解题思路** + +深度优先遍历,使用isVisited数组记录元素是否被访问过。 + +```java +class Solution { + public int numIslands(char[][] grid) { + if (grid == null || grid.length == 0 || grid[0].length == 0) { + return 0; + } + + boolean[][] isVisited = new boolean[grid.length][grid[0].length]; + int count = 0; + + for (int i = 0; i < grid.length; i++) { + for (int j = 0; j < grid[0].length; j++) { + if (grid[i][j] == '1' && !isVisited[i][j]) { + dfs(grid, isVisited, i, j); + count++; + } + } + } + + return count; + } + + private void dfs(char[][] grid, boolean[][] isVisited, int i, int j) { + if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') { + return; + } + if (!isVisited[i][j]) { + isVisited[i][j] = true; + dfs(grid, isVisited, i + 1, j); + dfs(grid, isVisited, i - 1, j); + dfs(grid, isVisited, i, j + 1); + dfs(grid, isVisited, i, j - 1); + } + } +} +``` + diff --git a/docs/leetcode/hot120/206-reverse-linked-list.md b/docs/leetcode/hot120/206-reverse-linked-list.md new file mode 100644 index 0000000..4618e34 --- /dev/null +++ b/docs/leetcode/hot120/206-reverse-linked-list.md @@ -0,0 +1,44 @@ +# 206. 反转链表 + +[原题链接](https://leetcode.cn/problems/reverse-linked-list/) + +**题目描述** + +给你单链表的头节点 `head` ,请你反转链表,并返回反转后的链表。 + +**示例** + +```java +输入:head = [1,2,3,4,5] +输出:[5,4,3,2,1] +``` + +**解题思路**: + +1. 定义两个指针,第一个指针叫 pre,最初是指向 null 的。 +2. 第二个指针 cur 指向 head,然后不断遍历 cur。 +3. 每次迭代到 cur,都将 cur 的 next 指向 pre,然后 pre 和 cur 前进一位。 +4. 都迭代完了(cur 变成 null 了),pre 就是最后一个节点了。 + +```java +class Solution { + public ListNode reverseList(ListNode head) { + if (head == null) { + return null; + } + + ListNode pre = null; + ListNode cur = head; + ListNode tmp = null; + while (cur != null) { + tmp = cur.next; + cur.next = pre; + pre = cur; + cur = tmp; + } + + return pre; + } +} +``` + diff --git a/docs/leetcode/hot120/21-merge-two-sorted-lists.md b/docs/leetcode/hot120/21-merge-two-sorted-lists.md new file mode 100644 index 0000000..dd1ffc7 --- /dev/null +++ b/docs/leetcode/hot120/21-merge-two-sorted-lists.md @@ -0,0 +1,41 @@ +# 21. 合并两个有序链表 + +[21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) + +**题目描述** + +将两个升序链表合并为一个新的 **升序** 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 + +**示例** + +```java +输入:l1 = [1,2,4], l2 = [1,3,4] +输出:[1,1,2,3,4,4] +``` + +**参考代码** + +```java +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode ans = new ListNode(0); + ListNode tmp = ans; + + while (l1 != null && l2!= null) { + if (l1.val > l2.val) { + tmp.next = l2; //tmp.next = new ListNode(l2.val); 没必要这么做 + l2 = l2.next; + } else { + tmp.next = l1; + l1 = l1.next; + } + tmp = tmp.next; + } + + tmp.next = l1 == null ? l2 : l1; + + return ans.next; + } +} +``` + diff --git a/docs/leetcode/hot120/215-kth-largest-element-in-an-array.md b/docs/leetcode/hot120/215-kth-largest-element-in-an-array.md new file mode 100644 index 0000000..dd22f4a --- /dev/null +++ b/docs/leetcode/hot120/215-kth-largest-element-in-an-array.md @@ -0,0 +1,94 @@ +# 215. 数组中的第K个最大元素 + +[题目链接](https://leetcode.cn/problems/kth-largest-element-in-an-array/) + +**题目描述** + +给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。 + +请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。 + +你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。 + +**示例** + +```java +输入: [3,2,1,5,6,4], k = 2 +输出: 5 +``` + +**解题思路** + +利用快速排序,每次得到pivot的下标,与(arr.length-1)比较,相等则为所求元素。 + +```java +class Solution { + public int findKthLargest(int[] nums, int k) { + int left = 0; + int right = nums.length - 1; + int index = nums.length - k; + + while (left < right) { + int pivot = partition(nums, left, right); + if (pivot == index) { + return nums[pivot]; + } else if (index > pivot) { + left = pivot + 1; + } else { + right = pivot - 1; + } + } + + return nums[left]; + } + + private int partition(int[] nums, int left, int right) { + int i = left, j = right; + median3(nums, left, right); + int pivot = nums[left]; + + while (i < j) { + while (i < j && nums[j] >= pivot) { + j--; + } + + while (i < j && nums[i] <= pivot) { + i++; + } + + if (i < j) { + swap(nums, i, j); + } else { + break; + } + } + + nums[left] = nums[j]; + nums[j] = pivot; + + return j; + } + + private void median3(int[] nums, int left, int right) { + int mid = (left + right) / 2; + if (nums[left] > nums[mid]) { + swap(nums, left, mid); + } + if (nums[left] > nums[right]) { + swap(nums, left, right); + } + if (nums[mid] > nums[right]) { + swap(nums, mid, right); + } + + swap(nums, left, mid); + } + + private void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } +} +``` + diff --git a/docs/leetcode/hot120/22-generate-parentheses.md b/docs/leetcode/hot120/22-generate-parentheses.md new file mode 100644 index 0000000..f5f1eed --- /dev/null +++ b/docs/leetcode/hot120/22-generate-parentheses.md @@ -0,0 +1,105 @@ +22. # 22. 括号生成 + +[22. 括号生成](https://leetcode.cn/problems/generate-parentheses/) + +**题目描述** + +数字 `n` 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 **有效的** 括号组合。 + +**示例** + +```java +输入:n = 3 +输出:["((()))","(()())","(())()","()(())","()()()"] +``` + +**解题思路** + +深度优先算法。 + +> 作者:liweiwei1419 +> 链接:https://leetcode.cn/problems/generate-parentheses/solution/hui-su-suan-fa-by-liweiwei1419/ +> 来源:力扣(LeetCode) +> 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +如果完成一件事情有很多种方法,并且每一种方法分成若干步骤,那多半就可以使用“回溯”算法完成。 + +“回溯”算法的基本思想是“尝试搜索”,一条路如果走不通(不能得到想要的结果),就回到上一个“路口”,尝试走另一条路。 + +因此,“回溯”算法的时间复杂度一般不低。如果能提前分析出,走这一条路并不能得到想要的结果,可以跳过这个分支,这一步操作叫“剪枝”。 + +做“回溯”算法问题的基本套路是: + +1、使用题目中给出的示例,画树形结构图,以便分析出递归结构; + +一般来说,树形图不用画完,就能够分析出递归结构和解题思路。 + +2、分析一个结点可以产生枝叶的条件、递归到哪里终止、是否可以剪枝、符合题意的结果在什么地方出现(可能在叶子结点,也可能在中间的结点); + +3、完成以上两步以后,就要编写代码实现上述分析的过程,使用代码在画出的树形结构上搜索符合题意的结果。 + +在树形结构上搜索结果集,使用的方法是执行一次“深度优先遍历”。在遍历的过程中,可能需要使用“状态变量”。 + +我们以 `n = 2` 为例,画树形结构图。 + +![](http://img.topjavaer.cn/img/括号生成.jpg) + +画图以后,可以分析出的结论: + +- 左右都有可以使用的括号数量,即严格大于 0 的时候,才产生分支; +- 左边不受右边的限制,它只受自己的约束; +- 右边除了自己的限制以外,还收到左边的限制,即:右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以“节外生枝”; +- 在左边和右边剩余的括号数都等于 0 的时候结算。 + +参考代码如下: + +```java +import java.util.ArrayList; +import java.util.List; + +public class Solution { + + // 做减法 + + public List generateParenthesis(int n) { + List res = new ArrayList<>(); + // 特判 + if (n == 0) { + return res; + } + + // 执行深度优先遍历,搜索可能的结果 + dfs("", n, n, res); + return res; + } + + /** + * @param curStr 当前递归得到的结果 + * @param left 左括号还有几个可以使用 + * @param right 右括号还有几个可以使用 + * @param res 结果集 + */ + private void dfs(String curStr, int left, int right, List res) { + // 因为每一次尝试,都使用新的字符串变量,所以无需回溯 + // 在递归终止的时候,直接把它添加到结果集即可,注意与「力扣」第 46 题、第 39 题区分 + if (left == 0 && right == 0) { + res.add(curStr); + return; + } + + // 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节) + if (left > right) { + return; + } + + if (left > 0) { + dfs(curStr + "(", left - 1, right, res); + } + + if (right > 0) { + dfs(curStr + ")", left, right - 1, res); + } + } +} +``` + diff --git a/docs/leetcode/hot120/234-palindrome-linked-list.md b/docs/leetcode/hot120/234-palindrome-linked-list.md new file mode 100644 index 0000000..c14efe2 --- /dev/null +++ b/docs/leetcode/hot120/234-palindrome-linked-list.md @@ -0,0 +1,56 @@ +# 234. 回文链表 + +[234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/) + +**题目描述** + +给你一个单链表的头节点 `head` ,请你判断该链表是否为回文链表。如果是,返回 `true` ;否则,返回 `false` 。 + +**示例** + +```java +给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。 +``` + +**解题思路** + +快慢指针。 + +快指针走到末尾,慢指针刚好到中间。其中慢指针将前半部分反转。然后比较。 + +**参考代码**: + +```java + public boolean isPalindrome(ListNode head) { + if(head == null || head.next == null) { + return true; + } + ListNode slow = head, fast = head; + ListNode pre = head, prepre = null; + while(fast != null && fast.next != null) { + pre = slow; + slow = slow.next; + fast = fast.next.next; + pre.next = prepre; + prepre = pre; + } + if(fast != null) { + slow = slow.next; + } + while(pre != null && slow != null) { + if(pre.val != slow.val) { + return false; + } + pre = pre.next; + slow = slow.next; + } + return true; + } + +作者:nuan +链接:https://leetcode.cn/problems/palindrome-linked-list/solution/wo-de-kuai-man-zhi-zhen-du-cong-tou-kai-shi-gan-ju/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 +``` + + diff --git a/docs/leetcode/hot120/236-lowest-common-ancestor-of-a-binary-tree.md b/docs/leetcode/hot120/236-lowest-common-ancestor-of-a-binary-tree.md new file mode 100644 index 0000000..4c6463a --- /dev/null +++ b/docs/leetcode/hot120/236-lowest-common-ancestor-of-a-binary-tree.md @@ -0,0 +1,41 @@ +# 236. 二叉树的最近公共祖先 + +[题目链接](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) + +**题目描述** + +给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 + +百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。 + +**示例** + +```java +输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +输出:3 +解释:节点 5 和节点 1 的最近公共祖先是节点 3 。 +``` + +**解题思路** + +[后序遍历](https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/solution/236-er-cha-shu-de-zui-jin-gong-gong-zu-xian-hou-xu/) + +```java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null || root == p || root == q) { + return root; + } + + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + + if (left != null && right != null) { + return root; + } else { + return left == null ? right : left; + } + } +} +``` + diff --git a/docs/leetcode/hot120/24-swap-nodes-in-pairs.md b/docs/leetcode/hot120/24-swap-nodes-in-pairs.md new file mode 100644 index 0000000..cc158f9 --- /dev/null +++ b/docs/leetcode/hot120/24-swap-nodes-in-pairs.md @@ -0,0 +1,42 @@ +# 24. 两两交换链表中的节点 + +[两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/) + +**题目描述** + +给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。 + +![](http://img.topjavaer.cn/img/swap_ex1.jpg) + +**示例** + +```java +输入:head = [1,2,3,4] +输出:[2,1,4,3] +``` + +**解题思路** + +使用递归实现。 + +```java +class Solution { + public ListNode swapPairs(ListNode head) { + if (head == null) { + return head; + } + ListNode left = head; + ListNode right = head.next; + + if (right == null) { + right = left; + } else { + left.next = swapPairs(right.next); + right.next = left; + } + + return right; + } +} +``` + diff --git a/docs/leetcode/hot120/26-remove-duplicates-from-sorted-array.md b/docs/leetcode/hot120/26-remove-duplicates-from-sorted-array.md new file mode 100644 index 0000000..9c53f38 --- /dev/null +++ b/docs/leetcode/hot120/26-remove-duplicates-from-sorted-array.md @@ -0,0 +1,56 @@ +# 26. 删除有序数组中的重复项 + +[题目链接](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) + +**题目描述** + +给你一个 **升序排列** 的数组 `nums` ,请你**[ 原地](http://baike.baidu.com/item/原地算法)** 删除重复出现的元素,使每个元素 **只出现一次** ,返回删除后数组的新长度。元素的 **相对顺序** 应该保持 **一致** 。 + +由于在某些语言中不能改变数组的长度,所以必须将结果放在数组nums的第一部分。更规范地说,如果在删除重复项之后有 `k` 个元素,那么 `nums` 的前 `k` 个元素应该保存最终结果。 + +将最终结果插入 `nums` 的前 `k` 个位置后返回 `k` 。 + +不要使用额外的空间,你必须在 **[原地 ](https://baike.baidu.com/item/原地算法)修改输入数组** 并在使用 O(1) 额外空间的条件下完成。 + +**示例**: + +```java +输入:nums = [1,1,2] +输出:2, nums = [1,2,_] +解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 +``` + +**参考代码** + +```java +class Solution { + public int removeDuplicates(int[] nums) { + if (nums == null || nums.length == 0) { + return 0; + } + int left = 0; + int right = 1; + int count = 1; + + while (right < nums.length) { + if (nums[right] == nums[left]) { + if (count < 2) { + nums[++left] = nums[right++]; + } else { + right++; + } + count++; + } else { + count = 1; + nums[++left] = nums[right++]; + } + } + return left + 1; + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/27-remove-element.md b/docs/leetcode/hot120/27-remove-element.md new file mode 100644 index 0000000..9efd62c --- /dev/null +++ b/docs/leetcode/hot120/27-remove-element.md @@ -0,0 +1,44 @@ +# 27. 移除元素 + +[移除元素](https://leetcode.cn/problems/remove-element/) + +**题目描述** + +给你一个数组 `nums` 和一个值 `val`,你需要 **[原地](https://baike.baidu.com/item/原地算法)** 移除所有数值等于 `val` 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 `O(1)` 额外空间并 **[原地 ](https://baike.baidu.com/item/原地算法)修改输入数组**。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + +**示例** + +```java +输入:nums = [3,2,2,3], val = 3 +输出:2, nums = [2,2] +解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。 +``` + +**解题思路** + +![](http://img.topjavaer.cn/img/image-20221106172125452.png) + +> 作者:画手大鹏 +> 链接:https://leetcode.cn/problems/remove-element/solutions/10388/hua-jie-suan-fa-27-yi-chu-yuan-su-by-guanpengchn/ +> 来源:力扣(LeetCode) +> 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +```java +class Solution { + public int removeElement(int[] nums, int val) { + int ans = 0; + for(int num: nums) { + if(num != val) { + nums[ans] = num; + ans++; + } + } + return ans; + } +} +``` + diff --git a/docs/leetcode/hot120/28-mirror-binary-tree.md b/docs/leetcode/hot120/28-mirror-binary-tree.md new file mode 100644 index 0000000..b1b1128 --- /dev/null +++ b/docs/leetcode/hot120/28-mirror-binary-tree.md @@ -0,0 +1,44 @@ +# 28. 对称的二叉树 + +[28. 对称的二叉树](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/) + +**题目描述** + +请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。 + +例如,二叉树 [1,2,2,3,4,4,3] 是对称的。 + +**示例** + +```java +输入:root = [1,2,2,3,4,4,3] +输出:true +``` + +**解题思路** + +从顶至底递归,判断每对节点是否对称,从而判断树是否为对称二叉树。 + +```java +class Solution { + public boolean isSymmetric(TreeNode root) { + if (root == null) { + return true; + } + return isMirror(root.left, root.right); + } + + public boolean isMirror(TreeNode node1, TreeNode node2) { + if (node1 == null && node2 == null) { + return true; + } + + if (node1 == null || node2 == null || node1.val != node2.val) { + return false; + } + + return isMirror(node1.left, node2.right) && isMirror(node1.right, node2.left); + } +} +``` + diff --git a/docs/leetcode/hot120/29-divide-two-integers.md b/docs/leetcode/hot120/29-divide-two-integers.md new file mode 100644 index 0000000..2f08002 --- /dev/null +++ b/docs/leetcode/hot120/29-divide-two-integers.md @@ -0,0 +1,65 @@ +# 29. 两数相除 + +[29. 两数相除](https://leetcode.cn/problems/divide-two-integers/) + +**题目描述** + +给定两个整数,被除数 `dividend` 和除数 `divisor`。将两数相除,要求不使用乘法、除法和 mod 运算符。 + +返回被除数 `dividend` 除以除数 `divisor` 得到的商。 + +整数除法的结果应当截去(`truncate`)其小数部分,例如:`truncate(8.345) = 8` 以及 `truncate(-2.7335) = -2` + +**示例** + +```java +输入: dividend = 10, divisor = 3 +输出: 3 +解释: 10/3 = truncate(3.33333..) = truncate(3) = 3 +``` + +**解题思路** + +Integer.MIN_VALUE 转为正数会溢出,故将 dividend 和 divisor 都转化为负数。**两个负数相加溢出会大于0**。 + +```java +class Solution { + //dividend / divisor + public int divide(int dividend, int divisor) { + if (dividend == Integer.MIN_VALUE && divisor == -1) { + return Integer.MAX_VALUE; + } + if (divisor == 1) { //不加上会超时 + return dividend; + } + int sign = 1; + if ((dividend < 0 && divisor > 0) || (dividend > 0 && divisor < 0)) { + sign = -1; + } + int a = dividend > 0 ? -dividend : dividend; + int b = divisor > 0 ? -divisor : divisor; + if (a > b) { + return 0; + } + int ans = divideHelper(a, b); + + return sign > 0 ? ans : -ans; + } + + private int divideHelper(int a, int b) { + if (a > b) { + return 0; + } + + int count = 1; + int tmp = b; + while (tmp + tmp >= a && tmp + tmp < 0) { //两个负数相加溢出会大于0 + tmp += tmp; + count += count; + } + + return count + divideHelper(a - tmp, b); + } +} +``` + diff --git a/docs/leetcode/hot120/3-longest-substring-without-repeating-characters.md b/docs/leetcode/hot120/3-longest-substring-without-repeating-characters.md new file mode 100644 index 0000000..53b36f7 --- /dev/null +++ b/docs/leetcode/hot120/3-longest-substring-without-repeating-characters.md @@ -0,0 +1,59 @@ +# 3 . 无重复字符的最长子串 + +[题目链接](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) + +**题目描述** + +给定一个字符串 `s` ,请你找出其中不含有重复字符的 **最长子串** 的长度。 + +**示例** + +``` +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +``` + +**解题思路** + +这道题主要用到思路是:滑动窗口 + +**什么是滑动窗口**? + +其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列! + +**如何移动**? + +我们只要把队列的左边的元素移出就行了,直到满足题目要求! + +一直维持这样的队列,找出队列出现最长的长度时候,求出解! + +时间复杂度:O(n) + +**代码实现** + +```java +class Solution { + public int lengthOfLongestSubstring(String s) { + if (s.length()==0) return 0; + HashMap map = new HashMap(); + int max = 0; + int left = 0; + for(int i = 0; i < s.length(); i ++){ + if(map.containsKey(s.charAt(i))){ + left = Math.max(left,map.get(s.charAt(i)) + 1); + } + map.put(s.charAt(i),i); + max = Math.max(max,i-left+1); + } + return max; + + } +} +``` + + + +> 作者:powcai +> 链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/ +> 来源:力扣(LeetCode) \ No newline at end of file diff --git a/docs/leetcode/hot120/31-next-permutation.md b/docs/leetcode/hot120/31-next-permutation.md new file mode 100644 index 0000000..6cef380 --- /dev/null +++ b/docs/leetcode/hot120/31-next-permutation.md @@ -0,0 +1,53 @@ +# 31. 下一个排列 + +[31. 下一个排列](https://leetcode.cn/problems/next-permutation/) + +**题目描述** + +整数数组的一个 **排列** 就是将其所有成员以序列或线性顺序排列。 + +- 例如,`arr = [1,2,3]` ,以下这些都可以视作 `arr` 的排列:`[1,2,3]`、`[1,3,2]`、`[3,1,2]`、`[2,3,1]` 。 + +整数数组的 **下一个排列** 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 **下一个排列** 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。 + +- 例如,`arr = [1,2,3]` 的下一个排列是 `[1,3,2]` 。 +- 类似地,`arr = [2,3,1]` 的下一个排列是 `[3,1,2]` 。 +- 而 `arr = [3,2,1]` 的下一个排列是 `[1,2,3]` ,因为 `[3,2,1]` 不存在一个字典序更大的排列。 + +给你一个整数数组 `nums` ,找出 `nums` 的下一个排列。 + +必须**[ 原地 ](https://baike.baidu.com/item/原地算法)**修改,只允许使用额外常数空间。 + +**示例** + +```java +输入:nums = [1,2,3] +输出:[1,3,2] +``` + +**参考代码** + +```java +class Solution { + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + ListNode ans = new ListNode(0); + ListNode tmp = ans; + + while (l1 != null && l2!= null) { + if (l1.val > l2.val) { + tmp.next = l2; //tmp.next = new ListNode(l2.val); 没必要这么做 + l2 = l2.next; + } else { + tmp.next = l1; + l1 = l1.next; + } + tmp = tmp.next; + } + + tmp.next = l1 == null ? l2 : l1; + + return ans.next; + } +} +``` + diff --git a/docs/leetcode/hot120/34-find-first-and-last-position-of-element-in-sorted-array.md b/docs/leetcode/hot120/34-find-first-and-last-position-of-element-in-sorted-array.md new file mode 100644 index 0000000..cebdf3e --- /dev/null +++ b/docs/leetcode/hot120/34-find-first-and-last-position-of-element-in-sorted-array.md @@ -0,0 +1,56 @@ +# 34. 在排序数组中查找元素的第一个和最后一个位置 + +[34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) + +**题目描述** + +给你一个按照非递减顺序排列的整数数组 `nums`,和一个目标值 `target`。请你找出给定目标值在数组中的开始位置和结束位置。 + +如果数组中不存在目标值 `target`,返回 `[-1, -1]`。 + +你必须设计并实现时间复杂度为 `O(log n)` 的算法解决此问题。 + +**示例** + +```java +输入:nums = [5,7,7,8,8,10], target = 8 +输出:[3,4] +``` + +**参考代码** + +```java +//输入: nums = [5,7,7,8,8,10], target = 8 +//输出: [3,4] + +class Solution { + public int[] searchRange(int[] nums, int target) { + if (nums == null || nums.length == 0) { + return new int[]{-1, -1}; + } + int left = searchRangeHelper(nums, target); + int right = searchRangeHelper(nums, target + 1);//复用代码 + + if (left == nums.length || nums[left] != target) {//left==nums.length数组元素比target小 + return new int[]{-1, -1}; + } + return new int[]{left, right - 1}; + } + + private int searchRangeHelper(int[] nums, int target) { + int left = 0; + int right = nums.length;//特殊情况[1] 1 + while (left < right) { + int mid = (left + right) >> 1; + if (nums[mid] < target) { + left = mid + 1; + } else { + right = mid; + } + } + + return left; + } +} +``` + diff --git a/docs/leetcode/hot120/36-valid-sudoku.md b/docs/leetcode/hot120/36-valid-sudoku.md new file mode 100644 index 0000000..8e23fda --- /dev/null +++ b/docs/leetcode/hot120/36-valid-sudoku.md @@ -0,0 +1,75 @@ +# 36. 有效的数独 + +[36. 有效的数独](https://leetcode.cn/problems/valid-sudoku/) + +**题目描述** + +请你判断一个 `9 x 9` 的数独是否有效。只需要 **根据以下规则** ,验证已经填入的数字是否有效即可。 + +1. 数字 `1-9` 在每一行只能出现一次。 +2. 数字 `1-9` 在每一列只能出现一次。 +3. 数字 `1-9` 在每一个以粗实线分隔的 `3x3` 宫内只能出现一次。(请参考示例图) + +**注意:** + +- 一个有效的数独(部分已被填充)不一定是可解的。 +- 只需要根据以上规则,验证已经填入的数字是否有效即可。 +- 空白格用 `'.'` 表示。 + +**示例** + +![](http://img.topjavaer.cn/img/sudoku.png) + +```java +输入:board = +[["5","3",".",".","7",".",".",".","."] +,["6",".",".","1","9","5",".",".","."] +,[".","9","8",".",".",".",".","6","."] +,["8",".",".",".","6",".",".",".","3"] +,["4",".",".","8",".","3",".",".","1"] +,["7",".",".",".","2",".",".",".","6"] +,[".","6",".",".",".",".","2","8","."] +,[".",".",".","4","1","9",".",".","5"] +,[".",".",".",".","8",".",".","7","9"]] +输出:true +``` + +**解题思路** + +关键在于找到子数独的规律:`box_index = (row / 3) * 3 + columns / 3` + +![图片来源于网络,出处不详](http://img.topjavaer.cn/image/1587260486363.png) + +```java +class Solution { + public boolean isValidSudoku(char[][] board) { + int[][] row = new int[9][9]; //二维数组初始化 + int[][] column = new int[9][9]; + int[][] box = new int[9][9]; + + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + if (board[i][j] == '.') { + continue; + } + int num = board[i][j] - '1';//数字1-9对应下标0-8 + int boxIndex = (i/3)*3 + j/3; + + if (row[i][num] > 0 || column[j][num] > 0 || box[boxIndex][num] > 0) { + return false; + } + + row[i][num] = 1; + column[j][num] = 1; + box[boxIndex][num] = 1; + + } + } + + return true; + } +} +``` + + + diff --git a/docs/leetcode/hot120/40-combination-sum-ii.md b/docs/leetcode/hot120/40-combination-sum-ii.md new file mode 100644 index 0000000..f861837 --- /dev/null +++ b/docs/leetcode/hot120/40-combination-sum-ii.md @@ -0,0 +1,68 @@ +# 40. 组合总和 II + +[40. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/) + +**题目描述** + +给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的每个数字在每个组合中只能使用 一次 。 + +注意:解集不能包含重复的组合。 + +**示例**: + +``` +输入: candidates = [2,5,2,1,2], target = 5, +输出: +[ +[1,2,2], +[5] +] +``` + +**解题思路** + +数组元素可能重复。使用回溯算法。 + +剪枝: + +![](http://img.topjavaer.cn/img/1587051935261.png) + +去重复组合: + +![](http://img.topjavaer.cn/img/1587050948930.png) + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> combinationSum2(int[] candidates, int target) { + if (candidates == null || candidates.length == 0) { + return ans; + } + Arrays.sort(candidates);//排序方便回溯剪枝 + Deque path = new ArrayDeque<>();//作为栈来使用,效率高于Stack;也可以作为队列来使用,效率高于LinkedList;线程不安全 + combinationSum2Helper(candidates, target, 0, path); + return ans; + } + + public void combinationSum2Helper(int[] arr, int target, int start, Deque path) { + if (target == 0) { + ans.add(new ArrayList(path)); + } + + for (int i = start; i < arr.length; i++) { + if (target < arr[i]) {//剪枝 + return; + } + if (i > start && arr[i] == arr[i - 1]) {//在一个层级,会产生重复 + continue; + } + path.addLast(arr[i]); + combinationSum2Helper(arr, target - arr[i], i + 1, path); + path.removeLast(); + } + } +} +``` + diff --git a/docs/leetcode/hot120/415-add-strings.md b/docs/leetcode/hot120/415-add-strings.md new file mode 100644 index 0000000..8f10fce --- /dev/null +++ b/docs/leetcode/hot120/415-add-strings.md @@ -0,0 +1,48 @@ +# 415. 字符串相加 + +[415. 字符串相加](https://leetcode.cn/problems/add-strings/) + +**题目描述** + +给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。 + +你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。 + +**示例** + +```java +输入:num1 = "11", num2 = "123" +输出:"134" +``` + +**解题思路** + +![image-20221029095055775](http://img.topjavaer.cn/img/image-20221029095055775.png) + +**参考代码**: + +```java +class Solution { + public String addStrings(String num1, String num2) { + StringBuilder res = new StringBuilder(""); + int i = num1.length() - 1, j = num2.length() - 1, carry = 0; + while(i >= 0 || j >= 0){ + int n1 = i >= 0 ? num1.charAt(i) - '0' : 0; + int n2 = j >= 0 ? num2.charAt(j) - '0' : 0; + int tmp = n1 + n2 + carry; + carry = tmp / 10; + res.append(tmp % 10); + i--; j--; + } + if(carry == 1) res.append(1); + return res.reverse().toString(); + } +} + +作者:jyd +链接:https://leetcode.cn/problems/add-strings/solution/add-strings-shuang-zhi-zhen-fa-by-jyd/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 +``` + + diff --git a/docs/leetcode/hot120/43-multiply-strings.md b/docs/leetcode/hot120/43-multiply-strings.md new file mode 100644 index 0000000..f275530 --- /dev/null +++ b/docs/leetcode/hot120/43-multiply-strings.md @@ -0,0 +1,53 @@ +# 43. 字符串相乘 + +[43. 字符串相乘](https://leetcode.cn/problems/multiply-strings/) + +**题目描述** + +给定两个以字符串形式表示的非负整数 `num1` 和 `num2`,返回 `num1` 和 `num2` 的乘积,它们的乘积也表示为字符串形式。 + +**注意:**不能使用任何内置的 BigInteger 库或直接将输入转换为整数。 + +**示例** + +```java +输入: num1 = "2", num2 = "3" +输出: "6" +``` + +**解题思路** + +参考自:https://leetcode-cn.com/problems/multiply-strings/solution/you-hua-ban-shu-shi-da-bai-994-by-breezean/ + +![](http://img.topjavaer.cn/img/1587226241610.png) + +```java +class Solution { + public String multiply(String num1, String num2) { + if (num1.equals("0") || num2.equals("0")) { //乘0 + return "0"; + } + int[] arr = new int[num1.length() + num2.length()]; + for (int i = num1.length() - 1; i >= 0; i--) { + int m = num1.charAt(i) - '0'; + for (int j = num2.length() - 1; j >= 0; j--) { + int n = num2.charAt(j) - '0'; + int sum = arr[i + j + 1] + m * n; + arr[i + j + 1] = sum % 10; + arr[i + j] += sum / 10; //+=,不能忘了原先的数 + } + } + + StringBuilder ans = new StringBuilder(""); + if (arr[0] != 0) { + ans.append(arr[0]); + } + for (int i = 1; i < arr.length; i++) { + ans.append(arr[i]); + } + + return ans.toString(); + } +} +``` + diff --git a/docs/leetcode/hot120/46-permutations.md b/docs/leetcode/hot120/46-permutations.md new file mode 100644 index 0000000..0186dd7 --- /dev/null +++ b/docs/leetcode/hot120/46-permutations.md @@ -0,0 +1,49 @@ +# 46. 全排列 + +[题目链接](https://leetcode.cn/problems/permutations/) + +**题目描述** + +给定一个不含重复数字的数组 `nums` ,返回其 *所有可能的全排列* 。你可以 **按任意顺序** 返回答案。 + +示例: + +``` +输入:nums = [1,2,3] +输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] +``` + +**解题思路** + +使用回溯。注意与组合总和的区别(数字有无顺序)。 + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> permute(int[] nums) { + boolean[] flag = new boolean[nums.length]; + ArrayDeque path = new ArrayDeque<>(); + permuteHelper(nums, flag, path); + + return ans; + } + + private void permuteHelper(int[] nums, boolean[] flag, ArrayDeque path) { + if (path.size() == nums.length) { + ans.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (flag[i]) { + continue;//继续循环 + } + path.addLast(nums[i]); + flag[i] = true; + permuteHelper(nums, flag, path); + path.removeLast(); + flag[i] = false; + } + } +} +``` + diff --git a/docs/leetcode/hot120/47-permutations-ii.md b/docs/leetcode/hot120/47-permutations-ii.md new file mode 100644 index 0000000..3920ec8 --- /dev/null +++ b/docs/leetcode/hot120/47-permutations-ii.md @@ -0,0 +1,63 @@ +# 47. 全排列 II + +[题目链接](https://leetcode.cn/problems/permutations-ii/) + +**题目描述** + +给定一个可包含重复数字的序列,返回所有不重复的全排列。注意与组合总和的区别。 + +示例: + +```java +输入:nums = [1,2,3] +输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] +``` + +**解题思路** + +1、排序; + +2、同一层级相同元素剪枝。 + +> 参考自:https://leetcode-cn.com/problems/permutations-ii/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liwe-2/ + +![](http://img.topjavaer.cn/img/permutations-ii.png) + +```java +class Solution { + private List> ans = new ArrayList<>(); + public List> permuteUnique(int[] nums) { + if (nums == null || nums.length == 0) { + return ans; + } + ArrayDeque path = new ArrayDeque<>(); + boolean[] used = new boolean[nums.length]; + Arrays.sort(nums);//切记 + dps(nums, used, path); + + return ans; + } + + private void dps(int[] nums, boolean[] used, ArrayDeque path) { + if (path.size() == nums.length) { + ans.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + if (used[i]) { + continue; + } + if ((i > 0 && nums[i] == nums[i - 1]) && !used[i - 1]) {//同一层相同的元素,剪枝 + continue;//继续循环,不是return退出循环 + } + path.addLast(nums[i]); + used[i] = true; + dps(nums, used, path); + path.removeLast(); + used[i] = false; + } + } +} +``` + + diff --git a/docs/leetcode/hot120/48-rotate-image.md b/docs/leetcode/hot120/48-rotate-image.md new file mode 100644 index 0000000..821984e --- /dev/null +++ b/docs/leetcode/hot120/48-rotate-image.md @@ -0,0 +1,49 @@ +# 48. 旋转图像 + +[48. 旋转图像](https://leetcode.cn/problems/rotate-image/) + +**题目描述** + +给定一个 *n* × *n* 的二维矩阵 `matrix` 表示一个图像。请你将图像顺时针旋转 90 度。 + +你必须在**[ 原地](https://baike.baidu.com/item/原地算法)** 旋转图像,这意味着你需要直接修改输入的二维矩阵。**请不要** 使用另一个矩阵来旋转图像。 + +**示例** + +![](http://img.topjavaer.cn/img/mat1.jpg) + +```java +输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +输出:[[7,4,1],[8,5,2],[9,6,3]] +``` + +**解题思路** + +先转置,然后翻转每一行。 + +```java +class Solution { + public void rotate(int[][] matrix) { + if (matrix == null || matrix.length <= 1) { + return; + } + //转置 + for (int i = 0; i < matrix.length; i++) { + for (int j = i + 1; j < matrix.length; j++) { + int tmp = matrix[i][j]; + matrix[i][j] = matrix[j][i]; + matrix[j][i] = tmp; + } + } + //翻转每一行 + for (int i = 0; i < matrix.length; i++) { + for (int j = 0; j < matrix.length / 2; j++) { + int tmp = matrix[i][j]; + matrix[i][j] = matrix[i][matrix.length - j - 1]; + matrix[i][matrix.length - j - 1] = tmp; + } + } + } +} +``` + diff --git a/docs/leetcode/hot120/49-group-anagrams.md b/docs/leetcode/hot120/49-group-anagrams.md new file mode 100644 index 0000000..13ac24b --- /dev/null +++ b/docs/leetcode/hot120/49-group-anagrams.md @@ -0,0 +1,44 @@ +# 49. 字母异位词分组 + +[49. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/description/) + +**题目描述** + +给你一个字符串数组,请你将 **字母异位词** 组合在一起。可以按任意顺序返回结果列表。 + +**字母异位词** 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。 + +**示例** + +```java +输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] +输出: [["bat"],["nat","tan"],["ate","eat","tea"]] +``` + +**解题思路** + +由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。 + +```java +class Solution { + public List> groupAnagrams(String[] strs) { + if (strs == null || strs.length == 0) { + return new ArrayList(); + } + + Map ans = new HashMap<>(); + for (String s : strs) { + char[] arr = s.toCharArray(); + Arrays.sort(arr); + String key = String.valueOf(arr); + if (!ans.containsKey(key)) { + ans.put(key, new ArrayList()); + } + ans.get(key).add(s); + } + + return new ArrayList(ans.values());//没有<> + } +} +``` + diff --git a/docs/leetcode/hot120/5-longest-palindromic-substring.md b/docs/leetcode/hot120/5-longest-palindromic-substring.md new file mode 100644 index 0000000..530133a --- /dev/null +++ b/docs/leetcode/hot120/5-longest-palindromic-substring.md @@ -0,0 +1,68 @@ +# 5. 最长回文子串 + +[原题链接](https://leetcode.cn/problems/longest-palindromic-substring/) + +## 题目描述 + +从给定的字符串 `s` 中找到最长的回文子串的长度。 + +例如 `s = "babbad"` 的最长回文子串是 `"abba"` ,长度是 `4` 。 + +## 解题思路 + +1. 定义状态。`dp[i][j]` 表示子串 `s[i..j]` 是否为回文子串 + +2. 状态转移方程:`dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]` +3. 初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 `true`,即 `dp[i][i] = true` 。 +4. 只要一得到 `dp[i][j] = true`,就记录子串的长度和起始位置 + +注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。 + +![](http://img.topjavaer.cn/img/image-20201115230411764.png) + +时间复杂度O(N2),空间复杂度O(N2),因为使用了二维数组。 + +```java +public class Solution { + + public String longestPalindrome(String s) { + // 特判 + int len = s.length(); + if (len < 2) { + return s; + } + + int maxLen = 1; + int begin = 0; + + // dp[i][j] 表示 s[i, j] 是否是回文串 + boolean[][] dp = new boolean[len][len]; + char[] charArray = s.toCharArray(); + + for (int i = 0; i < len; i++) { + dp[i][i] = true; + } + for (int j = 1; j < len; j++) { + for (int i = 0; i < j; i++) { + if (charArray[i] != charArray[j]) { + dp[i][j] = false; + } else { + if (j - i < 3) { + dp[i][j] = true; + } else { + dp[i][j] = dp[i + 1][j - 1]; + } + } + + // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置 + if (dp[i][j] && j - i + 1 > maxLen) { + maxLen = j - i + 1; + begin = i; + } + } + } + return s.substring(begin, begin + maxLen); //substring(i, j)截取i到j(不包含j)的字符串 + } +} +``` + diff --git a/docs/leetcode/hot120/50-powx-n.md b/docs/leetcode/hot120/50-powx-n.md new file mode 100644 index 0000000..961ed8c --- /dev/null +++ b/docs/leetcode/hot120/50-powx-n.md @@ -0,0 +1,41 @@ +# 50. Pow(x, n) + +[50. Pow(x, n)](https://leetcode.cn/problems/powx-n/) + +**题目描述** + +实现 pow(x, n) ,即计算 `x` 的整数 `n` 次幂函数(即,`x^n` )。 + +**示例** + +```java +输入:x = 2.00000, n = 10 +输出:1024.00000 +``` + +**解题思路** + +快速幂算法。 + +```java +class Solution { + public double myPow(double x, int n) { + if (n == 0) { + return 1.0; + } + if (n == -1) {//负数边界 + return 1 / x; + } + double res = myPow(x, n / 2); + double ans = 0; + if (n % 2 == 0) { + ans = res * res; + } else { + ans = n > 0 ? res * res * x : res * res / x; + } + + return ans; + } +} +``` + diff --git a/docs/leetcode/hot120/53-maximum-subarray.md b/docs/leetcode/hot120/53-maximum-subarray.md new file mode 100644 index 0000000..4fc330f --- /dev/null +++ b/docs/leetcode/hot120/53-maximum-subarray.md @@ -0,0 +1,37 @@ +# 53. 最大子数组和 + +[原题链接](https://leetcode.cn/problems/maximum-subarray/) + +## 题目描述 + +给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +子数组 是数组中的一个连续部分。 + + ```java +示例 1: + +输入:nums = [-2,1,-3,4,-1,2,1,-5,4] +输出:6 +解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。 + ``` + +## 解题 + +```java +class Solution { + public int maxSubArray(int[] nums) { + int res = nums[0]; + int sum = 0; + for (int num : nums) { + if (sum > 0) + sum += num; + else + sum = num; + res = Math.max(res, sum); + } + return res; + } +} +``` + diff --git a/docs/leetcode/hot120/54-spiral-matrix.md b/docs/leetcode/hot120/54-spiral-matrix.md new file mode 100644 index 0000000..85eeff0 --- /dev/null +++ b/docs/leetcode/hot120/54-spiral-matrix.md @@ -0,0 +1,69 @@ +# 54. 螺旋矩阵 + +[54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) + +**题目描述** + +给你一个 `m` 行 `n` 列的矩阵 `matrix` ,请按照 **顺时针螺旋顺序** ,返回矩阵中的所有元素。 + +**示例** + +```java +输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +输出:[1,2,3,6,9,8,7,4,5] +``` + +**解题思路** + +- 首先设定上下左右边界 +- 其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界 +- 判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案 +- 若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理 +- 不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案 + +> 作者:YouLookDeliciousC +> 链接:https://leetcode.cn/problems/spiral-matrix/solutions/7155/cxiang-xi-ti-jie-by-youlookdeliciousc-3/ + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + List ans = new ArrayList<>(); + if (matrix == null || matrix.length == 0) { + return ans; + } + int top = 0; + int bottom = matrix.length - 1; + int left = 0; + int right = matrix[0].length - 1; + while (true) { + for (int i = left; i <= right; i++) { + ans.add(matrix[top][i]); + } + if (++top > bottom) { + break; + } + for (int j = top; j <= bottom; j++) { + ans.add(matrix[j][right]); + } + if (--right < left) { + break; + } + for (int m = right; m >= left; m--) { + ans.add(matrix[bottom][m]); + } + if (--bottom < top) { + break; + } + for (int n = bottom; n >= top; n--) { + ans.add(matrix[n][left]); + } + if (++left > right) { + break; + } + } + + return ans; + } +} +``` + diff --git a/docs/leetcode/hot120/543-diameter-of-binary-tree.md b/docs/leetcode/hot120/543-diameter-of-binary-tree.md new file mode 100644 index 0000000..32ecbaf --- /dev/null +++ b/docs/leetcode/hot120/543-diameter-of-binary-tree.md @@ -0,0 +1,47 @@ +# 543. 二叉树的直径 + +[题目链接](https://leetcode.cn/problems/diameter-of-binary-tree/) + +**题目描述** + +给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。 + +**示例** + +```java + 1 + / \ + 2 3 + / \ + 4 5 + +返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。 +``` + +**解题思路** + +**深度优先搜索** + +```java +class Solution { + private int max = 0; + + public int diameterOfBinaryTree(TreeNode root) { + depth(root); + return max; + } + + private int depth(TreeNode node) { + if (node == null) { + return 0; + } + + int left = depth(node.left); + int right = depth(node.right); + + max = Math.max(max, left + right); + return Math.max(left, right) + 1; + } +} +``` + diff --git a/docs/leetcode/hot120/55-jump-game.md b/docs/leetcode/hot120/55-jump-game.md new file mode 100644 index 0000000..d060cc9 --- /dev/null +++ b/docs/leetcode/hot120/55-jump-game.md @@ -0,0 +1,47 @@ +# 55. 跳跃游戏 + +[题目链接](https://leetcode.cn/problems/jump-game/) + +**题目描述** + +给定一个非负整数数组 `nums` ,你最初位于数组的 **第一个下标** 。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个下标。 + +**示例**: + +```java +输入:nums = [2,3,1,1,4] +输出:true +解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 +``` + +**解题思路**: + +1. 如果某一个作为 起跳点 的格子可以跳跃的距离是 3,那么表示后面 3 个格子都可以作为 起跳点 +2. 可以对每一个能作为 起跳点 的格子都尝试跳一次,把 能跳到最远的距离 不断更新 +3. 如果可以一直跳到最后,就成功了 + +```java +class Solution { + public boolean canJump(int[] nums) { + if (nums == null || nums.length == 0) { + return true; + } + + int maxIndex = nums[0]; + for (int i = 1; i < nums.length; i++) { + if (maxIndex >= i) { + maxIndex = Math.max(maxIndex, i + nums[i]); + } else { + return false; + } + } + + return true; + } +} +``` + diff --git a/docs/leetcode/hot120/56-merge-intervals.md b/docs/leetcode/hot120/56-merge-intervals.md new file mode 100644 index 0000000..e9c6892 --- /dev/null +++ b/docs/leetcode/hot120/56-merge-intervals.md @@ -0,0 +1,43 @@ +# 56. 合并区间 + +[56. 合并区间](https://leetcode.cn/problems/merge-intervals/) + +**题目描述** + +以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 + +**示例** + +```java +输入:intervals = [[1,3],[2,6],[8,10],[15,18]] +输出:[[1,6],[8,10],[15,18]] +解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. +``` + +**解题思路** + +先对区间左边界排序 `Array.sort(arr, (i1, i2) -> i1[0] - i2[0])`,然后新建数组进行合并。 + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals == null || intervals.length == 0) { + throw new IllegalArgumentException("array is null or array is empty"); + } + + Arrays.sort(intervals, (a, b) -> a[0] - b[0]); //lambda表达式写法,返回值是int + + int index = 0; + for (int i = 1; i < intervals.length; i++) { + if (intervals[index][1] < intervals[i][0]) { + intervals[++index] = intervals[i]; //++index,先自增,再取值 + } else { + intervals[index][1] = Math.max(intervals[i][1], intervals[index][1]); + } + } + + return Arrays.copyOf(intervals, index + 1); //第二个参数是数组长度 + } +} +``` + diff --git a/docs/leetcode/hot120/59-spiral-matrix-ii.md b/docs/leetcode/hot120/59-spiral-matrix-ii.md new file mode 100644 index 0000000..245bf86 --- /dev/null +++ b/docs/leetcode/hot120/59-spiral-matrix-ii.md @@ -0,0 +1,60 @@ +# 59. 螺旋矩阵II + +[题目链接](https://leetcode.cn/problems/spiral-matrix-ii/) + +**题目描述** + +给你一个正整数 `n` ,生成一个包含 `1` 到 `n2` 所有元素,且元素按顺时针顺序螺旋排列的 `n x n` 正方形矩阵 `matrix` 。 + +**示例** + +```java +输入:n = 3 +输出:[[1,2,3],[8,9,4],[7,6,5]] +``` + +**解题思路** + +tips:定义上下左右边界。 + +```java +class Solution { + public int[][] generateMatrix(int n) { + int left = 0; + int right = n - 1; + int top = 0; + int bottom = n - 1; + + int[][] ans = new int[n][n]; + int num = 1; + while (true) { + for (int i = left; i <= right; i++) { + ans[top][i] = num++;//注意下标顺序 + } + if (++top > bottom) { + break; + } + for (int j = top; j <= bottom; j++) { + ans[j][right] = num++; + } + if (--right < left) { + break; + } + for (int m = right; m >= left; m--) { + ans[bottom][m] = num++; + } + if (--bottom < top) { + break; + } + for (int k = bottom; k >= top; k--) { + ans[k][left] = num++; + } + if (++left > right) { + break; + } + } + return ans; + } +} +``` + diff --git a/docs/leetcode/hot120/62-unique-paths.md b/docs/leetcode/hot120/62-unique-paths.md new file mode 100644 index 0000000..76d3254 --- /dev/null +++ b/docs/leetcode/hot120/62-unique-paths.md @@ -0,0 +1,120 @@ +# 62. 不同路径 + +[题目链接](https://leetcode.cn/problems/unique-paths/) + +**题目描述** + +一个机器人位于一个 m x n 网格的左上角 。机器人每次只能**向下或者向右移动一步**。机器人试图达到网格的右下角。 + +问总共有多少条不同的路径? + +![](http://img.topjavaer.cn/img/uniquePaths1.png) + +**示例**: + +```java +输入:m = 3, n = 7 +输出:28 + +从左上角开始,总共有 3 条路径可以到达右下角。 +1. 向右 -> 向下 -> 向下 +2. 向下 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向下 +``` + +**解题思路** + +首先了解下**动态规划**的思想。 + +动态规划用于处理有重叠子问题的问题。其基本思想:假如要解一个问题,需要先将问题分解成子问题,求出子问题的解,**再根据子问题的解得出原问题的解**。 + +动态规划算法会将计算出来的**子问题的解存储起来**,以便下次遇到同一个子问题的时候直接可以得到该子问题的解,减少重复计算。 + +**动态规划的解题思路:1、状态定义;2、状态转移方程;3、初始状态;4、确定遍历顺序。** + +接下来分步骤讲解本题目的思路。 + +1、首先是**状态定义**。假设 `dp[i][j]` 是到达 `(i, j)` 的路径数量,`dp[2][2]`就是到达`(2, 2)`的路径数量。 + +2、然后是**状态转移方程**。根据题意,只能向右和向下运动,当前位置`(i, j)`只能从`(i-1, j)`和`(i, j-1)`两个方向走过来,由此可以确定状态方程为`dp[i][j] = dp[i-1][j] + dp[i][j-1]`。 + +![](http://img.topjavaer.cn/img/uniquePaths2.png) + +3、**初始状态**。对于第一行 `dp[0][j]`和第一列 `dp[i][0]`,由于都在边界,只有一个方向可以走,所以只能为 1。 + +4、**确定遍历顺序**。`dp[i][j]`是从其上边和左边推导而来,所以按照从左到右,从上到下的顺序来遍历。 + +**代码实现** + +使用二维数组`dp[][]`保存中间状态,时间复杂度和空间复杂度都是`O(m*n)`。 + +```java +public int uniquePaths(int m, int n) { + int[][] dp = new int[m][n]; + //初始化 + for (int i = 0; i < n; i++) { + dp[0][i] = 1; + } + //初始化 + for (int i = 0; i < m; i++) { + dp[i][0] = 1; + } + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + //状态方程 + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; +} +``` + +**优化1**:根据状态转移方程`dp[i][j] = dp[i-1][j] + dp[i][j-1]`可知,只需要保存当前行与上一行的数据即可,空间复杂度可以优化为`O(2n)`,具体代码如下: + +```java +public int uniquePaths(int m, int n) { + int[] preRow = new int[n]; + int[] curRow = new int[n]; + //初始化 + for (int i = 0; i < n; i++) { + preRow[i] = 1; + curRow[i] = 1; + } + for (int i = 1; i < m; i++){ + for (int j = 1; j < n; j++){ + curRow[j] = curRow[j-1] + preRow[j]; + } + preRow = curRow.clone(); + } + return curRow[n-1]; +} +``` + +**优化2**:上述代码还可以继续优化,对于`curRow[j] = curRow[j-1] + preRow[j]`,在未赋值之前`curRow[j]`就是**当前行第`i`行的上一行第`j`列的值**(这里可能不太好理解,小伙伴们好好思考一下),也就是说未赋值之前`curRow[j]`与`preRow[j]`相等,因此`curRow[j] = curRow[j-1] + preRow[j]`可以写成`curRow[j] += curRow[j-1]`,优化1代码中的`preRow`数组可以不用,只需要`curRow`数组即可,代码如下: + +```java +public int uniquePaths(int m, int n) { + int[] curRow = new int[n]; + //初始化 + for (int i = 0; i < n; i++) { + curRow[i] = 1; + } + for (int i = 1; i < m; i++){ + for (int j = 1; j < n; j++){ + curRow[j] += curRow[j-1]; + } + } + return curRow[n-1]; +} +``` + + + +这道腾讯面试题,算是动态规划里面比较简单的题目,虽然不难,但是在面试时氛围比较紧张的情况下,想要一次性bug free做出来,并且做到最优解,还是有点难度的。 + + + + + + + diff --git a/docs/leetcode/hot120/7-reverse-integer.md b/docs/leetcode/hot120/7-reverse-integer.md new file mode 100644 index 0000000..43f8753 --- /dev/null +++ b/docs/leetcode/hot120/7-reverse-integer.md @@ -0,0 +1,58 @@ +# 7. 整数反转 + +[题目链接](https://leetcode.cn/problems/reverse-integer/) + +**题目描述** + +给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。 + +如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。 + +假设环境不允许存储 64 位整数(有符号或无符号)。 + +**示例** + +```java +输入:x = 123 +输出:321 + +输入:x = 120 +输出:21 +``` + +**解题思路** + +- 本题如果不考虑溢出问题,是非常简单的。解决溢出问题有两个思路,第一个思路是通过字符串转换加`try catch`的方式来解决,第二个思路就是通过数学计算来解决。 +- 由于字符串转换的效率较低且使用较多库函数,所以解题方案不考虑该方法,而是通过数学计算来解决。 +- 通过循环将数字`x`的每一位拆开,在计算新值时每一步都判断是否溢出。 +- 溢出条件有两个,一个是大于整数最大值`MAX_VALUE`,另一个是小于整数最小值`MIN_VALUE`,设当前计算结果为`ans`,下一位为`pop`。 +- 从`ans * 10 + pop > MAX_VALUE`这个溢出条件来看 + - 当出现 `ans > MAX_VALUE / 10` 且 `还有pop需要添加` 时,则一定溢出 + - 当出现 `ans == MAX_VALUE / 10` 且 `pop > 7` 时,则一定溢出,`7`是`2^31 - 1`的个位数 +- 从`ans * 10 + pop < MIN_VALUE`这个溢出条件来看 + - 当出现 `ans < MIN_VALUE / 10` 且 `还有pop需要添加` 时,则一定溢出 + - 当出现 `ans == MIN_VALUE / 10` 且 `pop < -8` 时,则一定溢出,`8`是`-2^31`的个位数 + +> 作者:guanpengchn +> 链接:https://leetcode.cn/problems/reverse-integer/solution/hua-jie-suan-fa-7-zheng-shu-fan-zhuan-by-guanpengc/ +> 来源:力扣(LeetCode) +> 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +```java +class Solution { + public int reverse(int x) { + int ans = 0; + while (x != 0) { + int pop = x % 10; + if (ans > Integer.MAX_VALUE / 10 || (ans == Integer.MAX_VALUE / 10 && pop > 7)) + return 0; + if (ans < Integer.MIN_VALUE / 10 || (ans == Integer.MIN_VALUE / 10 && pop < -8)) + return 0; + ans = ans * 10 + pop; + x /= 10; + } + return ans; + } +} +``` + diff --git a/docs/leetcode/hot120/71-simplify-path.md b/docs/leetcode/hot120/71-simplify-path.md new file mode 100644 index 0000000..d031608 --- /dev/null +++ b/docs/leetcode/hot120/71-simplify-path.md @@ -0,0 +1,73 @@ +# 71. 简化路径 + +[题目链接](https://leetcode.cn/problems/simplify-path/) + +**题目描述** + +给你一个字符串 `path` ,表示指向某一文件或目录的 Unix 风格 **绝对路径** (以 `'/'` 开头),请你将其转化为更加简洁的规范路径。 + +在 Unix 风格的文件系统中,一个点(`.`)表示当前目录本身;此外,两个点 (`..`) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,`'//'`)都被视为单个斜杠 `'/'` 。 对于此问题,任何其他格式的点(例如,`'...'`)均被视为文件/目录名称。 + +请注意,返回的 **规范路径** 必须遵循下述格式: + +- 始终以斜杠 `'/'` 开头。 +- 两个目录名之间必须只有一个斜杠 `'/'` 。 +- 最后一个目录名(如果存在)**不能** 以 `'/'` 结尾。 +- 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 `'.'` 或 `'..'`)。 + +返回简化后得到的 **规范路径** 。 + +**示例**: + +```java +输入:path = "/home/" +输出:"/home" +解释:注意,最后一个目录名后面没有斜杠。 + +输入:path = "/../" +输出:"/" +解释:从根目录向上一级是不可行的,因为根目录是你可以到达的最高级。 +``` + +**解题思路** + +使用栈实现。 + +```java +//输入:"/a/./b/../../c/" +//输出:"/c" +class Solution { + public String simplifyPath(String path) { + Stack stack = new Stack<>(); + String[] strs = path.split("/"); + + for (String s : strs) { + if ("..".equals(s)) { + if (!stack.isEmpty()) { + stack.pop(); + } + continue;//只要是"..",就执行下一个循环,不能放入栈 + } + if (!"".equals(s) && !".".equals(s)) { + stack.push(s); + } + } + + if (stack.isEmpty()) { + return "/"; + } + + StringBuilder sb = new StringBuilder(); + int size = stack.size(); + for (int i = 0; i < size; i++) { + sb.append("/").append(stack.get(i));//tips + } + return sb.toString(); + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/73-set-matrix-zeroes.md b/docs/leetcode/hot120/73-set-matrix-zeroes.md new file mode 100644 index 0000000..e30675a --- /dev/null +++ b/docs/leetcode/hot120/73-set-matrix-zeroes.md @@ -0,0 +1,76 @@ +# 73. 矩阵置零 + +[题目链接](https://leetcode.cn/problems/set-matrix-zeroes/) + +**题目描述** + +给定一个 `*m* x *n*` 的矩阵,如果一个元素为 **0** ,则将其所在行和列的所有元素都设为 **0** 。请使用 **[原地](http://baike.baidu.com/item/原地算法)** 算法**。** + +![](http://img.topjavaer.cn/img/image-20221121020953873.png) + +**示例**: + +```java +输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] +输出:[[1,0,1],[0,0,0],[1,0,1]] +``` + +**解题思路** + +[参考解法](https://leetcode-cn.com/problems/set-matrix-zeroes/solution/ju-zhen-zhi-ling-by-leetcode/) + +1. 遍历整个矩阵,如果 cell[i][j] == 0 就将第 i 行和第 j 列的第一个元素标记。 +2. 第一行和第一列的标记是相同的,都是 cell[0][0],所以需要一个额外的变量告知第一列是否被标记,同时用 cell[0][0] 继续表示第一行的标记。 +3. 然后,从第二行第二列的元素开始遍历,如果第 r 行或者第 c 列被标记了,那么就将 cell[r][c] 设为 0。这里第一行和第一列的作用就相当于方法一中的 row_set 和 column_set 。 +4. 然后我们检查是否 cell[0][0] == 0 ,如果是则赋值第一行的元素为零。 +5. 然后检查第一列是否被标记,如果是则赋值第一列的元素为零。 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + if (matrix == null || matrix.length == 0) { + return; + } + boolean isCol = false; + int rows = matrix.length; + int cols = matrix[0].length; + + for (int i = 0; i < rows; i++) { + if (matrix[i][0] == 0) { + isCol = true; + } + for (int j = 1; j < cols; j++) { + if (matrix[i][j] == 0) { + matrix[i][0] = 0; + matrix[0][j] = 0; + } + } + } + + for (int i = 1; i < rows; i++) { + for (int j = 1; j < cols;j++) { + if (matrix[i][0] == 0 || matrix[0][j] == 0) { + matrix[i][j] = 0; + } + } + } + + if (matrix[0][0] == 0) {//顺序 + for (int j = 0; j < cols; j++) { + matrix[0][j] = 0; + } + } + + if (isCol) {//顺序 + for (int i = 0; i < rows; i++) { + matrix[i][0] = 0; + } + } + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/74-search-a-2d-matrix.md b/docs/leetcode/hot120/74-search-a-2d-matrix.md new file mode 100644 index 0000000..b78aa18 --- /dev/null +++ b/docs/leetcode/hot120/74-search-a-2d-matrix.md @@ -0,0 +1,54 @@ +# 74. 搜索二维矩阵 + +[题目链接](https://leetcode.cn/problems/search-a-2d-matrix/) + +**题目描述** + +编写一个高效的算法来判断 `m x n` 矩阵中,是否存在一个目标值。该矩阵具有如下特性: + +- 每行中的整数从左到右按升序排列。 +- 每行的第一个整数大于前一行的最后一个整数。 + +**示例**: + +![](http://img.topjavaer.cn/img/image-20221121021212085.png) + +```java +输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3 +输出:true +``` + +**参考代码** + +```java +class Solution { + public boolean searchMatrix(int[][] matrix, int target) { + if (null == matrix || matrix.length == 0 || matrix[0].length == 0) { + return false; + } + int rows = matrix.length; + int cols = matrix[0].length; + int left = 0; + int right = rows * cols - 1; + + while (left <= right) { + int mid = (left + right) >> 1; //! + int val = matrix[mid / cols][mid % cols]; //cols!不是rows + if (target == val) { + return true; + } else if (target > val) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return false; + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/75-sort-colors.md b/docs/leetcode/hot120/75-sort-colors.md new file mode 100644 index 0000000..ee507a7 --- /dev/null +++ b/docs/leetcode/hot120/75-sort-colors.md @@ -0,0 +1,51 @@ +# 75. 颜色分类 + +[题目链接](https://leetcode.cn/problems/sort-colors/) + +**题目描述** + +给定一个包含红色、白色和蓝色、共 `n` 个元素的数组 `nums` ,**[原地](https://baike.baidu.com/item/原地算法)**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 + +我们使用整数 `0`、 `1` 和 `2` 分别表示红色、白色和蓝色。 + +> 必须在不使用库的sort函数的情况下解决这个问题。 + +**示例**: + +```java +输入:nums = [2,0,2,1,1,0] +输出:[0,0,1,1,2,2] +``` + +**解题思路** + +三指针。 + +```java +class Solution { + public void sortColors(int[] nums) { + if (nums == null || nums.length == 0) { + return; + } + int p1 = 0, cur = 0, p2 = nums.length - 1; + while (cur <= p2) { //边界,nums全为1的情况 + if (nums[cur] == 0) { + int tmp = nums[p1]; + nums[p1++] = 0; + nums[cur++] = tmp; //cur++ + } else if (nums[cur] == 2) { + int tmp = nums[p2]; + nums[p2--] = 2; + nums[cur] = tmp; //nums[cur]可能为2,不自增 + } else { + cur++; + } + } + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/77-combinations.md b/docs/leetcode/hot120/77-combinations.md new file mode 100644 index 0000000..042756f --- /dev/null +++ b/docs/leetcode/hot120/77-combinations.md @@ -0,0 +1,67 @@ +# 77. 组合 + +[题目链接](https://leetcode.cn/problems/combinations/) + +**题目描述** + +给定两个整数 `n` 和 `k`,返回范围 `[1, n]` 中所有可能的 `k` 个数的组合。 + +你可以按 **任何顺序** 返回答案。 + +**示例**: + +```java +输入:n = 4, k = 2 +输出: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] + +输入:n = 1, k = 1 +输出:[[1]] +``` + +**解题思路** + +回溯。剪枝优化。 + +![](http://img.topjavaer.cn/img/image-20200526090917688.png) + +```java +class Solution { + public List> combine(int n, int k) { + List> res = new ArrayList<>(); + if (n <= 0 || k <= 0 || n < k) { + return res; + } + Stack stack = new Stack<>(); + combineHelper(res, stack, 1, n, k); + return res; + } + + public void combineHelper(List> res, Stack stack, int index,int n, int k) { + if (stack.size() == k) { + res.add(new ArrayList<>(stack)); + return; + } + // i 的极限值满足: n - i + 1 = (k - pre.size())。 + // 【关键】n - i + 1 是闭区间 [i,n] 的长度。 + // k - pre.size() 是剩下还要寻找的数的个数。 + for (int i = index; i <= n - (k - stack.size()) + 1; i++) { + stack.push(i); + combineHelper(res, stack, i + 1, n, k); + stack.pop(); + } + } +} +``` + + + + + diff --git a/docs/leetcode/hot120/83-remove-duplicates-from-sorted-list.md b/docs/leetcode/hot120/83-remove-duplicates-from-sorted-list.md new file mode 100644 index 0000000..091726c --- /dev/null +++ b/docs/leetcode/hot120/83-remove-duplicates-from-sorted-list.md @@ -0,0 +1,39 @@ +# 83. 删除排序链表中的重复元素 + +[83. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) + +**题目描述** + +给定一个已排序的链表的头 `head` , *删除所有重复的元素,使每个元素只出现一次* 。返回 *已排序的链表* 。 + +**示例** + +```java +输入:head = [1,1,2] +输出:[1,2] +``` + +**解题思路** + +使用快慢指针。 + +```java +class Solution { + public int removeDuplicates(int[] nums) { + if (nums == null || nums.length <= 0) { + return 0; + } + + int slow = 0; + for (int fast = 1; fast < nums.length; fast++) { + if (nums[slow] != nums[fast]) { + slow++; + nums[slow] = nums[fast]; + } + } + + return slow + 1; + } +} +``` + diff --git a/docs/leetcode/hot120/9-palindrome-number.md b/docs/leetcode/hot120/9-palindrome-number.md new file mode 100644 index 0000000..db1ec32 --- /dev/null +++ b/docs/leetcode/hot120/9-palindrome-number.md @@ -0,0 +1,51 @@ +# 9. 回文数 + +[9. 回文数](https://leetcode.cn/problems/palindrome-number/) + +**题目描述** + +给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。 + +回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。 + +例如,121 是回文,而 123 不是。 + +**示例** + +```java +输入:x = 121 +输出:true + +输入:x = -121 +输出:false +解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。 +``` + +**解题思路** + +将数字后半段反转,跟数字前半段相比即可。 + +```java +class Solution { + public boolean isPalindrome(int x) { + //排除负数和整十的数 + if (x < 0 || (x % 10 == 0 && x != 0)) { + return false; + } + + int reverseNum = 0; + //反转x的后半段数字,再做比较即可 + while(x > reverseNum) { + reverseNum = reverseNum * 10 + x % 10; + x /= 10; + } + + //x为奇数位时,转换后reverseNum会比x多一位 + //如x为1234321,转换后reverseNum为1234,x为123 + return x == reverseNum || x == reverseNum / 10; + } +} +``` + + + diff --git a/docs/leetcode/hot120/92-reverse-linked-list-ii.md b/docs/leetcode/hot120/92-reverse-linked-list-ii.md new file mode 100644 index 0000000..a582d16 --- /dev/null +++ b/docs/leetcode/hot120/92-reverse-linked-list-ii.md @@ -0,0 +1,41 @@ +# 92. 反转链表 II + +[原题链接](https://leetcode.cn/problems/reverse-linked-list-ii/) + +**题目描述** + +给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 + +**示例** + +```java +输入:head = [1,2,3,4,5], left = 2, right = 4 +输出:[1,4,3,2,5] +``` + +**解题思路** + +双指针+头插法。 + +```java +class Solution { + public ListNode reverseBetween(ListNode head, int m, int n) { + ListNode dummy = new ListNode(0); + dummy.next = head; + ListNode pre = dummy; + for (int i = 1; i < m; i++) { + pre = pre.next; + } + head = pre.next; + for (int i = m; i < n; i++) { + ListNode cur = head.next; + head.next = cur.next; + cur.next = pre.next; + pre.next = cur; + } + + return dummy.next; + } +} +``` + diff --git a/docs/leetcode/hot120/958-check-completeness-of-a-binary-tree.md b/docs/leetcode/hot120/958-check-completeness-of-a-binary-tree.md new file mode 100644 index 0000000..0a303e6 --- /dev/null +++ b/docs/leetcode/hot120/958-check-completeness-of-a-binary-tree.md @@ -0,0 +1,57 @@ +# 28. 对称的二叉树 + +[28. 对称的二叉树](https://leetcode.cn/problems/dui-cheng-de-er-cha-shu-lcof/) + +**题目描述** + +给定一个二叉树的 root ,确定它是否是一个 完全二叉树 。 + +在一个 完全二叉树 中,除了最后一个关卡外,所有关卡都是完全被填满的,并且最后一个关卡中的所有节点都是尽可能靠左的。它可以包含 1 到 2^h 节点之间的最后一级 h 。 + +**示例** + +```java +输入:root = [1,2,3,4,5,6] +输出:true +解释:最后一层前的每一层都是满的(即,结点值为 {1} 和 {2,3} 的两层),且最后一层中的所有结点({4,5,6})都尽可能地向左。 +``` + +**解题思路** + +层序遍历,当且仅当存在两个相邻节点:前一个为null,后一个不为null时,则不是完全二叉树。 + +``` + 1 + / \ + 2 3 + / \ \ + 4 5 6 +层序遍历序列为:[1, 2, 3, 4, 5, null, 6],其中 null 出现在了6前面,所以不是完全二叉树 +``` + +代码实现: + +```java +class Solution { + public boolean isCompleteTree(TreeNode root) { + LinkedList list = new LinkedList<>(); + TreeNode pre = root; + TreeNode cur = root; + list.addLast(root); + + while (!list.isEmpty()) { + cur = list.removeFirst(); + if (cur != null) { + if (pre == null) { + return false; + } + list.addLast(cur.left); + list.addLast(cur.right); + } + pre = cur; + } + return true; + } +} +``` + diff --git a/docs/leetcode/hot120/98-validate-binary-search-tree.md b/docs/leetcode/hot120/98-validate-binary-search-tree.md new file mode 100644 index 0000000..a932e48 --- /dev/null +++ b/docs/leetcode/hot120/98-validate-binary-search-tree.md @@ -0,0 +1,56 @@ +# 98. 验证二叉搜索树 + +[题目链接](https://leetcode.cn/problems/validate-binary-search-tree/) + +**题目描述** + +给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。 + +有效 二叉搜索树定义如下: + +- 节点的左子树只包含 小于 当前节点的数。 +- 节点的右子树只包含 大于 当前节点的数。 +- 所有左子树和右子树自身必须也是二叉搜索树。 + +**示例** + +```java +输入:root = [2,1,3] +输出:true + +输入:root = [5,1,4,null,null,3,6] +输出:false +解释:根节点的值是 5 ,但是右子节点的值是 4 。 +``` + +**解题思路** + +中序遍历时,判断当前节点是否大于中序遍历的前一个节点,如果大于,说明满足 BST,继续遍历;否则直接返回 false。 + +> 作者:sweetiee +> 链接:https://leetcode.cn/problems/validate-binary-search-tree/solution/zhong-xu-bian-li-qing-song-na-xia-bi-xu-miao-dong-/ +> 来源:力扣(LeetCode) +> 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +```java +class Solution { + long pre = Long.MIN_VALUE; + public boolean isValidBST(TreeNode root) { + if (root == null) { + return true; + } + // 访问左子树 + if (!isValidBST(root.left)) { + return false; + } + // 访问当前节点:如果当前节点小于等于中序遍历的前一个节点,说明不满足BST,返回 false;否则继续遍历。 + if (root.val <= pre) { + return false; + } + pre = root.val; + // 访问右子树 + return isValidBST(root.right); + } +} +``` + diff --git a/docs/leetcode/hot120/README.md b/docs/leetcode/hot120/README.md new file mode 100644 index 0000000..5cc5691 --- /dev/null +++ b/docs/leetcode/hot120/README.md @@ -0,0 +1,41 @@ +## 目录 + +- [1.两数之和-梦开始的地方!!](./1-two-sum.md) +- [3.无重复字符的最长子串](./3-longest-substring-without-repeating-characters.md) +- [5.最长回文子串](./5-longest-palindromic-substring.md) +- [7.整数反转](./7-reverse-integer.md) +- [15.三数之和](./15-3sum.md) +- [19.删除链表的倒数第N个结点](./19-remove-nth-node-from-end-of-list.md) +- [22.括号生成](./22-generate-parentheses.md) +- [28.对称的二叉树](./28-mirror-binary-tree.md) +- [40.组合总和II](./40-combination-sum-ii.md) +- [46.全排列](./46-permutations.md) +- [47.全排列II](./47-permutations-ii.md) +- [53.最大子数组和](./53-maximum-subarray.md) +- [55.跳跃游戏](./55-jump-game.md) +- [56.合并区间](./56-merge-intervals.md) +- [62.不同路径](./62-unique-paths.md) +- [92.反转链表II](./92-reverse-linked-list-ii.md) +- [98.验证二叉搜索树](./98-validate-binary-search-tree.md) +- [103.二叉树的锯齿形层序遍历](./103-binary-tree-zigzag-level-order-traversal.md) +- [104.二叉树的最大深度](./104-maximum-depth-of-binary-tree.md) +- [120.三角形最小路径和](./120-triangle.md) +- [121.买卖股票的最佳时机](./121-best-time-to-buy-and-sell-stock.md) +- [122.买卖股票的最佳时机II](./122-best-time-to-buy-and-sell-stock-ii.md) +- [131.分割回文串](./131-palindrome-partion.md) +- [133.克隆图](./133-clone-graph.md) +- [134.加油站](./134-gas-station.md) +- [141.环形链表II](./141-linked-list-cycle.md) +- [146.LRU缓存](./146-lru-cache.md) +- [152.乘积最大子数组](./152-maximum-product-subarray.md) +- [160.相交链表](./160-intersection-of-two-linked-lists.md) +- [169.多数元素](./169-majority-element.md) +- [199.二叉树的右视图](./199-binary-tree-right-side-view.md) +- [200.岛屿数量](./200-number-of-islands.md) +- [206.反转链表](./206-reverse-linked-list.md) +- [215.数组中的第K个最大元素](./215-kth-largest-element-in-an-array.md) +- [234.回文链表](./234-palindrome-linked-list.md) +- [236.二叉树的最近公共祖先](./236-lowest-common-ancestor-of-a-binary-tree.md) +- [415.字符串相加](./415-add-strings.md) +- [543.二叉树的直径](./543-diameter-of-binary-tree.md) +- [1143.最长公共子序列](./1143-longest-common-subquence.md) diff --git a/docs/leetcode/leetcode-share.md b/docs/leetcode/leetcode-share.md new file mode 100644 index 0000000..522378c --- /dev/null +++ b/docs/leetcode/leetcode-share.md @@ -0,0 +1,29 @@ +## 关于刷题的一些碎碎念 + +在v站看到一篇关于刷题的文章,分享给大家~ + +> 链接:https://www.v2ex.com/t/910785 + +混 V 站久了,总会看到几个关于 LeetCode 刷题的帖子,刚刚又刷到一贴,《[刷算法题有感](https://www.v2ex.com/t/910741)》,在底下想评论,奈何写了太多,最后决定单独开贴,一吐为快。 + +对**码农**来说,LeetCode 这种 OJ 适当刷刷能应付面试就得了,**别太当真,除非你就是喜欢这玩意儿**,本质上跟有人喜欢解数学题、有人喜欢写毛笔字、有人喜欢画漫画……等等等等没什么不同的话,那这个另算。 + +可是,我相信**发自内心喜欢这个的人终究是少数**。大多数自诩对算法有热情的,无非是别有目的,也可以说是动机不纯,只是刚到了刷题的甜蜜点,自我催眠一下罢了。不然遇到难题想破脑袋都想不出来,那是真的打击积极性,不自我暗示一下根本坚持不下去。 + +刷题只是为了过笔试找工作,这本身并没什么不对,**进大厂多赚点钱嘛不寒碜,但这并不能称之为热爱**。我始终认为,真正热爱算法的人,也一定会喜欢数学。因为**算法的本质就是数学**,解题时的思考过程也相差无几。不信你大可以去看看那些算法书,真较起真来是要用数学来证明算法的正确性和效率的。 + +至于去讨论区看题解,发现各路高手贴出无比巧妙刁钻优雅简洁的解法,同时还附赠了简单的数学证明过程,当然会有 mind blown 时刻,然后相比之下自己就是个渣渣,就忍不住开始自我怀疑。 + +然而,你跟那些高手比,就相当于你一个普通念书备战高考的,跟校田径队的体育特长生去比短跑长跑。你根本没花心思练过,某天心血来潮,看人家跑挺好,自己也想试试,结果跟着跑了两步,发现完全跟不上,然后说我靠这帮孙子跑太快了,我是不是体质不行没有天赋不适合跑步啊——大可不必如此。 + +不说那些从初高中开始就玩竞赛的,毕竟人家可能根本瞧不上 LeetCode 这个平台的出题水平,哪怕是读了研才开始**自学转码**,只要下足功夫,也能做到 LeetCode 竞赛分数 2400 分左右——这是什么概念? Top 0.6%。怎么做到的?先刷够 2500 道题再说。**扪心自问,你做得到吗?**先不论是否可行,只看主观能动性,你真的愿意付出大量的时间和精力在这上面吗? + +话说回来,刷题有没有用,要看你的目的是什么。如果像上面提到的那样,为了刷题过笔试找工作进大厂赚高薪,那肯定是有用,不然 LeetCode 不会火遍全球。但 LeetCode 刷得溜,并不代表你工程能力强,能写出易于维护、方便扩展的代码,或是能做出超炫酷的软件,也不代表你的算法造诣深,能像 Dijkstra 那样发明新的算法,同样不代表你能设计出一个数据库、编译器、网络协议或是操作系统,这些领域对算法确实有相当的要求,但刷题本身跟这些完全是两码事。 + +抛开功利性的目的不谈,刷题本身对码农来说也是种思维和编程上的双重训练。能解难题,说明对某些**明确了特定条件**的问题,有较强的抽象能力。很多时候眼睁睁看着归类标签,明知道要用某一算法来解,读完了题都不知道从哪开始下手。另外题解代码能同时做到“高效、优雅、简洁、易读”,说明你比较熟悉某一门编程语言,能用一些语法糖和语言特定的“奇技淫巧”,以及在工程上至少某一局部的代码,比如明确了输入输出的某个 util 函数,能写得比较优质。 + +君不见有太多**基本编程素养不过关**的码农,为了解决一个很简单的功能,循环嵌套了一层又一层,却根本不知道一个递归就能简洁明了地搞定。对于这样的选手,简单刷几十道题就会有明显进步,那刷题肯定是有用的。 + +再简单推荐两个我的偶像,LeetCode 上比较老的题的讨论区很容易找到 [Stefan](https://leetcode.com/stefanpochmann/),这位好几个语言都玩得贼溜,但很久不更新了,大概对他来说解题只是娱乐的一种吧,感兴趣的可以去他的上古个人博客看了解更多。新题则是国人之光 [Lee215](https://leetcode.com/lee215/),~~我愿称之为 LeetCode 界的 Faker (不是)~~,基本每个题解都会有 C++/Java/Python 三种写法。国服大佬也很多,但我只刷过美服,就不太了解了。 + +虽然可能没人感兴趣,但都写了这么多,就干脆再简单聊下自己。我刷题坚持刷了一年多,每天 996 下班回家之后还开机刷道题,365 天风雨无阻毫无例外,当时自我感动得不行,最后也只是拿了个 LeetCode 的 Annual Medal 2021 徽章而已,依然菜得抠脚。到目前不刷题也已经快一年了,因为意识到在软件工程领域,无论是 Web 前后端还是别的什么,都还有更多有趣的东西可以去探索把玩。同时也是因为根本没可能去 FAANG 或是 GAMAM ,whatever ,就看开了。 \ No newline at end of file diff --git "a/\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\347\273\237\350\256\241\344\270\215\345\220\214\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\344\270\252\346\225\260.md" b/docs/mass-data/1-count-phone-num.md similarity index 70% rename from "\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\347\273\237\350\256\241\344\270\215\345\220\214\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\344\270\252\346\225\260.md" rename to docs/mass-data/1-count-phone-num.md index c2ac733..7f09f2e 100644 --- "a/\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\347\273\237\350\256\241\344\270\215\345\220\214\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\344\270\252\346\225\260.md" +++ b/docs/mass-data/1-count-phone-num.md @@ -1,10 +1,32 @@ -大家好,我是大彬~ +--- +sidebar: heading +title: 统计不同号码的个数 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,电话号码统计,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- -海量数据题目是面试经常会考的题型,今天给大家分享一道百度二面的题目。 +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +# 统计不同号码的个数 + +题目来自百度二面。 ## 题目描述 -**已知某个文件内包含大量电话号码,每个号码为8位数字,如何统计不同号码的个数?** +**已知某个文件内包含大量电话号码,每个号码为8位数字,如何统计不同号码的个数?内存限制100M** ## 思路分析 @@ -47,7 +69,7 @@ 3. 将位图中对应的位设置为 1,即arr[2] = arr[2] **|** 000..00010000。 4. 这就将电话号码映射到了位图的某一位了。 -![](http://img.dabin-coder.cn/image/20220423094735.png) +![](http://img.topjavaer.cn/img/20220423094735.png) 最后,统计位图中bit值为1的数量,便能得到不同电话号码的个数了。 diff --git "a/\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\345\246\202\344\275\225\344\273\216\346\265\267\351\207\217\346\225\260\346\215\256\346\211\276\345\207\272\351\253\230\351\242\221\350\257\215.md" b/docs/mass-data/2-find-hign-frequency-word.md similarity index 87% rename from "\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\345\246\202\344\275\225\344\273\216\346\265\267\351\207\217\346\225\260\346\215\256\346\211\276\345\207\272\351\253\230\351\242\221\350\257\215.md" rename to docs/mass-data/2-find-hign-frequency-word.md index 70a08a9..f5eecbe 100644 --- "a/\346\265\267\351\207\217\346\225\260\346\215\256\345\234\272\346\231\257\351\242\230/\345\246\202\344\275\225\344\273\216\346\265\267\351\207\217\346\225\260\346\215\256\346\211\276\345\207\272\351\253\230\351\242\221\350\257\215.md" +++ b/docs/mass-data/2-find-hign-frequency-word.md @@ -1,6 +1,19 @@ -大家好,我是大彬~ - -今天分享一道面试常考的海量数据场景题。 +--- +sidebar: heading +title: 出现频率最高的100个词 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,高频词统计,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +# 出现频率最高的100个词 ## 题目描述 @@ -71,14 +84,3 @@ 解法2相对解法1,更加严谨,如果某个词词频过高或者整个文件都是同一个词的话,解法1不适用。 - -## 点关注,不迷路 - -大彬,**非科班出身,自学Java**,校招斩获京东、携程、华为等offer。作为一名转码选手,深感这一路的不易。 - -希望我的分享能帮助到更多的小伙伴,**我踩过的坑你们不要再踩**。想与大彬交流的话,可以到公众号后台获取大彬的微信~ - -后台回复『 **笔记**』即可领取大彬斩获大厂offer的**面试笔记**。 - -![](http://img.dabin-coder.cn/image/公众号.jpg) - diff --git a/docs/mass-data/3-find-same-url.md b/docs/mass-data/3-find-same-url.md new file mode 100644 index 0000000..f704049 --- /dev/null +++ b/docs/mass-data/3-find-same-url.md @@ -0,0 +1,41 @@ +--- +sidebar: heading +title: 查找两个大文件共同的URL +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,大文件共同的URL,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +# 查找两个大文件共同的URL + +## 题目 + +给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,找出 a、b 两个文件共同的 URL。内存限制是 4G。 + +## 分析 + +每个 URL 占 64B,那么 50 亿个 URL占用的空间大小约为 320GB。 + +5,000,000,000 * 64B ≈ 320GB + +由于内存大小只有 4G,因此,不可能一次性把所有 URL 加载到内存中处理。 + +可以采用分治策略,也就是把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。 + +首先遍历文件a,对遍历到的 URL 进行哈希取余 `hash(URL) % 1000`,根据计算结果把遍历到的 URL 存储到 a0, a1,a2, ..., a999,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b0, b1, b2, ..., b999 中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a0 对应 b0, ..., a999 对应 b999,不对应的小文件不可能有相同的 URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。 + +接着遍历 ai( `i∈[0,999]`),把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。 + +## 总结 + +最后总结一下: + +1. 分而治之,进行哈希取余; +2. 对每个子文件进行 HashSet 统计。 diff --git a/docs/mass-data/4-find-mid-num.md b/docs/mass-data/4-find-mid-num.md new file mode 100644 index 0000000..6e94690 --- /dev/null +++ b/docs/mass-data/4-find-mid-num.md @@ -0,0 +1,46 @@ +--- +sidebar: heading +title: 如何在100亿数据中找到中位数? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,100亿数据找中位数,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +# 如何在100亿数据中找到中位数? + +## 题目描述 + +给定100亿个无符号的乱序的整数序列,如何求出这100亿个数的中位数(中位数指的是排序后最中间那个数),内存只有**512M**。 + +## 分析 + +中位数问题可以看做一个统计问题,而不是排序问题,无符号整数大小为4B,则能表示的数的范围为为0 ~ 2^32 - 1(40亿),如果没有限制内存大小,则可以用一个2^32(4GB)大小的数组(也叫做桶)来保存100亿个数中每个无符号数出现的次数。遍历这100亿个数,当元素值等于桶元素索引时,桶元素的值加1。当统计完100亿个数以后,则从索引为0的值开始累加桶的元素值,当累加值等于50亿时,这个值对应的索引为中位数。时间复杂度为O(n)。 + +因为题目要求内存限制512M,所以上述解法不合适。 + +下面分享另一种解法(**分治法**的思想) + +如果100亿个数字保存在一个大文件中,可以依次读一部分文件到内存(不超过内存限制),将每个数字用二进制表示,比较二进制的最高位(第32位,符号位,0是正,1是负)。 + +如果数字的最高位为0,则将这个数字写入 file_0文件中;如果最高位为 1,则将该数字写入file_1文件中。 从而将100亿个数字分成了两个文件。 + +假设 file_0文件中有 60亿 个数字,file_1文件中有 40亿 个数字。那么中位数就在 file_0 文件中,并且是 file_0 文件中所有数字排序之后的第 10亿 个数字。因为file_1中的数都是负数,file_0中的数都是正数,也即这里一共只有40亿个负数,那么排序之后的第50亿个数一定位于file_0中。 + +现在,我们只需要处理 file_0 文件了,不需要再考虑file_1文件。对于 file_0 文件,同样采取上面的措施处理:将file_0文件依次读一部分到内存(不超过内存限制),将每个数字用二进制表示,比较二进制的 次高位(第31位),如果数字的次高位为0,写入file_0_0文件中;如果次高位为1,写入file_0_1文件 中。 + +现假设 file_0_0文件中有30亿个数字,file_0_1中也有30亿个数字,则中位数就是:file_0_0文件中的数字从小到大排序之后的第10亿个数字。 + +抛弃file_0_1文件,继续对 file_0_0文件 根据 次次高位(第30位) 划分,假设此次划分的两个文件为:file_0_0_0中有5亿个数字,file_0_0_1中有25亿个数字,那么中位数就是 file_0_0_1文件中的所有数字排序之后的 第 5亿 个数。 + +按照上述思路,直到划分的文件可直接加载进内存时,就可以直接对数字进行快速排序,找出中位数了。 + + + +> 参考链接:https://blog.csdn.net/qq_41306849/article/details/119828746 diff --git a/docs/mass-data/5-find-hot-string.md b/docs/mass-data/5-find-hot-string.md new file mode 100644 index 0000000..4829bb6 --- /dev/null +++ b/docs/mass-data/5-find-hot-string.md @@ -0,0 +1,65 @@ +--- +sidebar: heading +title: 如何查询最热门的查询串? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,查询最热门的查询串,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +# 如何查询最热门的查询串? + +## 题目描述 + +搜索引擎会通过日志文件把用户每次检索使用的所有查询串都记录下来,每个查询床的长度不超过 255 字节。 + +假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。) + +## 解答思路 + +每个查询串最长为 255B,1000w 个串需要占用 约 2.55G 内存,因此,我们无法将所有字符串全部读入到内存中处理。 + +### 方法一:分治法 + +分治法依然是一个非常实用的方法。 + +划分为多个小文件,保证单个小文件中的字符串能被直接加载到内存中处理,然后求出每个文件中出现次数最多的 10 个字符串;最后通过一个小顶堆统计出所有文件中出现最多的 10 个字符串。 + +方法可行,但不是最好,下面介绍其他方法。 + +### 方法二:HashMap 法 + +虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4表示整数占用的4个字节)。由此可见,1G 的内存空间完全够用。 + +**思路如下**: + +首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 `O(N)`。 + +接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。 + +遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 `O(Nlog10)`。 + +### 方法三:前缀树法 + +方法二使用了 HashMap 来统计次数,当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。 + +**思路如下**: + +在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。 + +最后依然使用小顶堆来对字符串的出现次数进行排序。 + +## 方法总结 + +前缀树经常被用来统计字符串的出现次数。它的另外一个大的用途是字符串查找,判断是否有重复的字符串等。 + + + +> 作者:yanglbme +> 链接:https://juejin.cn/post/6844904003998842887 diff --git a/docs/mass-data/6-top-500-num.md b/docs/mass-data/6-top-500-num.md new file mode 100644 index 0000000..751136a --- /dev/null +++ b/docs/mass-data/6-top-500-num.md @@ -0,0 +1,127 @@ +--- +sidebar: heading +title: 如何找出排名前 500 的数? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,排名前500的数,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +## 如何找出排名前 500 的数? + +## 题目描述 + +有 1w 个数组,每个数组有 500 个元素,并且有序排列。如何在这 10000*500 个数中找出前 500 的数? + +## 方法1 + +题目中每个数组是排好序的,可以使用**归并**的方法。 + +先将第1个和第2个归并,得到500个数据。然后再将结果和第3个数组归并,得到500个数据,以此类推,直到最后找出前500个的数。 + +## 方法2 + +对于这种topk问题,更常用的方法是使用**堆排序**。 + +对本题而言,假设数组降序排列,可以采用以下方法: + +首先建立大顶堆,堆的大小为数组的个数,即为10000 ,把每个数组最大的值存到堆中。 + +接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素。 + +重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。 + +**示例代码如下**: + +```java +import lombok.Data; + +import java.util.Arrays; +import java.util.PriorityQueue; + +@Data +public class DataWithSource implements Comparable { + /** + * 数值 + */ + private int value; + + /** + * 记录数值来源的数组 + */ + private int source; + + /** + * 记录数值在数组中的索引 + */ + private int index; + + public DataWithSource(int value, int source, int index) { + this.value = value; + this.source = source; + this.index = index; + } + + /** + * + * 由于 PriorityQueue 使用小顶堆来实现,这里通过修改 + * 两个整数的比较逻辑来让 PriorityQueue 变成大顶堆 + */ + @Override + public int compareTo(DataWithSource o) { + return Integer.compare(o.getValue(), this.value); + } +} + + +class Test { + public static int[] getTop(int[][] data) { + int rowSize = data.length; + int columnSize = data[0].length; + + // 创建一个columnSize大小的数组,存放结果 + int[] result = new int[columnSize]; + + PriorityQueue maxHeap = new PriorityQueue<>(); + for (int i = 0; i < rowSize; ++i) { + // 将每个数组的最大一个元素放入堆中 + DataWithSource d = new DataWithSource(data[i][0], i, 0); + maxHeap.add(d); + } + + int num = 0; + while (num < columnSize) { + // 删除堆顶元素 + DataWithSource d = maxHeap.poll(); + result[num++] = d.getValue(); + if (num >= columnSize) { + break; + } + + d.setValue(data[d.getSource()][d.getIndex() + 1]); + d.setIndex(d.getIndex() + 1); + maxHeap.add(d); + } + return result; + + } + + public static void main(String[] args) { + int[][] data = { + {29, 17, 14, 2, 1}, + {19, 17, 16, 15, 6}, + {30, 25, 20, 14, 5}, + }; + + int[] top = getTop(data); + System.out.println(Arrays.toString(top)); // [30, 29, 25, 20, 19] + } +} +``` + diff --git a/docs/mass-data/7-query-frequency-sort.md b/docs/mass-data/7-query-frequency-sort.md new file mode 100644 index 0000000..f10013f --- /dev/null +++ b/docs/mass-data/7-query-frequency-sort.md @@ -0,0 +1,44 @@ +--- +sidebar: heading +title: 如何按照 query 的频度排序? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +## 如何按照 query 的频度排序? + +### 题目描述 + +有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。 + +### 解答思路 + +如果 query 的重复度比较大,可以考虑一次性把所有 query 读入内存中处理;如果 query 的重复率不高,那么可用内存不足以容纳所有的 query,这时候就需要采用分治法或其他的方法来解决。 + +#### 方法一:HashMap 法 + +如果 query 重复率高,说明不同 query 总数比较小,可以考虑把所有的 query 都加载到内存中的 HashMap 中。接着就可以按照 query 出现的次数进行排序。 + +#### 方法二:分治法 + +分治法需要根据数据量大小以及可用内存的大小来确定问题划分的规模。对于这道题,可以顺序遍历 10 个文件中的 query,通过 Hash 函数 `hash(query) % 10` 把这些 query 划分到 10 个小文件中。之后对每个小文件使用 HashMap 统计 query 出现次数,根据次数排序并写入到零外一个单独文件中。 + +接着对所有文件按照 query 的次数进行排序,这里可以使用归并排序(由于无法把所有 query 都读入内存,因此需要使用外排序)。 + +### 方法总结 + +- 内存若够,直接读入进行排序; +- 内存不够,先划分为小文件,小文件排好序后,整理使用外排序进行归并。 + + + +> 作者:yanglbme +> 链接:https://juejin.cn/post/6844904003998842887 diff --git a/docs/mass-data/8-topk-template.md b/docs/mass-data/8-topk-template.md new file mode 100644 index 0000000..fee618e --- /dev/null +++ b/docs/mass-data/8-topk-template.md @@ -0,0 +1,181 @@ +--- +sidebar: heading +title: 数据中 TopK 问题的常用套路 +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,topk问题,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +## 大数据中 TopK 问题的常用套路 + +今天想跟大家聊一些**常见的 topK 问题**。 + +对于海量数据到处理经常会涉及到 topK 问题。在设计数据结构和算法的时候,主要需要考虑的应该是当前算法(包括数据结构)跟给定情境(比如数据量级、数据类型)的适配程度,和当前问题最核心的瓶颈(如降低时间复杂度,还是降低空间复杂度)是什么。 + +首先,我们来举几个常见的 topK 问题的例子: + +1. 给定 100 个 int 数字,在其中找出最大的 10 个; +1. 给定 10 亿个 int 数字,在其中找出最大的 10 个(这 10 个数字可以无序); +1. 给定 10 亿个 int 数字,在其中找出最大的 10 个(这 10 个数字依次排序); +1. 给定 10 亿个不重复的 int 数字,在其中找出最大的 10 个; +1. 给定 10 个数组,每个数组中有 1 亿个 int 数字,在其中找出最大的 10 个; +1. 给定 10 亿个 string 类型的数字,在其中找出最大的 10 个(仅需要查 1 次); +1. 给定 10 亿个 string 类型的数字,在其中找出最大的 k 个(需要反复多次查询,其中 k 是一个随机数字)。 + +上面这些问题看起来很相似,但是解决的方式却千差万别。稍有不慎,就可能使得 topK 问题成为系统的瓶颈。不过也不用太担心,接下来我会总结几种常见的解决思路,遇到问题的时候,大家把这些基础思路融会贯通并且杂糅组合,即可做到见招拆招。 +
+ +### 1. 堆排序法 + +这里说的是堆排序法,而不是快排或者希尔排序。虽然理论时间复杂度都是 `O(nlogn)`,但是堆排在做 topK 的时候有一个优势,就是可以维护一个仅包含 k 个数字的小顶堆(想清楚,为啥是小顶堆哦),当新加入的数字大于堆顶数字的时候,将堆顶元素剔除,并加入新的数字。 + +用 C++ 来说明,堆在 stl 中是 priority_queue(不是 set)。 + +```cpp +int main() { + const int topK = 3; + vector vec = {4,1,5,8,7,2,3,0,6,9}; + priority_queue, greater<>> pq; // 小顶堆 + for (const auto& x : vec) { + pq.push(x); + if (pq.size() > topK) { + // 如果超出个数,则弹出堆顶(最小的)数据 + pq.pop(); + } + } + + while (!pq.empty()) { + cout << pq.top() << endl; // 输出依次为7,8,9 + pq.pop(); + } + + return 0; +} +``` + +> Java 中同样提供了 PriorityQueue 的数据结构。 + +### 2. 类似快排法 + +快排大家都知道,针对 topK 问题,可以对快排进行改进。仅对部分数据进行递归计算。比如,在 100 个数字中,找最大的 10 个,第一次循环的时候,povit 被移动到了 80 的位置,则接下来仅需要在后面的 20 个数字中找最大的 10 个即可。 + +这样做的优势是,理论最优时间复杂度可以达到 `O(n)`,不过平均时间复杂度还是 `O(nlogn)`。需要说明的是,通过这种方式,找出来的最大的 k 个数字之间,是无序的。 + +```cpp +int partition(vector& arr, int begin, int end) { + int left = begin; + int right = end; + int povit = arr[begin]; + + while (left < right) { + while (left < right && arr[right] >= povit) {right--;} + while (left < right && arr[left] <= povit) {left++;} + if (left < right) {swap(arr[left], arr[right]);} + } + + swap(arr[begin], arr[left]); + return left; +} + +void partSort(vector& arr, int begin, int end, int target) { + if (begin >= end) { + return; + } + + int povit = partition(arr, begin, end); + if (target < povit) { + partSort(arr, begin, povit - 1, target); + } else if (target > povit) { + partSort(arr, povit + 1, end, target); + } +} + +vector getMaxNumbers(vector& arr, int k) { + int size = (int)arr.size(); + // 把求最大的k个数,转换成求最小的size-k个数字 + int target = size - k; + partSort(arr, 0, size - 1, target); + vector ret(arr.end() - k, arr.end()); + return ret; +} + +int main() { + vector vec = {4,1,5,8,7,2,3,0,6,9}; + auto ret = getMaxNumbers(vec, 3); + + for (auto x : ret) { + cout << x << endl; // 输出7,8,9(理论上无序) + } + + return 0; +} +``` + +
+ +### 3. 使用 bitmap + +有时候 topK 问题会遇到数据量过大,内存无法全部加载。这个时候,可以考虑将数据存放至 bitmap 中,方便查询。 + +比如,给出 10 个 int 类型的数据,分别是【13,12,11,1,2,3,4,5,6,7】,int 类型的数据每个占据 4 个字节,那这个数组就占据了 40 个字节。现在,把它们放到一个 16 个长度 bool 的 bitmap 中,结果就是【0,1,1,1,1,1,1,1,0,0,0,1,1,1,0,0】,在将空间占用降低至 4 字节的同时,也可以很方便的看出,最大的 3 个数字,分别是 11,12 和 13。 + +需要说明的是,bitmap 结合跳表一起使用往往有奇效。比如以上数据还可以记录成:从第 1 位开始,有连续 7 个 1;从第 11 位开始,有连续 3 个 1。这样做,空间复杂度又得到了进一步的降低。 + +这种做法的优势,当然是降低了空间复杂度。不过需要注意一点,bitmap 比较适合不重复且有范围(比如,数据均在 0 ~ 10 亿之间)的数据的查询。至于有重复数据的情况,可以考虑与 hash 等结构的混用。 +
+ +### 4. 使用 hash + +如果遇到了查询 string 类型数据的大小,可以考虑 hash 方法。 + +举个例子,10 个 string 数字【"1001","23","1002","3003","2001","1111","65","834","5","987"】找最大的 3 个。我们先通过长度进行 hash,得到长度最大为 4,且有 5 个长度为 4 的 string。接下来再通过最高位值做 hash,发现有 1 个最高位为"3"的,1 个为"2"的,3 个为"1"的。接下来,可以通过再设计 hash 函数,或者是循环的方式,在 3 个最高位为"1"的 string 中找到最大的一个,即可找到 3 个最值大的数据。 + +这种方法比较适合网址或者电话号码的查询。缺点就是如果需要多次查询的话,需要多次计算 hash,并且需要根据实际情况设计多个 hash 函数。 +
+ +### 5. 字典树 + +字典树(trie)的具体结构和查询方式,不在这里赘述了,自行百度一下就有很多。这里主要说一下优缺点。 + +字典树的思想,还是通过前期建立索引信息,后期可以反复多次查询,并且后期增删数据也很方便。比较适合于需要反复多次查询的情况。 + +比如,反复多次查询字符序(例如:z>y>...>b>a)最大的 k 个 url 这种,使用字典树把数据存储一遍,就非常适合。既减少了空间复杂度,也加速了查询效率。 +
+ +### 6. 混合查询 + +以上几种方法,都是比较独立的方法。其实,在实际工作中,遇到更多的问题还是混合问题,这就需要我们对相关的内容,融会贯通并且做到活学活用。 + +我举个例子:我们的分布式服务跑在 10 台不同机器上,每台机器上部署的服务均被请求 10000 次,并且记录了个这 10000 次请求的耗时(耗时值为 int 数据),找出这 10\*10000 次请求中,从高到低的找出耗时最大的 50 个。看看这个问题,很现实吧。我们试着用上面介绍的方法,组合一下来求解。 + +#### 方法一 + +首先,对每台机器上的 10000 个做类似快排,找出每台机器上 top50 的耗时信息。此时,单机上的这 50 条数据是无序的。 + +然后,再将 10 台机器上的 50 条数据(共 500 条)放到一起,再做一次类似快排,找到最大的 50 个(此时应该这 50 个应该是无序的)。 + +最后,对这 50 个数据做快排,从而得到最终结果。 + +#### 方法二 + +首先通过堆排,分别找出 10 台机器上耗时最高的 50 个数据,此时的这 50 个数据,已经是从大到小有序的了。 + +然后,我们依次取出 10 台机器中,耗时最高的 5 条放入小顶堆中。 + +最后,遍历 10 台机器上的数据,每台机器从第 6 个数据开始往下循环,如果这个值比堆顶的数据大,则抛掉堆顶数据并且把它加入,继续用下一个值进行同样比较。如果这个值比堆顶的值小,则结束当前循环,并且在下一台机器上做同样操作。 + +以上我介绍了两种方法,并不是为了说明哪种方法更好,或者时间复杂度更低。而是想说同样的事情有多种不同的解决方法,而且随着数据量的增加,可能会需要更多组合形式。在这个领域,数据决定了数据结构,数据结构决定了算法。 + +**没有最好的方法,只有不断找寻更好的方法的程序员。适合的,才会是最好的。** + + + + + diff --git a/docs/mass-data/9-sort-500-million-large-files.md b/docs/mass-data/9-sort-500-million-large-files.md new file mode 100644 index 0000000..96ba8f3 --- /dev/null +++ b/docs/mass-data/9-sort-500-million-large-files.md @@ -0,0 +1,310 @@ +--- +sidebar: heading +title: 5亿个数的大文件怎么排序? +category: 海量数据 +tag: + - 海量数据 +head: + - - meta + - name: keywords + content: 海量数据面试题,5亿个数排序,海量数据 + - - meta + - name: description + content: 海量数据常见面试题总结,让天下没有难背的八股文! +--- + +## **问题** + +给你1个文件`bigdata`,大小4663M,5亿个数,文件中的数据随机,一行一个整数: + +```bash +6196302 +3557681 +6121580 +2039345 +2095006 +1746773 +7934312 +2016371 +7123302 +8790171 +2966901 +... +7005375 +``` + +现在要对这个文件进行排序,怎么做? + +## 内部排序 + +先尝试内排,选2种排序方式: + +### 3路快排: + +```java +private final int cutoff = 8; + +public void perform(Comparable[] a) { + perform(a,0,a.length - 1); + } + + private int median3(Comparable[] a,int x,int y,int z) { + if(lessThan(a[x],a[y])) { + if(lessThan(a[y],a[z])) { + return y; + } + else if(lessThan(a[x],a[z])) { + return z; + }else { + return x; + } + }else { + if(lessThan(a[z],a[y])){ + return y; + }else if(lessThan(a[z],a[x])) { + return z; + }else { + return x; + } + } + } + + private void perform(Comparable[] a,int low,int high) { + int n = high - low + 1; + //当序列非常小,用插入排序 + if(n <= cutoff) { + InsertionSort insertionSort = SortFactory.createInsertionSort(); + insertionSort.perform(a,low,high); + //当序列中小时,使用median3 + }else if(n <= 100) { + int m = median3(a,low,low + (n >>> 1),high); + exchange(a,m,low); + //当序列比较大时,使用ninther + }else { + int gap = n >>> 3; + int m = low + (n >>> 1); + int m1 = median3(a,low,low + gap,low + (gap << 1)); + int m2 = median3(a,m - gap,m,m + gap); + int m3 = median3(a,high - (gap << 1),high - gap,high); + int ninther = median3(a,m1,m2,m3); + exchange(a,ninther,low); + } + + if(high <= low) + return; + //lessThan + int lt = low; + //greaterThan + int gt = high; + //中心点 + Comparable pivot = a[low]; + int i = low + 1; + + /* + * 不变式: + * a[low..lt-1] 小于pivot -> 前部(first) + * a[lt..i-1] 等于 pivot -> 中部(middle) + * a[gt+1..n-1] 大于 pivot -> 后部(final) + * + * a[i..gt] 待考察区域 + */ + + while (i <= gt) { + if(lessThan(a[i],pivot)) { + //i-> ,lt -> + exchange(a,lt++,i++); + }else if(lessThan(pivot,a[i])) { + exchange(a,i,gt--); + }else{ + i++; + } + } + + // a[low..lt-1] < v = a[lt..gt] < a[gt+1..high]. + perform(a,low,lt - 1); + perform(a,gt + 1,high); + } +``` + +### 归并排序: + +```java + /** + * 小于等于这个值的时候,交给插入排序 + */ + private final int cutoff = 8; + + /** + * 对给定的元素序列进行排序 + * + * @param a 给定元素序列 + */ + @Override + public void perform(Comparable[] a) { + Comparable[] b = a.clone(); + perform(b, a, 0, a.length - 1); + } + + private void perform(Comparable[] src,Comparable[] dest,int low,int high) { + if(low >= high) + return; + + //小于等于cutoff的时候,交给插入排序 + if(high - low <= cutoff) { + SortFactory.createInsertionSort().perform(dest,low,high); + return; + } + + int mid = low + ((high - low) >>> 1); + perform(dest,src,low,mid); + perform(dest,src,mid + 1,high); + + //考虑局部有序 src[mid] <= src[mid+1] + if(lessThanOrEqual(src[mid],src[mid+1])) { + System.arraycopy(src,low,dest,low,high - low + 1); + } + + //src[low .. mid] + src[mid+1 .. high] -> dest[low .. high] + merge(src,dest,low,mid,high); + } + + private void merge(Comparable[] src,Comparable[] dest,int low,int mid,int high) { + + for(int i = low,v = low,w = mid + 1; i <= high; i++) { + if(w > high || v <= mid && lessThanOrEqual(src[v],src[w])) { + dest[i] = src[v++]; + }else { + dest[i] = src[w++]; + } + } + } +``` + +数据太多,递归太深,会导致栈溢出。数据太多,数组太长,会导致OOM。 + +可见这两种方式不适用。 + +## 位图法 + +BitMap算法的核心思想是用bit数组来记录0-1两种状态,然后再将具体数据映射到这个比特数组的具体位置,这个比特位设置成0表示数据不存在,设置成1表示数据存在。 + +BitMap算在在大量数据查询、去重等应用场景中使用的比较多,这个算法具有比较高的空间利用率。 + +实现代码如下: + +```csharp +private BitSet bits; + +public void perform( + String largeFileName, + int total, + String destLargeFileName, + Castor castor, + int readerBufferSize, + int writerBufferSize, + boolean asc) throws IOException { + + System.out.println("BitmapSort Started."); + long start = System.currentTimeMillis(); + bits = new BitSet(total); + InputPart largeIn = PartFactory.createCharBufferedInputPart(largeFileName, readerBufferSize); + OutputPart largeOut = PartFactory.createCharBufferedOutputPart(destLargeFileName, writerBufferSize); + largeOut.delete(); + + Integer data; + int off = 0; + try { + while (true) { + data = largeIn.read(); + if (data == null) + break; + int v = data; + set(v); + off++; + } + largeIn.close(); + int size = bits.size(); + System.out.println(String.format("lines : %d ,bits : %d", off, size)); + + if(asc) { + for (int i = 0; i < size; i++) { + if (get(i)) { + largeOut.write(i); + } + } + }else { + for (int i = size - 1; i >= 0; i--) { + if (get(i)) { + largeOut.write(i); + } + } + } + + largeOut.close(); + long stop = System.currentTimeMillis(); + long elapsed = stop - start; + System.out.println(String.format("BitmapSort Completed.elapsed : %dms",elapsed)); + }finally { + largeIn.close(); + largeOut.close(); + } +} + +private void set(int i) { + bits.set(i); +} + +private boolean get(int v) { + return bits.get(v); +} +``` + +## 外部排序 + +什么是外部排序? + +> 1. 内存极少的情况下,利用分治策略,利用外存保存中间结果,再用多路归并来排序; + +实现原理如下: + +![](http://img.topjavaer.cn/img/5亿个数大文件排序2.png) + +**1.分成有序的小文件** + +内存中维护一个极小的核心缓冲区`memBuffer`,将大文件`bigdata`按行读入,搜集到`memBuffer`满或者大文件读完时,对`memBuffer`中的数据调用内排进行排序,排序后将**有序结果**写入磁盘文件`bigdata.xxx.part.sorted`. +循环利用`memBuffer`直到大文件处理完毕,得到n个有序的磁盘文件: + +![](http://img.topjavaer.cn/img/5亿个数大文件排序3.png) + +**2.合并成1个有序的大文件** + +现在有了n个有序的小文件,怎么合并成1个有序的大文件? + +利用如下原理进行归并排序: + +![](http://img.topjavaer.cn/img/5亿个数大文件排序1.png) + +举个简单的例子: + +> 文件1:**3**,6,9。 +> 文件2:**2**,4,8。 +> 文件3:**1**,5,7。 +> +> 第一回合: +> 文件1的最小值:3 , 排在文件1的第1行。 +> 文件2的最小值:2,排在文件2的第1行。 +> 文件3的最小值:1,排在文件3的第1行。 +> 那么,这3个文件中的最小值是:min(1,2,3) = 1。 +> 也就是说,最终大文件的当前最小值,是文件1、2、3的当前最小值的最小值。 +> 上面拿出了最小值1,写入大文件。 + +> 第二回合: +> 文件1的最小值:3 , 排在文件1的第1行。 +> 文件2的最小值:2,排在文件2的第1行。 +> 文件3的最小值:5,排在文件3的第2行。 +> 那么,这3个文件中的最小值是:min(5,2,3) = 2。 +> 将2写入大文件。 +> +> 也就是说,最小值属于哪个文件,那么就从哪个文件当中取下一行数据。(因为小文件内部有序,下一行数据代表了它当前的最小值) + +感兴趣的小伙伴可以自己尝试去实现下~ diff --git a/docs/mass-data/README.md b/docs/mass-data/README.md new file mode 100644 index 0000000..e60cf7e --- /dev/null +++ b/docs/mass-data/README.md @@ -0,0 +1,12 @@ + +## 海量数据面试题(更新中) + +- [如何查询最热门的查询串?](./5-find-hot-string.md) +- [统计不同号码的个数](./1-count-phone-num.md) +- [出现频率最高的100个词](./2-find-hign-frequency-word.md) +- [查找两个大文件共同的URL](./3-find-same-url.md) +- [如何在100亿数据中找到中位数?](./4-find-mid-num) +- [如何查询最热门的查询串?](./5-find-hot-string) +- [如何找出排名前 500 的数?](./6-top-500-num) +- [如何按照 query 的频度排序?](./7-query-frequency-sort) +- [大数据中 TopK 问题的常用套路](./8-topk-template) diff --git a/docs/message-queue/kafka.md b/docs/message-queue/kafka.md new file mode 100644 index 0000000..57a7d1f --- /dev/null +++ b/docs/message-queue/kafka.md @@ -0,0 +1,179 @@ +--- +sidebar: heading +title: Kafka常见面试题总结 +category: 消息队列 +tag: + - Kafka +head: + - - meta + - name: keywords + content: Kafka 面试题,Kafka设计架构,Kafka分区,Kafka Producer,Kafka pull模式,Kafka topic,Kafka优缺点 + - - meta + - name: description + content: 高质量的Kafka常见知识点和面试题总结,让天下没有难背的八股文! +--- + + + +## Kafka 都有哪些特点? + +- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。 +- 可扩展性:kafka集群支持热扩展 +- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失 +- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败) +- 高并发:支持数千个客户端同时读写 + +## 请简述下你在哪些场景下会选择 Kafka? + +- 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、HBase、Solr等。 +- 消息系统:解耦和生产者和消费者、缓存消息等。 +- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。 +- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。 +- 流式处理:比如spark streaming和 Flink + +## Kafka 的设计架构你知道吗? + + +Kafka 架构分为以下几个部分: + +- Producer :消息生产者,就是向 kafka broker 发消息的客户端。 +- Consumer :消息消费者,向 kafka broker 取消息的客户端。 +- Topic :可以理解为一个队列,一个 Topic 又分为一个或多个分区, +- Consumer Group:这是 kafka 用来实现一个 topic 消息的广播(发给所有的 consumer)和单播(发给任意一个 consumer)的手段。一个 topic 可以有多个 Consumer Group。 +- Broker :一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。 +- Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker上,每个 partition 是一个有序的队列。partition 中的每条消息都会被分配一个有序的id(offset)。将消息发给 consumer,kafka 只保证按一个 partition 中的消息的顺序,不保证一个 topic 的整体(多个 partition 间)的顺序。 +- Offset:kafka 的存储文件都是按照 offset.kafka 来命名,用 offset 做名字的好处是方便查找。例如你想找位于 2049 的位置,只要找到 2048.kafka 的文件即可。当然 the first offset 就是 00000000000.kafka。 + +## Kafka 分区的目的? + +分区对于 Kafka 集群的好处是:实现负载均衡。分区对于消费者来说,可以提高并发度,提高效率。 + +## 你知道 Kafka 是如何做到消息的有序性? + +kafka 中的每个 partition 中的消息在写入时都是有序的,而且单独一个 partition 只能由一个消费者去消费,可以在里面保证消息的顺序性。但是分区之间的消息是不保证有序的。 + +## Kafka Producer 的执行过程? + +1,Producer生产消息 --> 2,从Zookeeper找到Partition的Leader --> 3,推送消息 --> 4,通过ISR列表通知给Follower --> 5, Follower从Leader拉取消息,并发送ack --> 6,Leader收到所有副本的ack,更新Offset,并向Producer发送ack,表示消息写入成功。 + +## 讲一下你使用 Kafka Consumer 消费消息时的[线程模型](https://link.segmentfault.com/?enc=sniyt20KkIXAqIjQ4fraxQ%3D%3D.cmMRO%2FsnA0vXdDv1sttq1%2BSlDgGBRoAPWJw1zI%2Fslu10a6nBqqaCr9uYIZXuLMdYNK9%2B%2B17KBqxtMLvdyqlxYQ%3D%3D),为何如此设计? + +Thread-Per-Consumer Model,这种多线程模型是利用Kafka的topic分多个partition的机制来实现并行:每个线程都有自己的consumer实例,负责消费若干个partition。各个线程之间是完全独立的,不涉及任何线程同步和通信,所以实现起来非常简单。 + +## 请谈一谈 Kafka 数据一致性原理 + +一致性就是说不论是老的 Leader 还是新选举的 Leader,Consumer 都能读到一样的数据。 + +假设分区的副本为3,其中副本0是 Leader,副本1和副本2是 follower,并且在 ISR 列表里面。虽然副本0已经写入了 Message4,但是 Consumer 只能读取到 Message2。因为所有的 ISR 都同步了 Message2,只有 High Water Mark 以上的消息才支持 Consumer 读取,而 High Water Mark 取决于 ISR 列表里面偏移量最小的分区,对应于上图的副本2,这个很类似于木桶原理。 + +这样做的原因是还没有被足够多副本复制的消息被认为是“不安全”的,如果 Leader 发生崩溃,另一个副本成为新 Leader,那么这些消息很可能丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。试想,一个消费者从当前 Leader(副本0) 读取并处理了 Message4,这个时候 Leader 挂掉了,选举了副本1为新的 Leader,这时候另一个消费者再去从新的 Leader 读取消息,发现这个消息其实并不存在,这就导致了数据不一致性问题。 + +当然,引入了 High Water Mark 机制,会导致 Broker 间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕)。延迟时间可以通过参数 replica.lag.time.max.ms 参数配置,它指定了副本在复制消息时可被允许的最大延迟时间。 + +## ISR、OSR、AR 是什么? + +ISR:In-Sync Replicas 副本同步队列 + +OSR:Out-of-Sync Replicas + +AR:Assigned Replicas 所有副本 + +ISR是由leader维护,follower从leader同步数据有一些延迟(具体可以参见 图文了解 Kafka 的副本复制机制),超过相应的阈值会把 follower 剔除出 ISR, 存入OSR(Out-of-Sync Replicas )列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。 + +## LEO、HW、LSO、LW等分别代表什么 + +LEO:是 LogEndOffset 的简称,代表当前日志文件中下一条 + +HW:水位或水印(watermark)一词,也可称为高水位(high watermark),通常被用在流式处理领域(比如Apache Flink、Apache Spark等),以表征元素或事件在基于时间层面上的进度。在Kafka中,水位的概念反而与时间无关,而是与位置信息相关。严格来说,它表示的就是位置信息,即位移(offset)。取 partition 对应的 ISR中 最小的 LEO 作为 HW,consumer 最多只能消费到 HW 所在的位置上一条信息。 + +LSO:是 LastStableOffset 的简称,对未完成的事务而言,LSO 的值等于事务中第一条消息的位置(firstUnstableOffset),对已完成的事务而言,它的值同 HW 相同 + +LW:Low Watermark 低水位, 代表 AR 集合中最小的 logStartOffset 值。 + +## 数据传输的事务有几种? + +数据传输的事务定义通常有以下三种级别: + +(1)最多一次: 消息不会被重复发送,最多被传输一次,但也有可能一次不传输 +(2)最少一次: 消息不会被漏发送,最少被传输一次,但也有可能被重复传输. +(3)精确的一次(Exactly once): 不会漏传输也不会重复传输,每个消息都传输被 + +## Kafka 消费者是否可以消费指定分区消息? + +Kafa consumer消费消息时,向broker发出fetch请求去消费特定分区的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,customer拥有了offset的控制权,可以向后回滚去重新消费之前的消息,这是很有意义的。 + +## Kafka消息是采用Pull模式,还是Push模式? + +Kafka最初考虑的问题是,customer应该从brokes拉取消息还是brokers将消息推送到consumer,也就是pull还push。在这方面,Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。 + +一些消息系统比如Scribe和Apache Flume采用了push模式,将消息推送到下游的consumer。这样做有好处也有坏处:由broker决定消息推送的速率,对于不同消费速率的consumer就不太好处理了。消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是,push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。 + +Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。Push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免consumer崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull模式下,consumer就可以根据自己的消费能力去决定这些策略。 + +Pull有个缺点是,如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到t达。为了避免这点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发 + +## Kafka 高效文件存储设计特点 + +Kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用。 + +通过索引信息可以快速定位message和确定response的最大大小。 + +通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作。 + +通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小 + +## Kafka创建Topic时如何将分区放置到不同的Broker中 + +副本因子不能大于 Broker 的个数; + +第一个分区(编号为0)的第一个副本放置位置是随机从 brokerList 选择的; + +其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推; + +剩余的副本相对于第一个副本放置位置其实是由 nextReplicaShift 决定的,而这个数也是随机产生的 + +## 谈一谈 Kafka 的再均衡 + +在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义就是重新均衡消费者消费。Rebalance的过程如下: + +第一步:所有成员都向coordinator发送请求,请求入组。一旦所有成员都发送了请求,coordinator会从中选择一个consumer担任leader的角色,并把组成员信息以及订阅信息发给leader。 + +第二步:leader开始分配消费方案,指明具体哪个consumer负责消费哪些topic的哪些partition。一旦完成分配,leader会将这个方案发给coordinator。coordinator接收到分配方案之后会把方案发给各个consumer,这样组内的所有成员就都知道自己应该消费哪些分区了。 + +所以对于Rebalance来说,Coordinator起着至关重要的作用 + +## Kafka 是如何实现高吞吐率的? + +Kafka是分布式消息系统,需要处理海量的消息,Kafka的设计是把所有的消息都写入速度低容量大的硬盘,以此来换取更强的存储能力,但实际上,使用硬盘并没有带来过多的性能损失。kafka主要使用了以下几个方式实现了超高的吞吐率: + +- 顺序读写; +- 零拷贝 +- 文件分段 +- 批量发送 +- 数据压缩。 + +## Kafka 缺点? + +- 由于是批量发送,数据并非真正的实时; +- 对于mqtt协议不支持; +- 不支持物联网传感数据直接接入; +- 仅支持统一分区内消息有序,无法实现全局消息有序; +- 监控不完善,需要安装插件; +- 依赖zookeeper进行元数据管理; + + +## Kafka 新旧消费者的区别 + +旧的 Kafka 消费者 API 主要包括:SimpleConsumer(简单消费者) 和 ZookeeperConsumerConnectir(高级消费者)。SimpleConsumer 名字看起来是简单消费者,但是其实用起来很不简单,可以使用它从特定的分区和偏移量开始读取消息。高级消费者和现在新的消费者有点像,有消费者群组,有分区再均衡,不过它使用 ZK 来管理消费者群组,并不具备偏移量和再均衡的可操控性。 + +现在的消费者同时支持以上两种行为,所以为啥还用旧消费者 API 呢? + +## Kafka 分区数可以增加或减少吗?为什么? + +我们可以使用 bin/kafka-topics.sh 命令对 Kafka 增加 Kafka 的分区数据,但是 Kafka 不支持减少分区数。 + +Kafka 分区数据不支持减少是由很多原因的,比如减少的分区其数据放到哪里去?是删除,还是保留?删除的话,那么这些没消费的消息不就丢了。如果保留这些消息如何放到其他分区里面?追加到其他分区后面的话那么就破坏了 Kafka 单个分区的有序性。如果要保证删除分区数据插入到其他分区保证有序性,那么实现起来逻辑就会非常复杂。 + + + +> 参考:https://blog.51cto.com/u_15127589/2679155 diff --git a/docs/message-queue/mq.md b/docs/message-queue/mq.md new file mode 100644 index 0000000..1a688bc --- /dev/null +++ b/docs/message-queue/mq.md @@ -0,0 +1,192 @@ +--- +sidebar: heading +title: 消息队列常见面试题总结 +category: 消息队列 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列面试题,消息队列优缺点,消息队列对比,MQ常用协议,MQ通讯模式,消息重复消费,消息队列高可用 + - - meta + - name: description + content: 高质量的消息队列常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 为什么要使用消息队列? + +总结一下,主要三点原因:**解耦、异步、削峰**。 + +1、解耦。比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单操作失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。 + +2、异步。将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务。 + +3、削峰。消费端慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。比如秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉。这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,如果消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。 + +## 使用了消息队列会有什么缺点 + +- 系统可用性降低。引入消息队列之后,如果消息队列挂了,可能会影响到业务系统的可用性。 +- 系统复杂性增加。加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。 + +## 常见的消息队列对比 + +| 对比方向 | 概要 | +| -------- | ------------------------------------------------------------ | +| 吞吐量 | 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 | +| 可用性 | 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 时效性 | RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 | +| 功能支持 | 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | +| 消息丢失 | ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 | + +**总结:** + +- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 +- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 +- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + +## 如何保证消息队列的高可用? + +RabbitMQ:**镜像集群模式** + +RabbitMQ 是基于主从做高可用性的,Rabbitmq有三种模式:单机模式、普通集群模式、镜像集群模式。单机模式一般在生产环境中很少用,普通集群模式只是提高了系统的吞吐量,让集群中多个节点来服务某个 Queue 的读写操作。那么真正实现 RabbitMQ 高可用的是镜像集群模式。 + +镜像集群模式跟普通集群模式不一样的是,创建的 Queue,无论元数据还是Queue 里的消息都会存在于多个实例上,然后每次你写消息到 Queue 的时候,都会自动和多个实例的 Queue 进行消息同步。这样设计,好处在于:任何一个机器宕机不影响其他机器的使用。坏处在于:1. 性能开销太大:消息同步所有机器,导致网络带宽压力和消耗很重;2. 扩展性差:如果某个 Queue 负载很重,即便加机器,新增的机器也包含了这个 Queue 的所有数据,并没有办法线性扩展你的 Queue。 + +Kafka:**partition 和 replica 机制** + +Kafka 基本架构是多个 broker 组成,每个 broker 是一个节点。创建一个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据,这就是天然的分布式消息队列。就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。 + +Kafka 0.8 以前,是没有 HA 机制的,任何一个 broker 宕机了,它的 partition 就没法写也没法读了,没有什么高可用性可言。 + +Kafka 0.8 以后,提供了 HA 机制,就是 replica 副本机制。每个 partition 的数据都会同步到其他机器上,形成自己的多个 replica 副本。然后所有 replica 会选举一个 leader 出来,生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上数据即可。Kafka 会均匀的将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 + +## MQ常用协议 + +- **AMQP协议** AMQP即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 + + > 优点:可靠、通用 + +- **MQTT协议** MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。 + + > 优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统 + +- **STOMP协议** STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。 + + > 优点:命令模式(非topic/queue模式) + +- **XMPP协议** XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。 + + > 优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大 + +- **其他基于TCP/IP自定义的协议**:有些特殊框架(如:redis、kafka、zeroMq等)根据自身需要未严格遵循MQ规范,而是基于TCP\IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。 + +## MQ的通讯模式 + +1. **点对点通讯**:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 +2. **多点广播**:MQ适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点(Destination List)。可以使用一条MQ指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ将消息的一个复制版本和该系统上接收者的名单发送到目标MQ系统。目标MQ系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 +3. **发布/订阅(Publish/Subscribe)模式**:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。在MQ家族产品中,MQ Event Broker是专门用于使用发布/订阅技术进行数据通讯的产品,它支持基于队列和直接基于TCP/IP两种方式的发布和订阅。 +4. **集群(Cluster)**:为了简化点对点通讯模式中的系统配置,MQ提供 Cluster 的解决方案。集群类似于一个 域(Domain) ,集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用 Cluster 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性 + +## 如何保证消息的顺序性? + +**RabbitMQ** + +拆分多个 Queue,每个 Queue一个 Consumer;或者就一个 Queue 但是对应一个 Consumer,然后这个 Consumer 内部用内存队列做排队,然后分发给底层不同的 Worker 来处理。 + +**Kafka** + +1. 一个 Topic,一个 Partition,一个 Consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 + +2. 写 N 个内存 Queue,具有相同 key 的数据都到同一个内存 Queue;然后对于 N 个线程,每个线程分别消费一个内存 Queue 即可,这样就能保证顺序性。 +## 如何避免消息重复消费? + +在消息生产时,MQ内部针对每条生产者发送的消息生成一个唯一id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列。 + +在消息消费时,要求消息体中也要有一全局唯一id作为去重和幂等的依据,避免同一条消息被重复消费。 + +## 大量消息在 MQ 里长时间积压,该如何解决? + +一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下: + +1. 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉; +2. 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量; +3. 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue; +4. 接着临时用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据; +5. 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。 + +## MQ 中的消息过期失效了怎么办? + +如果使用的是RabbitMQ的话,RabbtiMQ 是可以设置过期时间的(TTL)。如果消息在 Queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。这时的问题就不是数据会大量积压在 MQ 里,而是大量的数据会直接搞丢。这个情况下,就不是说要增加 Consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。 + +我们可以采取一个方案,就是批量重导。就是大量积压的时候,直接将数据写到数据库,然后等过了高峰期以后将这批数据一点一点的查出来,然后重新灌入 MQ 里面去,把丢的数据给补回来。 + +## 消息中间件如何做到高可用? + +以Kafka为例。 + + Kafka 的基础集群架构,由多个`broker`组成,每个`broker`都是一个节点。当你创建一个`topic`时,它可以划分为多个`partition`,而每个`partition`放一部分数据,分别存在于不同的 broker 上。也就是说,一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。 + +每个`partition`放一部分数据,如果对应的broker挂了,那这部分数据是不是就丢失了?那不是保证不了高可用吗? + +Kafka 0.8 之后,提供了复制多副本机制来保证高可用,即每个 partition 的数据都会同步到其它机器上,形成多个副本。然后所有的副本会选举一个 leader 出来,让leader去跟生产和消费者打交道,其他副本都是follower。写数据时,leader 负责把数据同步给所有的follower,读消息时,直接读 leader 上的数据即可。如何保证高可用的?就是假设某个 broker 宕机,这个broker上的partition 在其他机器上都有副本的。如果挂的是leader的broker呢?其他follower会重新选一个leader出来。 + +## 如何保证数据一致性,事务消息如何实现? + +一条普通的MQ消息,从产生到被消费,大概流程如下: + +![](http://img.topjavaer.cn/img/消息一致性1.png) + +1. 生产者产生消息,发送带MQ服务器 +2. MQ收到消息后,将消息持久化到存储系统。 +3. MQ服务器返回ACk到生产者。 +4. MQ服务器把消息push给消费者 +5. 消费者消费完消息,响应ACK +6. MQ服务器收到ACK,认为消息消费成功,即在存储中删除消息。 + +举个下订单的例子吧。订单系统创建完订单后,再发送消息给下游系统。如果订单创建成功,然后消息没有成功发送出去,下游系统就无法感知这个事情,出导致数据不一致。 +如何保证数据一致性呢?可以使用**事务消息**。一起来看下事务消息是如何实现的吧。 + +![](http://img.topjavaer.cn/img/消息一致性2.png) + +1. 生产者产生消息,发送一条半事务消息到MQ服务器 +2. MQ收到消息后,将消息持久化到存储系统,这条消息的状态是待发送状态。 +3. MQ服务器返回ACK确认到生产者,此时MQ不会触发消息推送事件 +4. 生产者执行本地事务 +5. 如果本地事务执行成功,即commit执行结果到MQ服务器;如果执行失败,发送rollback。 +6. 如果是正常的commit,MQ服务器更新消息状态为可发送;如果是rollback,即删除消息。 +7. 如果消息状态更新为可发送,则MQ服务器会push消息给消费者。消费者消费完就回ACK。 +8. 如果MQ服务器长时间没有收到生产者的commit或者rollback,它会反查生产者,然后根据查询到的结果执行最终状态。 + +## 如何设计一个消息队列? + +首先是消息队列的整体流程,producer发送消息给broker,broker存储好,broker再发送给consumer消费,consumer回复消费确认等。 + +producer发送消息给broker,broker发消息给consumer消费,那就需要两次RPC了,RPC如何设计呢?可以参考开源框架Dubbo,你可以说说服务发现、序列化协议等等 + +broker考虑如何持久化呢,是放文件系统还是数据库呢,会不会消息堆积呢,消息堆积如何处理呢。 + +消费关系如何保存呢? 点对点还是广播方式呢?广播关系又是如何维护呢?zk还是config server + +消息可靠性如何保证呢?如果消息重复了,如何幂等处理呢? + +消息队列的高可用如何设计呢? 可以参考Kafka的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。 + +消息事务特性,与本地业务同个事务,本地消息落库;消息投递到服务端,本地才删除;定时任务扫描本地消息库,补偿发送。 + +MQ得伸缩性和可扩展性,如果消息积压或者资源不够时,如何支持快速扩容,提高吞吐?可以参照一下 Kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了吗。 + +[参考链接](https://juejin.cn/post/7088909199475736612#heading-18) + +## 多线程异步和MQ的区别 + +- **CPU消耗**。多线程异步可能存在CPU竞争,而MQ不会消耗本机的CPU。 +- MQ 方式实现异步是完全**解耦**的,适合于大型互联网项目。 +- **削峰或者消息堆积能力**。当业务系统处于高并发,MQ可以将消息堆积在Broker实例中,而多线程会创建大量线程,甚至触发拒绝策略。 +- 使用MQ引入了中间件,增加了项目复杂度和运维难度。 + +总的来说,规模比较小的项目可以使用多线程实现异步,大项目建议使用MQ实现异步。 + + + +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/message-queue/mq/consume-by-order.md b/docs/message-queue/mq/consume-by-order.md new file mode 100644 index 0000000..e3d0a84 --- /dev/null +++ b/docs/message-queue/mq/consume-by-order.md @@ -0,0 +1,52 @@ +--- +sidebar: heading +title: 如何保证消息的顺序性? +category: 消息队列 +tag: + - 消息队列 +head: + - - meta + - name: keywords + content: 消息队列面试题,如何保证消息消费的顺序性 + - - meta + - name: description + content: 高质量的消息队列常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 如何保证消息的顺序性? + +假设有这样一个场景,使用 MySQL `binlog` 将数据从一个 MySQL 库原封不动地同步到另一个 MySQL 库里面去。 + +你在 MySQL 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,这时需要保证按照顺序去执行,不然本来是:增加、修改、删除;调换了顺序变成执行成删除、修改、增加,这就有问题了。 + +本来这个数据同步过来,应该最后这个数据被删除了;结果搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。 + +先看看顺序会错乱的俩场景: + +- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这明显就乱了。 + +![](http://img.topjavaer.cn/img/rabbitmq-order-01.png) + +- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 +- 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。 + +![](http://img.topjavaer.cn/img/kafka-order-01.png) + +### 解决方案 + +#### RabbitMQ + +拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点,这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。 + +![](http://img.topjavaer.cn/img/rabbitmq-order-02.png) + +或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。 + +注意,这里消费者不直接消费消息,而是将消息根据关键值(比如:订单 id)进行哈希,哈希值相同的消息保存到相同的内存队列里。也就是说,需要保证顺序的消息存到了相同的内存队列,然后由一个唯一的 worker 去处理。 + +#### Kafka + +- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 +- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 + +![](http://img.topjavaer.cn/img/kafka-order-02.png) diff --git "a/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ.md" b/docs/message-queue/rabbitmq.md similarity index 86% rename from "\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ.md" rename to docs/message-queue/rabbitmq.md index c89865f..169d0a8 100644 --- "a/\346\266\210\346\201\257\351\230\237\345\210\227/RabbitMQ.md" +++ b/docs/message-queue/rabbitmq.md @@ -1,41 +1,24 @@ - - - - -- [简介](#%E7%AE%80%E4%BB%8B) - - [基本概念](#%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5) - - [什么时候使用MQ](#%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BD%BF%E7%94%A8mq) - - [优缺点](#%E4%BC%98%E7%BC%BA%E7%82%B9) -- [Exchange 类型](#exchange-%E7%B1%BB%E5%9E%8B) - - [direct](#direct) - - [fanout](#fanout) - - [topic](#topic) - - [headers](#headers) -- [消息丢失](#%E6%B6%88%E6%81%AF%E4%B8%A2%E5%A4%B1) - - [生产者确认机制](#%E7%94%9F%E4%BA%A7%E8%80%85%E7%A1%AE%E8%AE%A4%E6%9C%BA%E5%88%B6) - - [路由不可达消息](#%E8%B7%AF%E7%94%B1%E4%B8%8D%E5%8F%AF%E8%BE%BE%E6%B6%88%E6%81%AF) - - [Return消息机制](#return%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6) - - [备份交换机](#%E5%A4%87%E4%BB%BD%E4%BA%A4%E6%8D%A2%E6%9C%BA) - - [消费者手动消息确认](#%E6%B6%88%E8%B4%B9%E8%80%85%E6%89%8B%E5%8A%A8%E6%B6%88%E6%81%AF%E7%A1%AE%E8%AE%A4) - - [持久化](#%E6%8C%81%E4%B9%85%E5%8C%96) - - [镜像队列](#%E9%95%9C%E5%83%8F%E9%98%9F%E5%88%97) -- [重复消费](#%E9%87%8D%E5%A4%8D%E6%B6%88%E8%B4%B9) -- [消费端限流](#%E6%B6%88%E8%B4%B9%E7%AB%AF%E9%99%90%E6%B5%81) -- [死信队列](#%E6%AD%BB%E4%BF%A1%E9%98%9F%E5%88%97) -- [其他](#%E5%85%B6%E4%BB%96) - - [pull模式](#pull%E6%A8%A1%E5%BC%8F) - - [消息过期时间](#%E6%B6%88%E6%81%AF%E8%BF%87%E6%9C%9F%E6%97%B6%E9%97%B4) -- [参考链接](#%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5) - - - -# 简介 +--- +sidebar: heading +title: RabbitMQ常见面试题总结 +category: 消息队列 +tag: + - RabbitMQ +head: + - - meta + - name: keywords + content: RabbitMQ面试题,RabbitMQ组件,RabbitMQ Exchange,RabbitMQ消息丢失,消息重复消费,死信队列 + - - meta + - name: description + content: RabbitMQ常见知识点和面试题总结,让天下没有难背的八股文! +--- +## 什么是RabbitMQ? RabbitMQ是一个由erlang开发的消息队列。消息队列用于应用间的异步协作。 -![](http://img.dabin-coder.cn/image/rabbitmq.png) +![](http://img.topjavaer.cn/img/rabbitmq.png) -## 基本概念 +## RabbitMQ的组件 Message:由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key、priority、delivery-mode(是否持久性存储)等。 @@ -57,15 +40,23 @@ Broker:消息队列服务器实体。 以常见的订单系统为例,用户点击下单按钮之后的业务逻辑可能包括:扣减库存、生成相应单据、发短信通知。这种场景下就可以用 MQ 。将短信通知放到 MQ 异步执行,在下单的主流程(比如扣减库存、生成相应单据)完成之后发送一条消息到 MQ, 让主流程快速完结,而由另外的线程消费MQ的消息。 -## 优缺点 +## RabbitMQ的优缺点 缺点:使用erlang实现,不利于二次开发和维护;性能较kafka差,持久化消息和ACK确认的情况下生产和消费消息单机吞吐量大约在1-2万左右,kafka单机吞吐量在十万级别。 优点:有管理界面,方便使用;可靠性高;功能丰富,支持消息持久化、消息确认机制、多种消息分发机制。 +## RabbitMQ 有哪些重要的角色? +RabbitMQ 中重要的角色有:生产者、消费者和代理。 -# Exchange 类型 +1. 生产者:消息的创建者,负责创建和推送数据到消息服务器; + +2. 消费者:消息的接收方,用于处理数据和确认消息; + +3. 代理:就是 RabbitMQ 本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色。 + +## Exchange 类型 Exchange分发消息时根据类型的不同分发策略不同,目前共四种类型:direct、fanout、topic、headers 。headers 模式根据消息的headers进行路由,此外 headers 交换器和 direct 交换器完全一致,但性能差很多。 @@ -78,35 +69,35 @@ Exchange规则。 | topic | 模糊匹配 | | headers | Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的header属性进行匹配。 | -## direct +**direct** direct交换机会将消息路由到binding key 和 routing key完全匹配的队列中。它是完全匹配、单播的模式。 -![](http://img.dabin-coder.cn/image/rabbitmq-direct.png) +![](http://img.topjavaer.cn/img/rabbitmq-direct.png) -## fanout +**fanout** 所有发到 fanout 类型交换机的消息都会路由到所有与该交换机绑定的队列上去。fanout 类型转发消息是最快的。 -![](http://img.dabin-coder.cn/image/rabbitmq-fanout.png) +![](http://img.topjavaer.cn/img/rabbitmq-fanout.png) -## topic +**topic** -topic交换机使用routing key和binding key进行模糊匹配,匹配成功则将消息发送到相应的队列。routing key和binding key都是句点号“. ”分隔的字符串,binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“\*”用于匹配一个单词,“#”用于匹配多个单词。 +topic交换机使用routing key和binding key进行模糊匹配,匹配成功则将消息发送到相应的队列。routing key和binding key都是句点号“. ”分隔的字符串,binding key中可以存在两种特殊字符“*”与“##”,用于做模糊匹配,其中“\*”用于匹配一个单词,“##”用于匹配多个单词。 -![](http://img.dabin-coder.cn/image/rabbitmq-topic.png) +![](http://img.topjavaer.cn/img/rabbitmq-topic.png) -## headers +**headers** headers交换机是根据发送的消息内容中的headers属性进行路由的。在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。 -# 消息丢失 +## 消息丢失 消息丢失场景:生产者生产消息到RabbitMQ Server消息丢失、RabbitMQ Server存储的消息丢失和RabbitMQ Server到消费者消息丢失。 消息丢失从三个方面来解决:生产者确认机制、消费者手动确认消息和持久化。 -## 生产者确认机制 +### 生产者确认机制 生产者发送消息到队列,无法确保发送的消息成功的到达server。 @@ -120,7 +111,7 @@ headers交换机是根据发送的消息内容中的headers属性进行路由的 ```yaml spring: rabbitmq: - #开启 confirm 确认机制 + ##开启 confirm 确认机制 publisher-confirms: true ``` @@ -139,20 +130,20 @@ final RabbitTemplate.ConfirmCallback confirmCallback = (CorrelationData correlat rabbitTemplate.setConfirmCallback(confirmCallback); ``` -## 路由不可达消息 +### 路由不可达消息 生产者确认机制只确保消息正确到达交换机,对于从交换机路由到Queue失败的消息,会被丢弃掉,导致消息丢失。 对于不可路由的消息,有两种处理方式:Return消息机制和备份交换机。 -### Return消息机制 +**Return消息机制** Return消息机制提供了回调函数 ReturnCallback,当消息从交换机路由到Queue失败才会回调这个方法。需要将`mandatory` 设置为 `true` ,才能监听到路由不可达的消息。 ```yaml spring: rabbitmq: - #触发ReturnCallback必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发ReturnCallback + ##触发ReturnCallback必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发ReturnCallback template.mandatory: true ``` @@ -167,11 +158,11 @@ rabbitTemplate.setReturnCallback(returnCallback); 当消息从交换机路由到Queue失败时,会返回 `return exchange: , routingKey: MAIL, replyCode: 312, replyText: NO_ROUTE`。 -### 备份交换机 +**备份交换机** 备份交换机alternate-exchange 是一个普通的exchange,当你发送消息到对应的exchange时,没有匹配到queue,就会自动转移到备份交换机对应的queue,这样消息就不会丢失。 -## 消费者手动消息确认 +### 消费者手动消息确认 有可能消费者收到消息还没来得及处理MQ服务就宕机了,导致消息丢失。因为消息者默认采用自动ack,一旦消费者收到消息后会通知MQ Server这条消息已经处理好了,MQ 就会移除这条消息。 @@ -180,7 +171,7 @@ rabbitTemplate.setReturnCallback(returnCallback); 消费者设置手动ack: ```java -#设置消费端手动 ack +##设置消费端手动 ack spring.rabbitmq.listener.simple.acknowledge-mode=manual ``` @@ -204,7 +195,7 @@ spring.rabbitmq.listener.simple.acknowledge-mode=manual 当消息消费失败时,消费端给broker回复nack,如果consumer设置了requeue为false,则nack后broker会删除消息或者进入死信队列,否则消息会重新入队。 -## 持久化 +### 持久化 如果RabbitMQ服务异常导致重启,将会导致消息丢失。RabbitMQ提供了持久化的机制,将内存中的消息持久化到硬盘上,即使重启RabbitMQ,消息也不会丢失。 @@ -216,15 +207,13 @@ spring.rabbitmq.listener.simple.acknowledge-mode=manual 当发布一条消息到交换机上时,Rabbit会先把消息写入持久化日志,然后才向生产者发送响应。一旦从队列中消费了一条消息的话并且做了确认,RabbitMQ会在持久化日志中移除这条消息。在消费消息前,如果RabbitMQ重启的话,服务器会自动重建交换机和队列,加载持久化日志中的消息到相应的队列或者交换机上,保证消息不会丢失。 -## 镜像队列 +### 镜像队列 当MQ发生故障时,会导致服务不可用。引入RabbitMQ的镜像队列机制,将queue镜像到集群中其他的节点之上。如果集群中的一个节点失效了,能自动地切换到镜像中的另一个节点以保证服务的可用性。 通常每一个镜像队列都包含一个master和多个slave,分别对应于不同的节点。发送到镜像队列的所有消息总是被直接发送到master和所有的slave之上。除了publish外所有动作都只会向master发送,然后由master将命令执行的结果广播给slave,从镜像队列中的消费操作实际上是在master上执行的。 - - -# 重复消费 +## 消息重复消费怎么处理? 消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复。 @@ -238,9 +227,7 @@ spring.rabbitmq.listener.simple.acknowledge-mode=manual 2. 如果不存在,则正常消费,消费完毕后写入redis/db 3. 如果存在,则证明消息被消费过,直接丢弃 - - -# 消费端限流 +## 消费端怎么进行限流? 当 RabbitMQ 服务器积压大量消息时,队列里的消息会大量涌入消费端,可能导致消费端服务器奔溃。这种情况下需要对消费端限流。 @@ -249,7 +236,7 @@ Spring RabbitMQ 提供参数 prefetch 可以设置单个请求处理的消息个 开启消费端限流: ```properties -#在单个请求中处理的消息个数,unack的最大数量 +##在单个请求中处理的消息个数,unack的最大数量 spring.rabbitmq.listener.simple.prefetch=2 ``` @@ -261,7 +248,7 @@ spring.rabbitmq.listener.simple.prefetch=2 void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException; ``` -# 死信队列 +## 什么是死信队列? 消费失败的消息存放的队列。 @@ -383,9 +370,7 @@ public class DeadListener { 当普通队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的死信交换机去,然后被路由到死信队列。可以监听死信队列中的消息做相应的处理。 -# 其他 - -## pull模式 +## 说说pull模式 pull模式主要是通过channel.basicGet方法来获取消息,示例代码如下: @@ -395,7 +380,7 @@ System.out.println(new String(response.getBody())); channel.basicAck(response.getEnvelope().getDeliveryTag(),false); ``` -## 消息过期时间 +## 怎么设置消息的过期时间? 在生产端发送消息的时候可以给消息设置过期时间,单位为毫秒(ms) @@ -406,7 +391,7 @@ msg.getMessageProperties().setExpiration("3000"); 也可以在创建队列的时候指定队列的ttl,从消息入队列开始计算,超过该时间的消息将会被移除。 -# 参考链接 +## 参考链接 [RabbitMQ基础](https://www.jianshu.com/p/79ca08116d57) @@ -420,4 +405,4 @@ msg.getMessageProperties().setExpiration("3000"); -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/note/README.md b/docs/note/README.md new file mode 100644 index 0000000..3a19bc2 --- /dev/null +++ b/docs/note/README.md @@ -0,0 +1,6 @@ +- [一小时彻底吃透Redis](https://topjavaer.cn/note/redis-note.html) +- [21个写SQL的好习惯](https://topjavaer.cn/note/write-sql.html) +- [Docker详解与部署微服务实战](https://topjavaer.cn/note/docker-note.html) +- [计算机专业的同学都看看这几点建议](https://topjavaer.cn/note/computor-advice.html) +- [建议计算机专业同学都看看这门课](https://topjavaer.cn/note/computor-advice.html) + diff --git a/docs/note/computer-blogger.md b/docs/note/computer-blogger.md new file mode 100644 index 0000000..9c23b7d --- /dev/null +++ b/docs/note/computer-blogger.md @@ -0,0 +1,44 @@ +## 湖科大教书匠——计算机网络 + +“宝藏老师”、“干货满满”、“羡慕湖科大”...这些都是网友对这门网课的评价,可见网课质量之高! + +湖南科技大学《计算机网络》微课堂是该校高军老师精心制作的视频课程,用简单的语言描述复杂的问题,用生动的动画演示抽象概念,更加便于学生理解和记忆。推荐初学者去看看,一定不会亏! + +![](http://img.topjavaer.cn/img/image-20221129085008654.png) + + + +## 鱼C-小甲鱼——带你学C带你飞 + +小甲鱼的课程是很多转码人的第一门编程课,非常不错,对于自学的初学者来说挺友好。小甲鱼会从学生的角度思考问题,会针对一些初学者的疑惑进行解答,比很多大学老师教学水平强太多! + +另外,小甲鱼的零基础入门学习python系列课程也很不错,推荐~ + +![](http://img.topjavaer.cn/img/image-20221116234052322.png) + +## 跟李沐学AI——机器学习 + +bilibili 2021新人奖UP主、亚马逊资深首席科学家,李沐老师的机器学习课程,可以说是机器学习入门课程的天花板,非常适合新手入门,没有很复杂的推导过程和数学知识,偏向于运用的角度。 + +![](http://img.topjavaer.cn/img/image-20221116235358812.png) + +## 莫烦Python——Python + +莫烦python的Python基础课程非常适合刚入门, 或者是以前使用过其语言的朋友,每一段视频都不会很长,节节相连,对于迅速掌握基础的使用方法很有帮助。 + +![](http://img.topjavaer.cn/img/image-20221116235001215.png) + + + +## 懒猫老师——用动画讲编程 + +每个视频都用心制作,形象生动,用动画讲编程,在快乐中学习编程,太赞了。 + +![](http://img.topjavaer.cn/img/image-20221116234925174.png) + +## 尚硅谷——Java教程 + +虽然是培训机构,但是尚硅谷也在某站上传了很多编程入门的视频,质量相对还是不错的,Java入门教程的播放量达到千万了,很多人都是看尚硅谷的视频学Java的哈哈(尚硅谷打qian!)。 + +![](http://img.topjavaer.cn/img/image-20221116235238701.png) + diff --git a/docs/note/computer-course.md b/docs/note/computer-course.md new file mode 100644 index 0000000..f6f859f --- /dev/null +++ b/docs/note/computer-course.md @@ -0,0 +1,65 @@ +## GitHub + +![](https://lthub.ubc.ca/files/2021/06/GitHub-Logo.png) + +GitHub是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。 + +作为开源代码库以及版本控制系统,Github拥有超过900万开发者用户。随着越来越多的应用程序转移到了云上,Github已经成为了管理软件开发以及发现已有代码的首选方法。 + +在GitHub,用户甚至可以十分轻易地找到海量的开源代码。 + + + +## LeetCode + +![](http://img.topjavaer.cn/img/力扣.jpeg) + +力扣,强推!力扣虐我千百遍,我待力扣如初恋! + +LeetCode是一个集合了大量算法面试题和AI面试题的网站,它为全世界的码农提供了练习自我技能的良好平台。 + +除了在题库中直接找题目之外,还可以根据该网站提供的阶梯训练进行练习。阶梯训练板块收纳了中美等不同公司的面试真题并以关卡的形式呈现给挑战者 + +## GeeksforGeeks + +![](http://img.topjavaer.cn/img/image-20221105170338602.png) + +GeeksforGeeks是一个主要专注于计算机科学的网站。它有大量的算法,解决方案和编程问题。该网站也有很多面试中经常问到的问题。由于该网站更多地涉及计算机科学,因此你可以找到很多编程问题在大多数著名语言下的解决方案。 + +## StackOverflow + +![](http://img.topjavaer.cn/img/image-20221105170639698.png) + +程序员最痛苦的事莫过于深陷于BUG的泥潭,而stack overflow作为全球最大的技术问答网站,可以说每个搞过技术的人是必上的网站。开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。 + +在 Stackoverflow 你也可以看到很多经典的问题,我们也可以从这些问题中学习如何去提问,如何和答题者沟通。 + +## Codebeautify + +![](http://img.topjavaer.cn/img/image-20221105170740833.png) + +由于我们是程序员,所以美不是我们所关心的。很多时候,我们的代码很难被其他人阅读。Codebeautify可以使你的代码易于阅读。该网站有大多数可以美化的语言。另外,如果你想让你的代码不能被某人读取,你也可以这样做。 + + + +## 菜鸟教程 + +菜鸟教程提供了编程的基础技术教程, 介绍了HTML、CSS、Javascript、Python,Java,Ruby,C,PHP , MySQL等各种编程语言的基础知识,对于计算机小白和初学者非常有用! + +![](http://img.topjavaer.cn/img/image-20221110233257596.png) + + + +## 中国大学MOOC + +![](http://img.topjavaer.cn/img/image-20221110233700261.png) + +中国大学MOOC(慕课) 是国内优质的中文MOOC学习平台,由网易有道与高教社携手推出的中国大学生MOOC承载了一万多门开放课、1400多门国家级精品课,与803所高校开展合作,已经成为最大的中文慕课平台。课程相对比较优质,推荐。 + + + +## 牛客网 + +![](http://img.topjavaer.cn/img/image-20221110233635419.png) + +牛客网是一个集笔面试系统、题库、课程教育、社群交流、招聘内推于一体的招聘类网站。牛客网题库中包含几万道题目,题库涵盖六类行业题目,包含:IT技术类、硬件类、产品运营类、金融财会类、市场营销类、管理类、职能类。而且牛客网每年都会组织往年求职者分享面试经验,以供后来者学习、参考、交流。 \ No newline at end of file diff --git a/docs/note/computer-site.md b/docs/note/computer-site.md new file mode 100644 index 0000000..bafafc6 --- /dev/null +++ b/docs/note/computer-site.md @@ -0,0 +1,77 @@ +## LeetCode + +力扣,强推!力扣虐我千百遍,我待力扣如初恋! + +![](http://img.topjavaer.cn/img/image-20221119231353893.png) + +![](http://img.topjavaer.cn/img/image-20221119231150228.png) + +从现在开始,每天一道力扣算法题,坚持几个月的时间,你会感谢我的(傲娇脸) + +我刚开始刷算法题的时候,就选择在力扣上刷。最初刷easy级别题目的时候,都感觉有点吃力,坚持半年之后,遇到中等题目甚至hard级别的题目都不慌了。 + +不过是熟能生巧罢了。 + +## Programming by Doing + +网站的宗旨就是:“学习的最好方法就是去做”。 + +以作业的形式整理的编程基础题,题目相对还是比较简单的,适合刚入门的初学者。 + +![](http://img.topjavaer.cn/img/image-20221119231133459.png) + +## 洛谷 + +洛谷上的题目很多,还有很多的基础题,使用体验良好。 + +缺点是没有相应的阶梯训练,筛选方式比较少。 + +![](http://img.topjavaer.cn/img/image-20221119231107703.png) + + + +## 牛客网 + +牛客网拥有超级丰富的 IT题库,题库+面试+学习+求职+讨论,基本涵盖所有面试笔试题型,堪称"互联网求职神器"。在这里不仅可以刷题,还可以跟其他牛友讨论交流,一起成长。牛客上还会各种的内推机会,对于求职的同学也是极其不错的。 + +![](http://img.topjavaer.cn/img/image-20221119233620798.png) + +## LintCode + +与Leetcode类似的刷题网站。 + +LeetCode/LintCode的题目量差不多。LeetCode的test case比较完备,并且LeetCode有讨论区,看别人的代码还是比较有意义的。 + +LintCode的UI、tagging、filter更加灵活,更有优点,大家选择其中一个进行刷题即可。 + +![](http://img.topjavaer.cn/img/image-20221119231323014.png) + +## AcCoder + +AtCoder是日本最大的算法竞技网站,支持日语和英语两种语言,顺带可以学学日文,太妙了! + +tips:右上角椭圆内可以切换英语日语 + +![](http://img.topjavaer.cn/img/image-20221119232242865.png) + +## Timus Online Judge + +俄罗斯最大的刷题网站——Timus Online Judge,网站有比较进阶的算法题目,难度偏高,想在算法层面精进的的小伙伴可以试一试哦。 + +![](http://img.topjavaer.cn/img/image-20221119232511963.png) + + + +## UVa Online Judge + +西班牙Valladolid大学的Online Judge,最古老也是全世界最知名的Online Judge,题库有详细的分类,题目类型非常广泛。最重要的是,题目类型属于中等,适合有一定基础的刷题选手。 + +![](http://img.topjavaer.cn/img/image-20221119231231975.png) + +## Codeforces + +Codeforce是一个位于俄罗斯的编程比赛网站,它会定期举办竞赛,会有全球顶尖的程序员们参赛。在这个网站,可以练习从初级到高级的题目。 + +Codeforce每周会有2-3场比赛,感兴趣的小伙伴可以去挑战下~ + +![](http://img.topjavaer.cn/img/image-20221119231248969.png) \ No newline at end of file diff --git a/docs/note/computer-teacher.md b/docs/note/computer-teacher.md new file mode 100644 index 0000000..69dd09f --- /dev/null +++ b/docs/note/computer-teacher.md @@ -0,0 +1,86 @@ +## C语言教程——翁凯老师、赫斌 + +翁恺老师是土生土长的浙大码农,从本科到博士都毕业于浙大计算机系,后来留校教书,一教就是20多年。 + +翁恺老师的c语言课程非常好,讲解特别有趣,很适合初学者学习。 + +![](http://img.topjavaer.cn/img/image-20221031005008417.png) + +郝斌老师的思路是以初学者的思路来思考的,非常适合小白,你不理解的问题,基本上他都会详细说一下。 + +![](http://img.topjavaer.cn/img/image-20221117084401974.png) + + + +## C++——侯捷老师 + +看了候老师的课有种醍醐灌顶的感觉,强烈建议自学c++ 者仔细看候捷老师的课,会受益匪浅。 + +![](http://img.topjavaer.cn/img/image-20221112212137063.png) + +## 数据结构课程——陈越、王卓老师 + +青岛大学王卓老师的数据结构与算法基础,课程评价很高,通俗易懂,适合零基础入门。 + +![](http://img.topjavaer.cn/img/image-20221112213039156.png) + +陈越老师,被同学们尊称为“姥姥”,为各种同学答疑解惑,事无巨细,非常有热情和接地气,是个亲切的长辈一样的存在。 + +陈姥姥的课简单易懂,评价也非常高,认真学了肯定有很大的收获~ + +![](http://img.topjavaer.cn/img/image-20221031005353949.png) + + + +## 操作系统——李治军老师、向勇、陈渝 + +李治军老师的操作系统课程非常棒,讲解很清晰、详细,配图也相当丰富,对初学者很友好。 + +![](http://img.topjavaer.cn/img/image-20221112213334202.png) + +我看过不同老师讲的操作系统课程,觉得比较好的入门级课程是清华大学开设的网课《操作系统》,该课程由清华大学老师向勇和陈渝授课,虽然大彬上不了清华大学,但是至少可以在网上选择听清华大学的课嘛。 + +![](http://img.topjavaer.cn/img/image-20221031005947517.png) + +## 计算机网络——郑烇、杨坚老师 + +中科大郑烇、杨坚老师的计算机网络,老师讲课很幽默,思路很清晰,最重要的是,可以跟中科大学生一起完成专业知识的学习~ + +![](http://img.topjavaer.cn/img/image-20221112213757805.png) + + + +## 数据库——战德臣老师 + +哈尔滨工业大学战德臣老师的数据库系统原理,是国家精品课程,值得大家去学习。 + +![](http://img.topjavaer.cn/img/image-20221112214011640.png) + +## 机器学习——吴恩达、李沐 + +吴恩达老师,斯坦福计算机系的副教授,师从机器学习的大师级人物 Michael I. Jordan。吴老是,徒弟遍布美国名校,他们这一大学派的主要研究和贡献集中在统计机器学习(Statistical Machine Learning)和图模型(Probabilistic Graphical model)等。 + +更重要的是,他在学术圈内圈外知名度很高!除了师承之外,还有一个重要原因是他在斯坦福公开课里面主讲机器学习,讲的的确是非常好,在工程界非常受欢迎。 + +![](http://img.topjavaer.cn/img/image-20221031005740777.png) + +bilibili 2021新人奖UP主、亚马逊资深首席科学家,李沐老师的机器学习课程,可以说是机器学习入门课程的天花板,非常适合新手入门,没有很复杂的推导过程和数学知识,偏向于运用的角度。 + +![](http://img.topjavaer.cn/img/image-20221116235358812.png) + +## python系列课程——嵩天 + +嵩天老师是国内大学Python教育的先行者, 是Python语言进入计算机等级考试的关键人物,为推广Python语言进入大学教育序列,作了很大贡献。嵩天老师的python系列视频深入浅出,值得零基础入学。 + +![](http://img.topjavaer.cn/img/image-20221031010632456.png) + + + +## Java基础课程——韩顺平 + +韩顺平老师的Java课程主要面向初学者,讲课幽默风趣,通俗易懂,善于用已知的概念解释编程问题,对初学者非常友好,是自学Java很不错的选择! + +![](http://img.topjavaer.cn/img/image-20221031011126265.png) + + + diff --git a/docs/note/computor-advice.md b/docs/note/computor-advice.md new file mode 100644 index 0000000..7db0369 --- /dev/null +++ b/docs/note/computor-advice.md @@ -0,0 +1,9 @@ +计算机专业的同学都看看这几点建议! + +![](http://img.topjavaer.cn/img/202306072222051.png) + +![](http://img.topjavaer.cn/img/202306072222065.png) + +![](http://img.topjavaer.cn/img/202306072223134.png) + +![](http://img.topjavaer.cn/img/202306072223890.png) \ No newline at end of file diff --git a/docs/note/crash-course-computer-science.md b/docs/note/crash-course-computer-science.md new file mode 100644 index 0000000..0c00bf6 --- /dev/null +++ b/docs/note/crash-course-computer-science.md @@ -0,0 +1,5 @@ +![1](http://img.topjavaer.cn/img/202305152053207.png) + +![2](http://img.topjavaer.cn/img/202305152053254.png) + +![3](http://img.topjavaer.cn/img/202305152053263.png) \ No newline at end of file diff --git a/docs/note/docker-note.md b/docs/note/docker-note.md new file mode 100644 index 0000000..68a8f62 --- /dev/null +++ b/docs/note/docker-note.md @@ -0,0 +1,15 @@ +![](http://img.topjavaer.cn/img/202308020050981.png) + +![](http://img.topjavaer.cn/img/202308020050993.png) + +![](http://img.topjavaer.cn/img/202308020050859.png) + +![5](http://img.topjavaer.cn/img/202308020050614.png) + +![6](http://img.topjavaer.cn/img/202308020051473.png) + +![7](http://img.topjavaer.cn/img/202308020051939.png) + +![8](http://img.topjavaer.cn/img/202308020051588.png) + +![](http://img.topjavaer.cn/img/202308020853566.png) \ No newline at end of file diff --git a/docs/note/freshman-planning.md b/docs/note/freshman-planning.md new file mode 100644 index 0000000..77d5149 --- /dev/null +++ b/docs/note/freshman-planning.md @@ -0,0 +1,46 @@ +自学计算机的大彬来分享下几点宝贵经验。 + +1、看下**计算机科学速成课**,一门很全面的计算机原理入门课程,短短10分钟可以把大学老师十几节课讲的东西讲清楚!整个系列一共41个视频,B站上有中文字幕版。 + +每个视频都是一个特定的主题,例如软件工程、人工智能、操作系统等,主题之间都是紧密相连的,比国内很多大学计算机课程强太多! + +这门课程通过生动形象的讲解方式,向普通人介绍了计算机科学相关的基础知识,包括**计算机的发展史、二进制、指令和程序、数据结构与算法、人工智能、计算机视觉、自然语言处理**等等。 + +每节课程短小精悍,只有短短十几分钟,适合平时碎片化时间观看。 + +![](http://img.topjavaer.cn/img/image-20220820092621327.png) + +2、**学会使用google搜索**。很多同学遇到问题,不会利用好搜索引擎,而是在一些交流群咨询,往往“事倍功半”,问了半天也没得到想要的答案。建议题主学习下搜索的技巧,多用谷歌搜索,少用百度搜索,谷歌搜出来答案更准确,而不是通篇复制粘贴的“垃圾”。 + +![](http://img.topjavaer.cn/img/image-20221103234022486.png) + +3、**多逛技术社区**。平时多逛逛全球最大的同xing交友社区Github、StackoverFlow等技术社区,关注最新的技术动态,尽量参与到开源项目建设,如果能给优秀的开源项目奉献自己的代码,那是非常nice的,对于以后找工作面试也有非常大的帮助。 + +![](http://img.topjavaer.cn/img/github.jpg) + +4、**多动手写代码**,切忌眼高手低!如果你确信自己对大多数的基础知识和概念足够熟悉,并且能够以某种方式将它们联系起来,那么你就可以进行下一步了,你可以开始尝试编写一些有趣的 Java 程序。刚开始动手编写程序时,请可能会困难重重。但是一旦挺过去,接下来即使这些问题再次出现,你也能轻松解决。 + +5、**阅读经典书籍**,比如《深入理解计算机系统》、《数据库系统概念》、《代码整洁之道》等等,这些都是非常优秀的书籍,每次阅读都会有新的收获。PS:不要看那种3天学会Java之类的垃圾书,内容很浅没深度! + +6、学好英语,干计算机这行,要想走在前列,就必须学好英语。因为计算机很多术语都是英文,中文翻译的话经常翻译的非常生涩。而且很多前沿的东西都是国外的,国内教材资料需要等待一段时间才能跟上,因此良好的英语能力能让你快人一步获取一手资料。 + +7、**每天刷一道算法题**,养成刷题的习惯。很多互联网公司都会考察手写算法题,如果平时没有练习,那么笔试或面试的时候大概率会脑袋空白,game over。建议从大二开始,每天抽空到leetcode上刷刷题。 + +8、**参与计算机竞赛**。比如ACM国际大学生程序设计竞赛、GPLT团队程序设计天梯赛、蓝桥杯、中国大学生计算机设计大赛等,或者企业主办的比赛,如华为软件杯精英挑战赛、百度之星程序设计大赛等,参加这些比赛对找工作和保研都有加分,并且对你的代码能力、团队合作能力和逻辑思维能力也有很大的提升。 + +9、**绩点要刷高一点**,绩点高对你保研、考研或者找工作都有很大的帮助。尽量提高绩点,还有就是不能挂科!挂科对你以后发展影响挺大,切记! + +10、**打牢计算机基础** + +要特别重视计算机基础,无论以后是找工作还是考研,基础很重要。 + +计算机专业课程里边,**计算机基础课程无非以下几个:** + +1. **计算机组成原理** +2. **操作系统** +3. **编译原理** +4. **计算机网络** +5. **数据结构与算法** +6. **数据库基础** + +11、**培养写文档的能力**。写文档是计算机专业学生的必备技能。有空可以学习下markdown语法,比word好用太多了。markdown编辑器推荐Typora(最近收费了)、语雀。 \ No newline at end of file diff --git a/docs/note/redis-note.md b/docs/note/redis-note.md new file mode 100644 index 0000000..02e5569 --- /dev/null +++ b/docs/note/redis-note.md @@ -0,0 +1,28 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 一小时彻底吃透Redis + +![1](http://img.topjavaer.cn/img/202308210012725.png) + +![](http://img.topjavaer.cn/img/202308210012963.png) + +![3](http://img.topjavaer.cn/img/202308210013864.png) + + + +![4](http://img.topjavaer.cn/img/202308210013844.png) + +![5](http://img.topjavaer.cn/img/202308210013997.png) + +![6](http://img.topjavaer.cn/img/202308210013008.png) + +![7](http://img.topjavaer.cn/img/202308210013799.png) + +![8](http://img.topjavaer.cn/img/202308210013103.png) + +![9](http://img.topjavaer.cn/img/202308210013210.png) \ No newline at end of file diff --git a/docs/note/write-sql.md b/docs/note/write-sql.md new file mode 100644 index 0000000..14faa3a --- /dev/null +++ b/docs/note/write-sql.md @@ -0,0 +1,93 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 21个写SQL的好习惯 + +![](http://img.topjavaer.cn/img/202308030823049.png) + +![2](http://img.topjavaer.cn/img/202308030823148.png) + +![3](http://img.topjavaer.cn/img/202308030823229.png) + +![4](http://img.topjavaer.cn/img/202308030823834.png) + +![5](http://img.topjavaer.cn/img/202308030824651.png) + +![6](http://img.topjavaer.cn/img/202308030824461.png) + +![7](http://img.topjavaer.cn/img/202308030824900.png) + +![8](http://img.topjavaer.cn/img/202308030824354.png) + +![9](http://img.topjavaer.cn/img/202308030824755.png) + + + + + +## MySQL面试题 + +下面分享MySQL常考的**面试题目**。 + +- [事务的四大特性?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E7%9A%84%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7) +- [数据库的三大范式](https://topjavaer.cn/database/mysql.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F) +- [事务隔离级别有哪些?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B) +- [生产环境数据库一般用的什么隔离级别呢?](https://topjavaer.cn/database/mysql.html#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%80%E8%88%AC%E7%94%A8%E7%9A%84%E4%BB%80%E4%B9%88%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E5%91%A2) +- [编码和字符集的关系](https://topjavaer.cn/database/mysql.html#%E7%BC%96%E7%A0%81%E5%92%8C%E5%AD%97%E7%AC%A6%E9%9B%86%E7%9A%84%E5%85%B3%E7%B3%BB) +- [utf8和utf8mb4的区别](https://topjavaer.cn/database/mysql.html#utf8%E5%92%8Cutf8mb4%E7%9A%84%E5%8C%BA%E5%88%AB) +- [什么是索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B4%A2%E5%BC%95) +- [索引的优缺点?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9) +- [索引的作用?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%9C%E7%94%A8) +- [什么情况下需要建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [什么情况下不建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%B8%8D%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [索引的数据结构](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) +- [Hash索引和B+树索引的区别?](https://topjavaer.cn/database/mysql.html#hash%E7%B4%A2%E5%BC%95%E5%92%8Cb%E6%A0%91%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8C%BA%E5%88%AB) +- [为什么B+树比B树更适合实现数据库索引?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88b%E6%A0%91%E6%AF%94b%E6%A0%91%E6%9B%B4%E9%80%82%E5%90%88%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95) +- [索引有什么分类?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%88%86%E7%B1%BB) +- [什么是最左匹配原则?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99) +- [什么是聚集索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95) +- [什么是覆盖索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95) +- [索引的设计原则?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99) +- [索引什么时候会失效?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E5%A4%B1%E6%95%88) +- [什么是前缀索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%89%8D%E7%BC%80%E7%B4%A2%E5%BC%95) +- [索引下推](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8) +- [常见的存储引擎有哪些?](https://topjavaer.cn/database/mysql.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E6%9C%89%E5%93%AA%E4%BA%9B) +- [MyISAM和InnoDB的区别?](https://topjavaer.cn/database/mysql.html#myisam%E5%92%8Cinnodb%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL有哪些锁?](https://topjavaer.cn/database/mysql.html#mysql%E6%9C%89%E5%93%AA%E4%BA%9B%E9%94%81) +- [MVCC 实现原理?](https://topjavaer.cn/database/mysql.html#mvcc-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) +- [快照读和当前读](https://topjavaer.cn/database/mysql.html#%E5%BF%AB%E7%85%A7%E8%AF%BB%E5%92%8C%E5%BD%93%E5%89%8D%E8%AF%BB) +- [共享锁和排他锁](https://topjavaer.cn/database/mysql.html#%E5%85%B1%E4%BA%AB%E9%94%81%E5%92%8C%E6%8E%92%E4%BB%96%E9%94%81) +- [bin log/redo log/undo log](https://topjavaer.cn/database/mysql.html#bin-logredo-logundo-log) +- [bin log和redo log有什么区别?](https://topjavaer.cn/database/mysql.html#bin-log%E5%92%8Credo-log%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [讲一下MySQL架构?](https://topjavaer.cn/database/mysql.html#%E8%AE%B2%E4%B8%80%E4%B8%8Bmysql%E6%9E%B6%E6%9E%84) +- [分库分表](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [什么是分区表?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%8C%BA%E8%A1%A8) +- [分区表类型](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E8%A1%A8%E7%B1%BB%E5%9E%8B) +- [分区的问题?](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E7%9A%84%E9%97%AE%E9%A2%98) +- [查询语句执行流程?](https://topjavaer.cn/database/mysql.html#%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B) +- [更新语句执行过程?](https://topjavaer.cn/database/mysql.html#%E6%9B%B4%E6%96%B0%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) +- [exist和in的区别?](https://topjavaer.cn/database/mysql.html#exist%E5%92%8Cin%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中int()和char()的区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADint10%E5%92%8Cchar10%E7%9A%84%E5%8C%BA%E5%88%AB) +- [truncate、delete与drop区别?](https://topjavaer.cn/database/mysql.html#truncatedelete%E4%B8%8Edrop%E5%8C%BA%E5%88%AB) +- [having和where区别?](https://topjavaer.cn/database/mysql.html#having%E5%92%8Cwhere%E5%8C%BA%E5%88%AB) +- [什么是MySQL主从同步?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AFmysql%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [为什么要做主从同步?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%81%9A%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [乐观锁和悲观锁是什么?](https://topjavaer.cn/database/mysql.html#%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81%E6%98%AF%E4%BB%80%E4%B9%88) +- [用过processlist吗?](https://topjavaer.cn/database/mysql.html#%E7%94%A8%E8%BF%87processlist%E5%90%97) +- [MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](https://topjavaer.cn/database/mysql.html#mysql%E6%9F%A5%E8%AF%A2-limit-100010-%E5%92%8Climit-10-%E9%80%9F%E5%BA%A6%E4%B8%80%E6%A0%B7%E5%BF%AB%E5%90%97) +- [深分页怎么优化?](https://topjavaer.cn/database/mysql.html#%E6%B7%B1%E5%88%86%E9%A1%B5%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [高度为3的B+树,可以存放多少数据?](https://topjavaer.cn/database/mysql.html#%E9%AB%98%E5%BA%A6%E4%B8%BA3%E7%9A%84b%E6%A0%91%E5%8F%AF%E4%BB%A5%E5%AD%98%E6%94%BE%E5%A4%9A%E5%B0%91%E6%95%B0%E6%8D%AE) +- [MySQL单表多大进行分库分表?](https://topjavaer.cn/database/mysql.html#mysql%E5%8D%95%E8%A1%A8%E5%A4%9A%E5%A4%A7%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [大表查询慢怎么优化?](https://topjavaer.cn/database/mysql.html#%E5%A4%A7%E8%A1%A8%E6%9F%A5%E8%AF%A2%E6%85%A2%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [说说count()、count()和count()的区别](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4count1count%E5%92%8Ccount%E5%AD%97%E6%AE%B5%E5%90%8D%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中DATETIME 和 TIMESTAMP有什么区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADdatetime-%E5%92%8C-timestamp%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [说说为什么不建议用外键?](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AE%E7%94%A8%E5%A4%96%E9%94%AE) +- [使用自增主键有什么好处?](https://topjavaer.cn/database/mysql.html#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E6%9C%89%E4%BB%80%E4%B9%88%E5%A5%BD%E5%A4%84) +- [自增主键保存在什么地方?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%BF%9D%E5%AD%98%E5%9C%A8%E4%BB%80%E4%B9%88%E5%9C%B0%E6%96%B9) +- [自增主键一定是连续的吗?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%B8%80%E5%AE%9A%E6%98%AF%E8%BF%9E%E7%BB%AD%E7%9A%84%E5%90%97) +- [InnoDB的自增值为什么不能回收利用?](https://topjavaer.cn/database/mysql.html#innodb%E7%9A%84%E8%87%AA%E5%A2%9E%E5%80%BC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%9B%9E%E6%94%B6%E5%88%A9%E7%94%A8) +- [MySQL数据如何同步到Redis缓存?](https://topjavaer.cn/database/mysql.html#mysql%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E5%90%8C%E6%AD%A5%E5%88%B0redis%E7%BC%93%E5%AD%98) \ No newline at end of file diff --git a/docs/other/leave-a-message.md b/docs/other/leave-a-message.md new file mode 100644 index 0000000..3013cc1 --- /dev/null +++ b/docs/other/leave-a-message.md @@ -0,0 +1,4 @@ +# 留言区 + +大家有什么建议,或者觉得某个知识点理解有误的,欢迎到评论区留言~ + diff --git a/docs/other/log-print.md b/docs/other/log-print.md new file mode 100644 index 0000000..b3c6a90 --- /dev/null +++ b/docs/other/log-print.md @@ -0,0 +1,119 @@ +# 代码日志打印规范 + +日志文件提供精确的系统记录,根据日志可以定位到错误详情和根源,方便开发排查问题。 + +**日志有什么作用呢** + +- **打印调试**:即用日志来记录变量或者某个逻辑。记录程序运行的流程,即程序运行了哪些代码,方便排查逻辑问题。 +- **问题定位**:程序出异常或者出故障时快速的定位问题,方便后期解决问题。因为线上生产环境无法 debug,在测试环境去模拟一套生产环境,费时费力。所以依靠日志记录的信息定位问题,这点非常重要。还可以记录流量,后期可以通过 ELK(包括 EFK 进行流量统计)。 +- **用户行为日志**:记录用户的操作行为,用于大数据分析,比如监控、风控、推荐等等。这种日志,一般是给其他团队分析使用,而且可能是多个团队,因此一般会有一定的格式要求,开发者应该按照这个格式来记录,便于其他团队的使用。当然,要记录哪些行为、操作,一般也是约定好的,因此,开发者主要是执行的角色。 +- **根因分析(甩锅必备)**:即在关键地方记录日志。方便在和各个终端定位问题时,别人说时你的程序问题,你可以理直气壮的拿出你的日志说,看,我这里运行了,状态也是对的。这样,对方就会乖乖去定位他的代码,而不是互相推脱。 + +## **什么时候记录日志?** + +上文说了日志的重要性,那么什么时候需要记录日志。 + +- **系统初始化**:系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录 INFO 日志,打印出参数以及启动完成态服务表述。 +- **编程语言提示异常**:如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用 WARN 或者 ERROR 级别。 +- **业务流程预期不符**:除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。 +- **系统核心角色,组件关键动作**:系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录 INFO 级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点 INFO 记录,其余酌情考虑 DEBUG 级别。 +- **第三方服务远程调用**:微服务架构体系中有一个重要的点就是第三方永远不可信,对于第三方服务远程调用建议打印请求和响应的参数,方便在和各个终端定位问题,不会因为第三方服务日志的缺失变得手足无措。 + +## **日志级别** + +常用的分为以下几种: + +**1、ERROR** + +**影响到程序正常运行、当前请求正常运行的异常情况**。如: + +1)打开配置文件失败 + +2)所有第三方对接的异常(包括第三方返回错误码) + +3)所有影响功能使用的异常,包括:SQLException 、空指针异常以及业务异常之外的所有异常 + +**2、WARN** + +**告警日志。不应该出现但是不影响程序、当前请求正常运行的异常情况。** + +但是一旦出现了也需要关注,因此一般该级别的日志达到一定的阈值之后,就得提示给用户或者需要关注的人了。如: + +1)有**容错机制**的时候出现的错误情况 + +2)找不到配置文件,但是系统能自动创建配置文件 + +**3、INFO** + +记录输入输出、程序关键节点等必要信息。平时无需关注,但出问题时可根据INFO日志诊断出问题 + +1)Service方法中对于系统/业务状态的变更 + +2)调用第三方时的调用参数和调用结果(入参和出参) + +3)提供方,需要记录入参 + +4)定时任务的开始执行与结束执行 + +**4、DEBUG** + +调试信息,对系统每一步的运行状态进行精确的记录。 + +**5、TRACE** + +特别详细的服务调用流程各个节点信息。业务代码中,除非涉及到多级服务的调用,否则不要使用(除非有特殊用意,否则请使用DEBUG级别替代) + +## **日志打印规范** + +1、`增删改`操作需要打印参数日志(以便定位一些异常业务问题); + +2、`条件分支`需要打印日志:包括条件值以及重要参数; + +3、明确日志打印`级别`与包含的`信息` + +1)`提供方`服务,建议以 **INFO** 级别记录入参,出参可选 + +2)`消费队列消息`,务必打印消息内容 + +3)`调用方服务`,建议以 **INFO** 级别记录入参和出参 + +4)`运行环境问题`,如网络错误、建议以 **WARN** 级别记录错误堆栈 + +5)`定时任务`,务必打印任务开始时间、结束时间。涉及扫描数据的任务,务必打印扫描范围 + +4、异常信息应该包括`两类信息`:`案发现场信息`和`异常堆栈信息`。如果不处理,那么通过关键字**throws/throw** 往上抛出,由父级方法处理 + +5、`谨慎地记录日志` + +1)`生产环境禁止输出 debug 日志` + +2)有选择地输出 **info** 日志 + +3)如果使用 **warn** 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志 + +6、可以使用 **warn** 日志级别来记录用户输入参数错误的情况 + +7、对 `trace/debug/info`级别的日志输出,必须使用`条件输出形式`或者使用`占位符`的方式 + +8、`不允许记录日志后又抛出异常`,因为这样会多次记录日志,只允许记录一次日志 + +9、不允许出现`System print`(包括System.out.println和System.error.println)语句作为日志的打印 + +10、不允许出现 `e.printStackTrace` + +11、`日志性能的考虑`。如果代码为核心代码,执行频率非常高,则输出日志建议增加判断,尤其是低级别的输出 + +12、【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 `SLF4J` 中的 API,使用`门面模式的日志框架`,有利于维护和各个类的日志处理方式统一 + +13、【强制】`避免重复打印日志,浪费磁盘空间`。务必在 log4j.xml 中设置 `additivity=false` + + + +> 参考链接: +> +> https://cloud.tencent.com/developer/article/1559519 +> +> https://www.jianshu.com/p/d94cf3069568 + + + diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md new file mode 100644 index 0000000..477801a --- /dev/null +++ b/docs/other/site-diary.md @@ -0,0 +1,129 @@ +--- +sidebar: heading +--- + +大彬的小破站是22年7月份开始搭建的,收录了我平时梳理的一些文章,网站内容还在不断更新中~ + +在此记录下小破站的迭代更新记录。 + +## 更新记录 + +- 2024.06.11,更新-Redis大key怎么处理? + +- 2024.06.11,新增-聊聊如何用Redis 实现分布式锁? + +- 2024.06.07,更新-为什么Redis单线程还这么快? + +- 2024.05.21,新增一条SQL是怎么执行的 + +- 2023.12.28,[增加源码解析模块,包含Sprign/SpringMVC/MyBatis(更新中)](/source/mybatis/1-overview.html)。 + +- 2023.12.28,[遭受黑客攻击,植入木马程序](/zsxq/article/site-hack.html)。 + +- 2023.08.10,分布式锁实现-[RedLock](http://topjavaer.cn/advance/distributed/2-distributed-lock.html)补充图片 + +- 2023.05.04,导航栏增加图标。 + +- 2023.05.01,新增[Tomcat基础知识总结](/web/tomcat.html) + +- 2023.03.25,新增[TCP面试题](/computer-basic/tcp.html)、[MongoDB面试题](/database/mongodb.html)、[ZooKeeper面试题](/zookeeper/zk.html) + +- 2023.02.08,新增[10w级别数据Excel导入优化](/advance/system-design) + +- 2022.12.18,新增[order by是怎么工作的?](/advance/excellent-article/13-order-by-work.html) + +- 2022.12.17,新增[单点登录(SSO)设计与实现](/advance/system-design) + +- 2022.12.05,新增[ES的分布式架构原理](/database/es/1-es-architect.html) + +- 2022.11.28,新增[系统设计-微信红包系统如何设计](/advance/system-design) + +- 2022.11.20,新增[系统设计-短链系统](/advance/system-design/4-short-url.html) + +- 2022.11.14,[修复Java集合uml图错误](https://topjavaer.cn/java/java-collection.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E9%9B%86%E5%90%88%E6%9C%89%E5%93%AA%E4%BA%9B) + +- 2022.11.12,[120道LeetCode题解(高频)](/leetcode/README.md),已整理**58**道 + +- 2022.10.28,[120道LeetCode题解(高频)](/leetcode/README.md),已整理**40**道 + +- 2022.10.15,增加计算机基础-[120道LeetCode题解(高频)](/leetcode/README.md),已整理**28**道 + +- 2022.10.08,增加校招-[Java和C++怎么选?](/campus-recruit/program-language/java-or-c++.md) + +- 2022.10.05,增加优质文章-[MySQL最大建议行数 2000w,靠谱吗?](/advance/excellent-article/12-mysql-table-max-rows.md) + +- 2022.10.04,增加优质文章-[8种架构模式](/advance/excellent-article/11-8-architect-pattern.md) + +- 2022.09.18,增加海量数据面试题-[如何找出排名前 500 的数?](/mass-data/6-top-500-num.md) + +- 2022.09.16,增加高并发-[限流算法](/advance/concurrent/1-current-limiting.md),[负载均衡](/advance/concurrent/2-load-balance.md) + +- 2022.09.15,增加海量数据面试题-[如何查询最热门的查询串?](/mass-data/5-find-hot-string.md) + +- 2022.09.09,增加框架-[SpringCloud面试题](/framework/springcloud-interview.md) + +- 2022.09.05,增加优质文章-[大文件上传时如何做到秒传?](/advance/excellent-article/10-file-upload.md) + +- 2022.09.04,增加[留言区](/other/leave-a-message.md),[网易面经汇总](/campus-recruit/interview/10-netease.md) + +- 2022.09.03,增加优质文章-[美团面试:熟悉哪些JVM调优参数?](/advance/excellent-article/9-jvm-optimize-param.md) + +- 2022.08.29,增加海量数据面试题-[如何在100亿数据中找到中位数?](/mass-data/4-find-mid-num.md),优质文章-[如何保证接口幂等性?](/advance/excellent-article/8-interface-idempotent.md) + +- 2022.08.22,增加框架中间件-[Kafka面试题](/message-queue/kafka.md)总结 + +- 2022.08.21,完善工具-[linux常用命令](/tools/linux/) + +- 2022.08.19,补充进阶之路-[海量数据场景题](/mass-data/),增加进阶之路-[优质文章汇总](/advance/excellent-article/) + +- 2022.08.18,增加秋招-[面经合集](/campus-recruit/interview/),包括阿里、腾讯、百度、字节跳动、京东等面经 + +- 2022.08.13,调整导航栏结构,将分布式、设计模式、系统设计、海量数据场景题移至进阶之路目录下 + +- 2022.08.08,网站改版,调整UI布局,导航栏增加图标等。 + + 改版后: + + ![](http://img.topjavaer.cn/img/image-20220809084502495.png) + + 改版前: + + ![](http://img.topjavaer.cn/img/image-20220801004446607.png) + +- 2022.08.07,完善设计模式、Docker、Git部分内容 + +- 2022.08.02,增加部分场景设计题目 + +- 2022.07.31,应粉丝要求,增加暗黑模式~很贴心有木有! + + ![](http://img.topjavaer.cn/img/image-20220801004603404.png) + + ![](http://img.topjavaer.cn/img/image-20220801004446607.png) + +- 2022.07.30,SEO优化,小破站被谷歌、百度等搜索引擎收录啦! + + ![](http://img.topjavaer.cn/img/image-20220801003633682.png) + +- 2022.07.29,增加工具-[Nginx面试题](https://mp.weixin.qq.com/s/SKKEeYxif0wWJo6n57rd6A) + +- 2022.07.26,完善计算机网络面试题部分 + +- 2022.07.24,增加编程内容-[设计模式](/advance/design-pattern/),工具-Git/Maven详解 + +- 2022.07.23,整理[力扣算法题目](/computer-basic/algorithm.md) + +- 2022.07.20,修复小伙伴通过微信、评论留言等渠道提出的bug + +- 2022.07.18,增加ElasticSearch面试题 + +- 2022.07.16,整理数据库相关面试题 + +- 2022.07.13,增加**分布式**相关内容,包括分布式事务、分布式锁、微服务等 + +- 2022.07.10,增加学习资源,包括面试手册、算法手册 + +- 2022.07.08,整理**计算机基础**内容,包括操作系统、计算机网络、数据结构与算法等 + +- 2022.07.06,7月6号正式上线,**首日上线PV过万**! + +- 2022.07.01-2022.07.06,网站搭建,整理大彬在公众号、知乎等平台发表的文章 diff --git a/docs/practice/service-performance-optimization.md b/docs/practice/service-performance-optimization.md new file mode 100644 index 0000000..ec2d0af --- /dev/null +++ b/docs/practice/service-performance-optimization.md @@ -0,0 +1,40 @@ +--- +sidebar: heading +title: 线上接口很慢怎么办? +category: 实践经验 +tag: + - 实践经验 +head: + - - meta + - name: keywords + content: 线上接口很慢怎么处理 + - - meta + - name: description + content: 编程实践经验分享 +--- + +## 线上接口很慢怎么办? + +首先需要明确一个问题,是只有**一个接口**变慢,还是**多个接口**变慢。 + +如果系统中有多个服务的接口都变慢,那可能是**系统共用的资源不足**导致的。比如数据库连接数太多、数据库有大量慢查询、共同依赖的下游服务性能问题等。 + +可以查看系统中调用量突然增多的服务,它的调用量是否导致数据库的并发达到了瓶颈,是不是共同调用的下游服务出现了性能问题,数据库中是不是有大量这个服务引起的慢查询等。 + +可以有以下针对性优化: + +- 如果数据库并发达到瓶颈,可以考虑用**读写分离、分库分表、加读缓存**等方式来解决 +- 如果下游接口出现性能问题,需要通知下游服务做优化,同时要加**降级开关** +- 如果数据库中有大量慢查询,需要**优化sql**或**加索引**等 + +如果是只有一个服务的接口变慢,那就要针对这个服务做分析,查看它的cpu占用率和gc频率是不是异常,做针对性的优化。 + +1. 比如在循环里获取远程数据,可以改成只调用一次,**批量获取数据** +2. 比如在链路中多次调用同一个远程接口获取相同数据,可以第一次调用之后就把数据**缓存**起来,后续直接从缓存中获取 +3. 比如如果多个比较耗时的操作是串行执行的,但它们又没有依赖关系,就可以把串行改成**并行**,可以用countDownLatch +4. 还有如果实在无法再缩短请求处理耗时的话,也可以考虑从产品逻辑上进行优化,比如把原来的同步请求改为**异步**的,前端再去轮询请求处理结果 +5. 比如为了得到一个复杂的model,需要调多个接口获取信息,但当前接口只需要其中部分比较简单的数据,那么需要根据实际情况,**重新写**一个简单model的获取方法 + + + +> 参考链接:https://juejin.cn/post/7064140627578978334 diff --git a/docs/redis/article/cache-db-consistency.md b/docs/redis/article/cache-db-consistency.md new file mode 100644 index 0000000..088fa50 --- /dev/null +++ b/docs/redis/article/cache-db-consistency.md @@ -0,0 +1,396 @@ +--- +sidebar: heading +title: 缓存和数据库一致性问题,看这篇就够了 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: 缓存和数据库一致性问题,缓存一致性 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +> 本文转自水滴与银弹 + +如何保证缓存和数据库一致性,这是一个老生常谈的话题了。 + +但很多人对这个问题,依旧有很多疑惑: + +- 到底是更新缓存还是删缓存? +- 到底选择先更新数据库,再删除缓存,还是先删除缓存,再更新数据库? +- 为什么要引入消息队列保证一致性? +- 延迟双删会有什么问题?到底要不要用? +- ... + +这篇文章,我们就来把这些问题讲清楚。 + +# 引入缓存提高性能 + +我们从最简单的场景开始讲起。 + +如果你的业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可,这时你的架构模型是这样的: + +![](http://img.topjavaer.cn/img/缓存与数据库一致性1.png) + +但随着业务量的增长,你的项目请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。 + +这个阶段通常的做法是,引入「缓存」来提高读性能,架构模型就变成了这样: + +![](http://img.topjavaer.cn/img/缓存与数据库一致性2.png) + +当下优秀的缓存中间件,当属 Redis 莫属,它不仅性能非常高,还提供了很多友好的数据类型,可以很好地满足我们的业务需求。 + +但引入缓存之后,你就会面临一个问题:**之前数据只存在数据库中,现在要放到缓存中读取,具体要怎么存呢?** + +最简单直接的方案是「全量数据刷到缓存中」: + +- 数据库的数据,全量刷入缓存(不设置失效时间) +- 写请求只更新数据库,不更新缓存 +- 启动一个定时任务,定时把数据库的数据,更新到缓存中 + +![](http://img.topjavaer.cn/img/缓存与数据库一致性3.png) + +这个方案的优点是,所有读请求都可以直接「命中」缓存,不需要再查数据库,性能非常高。 + +但缺点也很明显,有 2 个问题: + +1. **缓存利用率低**:不经常访问的数据,还一直留在缓存中 +2. **数据不一致**:因为是「定时」刷新缓存,缓存和数据库存在不一致(取决于定时任务的执行频率) + +所以,这种方案一般更适合业务「体量小」,且对数据一致性要求不高的业务场景。 + +那如果我们的业务体量很大,怎么解决这 2 个问题呢? + +# 缓存利用率和一致性问题 + +先来看第一个问题,如何提高缓存利用率? + +想要缓存利用率「最大化」,我们很容易想到的方案是,缓存中只保留最近访问的「热数据」。但具体要怎么做呢? + +我们可以这样优化: + +- 写请求依旧只写数据库 +- 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存 +- 同时,写入缓存中的数据,都设置失效时间 + +![](http://img.topjavaer.cn/img/缓存与数据库一致性4.png) + +这样一来,缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。 + +再来看数据一致性问题。 + +要想保证缓存和数据库「实时」一致,那就不能再用定时任务刷新缓存了。 + +所以,当数据发生更新时,我们不仅要操作数据库,还要一并操作缓存。具体操作就是,修改一条数据时,不仅要更新数据库,也要连带缓存一起更新。 + +但数据库和缓存都更新,又存在先后问题,那对应的方案就有 2 个: + +1. 先更新缓存,后更新数据库 +2. 先更新数据库,后更新缓存 + +哪个方案更好呢? + +先不考虑并发问题,正常情况下,无论谁先谁后,都可以让两者保持一致,但现在我们需要重点考虑「异常」情况。 + +因为操作分为两步,那么就很有可能存在「第一步成功、第二步失败」的情况发生。 + +这 2 种方案我们一个个来分析。 + +**1) 先更新缓存,后更新数据库** + +如果缓存更新成功了,但数据库更新失败,那么此时缓存中是最新值,但数据库中是「旧值」。 + +虽然此时读请求可以命中缓存,拿到正确的值,但是,一旦缓存「失效」,就会从数据库中读取到「旧值」,重建缓存也是这个旧值。 + +这时用户会发现自己之前修改的数据又「变回去」了,对业务造成影响。 + +**2) 先更新数据库,后更新缓存** + +如果数据库更新成功了,但缓存更新失败,那么此时数据库中是最新值,缓存中是「旧值」。 + +之后的读请求读到的都是旧数据,只有当缓存「失效」后,才能从数据库中得到正确的值。 + +这时用户会发现,自己刚刚修改了数据,但却看不到变更,一段时间过后,数据才变更过来,对业务也会有影响。 + +可见,无论谁先谁后,但凡后者发生异常,就会对业务造成影响。那怎么解决这个问题呢? + +别急,后面我会详细给出对应的解决方案。 + +我们继续分析,除了操作失败问题,还有什么场景会影响数据一致性? + +这里我们还需要重点关注:**并发问题**。 + +# 并发引发的一致性问题 + +假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢? + +有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景: + +1. 线程 A 更新数据库(X = 1) +2. 线程 B 更新数据库(X = 2) +3. 线程 B 更新缓存(X = 2) +4. 线程 A 更新缓存(X = 1) + +最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。 + +也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。 + +> 同样地,采用「先更新缓存,再更新数据库」的方案,也会有类似问题,这里不再详述。 + +除此之外,我们从「缓存利用率」的角度来评估这个方案,也是不太推荐的。 + +这是因为每次数据发生变更,都「无脑」更新缓存,但是缓存中的数据不一定会被「马上读取」,这就会导致缓存中可能存放了很多不常访问的数据,浪费缓存资源。 + +而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列「计算」得出一个值,才把这个值才写到缓存中。 + +由此可见,这种「更新数据库 + 更新缓存」的方案,不仅缓存利用率不高,还会造成机器性能的浪费。 + +所以此时我们需要考虑另外一种方案:**删除缓存**。 + +# 删除缓存可以保证一致性吗? + +删除缓存对应的方案也有 2 种: + +1. 先删除缓存,后更新数据库 +2. 先更新数据库,后删除缓存 + +经过前面的分析我们已经得知,但凡「第二步」操作失败,都会导致数据不一致。 + +这里我不再详述具体场景,你可以按照前面的思路推演一下,就可以看到依旧存在数据不一致的情况。 + +这里我们重点来看「并发」问题。 + +**1) 先删除缓存,后更新数据库** + +如果有 2 个线程要并发「读写」数据,可能会发生以下场景: + +1. 线程 A 要更新 X = 2(原值 X = 1) +2. 线程 A 先删除缓存 +3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1) +4. 线程 A 将新值写入数据库(X = 2) +5. 线程 B 将旧值写入缓存(X = 1) + +最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。 + +可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。 + +**2) 先更新数据库,后删除缓存** + +依旧是 2 个线程并发「读写」数据: + +1. 缓存中 X 不存在(数据库 X = 1) +2. 线程 A 读取数据库,得到旧值(X = 1) +3. 线程 B 更新数据库(X = 2) +4. 线程 B 删除缓存 +5. 线程 A 将旧值写入缓存(X = 1) + +最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。 + +这种情况「理论」来说是可能发生的,但实际真的有可能发生吗? + +其实概率「很低」,这是因为它必须满足 3 个条件: + +1. 缓存刚好已失效 +2. 读请求 + 写请求并发 +3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5) + +仔细想一下,条件 3 发生的概率其实是非常低的。 + +因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。 + +这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。 + +所以,我们应该采用这种方案,来操作数据库和缓存。 + +好,解决了并发问题,我们继续来看前面遗留的,**第二步执行「失败」导致数据不一致的问题**。 + +# 如何保证两步都执行成功? + +前面我们分析到,无论是更新缓存还是删除缓存,只要第二步发生失败,那么就会导致数据库和缓存不一致。 + +**保证第二步成功执行,就是解决问题的关键。** + +想一下,程序在执行过程中发生异常,最简单的解决办法是什么? + +答案是:**重试**。 + +是的,其实这里我们也可以这样做。 + +无论是先操作缓存,还是先操作数据库,但凡后者执行失败了,我们就可以发起重试,尽可能地去做「补偿」。 + +那这是不是意味着,只要执行失败,我们「无脑重试」就可以了呢? + +答案是否定的。现实情况往往没有想的这么简单,失败后立即重试的问题在于: + +- 立即重试很大概率「还会失败」 +- 「重试次数」设置多少才合理? +- 重试会一直「占用」这个线程资源,无法服务其它客户端请求 + +看到了么,虽然我们想通过重试的方式解决问题,但这种「同步」重试的方案依旧不严谨。 + +那更好的方案应该怎么做? + +答案是:**异步重试**。什么是异步重试? + +其实就是把重试请求写到「消息队列」中,然后由专门的消费者来重试,直到成功。 + +或者更直接的做法,为了避免第二步执行失败,我们可以把操作缓存这一步,直接放到消息队列中,由消费者来操作缓存。 + +到这里你可能会问,写消息队列也有可能会失败啊?而且,引入消息队列,这又增加了更多的维护成本,这样做值得吗? + +这个问题很好,但我们思考这样一个问题:如果在执行失败的线程中一直重试,还没等执行成功,此时如果项目「重启」了,那这次重试请求也就「丢失」了,那这条数据就一直不一致了。 + +所以,这里我们必须把重试或第二步操作放到另一个「服务」中,这个服务用「消息队列」最为合适。这是因为消息队列的特性,正好符合我们的需求: + +- **消息队列保证可靠性**:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心) +- **消息队列保证消息成功投递**:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景) + +至于写队列失败和消息队列的维护成本问题: + +- **写队列失败**:操作缓存和写消息队列,「同时失败」的概率其实是很小的 +- **维护成本**:我们项目中一般都会用到消息队列,维护成本并没有新增很多 + +所以,引入消息队列来解决这个问题,是比较合适的。这时架构模型就变成了这样: + +![](http://img.topjavaer.cn/img/缓存与数据库一致性5.png) + +那如果你确实不想在应用中去写消息队列,是否有更简单的方案,同时又可以保证一致性呢? + +方案还是有的,这就是近几年比较流行的解决方案:**订阅数据库变更日志,再操作缓存**。 + +具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。 + +那什么时候操作缓存呢?这就和数据库的「变更日志」有关了。 + +拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。 + +![](http://img.topjavaer.cn/img/缓存与数据库一致性6.png) + +订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于: + +- **无需考虑写消息队列失败情况**:只要写 MySQL 成功,Binlog 肯定会有 +- **自动投递到下游队列**:canal 自动把数据库变更日志「投递」给下游的消息队列 + +当然,与此同时,我们需要投入精力去维护 canal 的高可用和稳定性。 + +> 如果你有留意观察很多数据库的特性,就会发现其实很多数据库都逐渐开始提供「订阅变更日志」的功能了,相信不远的将来,我们就不用通过中间件来拉取日志,自己写程序就可以订阅变更日志了,这样可以进一步简化流程。 + +至此,我们可以得出结论,想要保证数据库和缓存一致性,**推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做**。 + +# 主从库延迟和延迟双删问题 + +到这里,还有 2 个问题,是我们没有重点分析过的。 + +**第一个问题**,还记得前面讲到的「先删除缓存,再更新数据库」方案,导致不一致的场景么? + +这里我再把例子拿过来让你复习一下: + +2 个线程要并发「读写」数据,可能会发生以下场景: + +1. 线程 A 要更新 X = 2(原值 X = 1) +2. 线程 A 先删除缓存 +3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1) +4. 线程 A 将新值写入数据库(X = 2) +5. 线程 B 将旧值写入缓存(X = 1) + +最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。 + +**第二个问题**:是关于「读写分离 + 主从复制延迟」情况下,缓存和数据库一致性的问题。 + +在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」其实也会导致不一致: + +1. 线程 A 更新主库 X = 2(原值 X = 1) +2. 线程 A 删除缓存 +3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1) +4. 从库「同步」完成(主从库 X = 2) +5. 线程 B 将「旧值」写入缓存(X = 1) + +最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。 + +看到了么?这 2 个问题的核心在于:**缓存都被回种了「旧值」**。 + +那怎么解决这类问题呢? + +最有效的办法就是,**把缓存删掉**。 + +但是,不能立即删,而是需要「延迟删」,这就是业界给出的方案:**缓存延迟双删策略**。 + +按照延时双删策略,这 2 个问题的解决方案是这样的: + +**解决第一个问题**:在线程 A 删除缓存、更新完数据库之后,先「休眠一会」,再「删除」一次缓存。 + +**解决第二个问题**:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。 + +这两个方案的目的,都是为了把缓存清掉,这样一来,下次就可以从数据库读取到最新值,写入缓存。 + +但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢? + +- 问题1:延迟时间要大于「主从复制」的延迟时间 +- 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间 + +但是,**这个时间在分布式和高并发场景下,其实是很难评估的****。** + +很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。 + +所以你看,采用这种方案,也只是尽可能保证一致性而已,极端情况下,还是有可能发生不一致。 + +所以实际使用中,我还是建议你采用「先更新数据库,再删除缓存」的方案,同时,要尽可能地保证「主从复制」不要有太大延迟,降低出问题的概率。 + +# 可以做到强一致吗? + +看到这里你可能会想,这些方案还是不够完美,我就想让缓存和数据库「强一致」,到底能不能做到呢? + +其实很难。 + +要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。 + +相反,这时我们换个角度思考一下,我们引入缓存的目的是什么? + +没错,**性能**。 + +一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。 + +而且,就拿我们前面讲到的方案来说,当操作数据库和缓存完成之前,只要有其它请求可以进来,都有可能查到「中间状态」的数据。 + +所以如果非要追求强一致,那必须要求所有更新操作完成之前期间,不能有「任何请求」进来。 + +虽然我们可以通过加「分布锁」的方式来实现,但我们要付出的代价,很可能会超过引入缓存带来的性能提升。 + +所以,既然决定使用缓存,就必须容忍「一致性」问题,我们只能尽可能地去降低问题出现的概率。 + +同时我们也要知道,缓存都是有「失效时间」的,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。 + +# 总结 + +好了,总结一下这篇文章的重点。 + +1、想要提高应用的性能,可以引入「缓存」来解决 + +2、引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」 + +3、更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,且存在「缓存资源浪费」和「机器性能浪费」的情况发生 + +4、在更新数据库 + 删除缓存的方案中,「先删除缓存,再更新数据库」在「并发」场景下依旧有数据不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估,所以推荐用「先更新数据库,再删除缓存」的方案 + +5、在「先更新数据库,再删除缓存」方案下,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据一致性 + +6、在「先更新数据库,再删除缓存」方案下,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率 + +# 后记 + +本以为这个老生常谈的话题,写起来很好写,没想到在写的过程中,还是挖到了很多之前没有深度思考过的细节。 + +在这里我也分享 4 点心得给你: + +1、性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案 + +2、掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题 + +3、失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案 + +4、订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致 + +很多一致性问题,都会采用这些方案来解决,希望我的这些心得对你有所启发。 diff --git a/docs/redis/article/redis-cluster-work.md b/docs/redis/article/redis-cluster-work.md new file mode 100644 index 0000000..94e7129 --- /dev/null +++ b/docs/redis/article/redis-cluster-work.md @@ -0,0 +1,150 @@ +--- +sidebar: heading +title: Redis 集群模式的工作原理 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis集群模式 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## Redis 集群模式的工作原理 + +Redis 集群模式的工作原理?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? + +在前几年,Redis 如果要搞几个节点,每个节点存储一部分的数据,得**借助一些中间件**来实现,比如说有 `codis` ,或者 `twemproxy` ,都有。有一些 Redis 中间件,你读写 Redis 中间件,Redis 中间件负责将你的数据分布式存储在多台机器上的 Redis 实例中。 + +这两年,Redis 不断在发展,Redis 也不断有新的版本,现在的 Redis 集群模式,可以做到在多台机器上,部署多个 Redis 实例,每个实例存储一部分的数据,同时每个 Redis 主实例可以挂 Redis 从实例,自动确保说,如果 Redis 主实例挂了,会自动切换到 Redis 从实例上来。 + +现在 Redis 的新版本,大家都是用 Redis cluster 的,也就是 Redis 原生支持的 Redis 集群模式,那么面试官肯定会就 Redis cluster 对你来个几连炮。要是你没用过 Redis cluster,正常,以前很多人用 codis 之类的客户端来支持集群,但是起码你得研究一下 Redis cluster 吧。 + +如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个 G,单机就足够了,可以使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,然后自己搭建一个 sentinel 集群去保证 Redis 主从架构的高可用性。 + +Redis cluster,主要是针对**海量数据+高并发+高可用**的场景。Redis cluster 支撑 N 个 Redis master node,每个 master node 都可以挂载多个 slave node。这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。 + +### Redis cluster 介绍 + +- 自动将数据进行分片,每个 master 上放一部分数据 +- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的 + +在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。 + +16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, `gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 + +### 节点间的内部通信机制 + +#### 基本通信原理 + +集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。 + +**集中式**是将集群元数据(节点信息、故障等等)集中存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm` 。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。 + +![](http://img.topjavaer.cn/img/redis集群原理1.png) + +Redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 + +![](http://img.topjavaer.cn/img/redis集群原理2.png) + +**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。 + +gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 + +- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong` 。 +- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。 + +#### gossip 协议 + +gossip 协议包含多种消息,包含 `ping` , `pong` , `meet` , `fail` 等等。 + +- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 + +```bash +Redis-trib.rb add-nodeCopy to clipboardErrorCopied +``` + +其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。 + +- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。 +- pong:返回 ping 和 meet,包含自己的状态和其它信息,也用于信息广播和更新。 +- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 + +#### ping 消息深入 + +ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 + +每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2` ,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 + +每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 `3` 个其它节点的信息,最多包含 `总节点数减 2` 个其它节点的信息。 + +### 分布式寻址算法 + +- hash 算法(大量缓存重建) +- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) +- Redis cluster 的 hash slot 算法 + +#### hash 算法 + +来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。 + +![](http://img.topjavaer.cn/img/redis集群原理3.png) + +#### 一致性 hash 算法 + +一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。 + +来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环**顺时针“行走”**,遇到的第一个 master 节点就是 key 所在位置。 + +在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。 + +燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 + +![](http://img.topjavaer.cn/img/redis集群原理4.png) + +#### Redis cluster 的 hash slot 算法 + +Redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。 + +Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。 + +任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。 + +![](http://img.topjavaer.cn/img/redis集群原理5.png) + +### Redis cluster 的高可用与主备切换原理 + +Redis cluster 的高可用的原理,几乎跟哨兵是类似的。 + +#### 判断节点宕机 + +如果一个节点认为另外一个节点宕机,那么就是 `pfail` ,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail` ,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。 + +在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong` ,那么就被认为 `pfail` 。 + +如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中, `ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail` 。 + +#### 从节点过滤 + +对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。 + +检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor` ,那么就**没有资格**切换成 `master` 。 + +#### 从节点选举 + +每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。 + +所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node `(N/2 + 1)` 都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。 + +从节点执行主备切换,从节点切换为主节点。 + +#### 与哨兵比较 + +整个流程跟哨兵相比,非常类似,所以说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。 + + + +> 参考链接:https://doocs.github.io/advanced-java/#/docs/high-concurrency/redis-cluster diff --git a/docs/redis/article/redis-duration.md b/docs/redis/article/redis-duration.md new file mode 100644 index 0000000..1aea863 --- /dev/null +++ b/docs/redis/article/redis-duration.md @@ -0,0 +1,190 @@ +--- +sidebar: heading +title: Redis持久化详解 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis持久化 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# Redis持久化详解 + +**一、持久化简介** + +**Redis** 的数据 **全部存储** 在 **内存** 中,如果 **突然宕机**,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 **持久化机制**,它会将内存中的数据库状态 **保存到磁盘** 中。 + +## 持久化发生了什么 | 从内存到磁盘 + +我们来稍微考虑一下 **Redis** 作为一个 **"内存数据库"** 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情: + +![](http://img.topjavaer.cn/img/redis持久化详解1.png) + +**详细版** 的文字描述大概就是下面这样: + +1. 客户端向数据库 **发送写命令** *(数据在客户端的内存中)* +2. 数据库 **接收** 到客户端的 **写请求** *(数据在服务器的内存中)* +3. 数据库 **调用系统 API** 将数据写入磁盘 *(数据在内核缓冲区中)* +4. 操作系统将 **写缓冲区** 传输到 **磁盘控控制器** *(数据在磁盘缓存中)* +5. 操作系统的磁盘控制器将数据 **写入实际的物理媒介** 中 *(数据在磁盘中)* + +**注意:** 上面的过程其实是 **极度精简** 的,在实际的操作系统中,**缓存** 和 **缓冲区** 会比这 **多得多**... + +## 如何尽可能保证持久化的安全 + +如果我们故障仅仅涉及到 **软件层面** *(该进程被管理员终止或程序崩溃)* 并且没有接触到内核,那么在 *上述步骤 3* 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。 + +如果我们考虑 **停电/ 火灾** 等 **更具灾难性** 的事情,那么只有在完成了第 **5** 步之后,才是安全的。 + +所以我们可以总结得出数据安全最重要的阶段是:**步骤三、四、五**,即: + +- 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少? +- 内核多久从缓冲区取数据刷新到磁盘控制器? +- 磁盘控制器多久把数据写入物理媒介一次? +- **注意:** 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 **尽可能地保证** 数据的安全,这对于所有数据库来说都是一样的。 + +我们从 **第三步** 开始。Linux 系统提供了清晰、易用的用于操作文件的 `POSIX file API`,`20` 多年过去,仍然还有很多人对于这一套 `API` 的设计津津乐道,我想其中一个原因就是因为你光从 `API` 的命名就能够很清晰地知道这一套 API 的用途: + +``` +int open(const char *path, int oflag, .../*,mode_t mode */); +int close (int filedes);int remove( const char *fname ); +ssize_t write(int fildes, constvoid *buf, size_t nbyte); +ssize_t read(int fildes, void *buf, size_t nbyte); +``` + +- 参考自:API 设计最佳实践的思考 - https://www.cnblogs.com/yuanjiangw/p/10846560.html + +所以,我们有很好的可用的 `API` 来完成 **第三步**,但是对于成功返回之前,我们对系统调用花费的时间没有太多的控制权。 + +然后我们来说说 **第四步**。我们知道,除了早期对电脑特别了解那帮人 *(操作系统就这帮人搞的)*,实际的物理硬件都不是我们能够 **直接操作** 的,都是通过 **操作系统调用** 来达到目的的。为了防止过慢的 I/O 操作拖慢整个系统的运行,操作系统层面做了很多的努力,譬如说 **上述第四步** 提到的 **写缓冲区**,并不是所有的写操作都会被立即写入磁盘,而是要先经过一个缓冲区,默认情况下,Linux 将在 **30 秒** 后实际提交写入。 + +但是很明显,**30 秒** 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 `PROSIX API` 提供了另一个解决方案:`fsync`,该命令会 **强制** 内核将 **缓冲区** 写入 **磁盘**,但这是一个非常消耗性能的操作,每次调用都会 **阻塞等待** 直到设备报告 IO 完成,所以一般在生产环境的服务器中,**Redis** 通常是每隔 1s 左右执行一次 `fsync` 操作。 + +到目前为止,我们了解到了如何控制 `第三步` 和 `第四步`,但是对于 **第五步**,我们 **完全无法控制**。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。 + +# 二、Redis 中的两种持久化方式 + +## 方式一:快照 + +**Redis 快照** 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在2分钟前创建的,并且现在已经至少有 *100* 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 `.rdb` 文件生成。 + +但我们知道,Redis 是一个 **单线程** 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。 + +还有一个重要的问题是,我们在 **持久化的同时**,**内存数据结构** 还可能在 **变化**,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,可是这才刚持久化结束,咋办? + +### 使用系统多进程 COW(Copy On Write) 机制 | fork 函数 + +操作系统多进程 **COW(Copy On Write) 机制** 拯救了我们。**Redis** 在持久化时会调用 `glibc` 的函数 `fork` 产生一个子进程,简单理解也就是基于当前进程 **复制** 了一个进程,主进程和子进程会共享内存里面的代码块和数据段: + +![](http://img.topjavaer.cn/img/redis持久化详解2.png) + +这里多说一点,**为什么 fork 成功调用后会有两个返回值呢?** 因为子进程在复制时复制了父进程的堆栈段,所以两个进程都停留在了 `fork` 函数中 *(都在同一个地方往下继续"同时"执行)*,等待返回,所以 **一次在父进程中返回子进程的 pid,另一次在子进程中返回零,系统资源不够时返回负数**。*(伪代码如下)* + +``` +pid = os.fork() +if pid > 0: + handle_client_request() # 父进程继续处理客户端请求 +if pid == 0: + handle_snapshot_write() # 子进程处理快照写磁盘 +if pid < 0: + # fork error +``` + +所以 **快照持久化** 可以完全交给 **子进程** 来处理,**父进程** 则继续 **处理客户端请求**。**子进程** 做数据持久化,它 **不会修改现有的内存数据结构**,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 **父进程** 不一样,它必须持续服务客户端请求,然后对 **内存数据结构进行不间断的修改**。 + +这个时候就会使用操作系统的 COW 机制来进行 **数据段页面** 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后 **对这个复制的页面进行修改**。这时 **子进程** 相应的页面是 **没有变化的**,还是进程产生时那一瞬间的数据。 + +子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 **Redis** 的持久化 **叫「快照」的原因**。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。 + +## 方式二:AOF + +**快照不是很持久**。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 `kill -9` 的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具有充分的耐用性,在这些情况下,快照并不是可行的选择。 + +**AOF(Append Only File - 仅追加文件)** 它的工作方式非常简单:每次执行 **修改内存** 中数据集的写操作时,都会 **记录** 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 **所有的修改性指令序列**,那么就可以通过对一个空的 Redis 实例 **顺序执行所有的指令**,也就是 **「重放」**,来恢复 Redis 当前实例的内存数据结构的状态。 + +为了展示 AOF 在实际中的工作方式,我们来做一个简单的实验: + +``` +./redis-server --appendonly yes # 设置一个新实例为 AOF 模式 +``` + +然后我们执行一些写操作: + +``` +redis 127.0.0.1:6379> set key1 Hello +OK +redis 127.0.0.1:6379> append key1 " World!" +(integer) 12 +redis 127.0.0.1:6379> del key1 +(integer) 1 +redis 127.0.0.1:6379> del non_existing_key +(integer) 0 +``` + +前三个操作实际上修改了数据集,第四个操作没有修改,因为没有指定名称的键。这是 AOF 日志保存的文本: + +``` +$ cat appendonly.aof +*2 +$6 +SELECT +$1 +0 +*3 +$3 +set +$4 +key1 +$5 +Hello +*3 +$6 +append +$4 +key1 +$7 + World! +*2 +$3 +del +$4 +key1 +``` + +如您所见,最后的那一条 `DEL` 指令不见了,因为它没有对数据集进行任何修改。 + +就是这么简单。当 Redis 收到客户端修改指令后,会先进行参数校验、逻辑处理,如果没问题,就 **立即** 将该指令文本 **存储** 到 AOF 日志中,也就是说,**先执行指令再将日志存盘**。这一点不同于 `MySQL`、`LevelDB`、`HBase` 等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 **Redis 为什么没有这么做呢?** + +> Emmm... 没找到特别满意的答案,引用一条来自知乎上的回答吧: +> +> - **@缘于专注** - 我甚至觉得没有什么特别的原因。仅仅是因为,由于AOF文件会比较大,为了避免写入无效指令(错误指令),必须先做指令检查?如何检查,只能先执行了。因为语法级别检查并不能保证指令的有效性,比如删除一个不存在的key。而MySQL这种是因为它本身就维护了所有的表的信息,所以可以语法检查后过滤掉大部分无效指令直接记录日志,然后再执行。 +> - 更多讨论参见:为什么Redis先执行指令,再记录AOF日志,而不是像其它存储引擎一样反过来呢?- https://www.zhihu.com/question/342427472 + +### AOF 重写 + +**Redis** 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 **AOF 日志 "瘦身"**。 + +**Redis** 提供了 `bgrewriteaof` 指令用于对 AOF 日志进行瘦身。其 **原理** 就是 **开辟一个子进程** 对内存进行 **遍历** 转换成一系列 Redis 的操作指令,**序列化到一个新的 AOF 日志文件** 中。序列化完毕后再将操作期间发生的 **增量 AOF 日志** 追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。 + +### fsync + +AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。 + +就像我们 *上方第四步* 描述的那样,我们需要借助 `glibc` 提供的 `fsync(int fd)` 函数来讲指定的文件内容 **强制从内核缓存刷到磁盘**。但 **"强制开车"** 仍然是一个很消耗资源的一个过程,需要 **"节制"**!通常来说,生产环境的服务器,Redis 每隔 1s 左右执行一次 `fsync` 操作就可以了。 + +Redis 同样也提供了另外两种策略,一个是 **永不 `fsync`**,来让操作系统来决定合适同步磁盘,很不安全,另一个是 **来一个指令就 `fsync` 一次**,非常慢。但是在生产环境基本不会使用,了解一下即可。 + +## Redis 4.0 混合持久化 + +重启 Redis 时,我们很少使用 `rdb` 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 `rdb` 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 + +**Redis 4.0** 为了解决这个问题,带来了一个新的持久化选项——**混合持久化**。将 `rdb` 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 **自持久化开始到持久化结束** 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小: + +![](http://img.topjavaer.cn/img/redis持久化详解3.png) + +于是在 Redis 重启的时候,可以先加载 `rdb` 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。 diff --git a/docs/redis/article/redis-multi-thread.md b/docs/redis/article/redis-multi-thread.md new file mode 100644 index 0000000..b8f88f3 --- /dev/null +++ b/docs/redis/article/redis-multi-thread.md @@ -0,0 +1,178 @@ +--- +sidebar: heading +title: 为什么Redis 6.0 引入多线程 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: redis多线程,redis6.0 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +> 本文转载自Hollis + +Redis是目前广为人知的一个内存数据库,在各个场景中都有着非常丰富的应用,前段时间Redis推出了6.0的版本,在新版本中采用了多线程模型。 + +因为我们公司使用的内存数据库是自研的,按理说我对Redis的关注其实并不算多,但是因为Redis用的比较广泛,所以我需要了解一下这样方便我进行面试。 + +总不能候选人用过Redis,但是我非要问人家阿里的Tair是怎么回事吧。 + +所以,在Redis 6.0 推出之后,我想去了解下为什么采用多线程,现在采用的多线程和以前版本有什么区别?为什么这么晚才使用多线程? + +**Redis不是已经采用了多路复用技术吗?不是号称很高的性能了吗?为啥还要采用多线程模型呢?** + +本文就来分析下这些问题以及背后的思考。 + +## Redis为什么最开始被设计成单线程的? + +Redis作为一个成熟的分布式缓存框架,它由很多个模块组成,如网络请求模块、索引模块、存储模块、高可用集群支撑模块、数据操作模块等。 + +很多人说Redis是单线程的,就认为Redis中所有模块的操作都是单线程的,其实这是不对的。 + +我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,**Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。** + +所以说,Redis中并不是没有多线程模型的,早在Redis 4.0的时候就已经针对部分命令做了多线程化。 + +那么,为什么网络操作模块和数据存储模块最初并没有使用多线程呢? + +这个问题的答案比较简单!因为:"没必要!" + +为什么没必要呢?我们先来说一下,什么情况下要使用多线程? + +**多线程适用场景** + +一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。 + +其中读写操作主要是涉及到的就是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。 + +**而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的利用率。** + +那么,Redis需不需要通过多线程的方式来提升提升I/O的利用率和CPU的利用率呢? + +首先,我们可以肯定的说,Redis不需要提升CPU利用率,因为**Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。** + +**所以,通过多线程技术来提升Redis的CPU利用率这一点是完全没必要的。** + +那么,使用多线程技术来提升Redis的I/O利用率呢?是不是有必要呢? + +Redis确实是一个I/O操作密集的框架,他的数据操作过程中,会有大量的网络I/O和磁盘I/O的发生。要想提升Redis的性能,是一定要提升Redis的I/O利用率的,这一点毋庸置疑。 + +但是,**提升I/O利用率,并不是只有采用多线程技术这一条路可以走!** + +## **多线程的弊端** + +我们在很多文章中介绍过一些Java中的多线程技术,如内存模型、锁、CAS等,这些都是Java中提供的一些在多线程情况下保证线程安全的技术。 + +> 线程安全:是编程中的术语,指某个函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 + +和Java类似,所有支持多线程的编程语言或者框架,都不得不面对的一个问题,那就是如何解决多线程编程模式带来的共享资源的并发控制问题。 + +**虽然,采用多线程可以帮助我们提升CPU和I/O的利用率,但是多线程带来的并发问题也给这些语言和框架带来了更多的复杂性。而且,多线程模型中,多个线程的互相切换也会带来一定的性能开销。** + +所以,在提升I/O利用率这个方面上,Redis并没有采用多线程技术,而是选择了**多路复用 I/O**技术。 + +## **小结** + +Redis并没有在网络请求模块和数据操作模块中使用多线程模型,主要是基于以下四个原因: + +- 1、Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU +- 2、使用单线程模型,可维护性更高,开发,调试和维护的成本更低 +- 3、单线程模型,避免了线程间切换带来的性能开销 +- 4、在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率 + +还是要记住:Redis并不是完全单线程的,只是有关键的网络IO和键值对读写是由一个线程完成的。 + + + +## Redis的多路复用 + +多路复用这个词,相信很多人都不陌生。我之前的很多文章中也够提到过这个词。 + +其中在介绍Linux IO模型的时候我们提到过它、在介绍HTTP/2的原理的时候,我们也提到过他。 + +那么,Redis的多路复用技术和我们之前介绍的又有什么区别呢? + +这里先讲讲**Linux多路复用技术,就是多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。** + +![](http://img.topjavaer.cn/img/redis多线程-多路复用.png) + +多看一遍上面这张图和上面那句话,后面可能还会用得到。 + +也就是说,通过一个线程来处理多个IO流。 + +IO多路复用在Linux下包括了三种,select、poll、epoll,抽象来看,他们功能是类似的,但具体细节各有不同。 + +其实,Redis的IO多路复用程序的所有功能都是通过包装操作系统的IO多路复用函数库来实现的。每个IO多路复用函数库在Redis源码中都有对应的一个单独的文件。 + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/6fuT3emWI5Jdhnbn0SIYAKmmRkT7fSLoTOGS1iancSvF6ngGUxbykTWBGmVibP0nVgiarbO7BEtfticEJkWz73VfLQ/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1) + +在Redis 中,每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。 + +一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。 + +所以,Redis选择使用多路复用IO技术来提升I/O利用率。 + +而之所以Redis能够有这么高的性能,不仅仅和采用多路复用技术和单线程有关,此外还有以下几个原因: + +- 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。 +- 2、数据结构简单,对数据操作也简单,如哈希表、跳表都有很高的性能。 +- 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU +- 4、使用多路I/O复用模型 + +## 为什么Redis 6.0 引入多线程 + +2020年5月份,Redis正式推出了6.0版本,这个版本中有很多重要的新特性,其中多线程特性引起了广泛关注。 + +但是,需要提醒大家的是,**Redis 6.0中的多线程,也只是针对处理网络请求过程采用了多线程,而数据的读写命令,仍然是单线程处理的。** + +但是,不知道会不会有人有这样的疑问: + +**Redis不是号称单线程也有很高的性能么?** + +**不是说多路复用技术已经大大的提升了IO利用率了么,为啥还需要多线程?** + +主要是因为我们对Redis有着更高的要求。 + +根据测算,Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,对于小数据包,Redis 服务器可以处理 80,000 到 100,000 QPS,这么高的对于 80% 的公司来说,单线程的 Redis 已经足够使用了。 + +但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的 QPS。 + +为了提升QPS,很多公司的做法是部署Redis集群,并且尽可能提升Redis机器数。但是这种做法的资源消耗是巨大的。 + +而经过分析,限制Redis的性能的主要瓶颈出现在网络IO的处理上,虽然之前采用了多路复用技术。但是我们前面也提到过,**多路复用的IO模型本质上仍然是同步阻塞型IO模型**。 + +下面是多路复用IO中select函数的处理过程: + +![](http://img.topjavaer.cn/img/redis多路复用图解.png) + +从上图我们可以看到,**在多路复用的IO模型中,在处理网络请求时,调用 select (其他函数同理)的过程是阻塞的,也就是说这个过程会阻塞线程,如果并发量很高,此处可能会成为瓶颈。** + +虽然现在很多服务器都是多个CPU核的,但是对于Redis来说,因为使用了单线程,在一次数据操作的过程中,有大量的CPU时间片是耗费在了网络IO的同步处理上的,并没有充分的发挥出多核的优势。 + +**如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。** + +所以,Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。 + +但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。 + +**那么,在引入多线程之后,如何解决并发带来的线程安全问题呢?** + +这就是为什么我们前面多次提到的"Redis 6.0的多线程只用来处理网络请求,而数据的读写还是单线程"的原因。 + +Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回给时,使用了多线程。而数据读写操作还是由单线程来完成的,所以,这样就不会出现并发问题了。 + + + +参考资料: + +https://www.cnblogs.com/Zzbj/p/13531622.html + +https://xie.infoq.cn/article/b3816e9fe3ac77684b4f29348 + +https://jishuin.proginn.com/p/763bfbd2a1c2 + +《极客时间:Redis核心技术与实战》 diff --git "a/Redis/Redis\345\205\245\351\227\250\346\214\207\345\215\227\346\200\273\347\273\223.md" b/docs/redis/redis-basic-all.md similarity index 99% rename from "Redis/Redis\345\205\245\351\227\250\346\214\207\345\215\227\346\200\273\347\273\223.md" rename to docs/redis/redis-basic-all.md index a3b0f32..e7527ec 100644 --- "a/Redis/Redis\345\205\245\351\227\250\346\214\207\345\215\227\346\200\273\347\273\223.md" +++ b/docs/redis/redis-basic-all.md @@ -428,7 +428,7 @@ ziplist是 Redis 为了节约内存而开发的, 由一系列特殊编码的 - 最底层的链表包含所有的元素 - 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn) -![](http://img.dabin-coder.cn/image/redis-skiplist.png) +![](http://img.topjavaer.cn/img/redis-skiplist.png) #### 对象 @@ -470,7 +470,7 @@ hash类型内部编码有两种: Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。 -![](http://img.dabin-coder.cn/image/list-api.png) +![](http://img.topjavaer.cn/img/list-api.png) 使用场景: @@ -555,7 +555,7 @@ GET #返回文章ID。 3. EXEC命令进行提交事务 -![](http://img.dabin-coder.cn/image/redis-multi.jpg) +![](http://img.topjavaer.cn/img/redis-multi.jpg) DISCARD:放弃事务,即该事务内的所有命令都将取消 @@ -578,7 +578,7 @@ QUEUED 事务里的命令执行时会读取最新的值: -![](http://img.dabin-coder.cn/image/redis-transaction.png) +![](http://img.topjavaer.cn/img/redis-transaction.png) ### WATCH命令 @@ -725,7 +725,7 @@ SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库。 5. 同步数据集。第一次同步的时候,从数据库启动后会向主数据库发送SYNC命令。主数据库接收到命令后开始在后台保存快照(RDB持久化过程),并将保存快照过程接收到的命令缓存起来。当快照完成后,Redis会将快照文件和缓存的命令发送到从数据库。从数据库接收到后,会载入快照文件并执行缓存的命令。以上过程称为复制初始化。 6. 复制初始化完成后,主数据库每次收到写命令就会将命令同步给从数据库,从而实现主从数据库数据的一致性。 -![](http://img.dabin-coder.cn/image/redis-replication.png) +![](http://img.topjavaer.cn/img/redis-replication.png) Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。 @@ -857,7 +857,7 @@ redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0 使用evalsha执行Lua脚本过程如下: -![](http://img.dabin-coder.cn/image/evalsha.png) +![](http://img.topjavaer.cn/img/evalsha.png) ### lua脚本作用 @@ -1000,7 +1000,7 @@ public void write(String key,Object data){ 解决方法: -![](http://img.dabin-coder.cn/image/image-20210913235221410.png) +![](http://img.topjavaer.cn/img/image-20210913235221410.png) > 图片来源:https://tech.it168.com diff --git a/docs/redis/redis-basic/1-introduce.md b/docs/redis/redis-basic/1-introduce.md new file mode 100644 index 0000000..a7d3657 --- /dev/null +++ b/docs/redis/redis-basic/1-introduce.md @@ -0,0 +1,51 @@ +--- +sidebar: heading +title: Redis简介 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis简介,redis优缺点,io多路复用,Memcached和Redis的区别,redis应用场景 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 简介 + +Redis是一个高性能的key-value数据库。Redis对数据的操作都是原子性的。 + +## 优缺点 + +优点: + +1. 基于内存操作,内存读写速度快。 +2. Redis是单线程的,避免线程切换开销及多线程的竞争问题。单线程是指在处理网络请求(一个或多个redis客户端连接)的时候只有一个线程来处理,redis运行时不止有一个线程,数据持久化或者向slave同步aof时会另起线程。 +3. 支持多种数据类型,包括String、Hash、List、Set、ZSet等 +4. 支持持久化。Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免数据丢失问题。 +5. redis 采用IO多路复用技术。多路指的是多个socket连接,复用指的是复用一个线程。redis使用单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。 + +缺点:对join或其他结构化查询的支持就比较差。 + +## io多路复用 + +将用户socket对应的文件描述符(file description)注册进epoll,然后epoll帮你监听哪些socket上有消息到达。当某个socket可读或者可写的时候,它可以给你一个通知。只有当系统通知哪个描述符可读了,才去执行read操作,可以保证每次read都能读到有效数据。这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的复用指的是复用同一个线程。 + +## 应用场景 + +1. 缓存热点数据,缓解数据库的压力。 +2. 利用Redis中原子性的自增操作,可以用使用实现计算器的功能,比如统计用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力。 +3. 简单消息队列,不要求高可靠的情况下,可以使用Redis自身的发布/订阅模式或者List来实现一个队列,实现异步操作。 +4. 好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能。 +5. 限速器,比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力。 + +## Memcached和Redis的区别 + +1. Redis只使用单核,而Memcached可以使用多核。 +2. MemCached数据结构单一,仅用来缓存数据,而Redis支持更加丰富的数据类型,也可以在服务器端直接对数据进行丰富的操作,这样可以减少网络IO次数和数据体积。 +3. MemCached不支持数据持久化,断电或重启后数据消失。Redis支持数据持久化和数据恢复,允许单点故障。 + + + diff --git a/docs/redis/redis-basic/10-lua.md b/docs/redis/redis-basic/10-lua.md new file mode 100644 index 0000000..8bf0f28 --- /dev/null +++ b/docs/redis/redis-basic/10-lua.md @@ -0,0 +1,85 @@ +--- +sidebar: heading +title: Redis LUA脚本 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis LUA脚本,lua脚本 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# LUA脚本 + +Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。 + +在Redis中执行Lua脚本有两种方法:eval和evalsha。 + +eval 命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。 + +``` +//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数 +> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second +1) "key1" +2) "key2" +3) "first" +4) "second" +``` + +## evalsha + +Redis还提供了evalsha命令来执行Lua脚本。首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和。Evalsha 命令根据给定的 sha1 校验和,执行缓存在服务器中的脚本。 + +script load命令可以将脚本内容加载到Redis内存中。 + +``` +redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'" +"232fd51614574cf0867b83d384a5e898cfd24e5a" + +redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0 +"hello moto" +``` + +使用evalsha执行Lua脚本过程如下: + +![](http://img.topjavaer.cn/img/evalsha.png) + +## lua脚本作用 + +1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。 + +2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。 + +## 应用场景 + +限制接口访问频率。 + +在Redis维护一个接口访问次数的键值对,key是接口名称,value是访问次数。每次访问接口时,会执行以下操作: + +- 通过aop拦截接口的请求,对接口请求进行计数,每次进来一个请求,相应的接口count加1,存入redis。 +- 如果是第一次请求,则会设置count=1,并设置过期时间。因为这里set()和expire()组合操作不是原子操作,所以引入lua脚本,实现原子操作,避免并发访问问题。 +- 如果给定时间范围内超过最大访问次数,则会抛出异常。 + +```java +private String buildLuaScript() { + return "local c" + + "\nc = redis.call('get',KEYS[1])" + + "\nif c and tonumber(c) > tonumber(ARGV[1]) then" + + "\nreturn c;" + + "\nend" + + "\nc = redis.call('incr',KEYS[1])" + + "\nif tonumber(c) == 1 then" + + "\nredis.call('expire',KEYS[1],ARGV[2])" + + "\nend" + + "\nreturn c;"; +} + +String luaScript = buildLuaScript(); +RedisScript redisScript = new DefaultRedisScript<>(luaScript, Number.class); +Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period()); +``` + diff --git a/docs/redis/redis-basic/11-deletion-policy.md b/docs/redis/redis-basic/11-deletion-policy.md new file mode 100644 index 0000000..6754972 --- /dev/null +++ b/docs/redis/redis-basic/11-deletion-policy.md @@ -0,0 +1,41 @@ +--- +sidebar: heading +title: Redis删除策略 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis删除策略 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 删除策略 + +1. 被动删除。在访问key时,如果发现key已经过期,那么会将key删除。 +2. 主动删除。定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。 + +3. 内存不够时清理。Redis有最大内存的限制,通过maxmemory参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进行内存释放, 在进行内存释放的时候,会按照配置的淘汰策略清理内存,淘汰策略一般有6种,Redis4.0版本后又增加了2种,主要由分为三类: + + - 第一类 不处理 noeviction。发现内存不够时,不删除key,执行写入命令时直接返回错误信息。(默认的配置) + + - 第二类 从所有结果集中的key中挑选,进行淘汰 + + - allkeys-random 就是从所有的key中随机挑选key,进行淘汰 + - allkeys-lru 就是从所有的key中挑选最近最少使用的数据淘汰 + - allkeys-lfu 就是从所有的key中挑选使用频率最低的key,进行淘汰。(这是Redis 4.0版本后新增的策略) + + - 第三类 从设置了过期时间的key中挑选,进行淘汰 + + 这种就是从设置了expires过期时间的结果集中选出一部分key淘汰,挑选的算法有: + + - volatile-random 从设置了过期时间的结果集中随机挑选key删除。 + - volatile-lru 从设置了过期时间的结果集中挑选最近最少使用的数据淘汰 + - volatile-ttl 从设置了过期时间的结果集中挑选可存活时间最短的key开始删除(也就是从哪些快要过期的key中先删除) + - volatile-lfu 从过期时间的结果集中选择使用频率最低的key开始删除(这是Redis 4.0版本后新增的策略) + + + diff --git a/docs/redis/redis-basic/12-others.md b/docs/redis/redis-basic/12-others.md new file mode 100644 index 0000000..755b96f --- /dev/null +++ b/docs/redis/redis-basic/12-others.md @@ -0,0 +1,96 @@ +--- +sidebar: heading +title: Redis其他知识点 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis客户端,redis慢查询,redis数据一致性 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 其他 + +## 客户端 + +Redis 客户端与服务端之间的通信协议是在TCP协议之上构建的。 + +Redis Monitor 命令用于实时打印出 Redis 服务器接收到的命令,调试用。 + +``` +redis 127.0.0.1:6379> MONITOR +OK +1410855382.370791 [0 127.0.0.1:60581] "info" +1410855404.062722 [0 127.0.0.1:60581] "get" "a" +``` + +## 慢查询 + +Redis原生提供慢查询统计功能,执行`slowlog get{n}`命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。慢查询队列长度默认128,可适当调大。 + +Redis客户端执行一条命令分为4个部分:发送命令;命令排队;命令执行;返回结果。慢查询只统计命令执行这一步的时间,所以没有慢查询并不代表客户端没有超时问题。 + +Redis提供了`slowlog-log-slower-than`(设置慢查询阈值,单位为微秒)和`slowlog-max-len`(慢查询队列大小)配置慢查询参数。 + +相关命令: + +``` +showlog get n //获取慢查询日志 +slowlog len //慢查询日志队列当前长度 +slowlog reset //重置,清理列表 +``` + +慢查询解决方案: + +1. 修改为低时间复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命令。 +2. 调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。 + +## pipeline + +redis客户端执行一条命令分4个过程: 发送命令-〉命令排队-〉命令执行-〉返回结果。使用Pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快。 + +使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。 + +原生批命令(mset, mget)与Pipeline对比: + +1. 原生批命令是原子性,pipeline是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。 + +2. 原生批命令只有一个命令, 但pipeline支持多命令。 + +## 数据一致性 + +缓存和DB之间怎么保证数据一致性: +读操作:先读缓存,缓存没有的话读DB,然后取出数据放入缓存,最后响应数据 +写操作:先删除缓存,再更新DB +为什么是删除缓存而不是更新缓存呢? + +1. 线程安全问题。同时有请求A和请求B进行更新操作,那么会出现(1)线程A更新了缓存(2)线程B更新了缓存(3)线程B更新了数据库(4)线程A更新了数据库,由于网络等原因,请求B先更新数据库,这就导致缓存和数据库不一致的问题。 +2. 如果业务需求写数据库场景比较多,而读数据场景比较少,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。 +3. 如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。 + +先删除缓存,再更新DB,同样也有问题。假如A先删除了缓存,但还没更新DB,这时B过来请求数据,发现缓存没有,去请求DB拿到旧数据,然后再写到缓存,等A更新完了DB之后就会出现缓存和DB数据不一致的情况了。 + +解决方法:采用延时双删策略。更新完数据库之后,延时一段时间,再次删除缓存,确保可以删除读请求造成的缓存脏数据。评估项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。 + +```java +public void write(String key,Object data){ + redis.delKey(key); + db.updateData(data); + Thread.sleep(1000);//确保读请求结束,写请求可以删除读请求造成的缓存脏数据 + redis.delKey(key); +} +``` + +可以将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,加大吞吐量。 + +当删缓存失败时,也会就出现数据不一致的情况。 + +解决方法: + +![](http://img.topjavaer.cn/img/image-20210913235221410.png) + +> 图片来源:https://tech.it168.com diff --git a/docs/redis/redis-basic/2-data-type.md b/docs/redis/redis-basic/2-data-type.md new file mode 100644 index 0000000..7625375 --- /dev/null +++ b/docs/redis/redis-basic/2-data-type.md @@ -0,0 +1,333 @@ +--- +sidebar: heading +title: Redis数据类型 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis数据类型 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 数据类型 + +Redis支持五种数据类型: + +- string(字符串) +- hash(哈希) +- list(列表) +- set(集合) +- zset(sorted set) + +## 字符串类型 + +字符串类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。 + +常用命令:set, get, incr, incrby, desr, keys, append, strlen + +- 赋值和取值 + +``` +SET name tyson +GET name +``` + +- 递增数字 + +``` +INCR num //若键值不是整数时,则会提示错误。 +INCRBY num 2 //增加指定整数 +DESR num //递减数字 +INCRBY num 2.7 //增加指定浮点数 +``` + +- 其他 + +`keys list*` 列出匹配的key + +`APPEND name " dai"` 追加值 + +`STRLEN name` 获取字符串长度 + +`MSET name tyson gender male` 同时设置多个值 + +`MGET name gender` 同时获取多个值 + +`GETBIT name 0` 获取0索引处二进制位的值 + +`FLUSHDB` 删除当前数据库所有的key + +`FLUSHALL` 删除所有数据库中的key + +**SETNX和SETEX** + +`SETNX key value`:当key不存在时,将key的值设为value。若给定的key已经存在,则SETNX不做任何操作。 + +`SETEX key seconds value`:比SET多了seconds参数,相当于`SET KEY value` + `EXPIRE KEY seconds`,而且SETEX是原子性操作。 + +**keys和scan** + +redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。 + +scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。 + +scan命令用于迭代当前数据库中的数据库键:`SCAN cursor [MATCH pattern] [COUNT count]` + +``` +scan 0 match * count 10 //返回10个元素 +``` + +SCAN相关命令包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令,分别用于集合、哈希键及有序集合。 + +**expire** + +``` +SET password 666 +EXPIRE password 5 +TTL password //查看键的剩余生存时间,-1为永不过期 +SETEX password 60 123abc //SETEX可以在设置键的同时设置它的生存时间 +``` + +EXPIRE时间单位是秒,PEXPIRE时间单位是毫秒。在键未过期前可以重新设置过期时间,过期之后则键被销毁。 + +在Redis 2.6和之前版本,如果key不存在或者已过期时返回`-1`。 + +从Redis2.8开始,错误返回值的结果有如下改变: + +- 如果key不存在或者已过期,返回 `-2` +- 如果key存在并且没有设置过期时间(永久有效),返回 `-1` 。 + +**type** + +TYPE 命令用于返回 key 所储存的值的类型。 + +``` +127.0.0.1:6379> type NEWBLOG +list +``` + +## 散列类型 + +常用命令:hset, hget, hmset, hmget, hgetall, hdel, hkeys, hvals + +- 赋值和取值 + +``` +HSET car price 500 //HSET key field value +HGET car price +``` + +同时设置获取多个字段的值 + +``` +HMSET car price 500 name BMW +HMGET car price name +HGETALL car +``` + +使用 HGETALL 命令时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型。 + +`HSETNX car price 400 //当字段不存在时赋值,HSETNX是原子操作,不存在竞态条件` + +- 增加数字 + `HINCRBY person score 60` +- 删除字段 + `HDEL car price` +- 其他 + +``` +HKEYS car //获取key +HVALS car //获取value +HLEN car //长度 +``` + +## 列表类型 + +常用命令:lpush, rpush, lpop, rpop, lrange, lrem + +**添加和删除元素** + +``` +LPUSH numbers 1 +RPUSH numbers 2 3 +LPOP numbers +RPOP numbers +``` + +**获取列表片段** + +``` +LRANGE numbers 0 2 +LRANGE numbers -2 -1 //支持负索引 -1是最右边第一个元素 +LRANGE numbers 0 -1 +``` + +**向列表插入值** + +首先从左到右寻找值为pivot的值,向列表插入value + +``` +LINSERT numbers AFTER 5 8 //往5后面插入8 +LINSERT numbers BEFORE 6 9 //往6前面插入9 +``` + +**删除元素** + +`LTRIM numbers 1 2` 删除索引1到2以外的所有元素 + +LPUSH常和LTRIM一起使用来限制列表的元素个数,如保留最近的100条日志 + +``` +LPUSH logs $newLog +LTRIM logs 0 99 +``` + +**删除列表指定的值** + +`LREM key count value` + + 1. count < 0, 则从右边开始删除前count个值为value的元素 + 2. count > 0, 则从左边开始删除前count个值为value的元素 + 3. count = 0, 则删除所有值为value的元素 `LREM numbers 0 2` + +**其他** + +``` +LLEN numbers //获取列表元素个数 +LINDEX numbers -1 //返回指定索引的元素,index是负数则从右边开始计算 +LSET numbers 1 7 //把索引为1的元素的值赋值成7 +``` + +## 集合类型 + +常用命令:sadd, srem, smembers, scard, sismember, sdiff + +集合中不能有相同的元素。 + +**增加/删除元素** + +``` +SADD letters a b c +SREM letters c d +``` + +**获取元素** + +``` +SMEMBERS letters +SCARD letters //获取集合元素个数 +``` + +**判断元素是否在集合中** +`SISMEMBER letters a` + +**集合间的运算** + +``` +SDIFF setA setB //差集运算 +SINTER setA setB //交集运算 +SUNION setA setB //并集运算 +``` + +三个命令都可以传进多个键 `SDIFF setA setB setC` + +**其他** + +`SDIFFSTORE result setA setB` 进行集合运算并将结果存储 + +`SRANDMEMBER key count` + +>随机获取集合里的一个元素,count大于0,则从集合随机获取count个不重复的元素,count小于0,则随机获取的count个元素有些可能相同。 + +## 有序集合类型 + +常用命令:zadd, zrem, zscore, zrange + +```java +zadd zsetkey 50 e1 60 e2 30 e3 +``` + +Zset(sorted set)是string类型的有序集合。zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是Zset每个元素都会关联一个double(超过17位使用科学计算法表示,可能丢失精度)类型的分数,通过分数来为集合中的成员进行排序。zset的成员是唯一的,但分数(score)可以重复。 + +**有序集合和列表相同点:** + +1. 都是有序的; +2. 都可以获得某个范围内的元素。 + +**有序集合和列表不同点:** + +1. 列表基于链表实现,获取两端元素速度快,访问中间元素速度慢; +2. 有序集合基于散列表和跳跃表实现,访问中间元素时间复杂度是OlogN; +3. 列表不能简单的调整某个元素的位置,有序列表可以(更改元素的分数); +4. 有序集合更耗内存。 + +**增加/删除元素** + +时间复杂度OlogN。 + +``` +ZADD scoreboard 89 Tom 78 Sophia +ZADD scoreboard 85.5 Tyson //支持双精度浮点数 +ZREM scoreboard Tyson +ZREMRANGEBYRANK scoreboard 0 2 //按照排名范围删除元素 +ZREMRANGEBYSCORE scoreboard (80 100 //按照分数范围删除元素,"("代表不包含 +``` + +**获取元素分数** + +时间复杂度O1。 + +`ZSCORE scoreboard Tyson` + +**获取排名在某个范围的元素列表** + +ZRANGE命令时间复杂度是O(log(n)+m), n是有序集合元素个数,m是返回元素个数。 + +``` +ZRANGE scoreboard 0 2 +ZRANGE scoreboard 1 -1 //-1表示最后一个元素 +ZRANGE scoreboard 0 -1 WITHSCORES //同时获得分数 +``` + +**获取指定分数范围的元素** + +ZRANGEBYSCORE命令时间复杂度是O(log(n)+m), n是有序集合元素个数,m是返回元素个数。 + +``` +ZRANGEBYSCORE scoreboard 80 100 +ZRANGEBYSCORE scoreboard 80 (100 //不包含100 +ZRANGEBYSCORE scoreboard (60 +inf LIMIT 1 3 //获取分数高于60的从第二个人开始的3个人 +``` + +**增加某个元素的分数** + +时间复杂度OlogN。 + +`ZINCRBY scoreboard 10 Tyson` + +**其他** + +``` +ZCARD scoreboard //获取集合元素个数,时间复杂度O1 +ZCOUNT scoreboard 80 100 //指定分数范围的元素个数 +ZRANK scoreboard Tyson //按从小到大的顺序获取元素排名 +ZREVRANK scoreboard Tyson //按从大到小的顺序获取元素排名 +``` + +## Bitmaps + +Bitmaps本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作,可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1。 + +bitmap的长度与集合中元素个数无关,而是与基数的上限有关。假如要计算上限为1亿的基数,则需要12.5M字节的bitmap。就算集合中只有10个元素也需要12.5M。 + +## HyperLogLog + +HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。 + +基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数即不重复元素为5。 + +应用场景:独立访客(unique visitor,uv)统计。 diff --git a/docs/redis/redis-basic/3-data-structure.md b/docs/redis/redis-basic/3-data-structure.md new file mode 100644 index 0000000..34842e1 --- /dev/null +++ b/docs/redis/redis-basic/3-data-structure.md @@ -0,0 +1,115 @@ +--- +sidebar: heading +title: Redis数据结构 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis数据结构,动态字符串 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 数据结构 + +## 动态字符串 + +SDS(simple dynamic string,SDS),简单动态字符串,其定义如下: + +```c +struct sdshdr { + + // 记录 buf 数组中已使用字节的数量 + // 等于 SDS 所保存字符串的长度 + int len; + + // 记录 buf 数组中未使用字节的数量 + int free; + + // 字节数组,用于保存字符串 + char buf[]; + +}; +``` + +在64位系统下,属性len和属性free各占4个字节,紧接着存放字节数组。 + +下面展示一个SDS示例: + +```c +set name "Redis" +``` + +![](http://img.topjavaer.cn/img/sds.png) + +free属性的值为0,表示这个SDS没有分配任何未使用空间。 + +len属性的值为5,表示这个SDS保存了一个物字节长的字符串。 + +buf属性是一个char类型的数组,数组的前五个字节分别保存了'R'、'e'、'd'、'i'、's'五个字符,而最后一个字节则保存了空字符'\0'。 + +SDS遵循C字符串以空字符结尾的惯例,保存空字符的1字节空间不计算在SDS的len属性里面,并且为空字符分配额外的1字节空间,以及添加空字符到字符串末尾等操作,都是由SDS函数自动完成的,所以这个空字符对于SDS的使用者来说是完全透明的。遵循空字符串结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数。 + +下面是SDS与C字符串的区别。 + +| C 字符串 | SDS | +| :--------------------------------------------------- | :--------------------------------------------------- | +| 获取字符串长度的复杂度为 O(N) 。 | 获取字符串长度的复杂度为 O(1) 。 | +| API 是不安全的,可能会造成缓冲区溢出。 | API 是安全的,不会造成缓冲区溢出。 | +| 修改字符串长度 `N` 次必然需要执行 `N` 次内存重分配。 | 修改字符串长度 `N` 次最多需要执行 `N` 次内存重分配。 | +| 只能保存文本数据。 | 可以保存文本或者二进制数据。 | +| 可以使用所有 `` 库中的函数。 | 可以使用一部分 `` 库中的函数。 | + +## 字典 + +字典使用hashtable作为底层实现。键值对的值可以是一个指针, 或者是一个 uint64_t 整数, 又或者是一个 int64_t 整数。 + +```c +typedef struct dictEntry { + + // 键 + void *key; + + // 值 + union { + void *val; + uint64_t u64; + int64_t s64; + } v; + + // 指向下个哈希表节点,形成链表 + struct dictEntry *next; + +} dictEntry; +``` + +## 整数集合 + +整数集合(intset)是 Redis 用于保存整数值的集合抽象数据结构, 它可以保存类型为 int16_t 、 int32_t 或者 int64_t 的整数值, 并且保证集合中不会出现重复元素。 + +## 压缩列表 + +ziplist是 Redis 为了节约内存而开发的, 由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。每个压缩列表节点都由 previous_entry_length 、 encoding 、 content 三个部分组成。 + +节点的 previous_entry_length 属性以字节为单位, 记录了压缩列表中前一个节点的长度。 +节点的 encoding 属性记录了节点的 content 属性所保存数据的类型以及长度。有两种编码方式,字节数组编码和整数编码。 + +压缩列表的从表尾向表头遍历操作就是使用这一原理实现的: 只要我们拥有了一个指向某个节点起始地址的指针, 那么通过这个指针以及这个节点的 previous_entry_length 属性, 程序就可以一直向前一个节点回溯, 最终到达压缩列表的表头节点。 + +## 跳表 + +跳表可以看成多层链表,它有如下的性质: + +- 多层的结构组成,每层是一个有序的链表 +- 最底层的链表包含所有的元素 +- 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn) + +![](http://img.topjavaer.cn/img/redis-skiplist.png) + +## 对象 + +Redis 的对象系统还实现了基于引用计数技术的内存回收机制: 当程序不再使用某个对象的时候, 这个对象所占用的内存就会被自动释放; 另外, Redis 还通过引用计数技术实现了对象共享机制, 这一机制可以在适当的条件下, 通过让多个数据库键共享同一个对象来节约内存。 + diff --git a/docs/redis/redis-basic/4-implement.md b/docs/redis/redis-basic/4-implement.md new file mode 100644 index 0000000..d846cd0 --- /dev/null +++ b/docs/redis/redis-basic/4-implement.md @@ -0,0 +1,83 @@ +--- +sidebar: heading +title: Redis底层实现 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis底层实现 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# + +## string + +字符串对象的编码可以是 int 、 raw 或者 embstr 。 + +1. 如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么会将编码设置为 int 。 +2. 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39 字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw 。 +3. 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。 + +| 值 | 编码 | +| :----------------------------------------------------------- | :------------------ | +| 可以用 `long` 类型保存的整数。 | `int` | +| 可以用 `long double` 类型保存的浮点数。 | `embstr` 或者 `raw` | +| 字符串值, 或者因为长度太大而没办法用 `long` 类型表示的整数, 又或者因为长度太大而没办法用 `long double` 类型表示的浮点数。 | `embstr` 或者 `raw` | + +## hash + +hash类型内部编码有两种: + +1. ziplist,压缩列表。当哈希类型元素个数小于512个,并且所有值都小于64字节时,Redis会使用ziplist作为哈希的内部实现。ziplist使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。 +2. hashtable。当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。 + +使用 ziplist 作为 hash 的底层实现时,添加元素的时候,同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后。 + +使用场景:记录博客点赞数量。`hset MAP_BLOG_LIKE_COUNT blogId likeCount`,key为MAP_BLOG_LIKE_COUNT,field为博客id,value为点赞数量。 + +## list + +列表list类型内部编码有两种: + +1. ziplist,压缩列表。当列表中的元素个数小于512个,同时列表中每个元素的值都小于64字节时,Redis会选用ziplist来作为列表的内部实现来减少内存的使用。 +2. 当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。 + +Redis3.2版本提供了quicklist内部编码,简单地说它是以一个ziplist为节点的linkedlist,它结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现。 + +![](http://img.topjavaer.cn/img/list-api.png) + +使用场景: + +1. 消息队列。Redis的lpush+brpop命令组合即可实现阻塞队列。 + +## set + +集合对象的编码可以是 intset 或者 hashtable 。 + +1. intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合(数组)里面。 +2. hashtable 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 而字典的值则全部被设置为 NULL 。 + +## zset + +有序集合的编码可以是 ziplist 或者 skiplist 。当有序集合的元素个数小于128,同时每个元素的值都小于64字节时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。否则,使用skiplist作为有序集合的内部实现。 + +1. ziplist 编码的有序集合对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序。 +2. skiplist 编码的有序集合对象使用字典和跳跃表实现。使用字典查找给定成员的分值,时间复杂度为O(1) (跳跃表查找时间复杂度为O(logN))。使用跳跃表可以对有序集合进行范围型操作。 + +## 使用场景 + +string:1、常规key-value缓存应用。常规计数如微博数、粉丝数。2、分布式锁。 + +hash:存放结构化数据,如用户信息(昵称、年龄、性别、积分等)。 + +list:热门博客列表、消息队列系统。使用list可以构建队列系统,比如:将Redis用作日志收集器,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。 + +set:1、好友关系,微博粉丝的共同关注、共同喜好、共同好友等;2、利用唯一性,统计访问网站的所有独立ip 。 + +zset:1、排行榜;2、优先级队列。 + diff --git a/docs/redis/redis-basic/5-sort.md b/docs/redis/redis-basic/5-sort.md new file mode 100644 index 0000000..294d874 --- /dev/null +++ b/docs/redis/redis-basic/5-sort.md @@ -0,0 +1,51 @@ +--- +sidebar: heading +title: Redis排序 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis排序 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 排序 + +``` +LPUSH myList 4 8 2 3 6 +SORT myList DESC +``` + +``` +LPUSH letters f l d n c +SORT letters ALPHA +``` + +**BY参数** + +``` +LPUSH list1 1 2 3 +SET score:1 50 +SET score:2 100 +SET score:3 10 +SORT list1 BY score:* DESC +``` + +**GET参数** + +GET参数命令作用是使SORT命令的返回结果是GET参数指定的键值。 + +`SORT tag:Java:posts BY post:*->time DESC GET post:*->title GET post:*->time GET #` + +GET #返回文章ID。 + +**STORE参数** + +`SORT tag:Java:posts BY post:*->time DESC GET post:*->title STORE resultCache` + +`EXPIRE resultCache 10 //STORE结合EXPIRE可以缓存排序结果` + diff --git a/docs/redis/redis-basic/6-transaction.md b/docs/redis/redis-basic/6-transaction.md new file mode 100644 index 0000000..67f0145 --- /dev/null +++ b/docs/redis/redis-basic/6-transaction.md @@ -0,0 +1,76 @@ +--- +sidebar: heading +title: Redis事务 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis事务 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 事务 + +事务的原理是将一个事务范围内的若干命令发送给Redis,然后再让Redis依次执行这些命令。 + +事务的生命周期: + +1. 使用MULTI开启一个事务 +2. 在开启事务的时候,每次操作的命令将会被插入到一个队列中,同时这个命令并不会被真的执行 + +3. EXEC命令进行提交事务 + +![](http://img.topjavaer.cn/img/redis-multi.jpg) + +DISCARD:放弃事务,即该事务内的所有命令都将取消 + +一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性: + +``` +127.0.0.1:6379> multi +OK +127.0.0.1:6379> set a 1 +QUEUED +127.0.0.1:6379> set b 1 2 +QUEUED +127.0.0.1:6379> set c 3 +QUEUED +127.0.0.1:6379> exec +1) OK +2) (error) ERR syntax error +3) OK +``` + +事务里的命令执行时会读取最新的值: + +![](http://img.topjavaer.cn/img/redis-transaction.png) + +## WATCH命令 + +WATCH命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。执行EXEC命令之后,就会自动取消监控。 + +``` +127.0.0.1:6379> watch name +OK +127.0.0.1:6379> set name 1 +OK +127.0.0.1:6379> multi +OK +127.0.0.1:6379> set name 2 +QUEUED +127.0.0.1:6379> set gender 1 +QUEUED +127.0.0.1:6379> exec +(nil) +127.0.0.1:6379> get gender +(nil) +``` + +UNWATCH:取消WATCH命令对多有key的监控,所有监控锁将会被取消。 + + + diff --git a/docs/redis/redis-basic/7-message-queue.md b/docs/redis/redis-basic/7-message-queue.md new file mode 100644 index 0000000..13bc12e --- /dev/null +++ b/docs/redis/redis-basic/7-message-queue.md @@ -0,0 +1,46 @@ +--- +sidebar: heading +title: Redis实现消息队列 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis实现消息队列 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 消息队列 + +使用一个列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。 + +BRPOP和RPOP命令相似,唯一的区别就是当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。 +`BRPOP queue 0 //0表示不限制等待时间` + +## 优先级队列 + +`BLPOP queue:1 queue:2 queue:3 0` +如果多个键都有元素,则按照从左到右的顺序取元素 + +## 发布/订阅模式 + +``` +PUBLISH channel1 hi +SUBSCRIBE channel1 +UNSUBSCRIBE channel1 //退订通过SUBSCRIBE命令订阅的频道。 +``` + +`PSUBSCRIBE channel?*` 按照规则订阅 +`PUNSUBSCRIBE channel?*` 退订通过PSUBSCRIBE命令按照某种规则订阅的频道。其中订阅规则要进行严格的字符串匹配,`PUNSUBSCRIBE *`无法退订`channel?*`规则。 + +缺点:在消费者下线的情况下,生产的消息会丢失。 + +## 延时队列 + +使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用`zrangebyscore`指令获取N秒之前的数据轮询进行处理。 + + + diff --git a/docs/redis/redis-basic/8-persistence.md b/docs/redis/redis-basic/8-persistence.md new file mode 100644 index 0000000..7691cb2 --- /dev/null +++ b/docs/redis/redis-basic/8-persistence.md @@ -0,0 +1,82 @@ +--- +sidebar: heading +title: Redis持久化 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis持久化,RDB,AOF + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 持久化 + +Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。 + +## RDB方式 + +RDB 是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis 重启会加载dump.rdb文件恢复数据。 + +RDB持久化的过程(执行SAVE命令除外): + +- 创建一个子进程; +- 父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时文件; +- 当子进程写完所有数据后会用该临时文件替换旧的RDB文件。 + +Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过RDB方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。 + +触发RDB快照: + +1. 手动触发: + - 用户执行SAVE或BGSAVE命令。SAVE命令执行快照的过程会阻塞所有来自客户端的请求,应避免在生产环境使用这个命令。BGSAVE命令可以在后台异步进行快照操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令; + +2. 被动触发: + - 根据配置规则进行自动快照,如`SAVE 300 10`,300秒内至少有10个键被修改则进行快照。 + - 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。 + - 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。 + - 执行debug reload命令重新加载Redis时,也会自动触发save操作。 + +优点:Redis加载RDB恢复数据远远快于AOF的方式。 + +缺点: + +1. RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。 +2. 存在老版本Redis服务和新版本RDB格式兼容性问题。RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。 + +## AOF方式 + +AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是**解决了数据持久化的实时性**,目前已经是Redis持久化的主流方式。 + +默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用`appendonly yes`。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。 + +默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过`appendfsync`参数设置同步的时机。 + +``` +appendfsync always //每次写入aof文件都会执行同步,最安全最慢,只能支持几百TPS写入,不建议配置 +appendfsync everysec //保证了性能也保证了安全,建议配置 +appendfsync no //由操作系统决定何时进行同步操作 +``` + +重写机制: + +随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。 + +优点: +(1)AOF可以更好的保护数据不丢失,一般AOF会每秒去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。 +(2)AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。 +缺点 +(1)对于同一份文件AOF文件比RDB数据快照要大。 +(2)不适合写多读少场景。 +(3)数据恢复比较慢。 + +RDB和AOF如何选择 +(1)仅使用RDB这样会丢失很多数据。 +(2)仅使用AOF,因为这一会有两个问题,第一通过AOF恢复速度慢;第二RDB每次简单粗暴生成数据快照,更加安全健壮。 +(3)综合AOF和RDB两种持久化方式,用AOF来保证数据不丢失,作为恢复数据的第一选择;用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复。 + + + diff --git a/docs/redis/redis-basic/9-cluster.md b/docs/redis/redis-basic/9-cluster.md new file mode 100644 index 0000000..712b514 --- /dev/null +++ b/docs/redis/redis-basic/9-cluster.md @@ -0,0 +1,136 @@ +--- +sidebar: heading +title: Redis集群 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis集群,主从复制,redis读写分离,哨兵Sentinel + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 集群 + +## 主从复制 + +redis的复制功能是支持多个数据库之间的数据同步。主数据库可以进行读写操作,当主数据库的数据发生变化时会自动将数据同步到从数据库。从数据库一般是只读的,它会接收主数据库同步过来的数据。一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。 + +``` +redis-server //启动Redis实例作为主数据库 +redis-server --port 6380 --slaveof 127.0.0.1 6379 //启动另一个实例作为从数据库 +slaveof 127.0.0.1 6379 +SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库。 +``` + +### 同步机制 + +1. 保存主节点信息。 +2. 主从建立socket连接。 +3. 从节点发送ping命令进行首次通信,主要用于检测网络状态。 +4. 权限认证。如果主节点设置了requirepass参数,则需要密码认证。从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证。 +5. 同步数据集。第一次同步的时候,从数据库启动后会向主数据库发送SYNC命令。主数据库接收到命令后开始在后台保存快照(RDB持久化过程),并将保存快照过程接收到的命令缓存起来。当快照完成后,Redis会将快照文件和缓存的命令发送到从数据库。从数据库接收到后,会载入快照文件并执行缓存的命令。以上过程称为复制初始化。 +6. 复制初始化完成后,主数据库每次收到写命令就会将命令同步给从数据库,从而实现主从数据库数据的一致性。 + +![](http://img.topjavaer.cn/img/redis-replication.png) + +Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制。 + +全量复制:一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。 + +部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。 + +### 读写分离 + +通过redis的复制功能可以实现数据库的读写分离,提高服务器的负载能力。主数据库主要进行写操作,而从数据库负责读操作。很多场景下对数据库的读频率大于写,当单机的Redis无法应付大量的读请求时,可以通过复制功能建立多个从数据库节点,主数据库负责写操作,从数据库负责读操作。这种一主多从的结构很适合读多写少的场景。 + +### 从数据库持久化 + +持久化的操作比较耗时,为了提高性能,可以建立一个从数据库,并在从数据库进行持久化,同时在主数据库禁用持久化。 + +## 哨兵Sentinel + +当master节点奔溃时,可以手动将slave提升为master,继续提供服务。 + +- 首先,从数据库使用`SLAVE NO ONE`将从数据库提升为主数据库继续服务; +- 启动奔溃的主数据库,通过`SLAVEOF`命令将其设置为新的主数据库的从数据库,即可将数据同步过来。 + +通过哨兵机制可以自动切换主从节点。哨兵是一个独立的进程,用于监控redis实例的是否正常运行。 + +### 作用 + +1. 监测redis实例的状态 +2. 如果master实例异常,会自动进行主从节点切换 + +客户端连接redis的时候,先连接哨兵,哨兵会告诉客户端redis主节点的地址,然后客户端连接上redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。 + +### 定时任务 + +1. 每隔10s,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构。 +2. 每隔2s,每个Sentinel节点会去获取其他Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,用于判断主节点是否客观下线和是否有新的Sentinel节点加入。 +3. 每隔1s,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点是否可达。 + +### 工作原理 + +- 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。 +- 如果一个实例距离最后一次有效回复 PING 命令的时间超过指定的值, 则这个实例会被 Sentinel 标记为主观下线。 +- 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master是否真正进入主观下线状态。 +- 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。 +- 哨兵节点会选举出哨兵领导者,负责故障转移的工作。 +- 哨兵领导者会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点。 + +```java +/** + * 测试Redis哨兵模式 + * @author liu + */ +public class TestSentinels { + @SuppressWarnings("resource") + @Test + public void testSentinel() { + JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); + jedisPoolConfig.setMaxTotal(10); + jedisPoolConfig.setMaxIdle(5); + jedisPoolConfig.setMinIdle(5); + // 哨兵信息 + Set sentinels = new HashSet<>(Arrays.asList("192.168.11.128:26379", + "192.168.11.129:26379","192.168.11.130:26379")); + // 创建连接池 + JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig,"123456"); + // 获取客户端 + Jedis jedis = pool.getResource(); + // 执行两个命令 + jedis.set("mykey", "myvalue"); + String value = jedis.get("mykey"); + System.out.println(value); + } +} +``` + +## cluster + +集群用于分担写入压力,主从用于灾难备份和高可用以及分担读压力。 + +主从复制存在不能自动故障转移、达不到高可用的问题。 +哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题,但还是存在主节点的写能力、容量受限于单机配置的问题。 +cluster模式实现了Redis的分布式存储,每个节点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题。 + +### 哈希分区算法 + +节点取余分区。使用特定的数据,如Redis的键或用户ID,对节点数量N取余:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。 +优点是简单性。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。 + +一致性哈希分区:为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。 +这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。 + +Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。 + +### 故障转移 + +Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。 + + + diff --git a/docs/redis/redis-basic/README.md b/docs/redis/redis-basic/README.md new file mode 100644 index 0000000..b005e2d --- /dev/null +++ b/docs/redis/redis-basic/README.md @@ -0,0 +1,26 @@ +--- +title: Redis基础 +icon: redis +date: 2022-08-06 +category: redis +star: true +--- + +**本专栏是大彬学习MySQL基础知识的学习笔记,如有错误,可以在评论区指出**~ + +![](http://img.topjavaer.cn/img/Redis知识点.jpg) + +## Redis总结 + +- [简介](./1-introduce.md) +- [数据类型](./2-data-type.md) +- [数据结构](./3-data-structure.md) +- [底层实现](./4-implement.md) +- [排序](./5-sort.md) +- [事务](./6-transaction.md) +- [消息队列](./7-message-queue.md) +- [持久化](./8-persistence.md) +- [集群](./9-cluster.md) +- [LUA脚本](./10-lua.md) +- [删除策略](./11-deletion-policy.md) +- [其他](./12-others.md) diff --git "a/Redis/Redis\351\235\242\350\257\225\351\242\230.md" b/docs/redis/redis.md similarity index 61% rename from "Redis/Redis\351\235\242\350\257\225\351\242\230.md" rename to docs/redis/redis.md index b9a05f4..0890449 100644 --- "a/Redis/Redis\351\235\242\350\257\225\351\242\230.md" +++ b/docs/redis/redis.md @@ -1,3 +1,30 @@ +--- +sidebar: heading +title: Redis常见面试题总结 +category: 缓存 +tag: + - Redis +head: + - - meta + - name: keywords + content: Redis面试题,Redis优缺点,Redis应用场景,Redis数据类型,Redis和Memcached,Redis keys命令,Redis事务,Redis持久化机制,Redis内存淘汰策略,缓存常见问题,LUA脚本,RedLock,Redis大key,Redis集群 + - - meta + - name: description + content: Redis常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 更新记录 + +- 2024.1.7,新增[Redis存在线程安全问题吗?](##Redis存在线程安全问题吗?) +- 2024.5.9,补充[Redis应用场景有哪些?](##Redis应用场景有哪些?) + ## Redis是什么? Redis(`Remote Dictionary Server`)是一个使用 C 语言编写的,高性能非关系型的键值对数据库。与传统数据库不同的是,Redis 的数据是存在内存中的,所以读写速度非常快,被广泛应用于缓存方向。Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。 @@ -25,6 +52,20 @@ Redis(`Remote Dictionary Server`)是一个使用 C 语言编写的,高性 - **IO多路复用模型**:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。 - **高效的数据结构**:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。 +## 既然Redis那么快,为什么不用它做主数据库,只用它做缓存? + +虽然Redis非常快,但它也有一些局限性,不能完全替代主数据库。有以下原因: + +**事务处理:**Redis只支持简单的事务处理,对于复杂的事务无能为力,比如跨多个键的事务处理。 + +**数据持久化:**Redis是内存数据库,数据存储在内存中,如果服务器崩溃或断电,数据可能丢失。虽然Redis提供了数据持久化机制,但有一些限制。 + +**数据处理:**Redis只支持一些简单的数据结构,比如字符串、列表、哈希表等。如果需要处理复杂的数据结构,比如关系型数据库中的表,那么Redis可能不是一个好的选择。 + +**数据安全:**Redis没有提供像主数据库那样的安全机制,比如用户认证、访问控制等等。 + +因此,虽然Redis非常快,但它还有一些限制,不能完全替代主数据库。所以,使用Redis作为缓存是一种很好的方式,可以提高应用程序的性能,并减少数据库的负载。 + ## 讲讲Redis的线程模型? Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。 @@ -36,11 +77,18 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 ## Redis应用场景有哪些? -1. **缓存热点数据**,缓解数据库的压力。 -2. 利用 Redis 原子性的自增操作,可以实现**计数器**的功能,比如统计用户点赞数、用户访问数等。 -3. **简单的消息队列**,可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 -4. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 -5. **好友关系**,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。 +Redis作为一种优秀的基于key/value的缓存,有非常不错的性能和稳定性,无论是在工作中,还是面试中,都经常会出现。 + +下面来聊聊Redis的常见的8种应用场景。 + +1. **缓存热点数据**,缓解数据库的压力。例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访问数据。 +2. **计数器**。利用 Redis 原子性的自增操作,可以实现**计数器**的功能,内存操作,性能非常好,非常适用于这些计数场景,比如统计用户点赞数、用户访问数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。 +3. **分布式会话**。集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 +4. **分布式锁**。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX(SET if Not eXists) 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 +5. **简单的消息队列**,消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 +6. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 +7. **社交网络**,点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。 +8. **排行榜**。很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。 ## Memcached和Redis的区别? @@ -103,6 +151,22 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。 +## Redis存在线程安全问题吗? + +首先,从Redis 服务端层面分析。 + +Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 + +虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。 + +第二个,从Redis客户端层面分析。 + +虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。 + +假设两个redis client同时获取Redis Server上的key1, 同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。 + +当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + ## keys命令存在的问题? redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是`O(1)`,但是要真正实现keys的功能,需要执行多次scan。 @@ -120,7 +184,7 @@ scan的缺点:在scan的过程中如果有键的变化(增加、删除、修 3. EXEC命令进行提交事务 -![](http://img.dabin-coder.cn/image/redis-multi.jpg) +![](http://img.topjavaer.cn/img/redis-multi.jpg) 一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性: @@ -185,13 +249,13 @@ Redis单条命令是原子性执行的,但事务不保证原子性,且没有 Redis支持两种方式的持久化,一种是`RDB`的方式,一种是`AOF`的方式。**前者会根据指定的规则定时将内存中的数据存储在硬盘上**,而**后者在每次执行完命令后将命令记录下来**。一般将两者结合使用。 -### RDB方式 +**RDB方式** `RDB`是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个`dump.rdb`文件。Redis 重启会加载`dump.rdb`文件恢复数据。 `bgsave`是主流的触发 RDB 持久化的方式,执行过程如下: -![](http://img.dabin-coder.cn/image/rdb持久化过程.png) +![](http://img.topjavaer.cn/img/rdb持久化过程.png) - 执行`BGSAVE`命令 - Redis 父进程判断当前**是否存在正在执行的子进程**,如果存在,`BGSAVE`命令直接返回。 @@ -220,7 +284,7 @@ Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过 1. **RDB方式数据无法做到实时持久化**。因为`BGSAVE`每次运行都要执行`fork`操作创建子进程,属于重量级操作,频繁执行成本比较高。 2. RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,**存在老版本 Redis 无法兼容新版 RDB 格式的问题**。 -### AOF方式 +**AOF方式** AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是**解决了数据持久化的实时性**,AOF 是Redis持久化的主流方式。 @@ -236,7 +300,7 @@ appendfsync no //由操作系统决定何时进行同步操作 接下来看一下 AOF 持久化执行流程: -![](http://img.dabin-coder.cn/image/aof工作流程0.png) +![](http://img.topjavaer.cn/img/aof工作流程0.png) 1. 所有的写入命令会追加到 AOP 缓冲区中。 2. AOF 缓冲区根据对应的策略向硬盘同步。 @@ -253,7 +317,7 @@ appendfsync no //由操作系统决定何时进行同步操作 1. 对于同一份文件AOF文件比RDB数据快照要大。 2. 数据恢复比较慢。 -### RDB和AOF如何选择? +## RDB和AOF如何选择? 通常来说,应该同时使用两种持久化方案,以保证数据安全。 @@ -296,7 +360,7 @@ Redis的复制功能是支持多个数据库之间的数据同步。主数据库 客户端连接Redis的时候,先连接哨兵,哨兵会告诉客户端Redis主节点的地址,然后客户端连接上Redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。 -![](http://img.dabin-coder.cn/image/sentinel.png) +![](http://img.topjavaer.cn/img/sentinel.png) **工作原理** @@ -315,7 +379,7 @@ Redis cluster集群节点最小配置6个节点以上(3主3从),其中主 Redis cluster采用**虚拟槽分区**,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。 -![](http://img.dabin-coder.cn/image/cluster_slots.png) +![](http://img.topjavaer.cn/img/cluster_slots.png) **工作原理:** @@ -383,25 +447,95 @@ Redis cluster采用**虚拟槽分区**,所有的键根据哈希函数映射到 **内存淘汰策略可以通过配置文件来修改**,相应的配置项是`maxmemory-policy`,默认配置是`noeviction`。 -## 如何保证缓存与数据库双写时的数据一致性? +## MySQL 与 Redis 如何保证数据一致性 + +**缓存不一致是如何产生的** + +如果数据一直没有变更,那么就不会出现缓存不一致的问题。 + +通常缓存不一致是发生在数据有变更的时候。 因为每次数据变更你需要同时操作数据库和缓存,而他们又属于不同的系统,无法做到同时操作成功或失败,总会有一个时间差。在并发读写的时候可能就会出现缓存不一致的问题(理论上通过分布式事务可以保证这一点,不过实际上基本上很少有人这么做)。 + +虽然没办法在数据有变更时,保证缓存和数据库强一致,但对缓存的更新还是有一定设计方法的,遵循这些设计方法,能够让这个不一致的影响时间和影响范围最小化。 + +缓存更新的设计方法大概有以下四种: + +- 先删除缓存,再更新数据库(这种方法在并发下最容易出现长时间的脏数据,不可取) +- 先更新数据库,删除缓存(Cache Aside Pattern) +- 只更新缓存,由缓存自己同步更新数据库(Read/Write Through Pattern) +- 只更新缓存,由缓存自己异步更新数据库(Write Behind Cache Pattern) + +**先删除缓存,再更新数据库** + +这种方法在并发读写的情况下容易出现缓存不一致的问题 + +![](http://img.topjavaer.cn/img/202304300910876.png) + +如上图所示,其可能的执行流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 删除缓存中数据A +- 客户端2 查询缓存中数据A,未命中 +- 客户端2 从数据库查询数据A,并更新到缓存中 +- 客户端1 更新数据库中数据A -**1、先删除缓存再更新数据库** +可见,最后缓存中的数据A跟数据库中的数据A是不一致的,缓存中的数据A是旧的脏数据。 -进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。 +因此一般不建议使用这种方式。 -存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。 +**先更新数据库,再让缓存失效** -**2、先更新数据库再删除缓存** +这种方法在并发读写的情况下,也可能会出现短暂缓存不一致的问题 -进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。 +![](http://img.topjavaer.cn/img/202304300912362.png) -存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。 +如上图所示,其可能执行的流程顺序为: -**3、异步更新缓存** +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端3 触发查询数据A的逻辑 +- 客户端1 更新数据库中数据A +- 客户端2 查询缓存中数据A,命中返回(旧数据) +- 客户端1 让缓存中数据A失效 +- 客户端3 查询缓存中数据A,未命中 +- 客户端3 查询数据库中数据A,并更新到缓存中 -数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。 +可见,最后缓存中的数据A和数据库中的数据A是一致的,理论上可能会出现一小段时间数据不一致,不过这种概率也比较低,大部分的业务也不会有太大的问题。 -以上几个方案都不完美,需要根据业务需求,评估哪种方案影响较小,然后选择相应的方案。 +**只更新缓存,由缓存自己同步更新数据库(Read/Write Through Pattern)** + +这种方法相当于是业务只更新缓存,再由缓存去同步更新数据库。 一个Write Through的 例子如下: + +![](http://img.topjavaer.cn/img/202304300913692.png) + +如上图所示,其可能执行的流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 更新缓存中数据A,缓存同步更新数据库中数据A,再返回结果 +- 客户端2 查询缓存中数据A,命中返回 + +Read Through 和 WriteThrough 的流程类似,只是在客户端查询数据A时,如果缓存中数据A失效了(过期或被驱逐淘汰),则缓存会同步去数据库中查询数据A,并缓存起来,再返回给客户端 + +这种方式缓存不一致的概率极低,只不过需要对缓存进行专门的改造。 + +**只更新缓存,由缓存自己异步更新数据库(Write Behind Cache Pattern)** + +这种方式性详单于是业务只操作更新缓存,再由缓存异步去更新数据库,例如: + +![](http://img.topjavaer.cn/img/202304300913082.png) + +如上图所示,其可能的执行流程顺序为: + +- 客户端1 触发更新数据A的逻辑 +- 客户端2 触发查询数据A的逻辑 +- 客户端1 更新缓存中的数据A,返回 +- 客户端2 查询缓存中的数据A,命中返回 +- 缓存异步更新数据A到数据库中 + +这种方式的优势是读写的性能都非常好,基本上只要操作完内存后就返回给客户端了,但是其是非强一致性,存在丢失数据的情况。 + +如果在缓存异步将数据更新到数据库中时,缓存服务挂了,此时未更新到数据库中的数据就丢失了。 ## 缓存常见问题 @@ -605,6 +739,180 @@ Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式 5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。 6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。 +## 说说为什么Redis过期了为什么内存没释放? + +第一种情况,可能是覆盖之前的key,导致key过期时间发生了改变。 + +当一个key在Redis中已经存在了,但是由于一些误操作使得key过期时间发生了改变,从而导致这个key在应该过期的时间内并没有过期,从而造成内存的占用。 + +第二种情况是,Redis过期key的处理策略导致内存没释放。 + +一般Redis对过期key的处理策略有两种:惰性删除和定时删除。 + +先说惰性删除的情况 + +当一个key已经确定设置了xx秒过期同时中间也没有修改它,xx秒之后它确实已经过期了,但是惰性删除的策略它并不会马上删除这个key,而是当再次读写这个key时它才会去检查是否过期,如果过期了就会删除这个key。也就是说,惰性删除策略下,就算key过期了,也不会立刻释放内容,要等到下一次读写这个key才会删除key。 + +而定时删除会在一定时间内主动淘汰一部分已经过期的数据,默认的时间是每100ms过期一次。因为定时删除策略每次只会淘汰一部分过期key,而不是所有的过期key,如果redis中数据比较多的话要是一次性全量删除对服务器的压力比较大,每一次只挑一批进行删除,所以很可能出现部分已经过期的key并没有及时的被清理掉,从而导致内存没有即时被释放。 + +## Redis突然变慢,有哪些原因? + +1. **存在bigkey**。如果Redis实例中存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。应该避免存储 bigkey,降低释放内存的耗时。 + +2. 如果Redis 实例**设置了内存上限 maxmemory**,有可能导致 Redis 变慢。当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。 + +3. **开启了内存大页**。当 Redis 在执行后台 RDB 和 AOF rewrite 时,采用 fork 子进程的方式来处理。但主进程 fork 子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。 + + 什么是写时复制? + + 这样做的好处是,父进程有任何写操作,并不会影响子进程的数据持久化。 + + 不过,主进程在拷贝内存数据时,会涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。 + + 解决方案就是关闭内存大页机制。 + +4. **使用了Swap**。操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍。尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。解决方案就是增加机器的内存,让 Redis 有足够的内存可以使用。或者整理内存空间,释放出足够的内存供 Redis 使用 + +5. **网络带宽过载**。网络带宽过载的情况下,服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。解决方案:1、及时确认占满网络带宽 Redis 实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例。2、运维层面,需要对 Redis 机器的各项指标增加监控,包括网络流量,在网络流量达到一定阈值时提前报警,及时确认和扩容。 + +6. **频繁短连接**。频繁的短连接会导致 Redis 大量时间耗费在连接的建立和释放上,TCP 的三次握手和四次挥手同样也会增加访问延迟。应用应该使用长连接操作 Redis,避免频繁的短连接。 + +## 为什么 Redis 集群的最大槽数是 16384 个? + +Redis Cluster 采用数据分片机制,定义了 16384个 Slot槽位,集群中的每个Redis 实例负责维护一部分槽以及槽所映射的键值数据。 + +Redis每个节点之间会定期发送ping/pong消息(心跳包包含了其他节点的数据),用于交换数据信息。 + +Redis集群的节点会按照以下规则发ping消息: + +- (1)每秒会随机选取5个节点,找出最久没有通信的节点发送ping消息 +- (2)每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster-node-timeout/2 则立刻发送ping消息 + +心跳包的消息头里面有个myslots的char数组,是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。 + +接下来,解答为什么 Redis 集群的最大槽数是 16384 个,而不是65536 个。 + +1、如果采用 16384 个插槽,那么心跳包的消息头占用空间 2KB (16384/8);如果采用 65536 个插槽,那么心跳包的消息头占用空间 8KB (65536/8)。可见采用 65536 个插槽,**发送心跳信息的消息头达8k,比较浪费带宽**。 + +2、一般情况下一个Redis集群**不会有超过1000个master节点**,太多可能导致网络拥堵。 + +3、哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩。bitmap的填充率越低,**压缩率**越高。其中bitmap 填充率 = slots / N (N表示节点数)。所以,插槽数越低, 填充率会降低,压缩率会提高。 + +## Redis存在线程安全的问题吗 + +首先从Redis 服务端层面来看。 + +Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 + +虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。 + +然后从Redis客户端层面来看。 + +虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。 + +假设两个redis client同时获取Redis Server上的key1, 同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。 + +对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + + + +## Redis遇到哈希冲突怎么办? + +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。 + +Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 `next` 指针, 多个哈希表节点可以用 `next` 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。 + +原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。 + +## Redis实现分布式锁有哪些方案? + +在这里分享六种Redis分布式锁的正确使用方式,由易到难。 + +方案一:SETNX+EXPIRE + +方案二:SETNX+value值(系统时间+过期时间) + +方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令) + +方案四::ET的扩展命令(SETEXPXNX) + +方案五:开源框架~Redisson + +方案六:多机实现的分布式锁Redlock + +**首先什么是分布式锁**? + +分布式锁是一种机制,用于确保在分布式系统中,多个节点在同一时刻只能有一个节点对共享资源进行操作。它是解决分布式环境下并发控制和数据一致性问题的关键技术之一。 + +分布式锁的特征: + +1、「互斥性」:任意时刻,只有一个客户端能持有锁。 + +2、「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。 + +3、「可重入性」“一个线程如果获取了锁之后,可以再次对其请求加锁。 + +4、「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除 + +5、「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。 + + + +**Redis分布式锁方案一:SETNX+EXPIRE** + +提到Redis的分布式锁,很多朋友可能就会想到setnx+expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。SETNX是SETIF NOT EXISTS的简写。日常命令格式是SETNXkey value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076327854-c75a4b72-4a8a-4afb-87fe-378082b36046.png) + +缺陷:加锁与设置过期时间是非原子操作,如果加锁后未来得及设置过期时间系统异常等,会导致其他线程永远获取不到锁。 + +**Redis分布式锁方案二**:SETNX+value值(系统时间+过期时间) + +为了解决方案一,「发生异常锁得不到释放的场景」,有小伙伴认为,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。 + +这个方案的优点是,避免了expire 单独设置过期时间的操作,把「过期时间放到setnx的value值」里面来。解决了方案一发生异常,锁得不到释放的问题。 + +但是这个方案有别的缺点:过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.get()和set(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。该锁没有保存持有者的唯一标识,可能坡别的客户端释放/解锁 + +**分布式锁方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令)** + +实际上,我们还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075869527-a3704805-53a6-4bd4-be07-2558cff533a2.png) + +加锁代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075859795-a0cfcfe0-7c56-49ac-9182-6b203739a99e.png) + +**Redis分布式锁方案四:SET的扩展命令(SET EX PX NX)** + +除了使用,使用Lua脚本,保证SETNX+EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数。(`SET key value[EX seconds]`PX milliseconds][NX|XX]`),它也是原子性的 + +`SET key value[EX seconds][PX milliseconds][NX|XX]` + +1. NX:表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁, 才能获取。 +2. EXseconds:设定key的过期时间,时间单位是秒。 +3. PX milliseconds:设定key的过期时间,单位为毫秒。 +4. XX:仅当key存在时设置值。 + +伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075985907-86dd8066-001a-4957-a998-897cdc27c831.png) + +**Redis分布式锁方案五:Redisson框架** + +方案四还是可能存在「锁过期释放,业务没执行完」的问题。设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。当前开源框架Redisson解决了这个问题。一起来看下Redisson底层原理图: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076061807-8b2419dd-13ff-441e-a238-30bf402b07fb.png) + +只要线程一加锁成功,就会启动一个watchdog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。 + +**分布式锁方案六:多机实现的分布式锁Redlock+Redisson** + +前面五种方案都是基于单机版的讨论,那么集群部署该怎么处理? + +答案是多机实现的分布式锁Redlock+Redisson + -![](http://img.dabin-coder.cn/image/20220612101342.png) +![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/resource/1-cs-books.md b/docs/resource/1-cs-books.md new file mode 100644 index 0000000..bdc0557 --- /dev/null +++ b/docs/resource/1-cs-books.md @@ -0,0 +1,329 @@ +--- +sidebar: heading +--- + +

200本计算机经典书籍分享

+

+
+ 微信交流群 +公众号 +知乎 +牛客网 +掘金 +八股文 +

+ +# 简介 + +大彬断断续续花了一个多月的时间,从各个学习网站上收集了**300多本经典的计算机电子书、个人笔记和面试资料**,在这里分享有需要的人,希望可以帮助到曾经像我一样的新手,节省下找资料的时间。 + +书籍也同步到Github仓库了,可以**star**一下,下次找书直接在上面搜索。 + +> Github地址:https://github.com/Tyson0314/java-books + +![](http://img.topjavaer.cn/img/image-20210902223746952.png) + +![](http://img.topjavaer.cn/img/image-20221030094126118.png) + +书单持续更新中,小伙伴们也可以帮忙一起完善书单,找不到想要的书籍可以评论区提出~ + +**注意:本网站的书籍来源于网络,版权归原作者所有,不以盈利为目的,仅限学习使用,如有侵权请联系 1713476357@qq.com 删除**。 + +> 👍**推荐一份大厂面试手册 [计算机校招社招高频面试题汇总](https://github.com/Tyson0314/Java-learning)** + +# Java + +- Java编程思想第四版 [百度云下载链接](https://pan.baidu.com/s/1b2-OMKNi9WcaiZDnP3Y2uw) 提取码:wsyh +- Java 8实战(中文版) [百度云下载链接](https://pan.baidu.com/s/1nqEvhapy3Las-U_k1PB7jA) 提取码:opzq +- 阿里巴巴Java开发手册(终极版) [百度云下载链接](https://pan.baidu.com/s/1fxQ5Sv21QYjR6JJYiZBPsQ) 提取码:nazl +- Head First Java [百度云下载链接](https://pan.baidu.com/s/1L2LMXF4ng7CgLfeNZ8e1yg) 提取码:atxr +- Java核心技术 卷1 基础知识 原书第10版 [百度云下载链接](https://pan.baidu.com/s/12iSfpIlLxMjGKrvDE2k9qQ) 提取码:hobd +- Effective Java [百度云下载链接](https://pan.baidu.com/s/1DIlEro6-wJZNJ7qmflqR4g) 提取码:jxqz +- Java核心技术 卷 2 高级特性 原书第10版 [百度云下载链接](https://pan.baidu.com/s/126BEN1e7ab1U6kPEjXWDRQ) 提取码:wmov +- 写给大忙人看的JavaSE8 [百度云下载链接](https://pan.baidu.com/s/1ZXaXyzFBcqK1e4lmoWQjuQ) 提取码:bvqf +- 深入理解Java虚拟机 [百度云下载链接](https://pan.baidu.com/s/1Ya1HjRV14kee-T_UQITVqg) 提取码:atis + +# 数据库 + +- SQL学习指南 [百度云下载链接](https://pan.baidu.com/s/1ZmFdhgEAJPPKFmjCO0gxRg) 提取码:ibwp +- MySQL性能优化的21个最佳实践 [百度云下载链接](https://pan.baidu.com/s/15bVDICpfCEcijNXjyU1Yjw) 提取码:qriw +- SQLite 权威指南 [百度云下载链接](https://pan.baidu.com/s/1-BxMGu0O7Qk1Dzj4I15vzg) 提取码:cibr +- 深入浅出MySQL:数据库开发、优化与管理维护 [百度云下载链接](https://pan.baidu.com/s/1UF3CBjYeXiQH65dsPI7muA) 提取码:euxk +- MySQL必知必会 [百度云下载链接](https://pan.baidu.com/s/14TrWxw2kbcmrC1EqXgEdiw) 提取码:nlgc +- MongoDB权威指南 [百度云下载链接](https://pan.baidu.com/s/16UeSqyw0d_R0ng0q32hjRA) 提取码:ewcl +- SQL必知必会 第4版 [百度云下载链接](https://pan.baidu.com/s/1ti1SRNov6ZVyxNERVNZveA) 提取码:wgin +- 高性能MySQL(第3版) [百度云下载链接](https://pan.baidu.com/s/1wfYewFoqG8bgvGyb50DhAQ) 提取码:jzeq +- 高性能mysql第三版 [百度云下载链接](https://pan.baidu.com/s/1M45u3refIdVVJd0PGzl3rA) 提取码:wako +- MySQL技术内幕 InnoDB存储引擎 第2版 [百度云下载链接](https://pan.baidu.com/s/16hklWAOb55sehRpZOAaDZw) 提取码:raqe +- 数据库系统概念 [百度云下载链接](https://pan.baidu.com/s/1FbRF00lfjjUcb27irHRDfA) 提取码:rtei + +# 缓存 + +- Redis开发与运维 [百度云下载链接](https://pan.baidu.com/s/1FfcllHYv1IM8xU5optlAKQ) 提取码:acmy +- Redis设计与实现 [百度云下载链接](https://pan.baidu.com/s/1bpEzyDiZvOwzIVNJTEvopg) 提取码:islw +- Redis实战 [百度云下载链接](https://pan.baidu.com/s/1elpeXuhAP2CkgPzQc3tgGQ) 提取码:fwdt +- Redis入门指南 第2版 [百度云下载链接](https://pan.baidu.com/s/1J44vHvyt8G2WKxWzldjBRA) 提取码:ieya + +# 消息队列 + +- Kafka权威指南 [百度云下载链接](https://pan.baidu.com/s/1Bib19ABwbtL83MS-EWLoeg) 提取码:ipte +- RabbitMQ实战 高效部署分布式消息队列 [百度云下载链接](https://pan.baidu.com/s/16QZFQ06ugVKCU4fXiTXHVQ) 提取码:lrjn + +# Web架构 + +- 亿级流量网站架构核心技术 [百度云下载链接](https://pan.baidu.com/s/1I7UaRqkn59vAD1domt696g) 提取码:hjsa +- Java EE互联网轻量级框架整合开发 SSM框架(Spring MVC+Spring+MyBatis)和Redis实现 [百度云下载链接](https://pan.baidu.com/s/1g5A15YZfqpwJTTnCsCVFCA) 提取码:ojyb +- 架构探险 从零开始写javaweb框架 [百度云下载链接](https://pan.baidu.com/s/1T2d5fz66qgP9Bg8838nFgQ) 提取码:cwyt +- 大型网站技术架构:核心原理与案例分析 [百度云下载链接](https://pan.baidu.com/s/12rIP_3GasPTJG6Aoix3YUA) 提取码:gkps +- 大型网站系统与JAVA中间件实践 [百度云下载链接](https://pan.baidu.com/s/1ZVNuDaS5dAzVvmpL-feFxw) 提取码:jaxm +- Head First Servlets and JSP 中文版 第2版 [百度云下载链接](https://pan.baidu.com/s/1VGPI1jdwX8SsBOT9ozxcTw) 提取码:dqby + +# 并发 + +- Java并发编程实战 [百度云下载链接](https://pan.baidu.com/s/1vx77KmOXuVyrIJbwRHgORQ) 提取码:hqyk +- 深入浅出 Java 多线程 [百度云下载链接](https://pan.baidu.com/s/1TevopQ6rvoTqY0G8UWEoUw) 提取码:nxcr +- Java多线程编程核心技术 [百度云下载链接](https://pan.baidu.com/s/1VdvJLMMQhnrPdLybMVTQhA) 提取码:tyug +- JAVA并发编程实践 [百度云下载链接](https://pan.baidu.com/s/1r0JzJuks2VdYRlNZrkM8Eg) 提取码:uldo +- JAVA多线程设计模式 [百度云下载链接](https://pan.baidu.com/s/1suHlbw-QLf-WVlmLuYF4IA) 提取码:vphc +- 实战Java高并发程序设计 [百度云下载链接](https://pan.baidu.com/s/1T6abSW0vIAF3inyoQGNB0A) 提取码:qhxd + +# 框架 + +- SPRING技术内幕 [百度云下载链接](https://pan.baidu.com/s/1oTzs-uKh3x4ZJt3PZHu6Ag) 提取码:suqt +- Spring MVC学习指南 [百度云下载链接](https://pan.baidu.com/s/1jsKdItKWYD1y7TVNkDKQcw) 提取码:yubj +- spring揭秘 [百度云下载链接](https://pan.baidu.com/s/1wylIipOQDQNpuk7lWI7EFg) 提取码:ivkt +- Deep into Spring Boot [百度云下载链接](https://pan.baidu.com/s/1Aqk1I7sQoLeuh-he68J4ag) 提取码:jzkg +- 看透springMvc源代码分析与实践 [百度云下载链接](https://pan.baidu.com/s/1ChEWlR9fSTFqtLgd4Mq8Qg) 提取码:saqw +- Hibernate实战 [百度云下载链接](https://pan.baidu.com/s/1ckTFs5q_aBwXX86abaPg8w) 提取码:vifl +- Spring源码深度解析 [百度云下载链接](https://pan.baidu.com/s/1P4e3ua8CiKr7iOlY8ii6NA) 提取码:viyz +- 深入浅出MyBatis技术原理与实战 [百度云下载链接](https://pan.baidu.com/s/1LoqF22KYKQCSE6aZi69ySQ) 提取码:quao +- MyBatis技术内幕 [百度云下载链接](https://pan.baidu.com/s/18S_UBMIVERSXubh2zkk1Jw) 提取码:reqk +- Spring Boot实战 [百度云下载链接](https://pan.baidu.com/s/1wjCZeNi7OahfrrJY4VJzvA) 提取码:buwr +- Spring Cloud与Docker微服务架构实战 [百度云下载链接](https://pan.baidu.com/s/13FBSyWIO24ISVJ20FZosGw) 提取码:gqxs +- Spring in action 中文版(第4版) [百度云下载链接](https://pan.baidu.com/s/1H2plA0SvhBsln8dnneJktQ) 提取码:mzpa +- Spring Cloud微服务实战 [百度云下载链接](https://pan.baidu.com/s/1IHeojC96rV4MsVk1eMWnKw) 提取码:plie + +# 计算机网络 + +- 图解HTTP [百度云下载链接](https://pan.baidu.com/s/1qIAc42Qr6W3uj6tAxWMgOw) 提取码:bghw +- 图解TCP IP [百度云下载链接](https://pan.baidu.com/s/1myv-uGxV3UpEAR2dMpHhlw) 提取码:dplo +- 计算机网络-自顶向下方法(第六版) [百度云下载链接](https://pan.baidu.com/s/19ImHNtMjhBtvQbNoBsi3lA) 提取码:dbat +- TCP IP详解卷1 原书第2版 [百度云下载链接](https://pan.baidu.com/s/1-NBccguGQLu92aYZ4NxdkA) 提取码:caxe +- Wireshark网络分析就这么简单 [百度云下载链接](https://pan.baidu.com/s/1-BBFEi-haLu08LKwWYu9Lg) 提取码:dciw +- 计算机网络(第7版)-谢希仁 [百度云下载链接](https://pan.baidu.com/s/1u3qdDs4oEjyDvKNMBKRQ1w) 提取码:fitz + +# 数据结构与算法 + +- 数据结构与算法分析—C语言描述 [百度云下载链接](https://pan.baidu.com/s/1-KnLmebSD480amniojO2bg) 提取码:yuvz +- 算法图解 [百度云下载链接](https://pan.baidu.com/s/1d-csiFMCaLoqpE_ZZr8dGA) 提取码:vkos +- 数据结构与算法分析 java语言描述(原书第3版) [百度云下载链接](https://pan.baidu.com/s/1TsISSLvJ8PhdTDP5k4GZ7Q) 提取码:suby +- 算法之美:指导工作与生活 [百度云下载链接](https://pan.baidu.com/s/1wfrTES2OwSEVjaPF52BLuw) 提取码:cbgk +- 背包九讲2.0 [百度云下载链接](https://pan.baidu.com/s/1h6NNsstEvZEYftalvfJQtg) 提取码:soxk +- 算法导论 第三版 [百度云下载链接](https://pan.baidu.com/s/1-indIwnhk8-wt7v6KLp3oQ) 提取码:hduz +- labuladong的算法小抄 [百度云下载链接](https://pan.baidu.com/s/1AFabRcS6CYa9LdFYUGokBQ) 提取码:kbci +- LeetCode刷题手册-阿里霜神 [百度云下载链接](https://pan.baidu.com/s/1Oz5w7dQPiS4lHgiRadociQ) 提取码:ekdx +- 谷歌大佬LeetCode刷题笔记 [百度云下载链接](https://pan.baidu.com/s/1NV-wCa9QhA8zEzAdjwiP0g) 提取码:vpui +- 编程珠玑 [百度云下载链接](https://pan.baidu.com/s/100eDgsFCnUN1mL7N_RuIEQ) 提取码:nzxt +- 剑指Offer [百度云下载链接](https://pan.baidu.com/s/1napasMTPIA-yIP8HG093EQ) 提取码:zlmt +- 数据结构(C语言版) [百度云下载链接](https://pan.baidu.com/s/1_NxoY-d3luDCIfnxprFAdA) 提取码:ksyw + +# 操作系统 + +- 深入理解计算机系统.pdf [百度云下载链接](https://pan.baidu.com/s/19Fsk4oQgn16QsqLVo7-OtA) 提取码:wlrf +- 30天自制操作系统 (图灵程序设计丛书)-2-753 [百度云下载链接](https://pan.baidu.com/s/1kh7QTXNmrvAnoMo_N4d_rA) 提取码:dutc +- 现代操作系统(第三版)中文版 [百度云下载链接](https://pan.baidu.com/s/1nCPyrsGXr6porYlsohUmmA) 提取码:gkps +- 操作系统精髓与设计原理(原书第6版) [百度云下载链接](https://pan.baidu.com/s/1V_JBro99aqnLET1FDMP52A) 提取码:fbzt +- 操作系统概念 [百度云下载链接](https://pan.baidu.com/s/16iSiLoqq0mugRnb61QflTA) 提取码:iqvo +- 深入理解计算机系统 [百度云下载链接](https://pan.baidu.com/s/1Vb21cODDySHFxZFr2JvO0w) 提取码:caoi +- 操作系统设计与实现 [百度云下载链接](https://pan.baidu.com/s/1-kAryblScC_X7HZV5XCxgw) 提取码:kbte + +# Linux + +- Linux学习笔记 [百度云下载链接](https://pan.baidu.com/s/1SZ8UrKE9roY5MQ7btXzo-A) 提取码:fzmc +- UNIX环境高级编程 第二版中文 [百度云下载链接](https://pan.baidu.com/s/1B-NM0bJMiPTHOBYV_zpNkA) 提取码:mdao +- 鸟哥的Linux私房菜 服务器篇(第三版) [百度云下载链接](https://pan.baidu.com/s/14cCH0_yvgzNuAMfqbVTbYQ) 提取码:rmjn +- Linux Shell脚本攻略(第2版) [百度云下载链接](https://pan.baidu.com/s/16uYXwvrxVoKYMwFr7FDo5Q) 提取码:thaj +- UNIX网络编程 卷2 进程间通信 [百度云下载链接](https://pan.baidu.com/s/1L3ZVi6sxnmQSPqC8-LxKMw) 提取码:kcae +- UNIX网络编程 卷1 套接字联网API [百度云下载链接](https://pan.baidu.com/s/1SUjwqH3IHbqzjG7wsKGTqQ) 提取码:jbwc +- 鸟哥的Linux私房菜 基础学习篇(第三版)-清晰版 [百度云下载链接](https://pan.baidu.com/s/1pAkMc0RoVmAKVDBqPakAiQ) 提取码:mawv + +# c + +- C程序设计语言(英文第2版)Prentice Hall.-.The C Programming Language(2nd Edition) [百度云下载链接](https://pan.baidu.com/s/1UMkZBeC-UVSUijUHGTAg9A) 提取码:kuec +- C程序设计语言(第2版)中文译版 [百度云下载链接](https://pan.baidu.com/s/1_ZdUbucFt6DniKyUDkJgtg) 提取码:apnu +- C和指针 [百度云下载链接](https://pan.baidu.com/s/1sCq_KW21QBJwop6zBr-S2w) 提取码:pakl +- C语言深度解剖 [百度云下载链接](https://pan.baidu.com/s/1CAMYvuqP52vj5io8joeTwg) 提取码:qzgu +- C语言函数大全 [百度云下载链接](https://pan.baidu.com/s/1mzeoJpWCMy7WMUP4ximEhg) 提取码:dxfw +- Microsoft编写优质无错C程序秘诀 [百度云下载链接](https://pan.baidu.com/s/163oTBX13HD3J--6DVck6yw) 提取码:ftba +- C语言参考手册第五版 [百度云下载链接](https://pan.baidu.com/s/1qUaZNDB1WcnuEsjaAjyz-Q) 提取码:urnh +- C专家编程 [百度云下载链接](https://pan.baidu.com/s/1LiceKF0W6R2HzaPdzw1_Cw) 提取码:geta +- C程序设计(第四版).谭浩强 [百度云下载链接](https://pan.baidu.com/s/1hXzs9vxvriBFfLOkkyV9kQ) 提取码:knox +- C标准库中文版 [百度云下载链接](https://pan.baidu.com/s/1M_PDS6UFL4spe8JvKICx9w) 提取码:pqxe +- C Primer Plus(第五版) [百度云下载链接](https://pan.baidu.com/s/1D7iUmPSiOEVH3ddeld9vbg) 提取码:haxs + +# c# + +- 深入理解C# [百度云下载链接](https://pan.baidu.com/s/1T3IvAWhhdOm0ReZ7lo2_5Q) 提取码:lvbr +- c#图解教程 第4版(Illustrated C# 2012) [百度云下载链接](https://pan.baidu.com/s/1lIIsJDy7Ma8t6FhcaACaeQ) 提取码:eklv + +# c++ + +- More Effective C++中文版 [百度云下载链接](https://pan.baidu.com/s/1kBqKs8kY2HP9dZZvHSVmLA) 提取码:njug +- 深入探索C++对象模型 [百度云下载链接](https://pan.baidu.com/s/1uTksFSLfWuTl6O0axQfDlg) 提取码:eslv +- 深度探索C++对象模型 [百度云下载链接](https://pan.baidu.com/s/1Lf7WhdTd2cDP7Qwkf-5rgA) 提取码:brpa +- STL源码剖析 [百度云下载链接](https://pan.baidu.com/s/1YqwPZgyrHHIi4h8U0ba87g) 提取码:otdz +- STL源码剖析简体中文完整版 [百度云下载链接](https://pan.baidu.com/s/19DW-eq2bp7HhpnYz71lg0g) 提取码:lhno +- 深入理解C++11:C++11新特性解析与应用 [百度云下载链接](https://pan.baidu.com/s/1Y2yIOhb7v8rWsh9do5wiKw) 提取码:rdlx +- 学习OpenCV(中文版) [百度云下载链接](https://pan.baidu.com/s/14ycBk6tMpRKxqbe4CBTP0Q) 提取码:voru +- OpenCV3编程入门 毛星云编著 电子工业出版 [百度云下载链接](https://pan.baidu.com/s/1-Mm3Iwvvt-HRYQhJFn3LeA) 提取码:yvtq +- C++ Template 全览, 中文版 [百度云下载链接](https://pan.baidu.com/s/15PG066--meojYuXW7p-WIg) 提取码:pgim +- Qt Creator快速入门 [百度云下载链接](https://pan.baidu.com/s/1bTFHqT2127b_kHhrdtgfag) 提取码:aesf +- C++ Primer中文版 第6版 [百度云下载链接](https://pan.baidu.com/s/1insd8sVXI4jHpqTrXKWN9Q) 提取码:iud2 +- C++并发编程 [百度云下载链接](https://pan.baidu.com/s/1SRJiDtleXA2rvWOjcxS5dA) 提取码:czlf +- C++编程规范-101条规则准则与最佳实践 [百度云下载链接](https://pan.baidu.com/s/1GQ10rT7z1V-JAdlAJV5dBQ) 提取码:oyvz +- C++primer 5th [百度云下载链接](https://pan.baidu.com/s/1bJUGemBydhPjt-4Eyq0PIQ) 提取码:tvaq +- google C++ 编码规范(完整版) [百度云下载链接](https://pan.baidu.com/s/12X9mE1cYdKwaqyekV8ueCw) 提取码:pmec +- Effective STL 中文版 [百度云下载链接](https://pan.baidu.com/s/18l9CI8_jtD8ilV2Ec2OCHA) 提取码:gban +- Effective C++ 改善程序与设计的55个具体做法 中文第三版 [百度云下载链接](https://pan.baidu.com/s/1thtQHg_6k9dLZbU75OfnVw) 提取码:oxke +- C++沉思录 [百度云下载链接](https://pan.baidu.com/s/1QpglHQWSqB3xMjjJ-iqiow) 提取码:ojgn + +# Go + +- 学习 Go 语言(Golang) [百度云下载链接](https://pan.baidu.com/s/1_pJnZgyAALpehgedVepbGA) 提取码:zadr +- Go web 编程 [百度云下载链接](https://pan.baidu.com/s/1V4t3MfcOFI_5qfoSNNMMJw) 提取码:znwa +- Go语言实战 [百度云下载链接](https://pan.baidu.com/s/1NJbeIJqVZf22Detdn8af7A) 提取码:uate +- Go并发编程实战 [百度云下载链接](https://pan.baidu.com/s/1YSQE_ENBbONjXMCAI9qPng) 提取码:fmst + +# Python + +- Python高级编程第2版 张亮 阿信(译) 人民邮电出版社 2017-10 v2 完整版 [百度云下载链接](https://pan.baidu.com/s/1qt6IYNKJ-T8zuaCZGPEZkg) 提取码:xjfi +- Python开发技术详解 [百度云下载链接](https://pan.baidu.com/s/1qE7mDBDHn8jw0EJf_sEP_Q) 提取码:dywg +- 编程小白的第一本python入门书 [百度云下载链接](https://pan.baidu.com/s/1uP2xRqwJOgsWxwypmcBWJQ) 提取码:dsmi +- Python开发实战 [百度云下载链接](https://pan.baidu.com/s/1ei4XdEWCt_DjqD9t1VEH0w) 提取码:hxjw +- Python学习手册(第4版) [百度云下载链接](https://pan.baidu.com/s/1dJhk1RBZ3LZdMITLDYSZ0A) 提取码:miey +- Python编程入门经典 [百度云下载链接](https://pan.baidu.com/s/1091A_q7XHtC6sKmNdbOELA) 提取码:vyfs + +# 编程之术 + +- 统计推断 statistical inference(英文版) [百度云下载链接](https://pan.baidu.com/s/1al-C2Je6eeS9AnmYU8-ajw) 提取码:azmx +- 重构:改善既有代码的设计(第2版) [百度云下载链接](https://pan.baidu.com/s/1x31IAg4cn30XNbQ6U7ac_w) 提取码:tvyp +- 编写可读代码的艺术 [百度云下载链接](https://pan.baidu.com/s/1AYNaWER4ZjzEf3sjDUS3zA) 提取码:yqxu +- 编程的奥秘 [百度云下载链接](https://pan.baidu.com/s/194oroXguaGLyogGbRWnpqA) 提取码:iycm +- 敏捷软件开发:原则、模式与实践 [百度云下载链接](https://pan.baidu.com/s/1B0tBpmKXMfVU9XIQbq4qHA) 提取码:xsub +- 代码大全2中文版 [百度云下载链接](https://pan.baidu.com/s/1FFTfDuegBTpq_YOsqHpSEg) 提取码:mwid +- 代码之美精选版 [百度云下载链接](https://pan.baidu.com/s/1sw6bOaKADfOJ748I0yUoTA) 提取码:qecg +- 代码整洁之道 [百度云下载链接](https://pan.baidu.com/s/1I00RoSEGKvr3i5GwWgNDPw) 提取码:qbeu +- 程序员的自我修养—链接、装载与库 [百度云下载链接](https://pan.baidu.com/s/1tJv2vJKju78ZmW57ZpewgQ) 提取码:xefk +- 编程之美 [百度云下载链接](https://pan.baidu.com/s/1U4hIi3QlWGhXRmcuOwxXpA) 提取码:fxsd +- 程序视角下的可计算和复杂性理论 Computability and Complexity(英文版) [百度云下载链接](https://pan.baidu.com/s/1byjn9mj8yIB56sdF--6JTA) 提取码:xadr +- 编码:隐匿在计算机软硬件背后的语言 [百度云下载链接](https://pan.baidu.com/s/1bi5u18o7Od8T1_jzMj7_iQ) 提取码:jprb + +# 大数据 + +- Hadoop权威指南 第3版 修订版 [百度云下载链接](https://pan.baidu.com/s/1Vlxty3N0Tiv4cvHauRMVuw) 提取码:dfyu +- Spark快速数据处理完整版 [百度云下载链接](https://pan.baidu.com/s/1b-rYRFU682GCLZeSVhwCHw) 提取码:iawf + +# 分布式 + +- 架构探险:从零开始写分布式服务架构 [百度云下载链接](https://pan.baidu.com/s/1AmKV6G-azET88E6jcmoBAA) 提取码:cvwu +- 深入分布式缓存 从原理到实践 [百度云下载链接](https://pan.baidu.com/s/1g3KehD1HAJqrZd8BgUIhdQ) 提取码:uwim + +# 工具 + +- Maven3实战 [百度云下载链接](https://pan.baidu.com/s/16y03dBloWJSYC3BlXypijg) 提取码:xilj +- IntelliJ IDEA 简体中文专题教程(电子版-2015) [百度云下载链接](https://pan.baidu.com/s/19UwnAgsi_IZBNLBrl-dTxA) 提取码:vfqm +- Maven实战 [百度云下载链接](https://pan.baidu.com/s/1EVCEe2MxyecSAYPZ6eOSYw) 提取码:bwid +- Vim 中文用户手册 [百度云下载链接](https://pan.baidu.com/s/1nGQGlOrTvTm9HoVoAHj7KQ) 提取码:ewvj +- progit [百度云下载链接](https://pan.baidu.com/s/1uzeT4wDruXObe__E6FzMVA) 提取码:mvap + +# 机器学习 + +- 机器学习实战 [百度云下载链接](https://pan.baidu.com/s/12iUnxSAbCXHp6_5i_J_uwQ) 提取码:kwqs +- 深度学习 中文版 [百度云下载链接](https://pan.baidu.com/s/1DSPObptqe4s0TCy-1pQHog) 提取码:oasp +- 贝叶斯思维统计建模的PYTHON学习法 [百度云下载链接](https://pan.baidu.com/s/1_sgg_zMJn3yKd46Yly6CkQ) 提取码:vyzg +- 图解机器学习 [百度云下载链接](https://pan.baidu.com/s/1Fva-HDLAXnCPkLmyKmUubw) 提取码:fqcz +- PYTHON机器学习及实践-从零开始通往KAGGLE竞赛之路 [百度云下载链接](https://pan.baidu.com/s/16oqijoi-v54-TXiqW8qbDw) 提取码:zqhd +- TensorFlow实践与智能系统 [百度云下载链接](https://pan.baidu.com/s/1noqSL35H-vzHYypc1IzqfQ) 提取码:qbli +- TensorFlow技术解析与实战 [百度云下载链接](https://pan.baidu.com/s/1T6KnGXVNHe3Fg2GAC27ZFA) 提取码:gozc +- Tensorflow 实战Google深度学习框架 [百度云下载链接](https://pan.baidu.com/s/1eVW2oHeEmdLRyy4xZPFpcw) 提取码:ecoz + +# 面试 + +- SpringCloud面试专题 [百度云下载链接](https://pan.baidu.com/s/1z6ASwOTOCQbpwQAQjVY4_A) 提取码:jrgi +- Tomcat优化相关问题 [百度云下载链接](https://pan.baidu.com/s/1mNQvAcEQ58BxhFwY-qM2dg) 提取码:cekn +- 面试必备之乐观锁与悲观锁 [百度云下载链接](https://pan.baidu.com/s/1kffc2ar3lHmMDkZvXcY70w) 提取码:mfqw +- 一线互联网企业面试题 [百度云下载链接](https://pan.baidu.com/s/1szgntkHQxHuwaNrhMGEz6w) 提取码:lcrs +- 字节跳动21届秋招全岗位面经集合 [百度云下载链接](https://pan.baidu.com/s/1OS4He8Tbs-_u9ZDwLam61g) 提取码:fydb +- Dubbo面试专题 [百度云下载链接](https://pan.baidu.com/s/1XHVbPR-j-nmww5aVCrKhDg) 提取码:ywpn +- 【笔记】concurrentHashMap [百度云下载链接](https://pan.baidu.com/s/1j9xOojZXdCPu6ScFx2U6NQ) 提取码:yxlf +- JVM性能优化相关问题 [百度云下载链接](https://pan.baidu.com/s/1Sa5zlZBYGPqxOLDd3KWrDQ) 提取码:vuie +- SpringBoot面试专题 [百度云下载链接](https://pan.baidu.com/s/14FGoW7et4D4IkjbVEDSUUA) 提取码:macu +- redis面试专题 [百度云下载链接](https://pan.baidu.com/s/1qQ_iXrrwjscVVl5GaFlN0Q) 提取码:famx +- 程序员面试宝典(第三版) [百度云下载链接](https://pan.baidu.com/s/1zVziyqJzJF99VvE8EH3JZQ) 提取码:kbqa +- JAVA核心面试知识整理 [百度云下载链接](https://pan.baidu.com/s/1sgvCVzNiS-nVEAuDPtmcFA) 提取码:neqb + +# 前端 + +- HTML与CSS入门经典(第7版).(美)奥利弗,(美)莫里森.扫描版 [百度云下载链接](https://pan.baidu.com/s/1qn3aSq2cIopbT7cXhHB87A) 提取码:gjcz +- JavaScript.DOM编程艺术(第2版) [百度云下载链接](https://pan.baidu.com/s/1Mjl4QlAYctlfenO1eX2EKw) 提取码:lcbz +- JavaScript高级程序设计(第3版) [百度云下载链接](https://pan.baidu.com/s/1Vayhr_IkG-VknzV9hI8RQg) 提取码:cmyx +- jQuery高级编程,中文完整扫描版(jb51.net) 2 [百度云下载链接](https://pan.baidu.com/s/15V9ZVJiKI2kU7kNkyD8NTA) 提取码:ckwh +- jQuery权威指南 [百度云下载链接](https://pan.baidu.com/s/1eZKd2R2o7wmum5U2TdrVgA) 提取码:mltc +- Node.js开发指南 [百度云下载链接](https://pan.baidu.com/s/1jzHETr0KCVV9uX6cPAgVRQ) 提取码:mqsc +- jQuery技术内幕 深入解析jQuery架构设计与实现原理 [百度云下载链接](https://pan.baidu.com/s/1QF4CRQvvk9ToZMbZZtwHUA) 提取码:qklc +- 疯狂ajax讲义 [百度云下载链接](https://pan.baidu.com/s/14P8jYleL_nq4rfi8qrKM4g) 提取码:ztdq +- Bootstrap实战 [百度云下载链接](https://pan.baidu.com/s/13PPzbbWAB37Mhny-kidYoA) 提取码:fxpe +- HTML5揭秘 [百度云下载链接](https://pan.baidu.com/s/18dJfVWdE6BRHdy9ZsoQ0gA) 提取码:ksnr +- HTML5与CSS3基础教程(第8版) [百度云下载链接](https://pan.baidu.com/s/1xmd6phEtyePtjmwUpC1riA) 提取码:ldfh +- Head First HTML与CSS 第2版 [百度云下载链接](https://pan.baidu.com/s/1Dyw_2i2Sx7mDLbEG90opIA) 提取码:ldcn + +# 分布式 + +- 架构探险:从零开始写分布式服务架构 [百度云下载链接](https://pan.baidu.com/s/1ROY7qowHOR-q9CnbekJfKA) 提取码:cxap +- 深入分布式缓存 从原理到实践 [百度云下载链接](https://pan.baidu.com/s/15nUR6wmcA2leCQvMpyLOag) 提取码:udhb + +# 设计模式 + +- 大话设计模式 [百度云下载链接](https://pan.baidu.com/s/14dCH7uGVK1t_91aSgHim9A) 提取码:uhln +- 设计模式之禅(第2版) [百度云下载链接](https://pan.baidu.com/s/1FvlKhGUJLWaXyt6KkNpWSQ) 提取码:jhnt +- HeadFirst设计模式 [百度云下载链接](https://pan.baidu.com/s/1jmXMgjCfxTR8PrDGqB09UQ) 提取码:fcsu +- 设计模式 可复用面向对象软件的基础 [百度云下载链接](https://pan.baidu.com/s/1KgYyRxNEYdiaOQGC-R1rLg) 提取码:efny +- 图解设计模式 [百度云下载链接](https://pan.baidu.com/s/1yReilEpnN3emamckAAEJCg) 提取码:yhuv +- 重学Java设计模式·小傅哥 [百度云下载链接](https://pan.baidu.com/s/176Z-SVRHZnhCdMpWft_S4g) 提取码:rsmk + +# 有趣 + +- 游戏开发物理学 [百度云下载链接](https://pan.baidu.com/s/123wSt5Zjrr20dLxqxFSeUg) 提取码:uogs +- 奔跑吧,程序员:从零开始打造产品、技术和团队 [百度云下载链接](https://pan.baidu.com/s/1Jmfxbkw7YDab6OBZUKAyUg) 提取码:uphr +- 正则表达式必知必会 [百度云下载链接](https://pan.baidu.com/s/1dCSDQEqzC5UdS2X6G2UJ1g) 提取码:xebg +- 自己动手写网络爬虫 [百度云下载链接](https://pan.baidu.com/s/1DQuJi4DFd-0cppKQ1_sG9Q) 提取码:xrcw +- 图灵的秘密 [百度云下载链接](https://pan.baidu.com/s/1wa5NItFkOeRkAp6qKREBOA) 提取码:erol +- 编程人生 [百度云下载链接](https://pan.baidu.com/s/1U4AxnlDj8bGLDVWazinhOA) 提取码:gobd +- 人月神话 [百度云下载链接](https://pan.baidu.com/s/1AwswyHcdOURRB3mtbLOs2g) 提取码:hnbm +- 入侵的艺术 凯文.米特尼克 [百度云下载链接](https://pan.baidu.com/s/13EmQTlLY6eSTxRSSgD6-jw) 提取码:jbeg +- 黑客与画家 [百度云下载链接](https://pan.baidu.com/s/1vmQuh8TeWjlA1aCuq8j5nA) 提取码:jcqm +- 浪潮之巅(完整版) [百度云下载链接](https://pan.baidu.com/s/1UoqR9gHV8Ced8kRHBLBVHA) 提取码:cnhr +- 数学之美 [百度云下载链接](https://pan.baidu.com/s/1yAxu3O4StwhCKQ4lMO8K6g) 提取码:euni + +# 免责声明 + +本仓库书籍资源均来源于网络其他人的整理,个人只是做了整理。若有**违规侵权**,请联系1713476357@qq.com ,本人立马删除相应的资源! + +本仓库仅作学习交流分享使用,**不作任何商用**。 + +# 赞赏 + +整理这些资源花了很多时间,如果觉得本仓库有用,可以请大彬**喝一杯咖啡**~ + +| 微信 | 支付宝 | +| ------------------------------------------------- | ----------------------------------------------------- | +| ![](http://img.topjavaer.cn/img/微信收款.png) | ![](http://img.topjavaer.cn/img/支付宝赞赏码.png) | + +> 最后给大家分享另一个Github仓库,用于分享**互联网大厂高频面试题、Java核心知识总结**,包括Java基础、并发、MySQL、Springboot、MyBatis、Redis、RabbitMQ等等,面试必备!欢迎大家star! +> +> Github地址:https://github.com/Tyson0314/Java-learning +> +> 如果github访问不了,可以访问gitee仓库。 +> +> gitee地址:https://gitee.com/tysondai/Java-learning + diff --git a/docs/snippets/ads.md b/docs/snippets/ads.md new file mode 100644 index 0000000..31525ce --- /dev/null +++ b/docs/snippets/ads.md @@ -0,0 +1,6 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: \ No newline at end of file diff --git "a/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" new file mode 100644 index 0000000..b691b56 --- /dev/null +++ "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" @@ -0,0 +1,266 @@ +# MyBatis 源码分析(七):接口层 + +## sql 会话创建工厂 + +`SqlSessionFactoryBuilder` 经过复杂的解析逻辑之后,会根据全局配置创建 `DefaultSqlSessionFactory`,该类是 `sql` 会话创建工厂抽象接口 `SqlSessionFactory` 的默认实现,其提供了若干 `openSession` 方法用于打开一个会话,在会话中进行相关数据库操作。这些 `openSession` 方法最终都会调用 `openSessionFromDataSource` 或 `openSessionFromConnection` 创建会话,即基于数据源配置创建还是基于已有连接对象创建。 + +### 基于数据源配置创建会话 + +要使用数据源打开一个会话需要先从全局配置中获取当前生效的数据源环境配置,如果没有生效配置或没用设置可用的事务工厂,就会创建一个 `ManagedTransactionFactory` 实例作为默认事务工厂实现,其与 `MyBatis` 提供的另一个事务工厂实现 `JdbcTransactionFactory` 的区别在于其生成的事务实现 `ManagedTransaction` 的提交和回滚方法是空实现,即希望将事务管理交由外部容器管理。 + +```java + private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { + Transaction tx = null; + try { + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + closeTransaction(tx); // may have fetched a connection so lets call close() + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 获取生效数据源环境配置的事务工厂 + */ + private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) { + if (environment == null || environment.getTransactionFactory() == null) { + // 未配置数据源环境或事务工厂,默认使用 ManagedTransactionFactory + return new ManagedTransactionFactory(); + } + return environment.getTransactionFactory(); + } +``` + +随后会根据入参传入的 `execType` 选择对应的执行器 `Executor`,`execType` 的取值来源于 `ExecutorType`,这是一个枚举类。在下一章将会详细分析各类 `Executor` 的作用及其实现。 + +获取到事务工厂配置和执行器对象后会结合传入的数据源自动提交属性创建 `DefaultSqlSession`,即 `sql` 会话对象。 + +### 基于数据库连接创建会话 + +基于连接创建会话的流程大致与基于数据源配置创建相同,区别在于自动提交属性 `autoCommit` 是从连接对象本身获取的。 + +```java + private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) { + try { + // 获取自动提交配置 + boolean autoCommit; + try { + autoCommit = connection.getAutoCommit(); + } catch (SQLException e) { + // Failover to true, as most poor drivers + // or databases won't support transactions + autoCommit = true; + } + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + final Transaction tx = transactionFactory.newTransaction(connection); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +## sql 会话 + +`SqlSession` 是 `MyBatis` 面向用户编程的接口,其提供了一系列方法用于执行相关数据库操作,默认实现为 `DefaultSqlSession`,在该类中,增删查改对应的操作最终会调用 `selectList`、`select` 和 `update` 方法,其分别用于普通查询、执行存储过程和修改数据库记录。 + +```java + /** + * 查询结果集 + */ + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 调用存储过程 + */ + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + executor.query(ms, wrapCollection(parameter), rowBounds, handler); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 修改 + */ + @Override + public int update(String statement, Object parameter) { + try { + dirty = true; + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.update(ms, wrapCollection(parameter)); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +以上操作均是根据传入的 `statement` 名称到全局配置中查找对应的 `MappedStatement` 对象,并将操作委托给执行器对象 `executor` 完成。`select`、`selectMap` 等方法则是对 `selectList` 方法返回的结果集做处理来实现的。 + +此外,提交和回滚方法也是基于 `executor` 实现的。 + +```java + /** + * 提交事务 + */ + @Override + public void commit(boolean force) { + try { + executor.commit(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 回滚事务 + */ + @Override + public void rollback(boolean force) { + try { + executor.rollback(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 非自动提交且事务未提交 || 强制提交或回滚 时返回 true + */ + private boolean isCommitOrRollbackRequired(boolean force) { + return (!autoCommit && dirty) || force; + } +``` + +在执行 `update` 方法时,会设置 `dirty` 属性为 `true` ,意为事务还未提交,当事务提交或回滚后才会将 `dirty` 属性修改为 `false`。如果当前会话不是自动提交且 `dirty` 熟悉为 `true`,或者设置了强制提交或回滚的标志,则会将强制标志提交给 `executor` 处理。 + +## Sql 会话管理器 + +`SqlSessionManager` 同时实现了 `SqlSessionFactory` 和 `SqlSession` 接口,使得其既能够创建 `sql` 会话,又能够执行 `sql` 会话的相关数据库操作。 + +```java + /** + * sql 会话创建工厂 + */ + private final SqlSessionFactory sqlSessionFactory; + + /** + * sql 会话代理对象 + */ + private final SqlSession sqlSessionProxy; + + /** + * 保存线程对应 sql 会话 + */ + private final ThreadLocal localSqlSession = new ThreadLocal<>(); + + private SqlSessionManager(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[]{SqlSession.class}, + new SqlSessionInterceptor()); + } + + @Override + public SqlSession openSession() { + return sqlSessionFactory.openSession(); + } + + /** + * 设置当前线程对应的 sql 会话 + */ + public void startManagedSession() { + this.localSqlSession.set(openSession()); + } + + /** + * sql 会话代理逻辑 + */ + private class SqlSessionInterceptor implements InvocationHandler { + + public SqlSessionInterceptor() { + // Prevent Synthetic Access + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 获取当前线程对应的 sql 会话对象并执行对应方法 + final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get(); + if (sqlSession != null) { + try { + return method.invoke(sqlSession, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } else { + // 如果当前线程没有对应的 sql 会话,默认创建不自动提交的 sql 会话 + try (SqlSession autoSqlSession = openSession()) { + try { + final Object result = method.invoke(autoSqlSession, args); + autoSqlSession.commit(); + return result; + } catch (Throwable t) { + autoSqlSession.rollback(); + throw ExceptionUtil.unwrapThrowable(t); + } + } + } + } + } +``` + +`SqlSessionManager` 的构造方法要求 `SqlSessionFactory` 对象作为入参传入,其各个创建会话的方法实际是由该传入对象完成的。执行 `sql` 会话的操作由 `sqlSessionProxy` 对象完成,这是一个由 `JDK` 动态代理创建的对象,当执行方法时会去 `ThreadLocal` 对象中查找当前线程有没有对应的 `sql` 会话对象,如果有则使用已有的会话对象执行,否则创建新的会话对象执行,而线程对应的会话对象需要使用 `startManagedSession` 方法来维护。 + +之所以 `SqlSessionManager` 需要为每个线程维护会话对象,是因为 `DefaultSqlSession` 是非线程安全的,多线程操作会导致执行错误。如上文中提到的 `dirty` 属性,其修改是没有经过任何同步操作的。 + +## 小结 + +`SqlSession` 是 `MyBatis` 提供的面向开发者编程的接口,其提供了一系列数据库相关操作,并屏蔽了底层细节。使用 `MyBatis` 的正确方式应该是像 `SqlSessionManager` 那样为每个线程创建 `sql` 会话对象,避免造成线程安全问题。 + +- `org.apache.ibatis.session.SqlSessionFactory`:`sql` 会话创建工厂。 +- `org.apache.ibatis.session.defaults.DefaultSqlSessionFactory`: `sql` 会话创建工厂默认实现。 +- `org.apache.ibatis.session.SqlSession`:`sql` 会话。 +- `org.apache.ibatis.session.defaults.DefaultSqlSession`:`sql` 会话默认实现。 +- `org.apache.ibatis.session.SqlSessionManager`:`sql` 会话管理器 \ No newline at end of file diff --git a/docs/source/mybatis/1-overview.md b/docs/source/mybatis/1-overview.md new file mode 100644 index 0000000..550a96a --- /dev/null +++ b/docs/source/mybatis/1-overview.md @@ -0,0 +1,43 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +`MyBatis` 是一款旨在帮助开发人员屏蔽底层重复性原生 `JDBC` 代码的持久化框架,其支持通过映射文件配置或注解将 `ResultSet` 映射为 `Java` 对象。相对于其它 `ORM` 框架,`MyBatis` 更为轻量级,支持定制化 `SQL` 和动态 `SQL`,方便优化查询性能,同时包含了良好的缓存机制。 + +## MyBatis 整体架构 + +![](http://img.topjavaer.cn/img/202312231153527.png) + +### 基础支持层 + +- 反射模块:提供封装的反射 `API`,方便上层调用。 +- 类型转换:为简化配置文件提供了别名机制,并且实现了 `Java` 类型和 `JDBC` 类型的互转。 +- 日志模块:能够集成多种第三方日志框架。 +- 资源加载模块:对类加载器进行封装,提供加载类文件和其它资源文件的功能。 +- 数据源模块:提供数据源实现并能够集成第三方数据源模块。 +- 事务管理:可以和 `Spring` 集成开发,对事务进行管理。 +- 缓存模块:提供一级缓存和二级缓存,将部分请求拦截在缓存层。 +- `Binding` 模块:在调用 `SqlSession` 相应方法执行数据库操作时,需要指定映射文件中的 `SQL` 节点,`MyBatis` 通过 `Binding` 模块将自定义 `Mapper` 接口与映射文件关联,避免拼写等错误导致在运行时才发现相应异常。 + +### 核心处理层 + +- 配置解析:`MyBatis` 初始化时会加载配置文件、映射文件和 `Mapper` 接口的注解信息,解析后会以对象的形式保存到 `Configuration` 对象中。 +- `SQL` 解析与 `scripting` 模块:`MyBatis` 支持通过配置实现动态 `SQL`,即根据不同入参生成 `SQL`。 +- `SQL` 执行与结果解析:`Executor` 负责维护缓存和事务管理,并将数据库相关操作委托给 `StatementHandler`,`ParmeterHadler` 负责完成 `SQL` 语句的实参绑定并通过 `Statement` 对象执行 `SQL`,通过 `ResultSet` 返回结果,交由 `ResultSetHandler` 处理。 + +- 插件:支持开发者通过插件接口对 `MyBatis` 进行扩展。 + +### 接口层 + +`SqlSession` 接口定义了暴露给应用程序调用的 `API`,接口层在收到请求时会调用核心处理层的相应模块完成具体的数据库操作。 \ No newline at end of file diff --git a/docs/source/mybatis/2-reflect.md b/docs/source/mybatis/2-reflect.md new file mode 100644 index 0000000..06e4ea7 --- /dev/null +++ b/docs/source/mybatis/2-reflect.md @@ -0,0 +1,634 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis反射,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +大家好,我是大彬。今天分享Mybatis源码的反射模块。 + +`MyBatis` 在进行参数处理、结果映射时等操作时,会涉及大量的反射操作。为了简化这些反射相关操作,`MyBatis` 在 `org.apache.ibatis.reflection` 包下提供了专门的反射模块,对反射操作做了近一步封装,提供了更为简洁的 `API`。 + +## 缓存类的元信息 + +`MyBatis` 提供 `Reflector` 类来缓存类的字段名和 `getter/setter` 方法的元信息,使得反射时有更好的性能。使用方式是将原始类对象传入其构造方法,生成 `Reflector` 对象。 + +```java + public Reflector(Class clazz) { + type = clazz; + // 如果存在,记录无参构造方法 + addDefaultConstructor(clazz); + // 记录字段名与get方法、get方法返回值的映射关系 + addGetMethods(clazz); + // 记录字段名与set方法、set方法参数的映射关系 + addSetMethods(clazz); + // 针对没有getter/setter方法的字段,通过Filed对象的反射来设置和读取字段值 + addFields(clazz); + // 可读的字段名 + readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); + // 可写的字段名 + writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); + // 保存一份所有字段名大写与原始字段名的隐射 + for (String propName : readablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + for (String propName : writablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + } +``` + +`addGetMethods` 和 `addSetMethods` 分别获取类的所有方法,从符合 `getter/setter` 规范的方法中解析出字段名,并记录方法的参数类型、返回值类型等信息: + +```java + private void addGetMethods(Class cls) { + // 字段名-get方法 + Map> conflictingGetters = new HashMap<>(); + // 获取类的所有方法,及其实现接口的方法,并根据方法签名去重 + Method[] methods = getClassMethods(cls); + for (Method method : methods) { + if (method.getParameterTypes().length > 0) { + // 过滤有参方法 + continue; + } + String name = method.getName(); + if ((name.startsWith("get") && name.length() > 3) + || (name.startsWith("is") && name.length() > 2)) { + // 由get属性获取对应的字段名(去除前缀,首字母转小写) + name = PropertyNamer.methodToProperty(name); + addMethodConflict(conflictingGetters, name, method); + } + } + // 保证每个字段只对应一个get方法 + resolveGetterConflicts(conflictingGetters); + } +``` + +对 `getter/setter` 方法进行去重是通过类似 `java.lang.String#getSignature:java.lang.reflect.Method` 的方法签名来实现的,如果子类在实现过程中,参数、返回值使用了不同的类型(使用原类型的子类),则会导致方法签名不一致,同一字段就会对应不同的 `getter/setter` 方法,因此需要进行去重。 + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + +```java + private void resolveGetterConflicts(Map> conflictingGetters) { + for (Entry> entry : conflictingGetters.entrySet()) { + Method winner = null; + // 属性名 + String propName = entry.getKey(); + for (Method candidate : entry.getValue()) { + if (winner == null) { + winner = candidate; + continue; + } + // 字段对应了多个get方法 + Class winnerType = winner.getReturnType(); + Class candidateType = candidate.getReturnType(); + if (candidateType.equals(winnerType)) { + // 返回值类型相同 + if (!boolean.class.equals(candidateType)) { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } else if (candidate.getName().startsWith("is")) { + // 返回值为boolean的get方法可能有多个,如getIsSave和isSave,优先取is开头的 + winner = candidate; + } + } else if (candidateType.isAssignableFrom(winnerType)) { + // OK getter type is descendant + // 可能会出现接口中的方法返回值是List,子类实现方法返回值是ArrayList,使用子类返回值方法 + } else if (winnerType.isAssignableFrom(candidateType)) { + winner = candidate; + } else { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } + } + // 记录字段名对应的get方法对象和返回值类型 + addGetMethod(propName, winner); + } + } +``` + +去重的方式是使用更规范的方法以及使用子类的方法。在确认字段名对应的唯一 `getter/setter` 方法后,记录方法名对应的方法、参数、返回值等信息。`MethodInvoker` 可用于调用 `Method` 类的 `invoke` 方法来执行 `getter/setter` 方法(`addSetMethods` 记录映射关系的方式与 `addGetMethods` 大致相同)。 + +```java +private void addGetMethod(String name, Method method) { + // 过滤$开头、serialVersionUID的get方法和getClass()方法 + if (isValidPropertyName(name)) { + // 字段名-对应get方法的MethodInvoker对象 + getMethods.put(name, new MethodInvoker(method)); + Type returnType = TypeParameterResolver.resolveReturnType(method, type); + // 字段名-运行时方法的真正返回类型 + getTypes.put(name, typeToClass(returnType)); + } +} +``` + +接下来会执行 `addFields` 方法,此方法针对没有 `getter/setter` 方法的字段,通过包装为 `SetFieldInvoker` 在需要时通过 `Field` 对象的反射来设置和读取字段值。 + +```java +private void addFields(Class clazz) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (!setMethods.containsKey(field.getName())) { + // issue #379 - removed the check for final because JDK 1.5 allows + // modification of final fields through reflection (JSR-133). (JGB) + // pr #16 - final static can only be set by the classloader + int modifiers = field.getModifiers(); + if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) { + // 非final的static变量,没有set方法,可以通过File对象做赋值操作 + addSetField(field); + } + } + if (!getMethods.containsKey(field.getName())) { + addGetField(field); + } + } + if (clazz.getSuperclass() != null) { + // 递归查找父类 + addFields(clazz.getSuperclass()); + } +} +``` + +## 抽象字段赋值与读取 + +`Invoker` 接口用于抽象设置和读取字段值的操作。对于有 `getter/setter` 方法的字段,通过 `MethodInvoker` 反射执行;对应其它字段,通过 `GetFieldInvoker` 和 `SetFieldInvoker` 操作 `Field` 对象的 `getter/setter` 方法反射执行。 + +```java +/** + * 用于抽象设置和读取字段值的操作 + * + * {@link MethodInvoker} 反射执行getter/setter方法 + * {@link GetFieldInvoker} {@link SetFieldInvoker} 反射执行Field对象的get/set方法 + * + * @author Clinton Begin + */ +public interface Invoker { + + /** + * 通过反射设置或读取字段值 + * + * @param target + * @param args + * @return + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException; + + /** + * 字段类型 + * + * @return + */ + Class getType(); +} +``` + +## 解析参数类型 + +针对 `Java-Type` 体系的多种实现,`TypeParameterResolver` 提供一系列方法来解析指定类中的字段、方法返回值或方法参数的类型。 + +`Type` 接口包含 4 个子接口和 1 个实现类: + +![Type接口](https://wch853.github.io/img/mybatis/Type%E6%8E%A5%E5%8F%A3.png) + +- `Class`:原始类型 +- `ParameterizedType`:泛型类型,如:`List` +- `TypeVariable`:泛型类型变量,如: `List` 中的 `T` +- `GenericArrayType`:组成元素是 `ParameterizedType` 或 `TypeVariable` 的数组类型,如:`List[]`、`T[]` +- `WildcardType`:通配符泛型类型变量,如:`List` 中的 `?` + +`TypeParameterResolver` 分别提供 `resolveFieldType`、`resolveReturnType`、`resolveParamTypes` 方法用于解析字段类型、方法返回值类型和方法入参类型,这些方法均调用 `resolveType` 来获取类型信息: + +```java +/** + * 获取类型信息 + * + * @param type 根据是否有泛型信息签名选择传入泛型类型或简单类型 + * @param srcType 引用字段/方法的类(可能是子类,字段和方法在父类声明) + * @param declaringClass 字段/方法声明的类 + * @return + */ +private static Type resolveType(Type type, Type srcType, Class declaringClass) { + if (type instanceof TypeVariable) { + // 泛型类型变量,如:List 中的 T + return resolveTypeVar((TypeVariable) type, srcType, declaringClass); + } else if (type instanceof ParameterizedType) { + // 泛型类型,如:List + return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass); + } else if (type instanceof GenericArrayType) { + // TypeVariable/ParameterizedType 数组类型 + return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass); + } else { + // 原始类型,直接返回 + return type; + } +} +``` + +`resolveTypeVar` 用于解析泛型类型变量参数类型,如果字段或方法在当前类中声明,则返回泛型类型的上界或 `Object` 类型;如果在父类中声明,则递归解析父类;父类也无法解析,则递归解析实现的接口。 + +```java +private static Type resolveTypeVar(TypeVariable typeVar, Type srcType, Class declaringClass) { + Type result; + Class clazz; + if (srcType instanceof Class) { + // 原始类型 + clazz = (Class) srcType; + } else if (srcType instanceof ParameterizedType) { + // 泛型类型,如 TestObj + ParameterizedType parameterizedType = (ParameterizedType) srcType; + // 取原始类型TestObj + clazz = (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass()); + } + + if (clazz == declaringClass) { + // 字段就是在当前引用类中声明的 + Type[] bounds = typeVar.getBounds(); + if (bounds.length > 0) { + // 返回泛型类型变量上界,如:T extends String,则返回String + return bounds[0]; + } + // 没有上界返回Object + return Object.class; + } + + // 字段/方法在父类中声明,递归查找父类泛型 + Type superclass = clazz.getGenericSuperclass(); + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass); + if (result != null) { + return result; + } + + // 递归泛型接口 + Type[] superInterfaces = clazz.getGenericInterfaces(); + for (Type superInterface : superInterfaces) { + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface); + if (result != null) { + return result; + } + } + return Object.class; +} +``` + +通过调用 `scanSuperTypes` 实现递归解析: + +```java +private static Type scanSuperTypes(TypeVariable typeVar, Type srcType, Class declaringClass, Class clazz, Type superclass) { + if (superclass instanceof ParameterizedType) { + // 父类是泛型类型 + ParameterizedType parentAsType = (ParameterizedType) superclass; + Class parentAsClass = (Class) parentAsType.getRawType(); + // 父类中的泛型类型变量集合 + TypeVariable[] parentTypeVars = parentAsClass.getTypeParameters(); + if (srcType instanceof ParameterizedType) { + // 子类可能对父类泛型变量做过替换,使用替换后的类型 + parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType); + } + if (declaringClass == parentAsClass) { + // 字段/方法在当前父类中声明 + for (int i = 0; i < parentTypeVars.length; i++) { + if (typeVar == parentTypeVars[i]) { + // 使用变量对应位置的真正类型(可能已经被替换),如父类 A,子类 B extends A,则返回String + return parentAsType.getActualTypeArguments()[i]; + } + } + } + // 字段/方法声明的类是当前父类的父类,继续递归 + if (declaringClass.isAssignableFrom(parentAsClass)) { + return resolveTypeVar(typeVar, parentAsType, declaringClass); + } + } else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class) superclass)) { + // 父类是原始类型,继续递归父类 + return resolveTypeVar(typeVar, superclass, declaringClass); + } + return null; +} +``` + +解析方法返回值和方法参数的逻辑大致与解析字段类型相同,`MyBatis` 源码的`TypeParameterResolverTest` 类提供了相关的测试用例。 + +## 元信息工厂 + +`MyBatis` 还提供 `ReflectorFactory` 接口用于实现 `Reflector` 容器,其默认实现为 `DefaultReflectorFactory`,其中可以使用 `classCacheEnabled` 属性来配置是否使用缓存。 + +```java +public class DefaultReflectorFactory implements ReflectorFactory { + + /** + * 是否缓存Reflector类信息 + */ + private boolean classCacheEnabled = true; + + /** + * Reflector缓存容器 + */ + private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + + public DefaultReflectorFactory() { + } + + @Override + public boolean isClassCacheEnabled() { + return classCacheEnabled; + } + + @Override + public void setClassCacheEnabled(boolean classCacheEnabled) { + this.classCacheEnabled = classCacheEnabled; + } + + /** + * 获取类的Reflector信息 + * + * @param type + * @return + */ + @Override + public Reflector findForClass(Class type) { + if (classCacheEnabled) { + // synchronized (type) removed see issue #461 + // 如果缓存Reflector信息,放入缓存容器 + return reflectorMap.computeIfAbsent(type, Reflector::new); + } else { + return new Reflector(type); + } + } + +} +``` + +## 对象创建工厂 + +`ObjectFactory` 接口是 `MyBatis` 对象创建工厂,其默认实现 `DefaultObjectFactory` 通过构造器反射创建对象,支持使用无参构造器和有参构造器。 + +## 属性工具集 + +`MyBatis` 在映射文件定义 `resultMap` 支持如下形式: + +```xml + + + + ... + +``` + +`orders[0].items[0].name` 这样的表达式是由 `PropertyTokenizer` 解析的,其构造方法能够对表达式进行解析;同时还实现了 `Iterator` 接口,能够迭代解析表达式。 + +```java +public PropertyTokenizer(String fullname) { + // orders[0].items[0].name + int delim = fullname.indexOf('.'); + if (delim > -1) { + // name = orders[0] + name = fullname.substring(0, delim); + // children = items[0].name + children = fullname.substring(delim + 1); + } else { + name = fullname; + children = null; + } + // orders[0] + indexedName = name; + delim = name.indexOf('['); + if (delim > -1) { + // 0 + index = name.substring(delim + 1, name.length() - 1); + // order + name = name.substring(0, delim); + } +} + + /** + * 是否有children表达式继续迭代 + * + * @return + */ + @Override + public boolean hasNext() { + return children != null; + } + + /** + * 分解出的 . 分隔符的 children 表达式可以继续迭代 + * @return + */ + @Override + public PropertyTokenizer next() { + return new PropertyTokenizer(children); + } +``` + +`PropertyNamer` 可以根据 `getter/setter` 规范解析字段名称;`PropertyCopier` 则支持对有相同父类的对象,通过反射拷贝字段值。 + +## 封装类信息 + +`MetaClass` 类依赖 `PropertyTokenizer` 和 `Reflector` 查找表达式是否可以匹配 `Java` 对象中的字段,以及对应字段是否有 `getter/setter` 方法。 + +```java +/** + * 验证传入的表达式,是否存在指定的字段 + * + * @param name + * @param builder + * @return + */ +private StringBuilder buildProperty(String name, StringBuilder builder) { + // 映射文件表达式迭代器 + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 复杂表达式,如name = items[0].name,则prop.getName() = items + String propertyName = reflector.findPropertyName(prop.getName()); + if (propertyName != null) { + builder.append(propertyName); + // items. + builder.append("."); + // 加载内嵌字段类型对应的MetaClass + MetaClass metaProp = metaClassForProperty(propertyName); + // 迭代子字段 + metaProp.buildProperty(prop.getChildren(), builder); + } + } else { + // 非复杂表达式,获取字段名,如:userid->userId + String propertyName = reflector.findPropertyName(name); + if (propertyName != null) { + builder.append(propertyName); + } + } + return builder; +} +``` + +## 包装字段对象 + +相对于 `MetaClass` 关注类信息,`MetalObject` 关注的是对象的信息,除了保存传入的对象本身,还会为对象指定一个 `ObjectWrapper` 将对象包装起来。`ObejctWrapper` 体系如下: + +![ObjectWrapper体系](https://wch853.github.io/img/mybatis/ObjectWrapper%E4%BD%93%E7%B3%BB.png) + +`ObjectWrapper` 的默认实现包括了对 `Map`、`Collection` 和普通 `JavaBean` 的包装。`MyBatis` 还支持通过 `ObjectWrapperFactory` 接口对 `ObejctWrapper` 进行扩展,生成自定义的包装类。`MetaObject` 对对象的具体操作,就委托给真正的 `ObjectWrapper` 处理。 + +```java +private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { + this.originalObject = object; + this.objectFactory = objectFactory; + this.objectWrapperFactory = objectWrapperFactory; + this.reflectorFactory = reflectorFactory; + + // 根据传入object类型不同,指定不同的wrapper + if (object instanceof ObjectWrapper) { + this.objectWrapper = (ObjectWrapper) object; + } else if (objectWrapperFactory.hasWrapperFor(object)) { + this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); + } else if (object instanceof Map) { + this.objectWrapper = new MapWrapper(this, (Map) object); + } else if (object instanceof Collection) { + this.objectWrapper = new CollectionWrapper(this, (Collection) object); + } else { + this.objectWrapper = new BeanWrapper(this, object); + } +} +``` + +例如赋值操作,`BeanWrapper` 的实现如下: + +```java + @Override + public void set(PropertyTokenizer prop, Object value) { + if (prop.getIndex() != null) { + // 当前表达式是集合,如:items[0],就需要获取items集合对象 + Object collection = resolveCollection(prop, object); + // 在集合的指定索引上赋值 + setCollectionValue(prop, collection, value); + } else { + // 解析完成,通过Invoker接口做赋值操作 + setBeanProperty(prop, object, value); + } + } + + protected Object resolveCollection(PropertyTokenizer prop, Object object) { + if ("".equals(prop.getName())) { + return object; + } else { + // 在对象信息中查到此字段对应的集合对象 + return metaObject.getValue(prop.getName()); + } + } +``` + +根据 `PropertyTokenizer` 对象解析出的当前字段是否存在 `index` 索引来判断字段是否为集合。如果当前字段对应集合,则需要在对象信息中查到此字段对应的集合对象: + +```javascript +public Object getValue(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 如果表达式仍可迭代,递归寻找字段对应的对象 + MetaObject metaValue = metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return null; + } else { + return metaValue.getValue(prop.getChildren()); + } + } else { + // 字段解析完成 + return objectWrapper.get(prop); + } +} +``` + +如果字段是简单类型,`BeanWrapper` 获取字段对应的对象逻辑如下: + +```java +@Override +public Object get(PropertyTokenizer prop) { + if (prop.getIndex() != null) { + // 集合类型,递归获取 + Object collection = resolveCollection(prop, object); + return getCollectionValue(prop, collection); + } else { + // 解析完成,反射读取 + return getBeanProperty(prop, object); + } +} +``` + +可以看到,仍然是会判断表达式是否迭代完成,如果未解析完字段会不断递归,直至找到对应的类型。前面说到 `Reflector` 创建过程中将对字段的读取和赋值操作通过 `Invoke` 接口抽象出来,针对最终获取的字段,此时就会调用 `Invoke` 接口对字段反射读取对象值: + +```java +/** + * 通过Invoker接口反射执行读取操作 + * + * @param prop + * @param object + */ +private Object getBeanProperty(PropertyTokenizer prop, Object object) { + try { + Invoker method = metaClass.getGetInvoker(prop.getName()); + try { + return method.invoke(object, NO_ARGUMENTS); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t); + } +} +``` + +对象读取完毕再通过 `setCollectionValue` 方法对集合指定索引进行赋值或通过 `setBeanProperty` 方法对简单类型反射赋值。`MapWrapper` 的操作与 `BeanWrapper` 大致相同,`CollectionWrapper` 相对更会简单,只支持对原始集合对象进行添加操作。 + +## 小结 + +`MyBatis` 根据自身需求,对反射 `API` 做了近一步封装。其目的是简化反射操作,为对象字段的读取和赋值提供更好的性能。 + +- `org.apache.ibatis.reflection.Reflector`:缓存类的字段名和 getter/setter 方法的元信息,使得反射时有更好的性能。 +- `org.apache.ibatis.reflection.invoker.Invoker:`:用于抽象设置和读取字段值的操作。 +- `org.apache.ibatis.reflection.TypeParameterResolver`:针对 Java-Type 体系的多种实现,解析指定类中的字段、方法返回值或方法参数的类型。 +- `org.apache.ibatis.reflection.ReflectorFactory`:反射信息创建工厂抽象接口。 +- `org.apache.ibatis.reflection.DefaultReflectorFactory`:默认的反射信息创建工厂。 +- `org.apache.ibatis.reflection.factory.ObjectFactory`:MyBatis 对象创建工厂,其默认实现 DefaultObjectFactory 通过构造器反射创建对象。 +- `org.apache.ibatis.reflection.property`:property 工具包,针对映射文件表达式进行解析和 Java 对象的反射赋值。 +- `org.apache.ibatis.reflection.MetaClass`:依赖 PropertyTokenizer 和 Reflector 查找表达式是否可以匹配 Java 对象中的字段,以及对应字段是否有 getter/setter 方法。 +- `org.apache.ibatis.reflection.MetaObject`:对原始对象进行封装,将对象操作委托给 ObjectWrapper 处理。 +- `org.apache.ibatis.reflection.wrapper.ObjectWrapper`:对象包装类,封装对象的读取和赋值等操作。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" new file mode 100644 index 0000000..234ffcf --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" @@ -0,0 +1,1563 @@ +## 类型转换 + +`JDBC` 规范定义的数据类型与 `Java` 数据类型并不是完全对应的,所以在 `PrepareStatement` 为 `SQL` 语句绑定参数时,需要从 `Java` 类型转为 `JDBC` 类型;而从结果集中获取数据时,则需要将 `JDBC` 类型转为 `Java` 类型。 + +### 类型转换操作 + +`MyBatis` 中的所有类型转换器都继承自 `BaseTypeHandler` 抽象类,此类实现了 `TypeHandler` 接口。接口中定义了 1 个向 `PreparedStatement` 对象中设置参数的方法和 3 个从结果集中取值的方法: + +```java + public interface TypeHandler { + + /** + * 为PreparedStatement对象设置参数 + * + * @param ps SQL 预编译对象 + * @param i 参数索引 + * @param parameter 参数值 + * @param jdbcType 参数 JDBC类型 + * @throws SQLException + */ + void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; + + /** + * 根据列名从结果集中取值 + * + * @param rs 结果集 + * @param columnName 列名 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, String columnName) throws SQLException; + + /** + * 根据索引从结果集中取值 + * @param rs 结果集 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, int columnIndex) throws SQLException; + + /** + * 根据索引从存储过程函数中取值 + * + * @param cs 存储过程对象 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(CallableStatement cs, int columnIndex) throws SQLException; + + } +``` + +### BaseTypeHandler 及其实现 + +`BaseTypeHandler` 实现了 `TypeHandler` 接口,针对 `null` 和异常处理做了封装,但是具体逻辑封装成 4 个抽象方法仍交由相应的类型转换器子类实现,以 `IntegerTypeHandler` 为例,其实现如下: + +```java + public class IntegerTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) + throws SQLException { + ps.setInt(i, parameter); + } + + @Override + public Integer getNullableResult(ResultSet rs, String columnName) + throws SQLException { + int result = rs.getInt(columnName); + // 如果列值为空值返回控制否则返回原值 + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + int result = rs.getInt(columnIndex); + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + int result = cs.getInt(columnIndex); + return result == 0 && cs.wasNull() ? null : result; + } + } +``` + +其实现主要是调用 `JDBC API` 设置查询参数或取值,并对 `null` 等特定情况做特殊处理。 + +### 类型转换器注册 + +`TypeHandlerRegistry` 是 `TypeHandler` 的注册类,在其无参构造方法中维护了 `JavaType`、`JdbcType` 和 `TypeHandler` 的关系。其主要使用的容器如下: + +```java + /** + * JdbcType - TypeHandler对象 + * 用于将Jdbc类型转为Java类型 + */ + private final Map> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class); + + /** + * JavaType - JdbcType - TypeHandler对象 + * 用于将Java类型转为指定的Jdbc类型 + */ + private final Map>> typeHandlerMap = new ConcurrentHashMap<>(); + + /** + * TypeHandler类型 - TypeHandler对象 + * 注册所有的TypeHandler类型 + */ + private final Map, TypeHandler> allTypeHandlersMap = new HashMap<>(); +``` + +## 别名注册 + +### 别名转换器注册 + +`TypeAliasRegistry` 提供了多种方式用于为 `Java` 类型注册别名。包括直接指定别名、注解指定别名、为指定包下类型注册别名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从Alias注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } + + /** + * 注册类型别名 + * @param alias 别名 + * @param value 指定类型全名 + */ + public void registerAlias(String alias, String value) { + try { + registerAlias(alias, Resources.classForName(value)); + } catch (ClassNotFoundException e) { + throw new TypeException("Error registering type alias " + alias + " for " + value + ". Cause: " + e, e); + } + } +``` + +所有别名均注册到名为 `typeAliases` 的容器中。`TypeAliasRegistry` 的无参构造方法默认为一些常用类型注册了别名,如 `Integer`、`String`、`byte[]` 等。 + +## 日志配置 + +`MyBatis` 支持与多种日志工具集成,包括 `Slf4j`、`log4j`、`log4j2`、`commons-logging` 等。这些第三方工具类对应日志的实现各有不同,`MyBatis` 通过适配器模式抽象了这些第三方工具的集成过程,按照一定的优先级选择具体的日志工具,并将真正的日志实现委托给选择的日志工具。 + +### 日志适配接口 + +`Log` 接口是 `MyBatis` 的日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 + +### 日志工厂 + +`LogFactory` 负责对第三方日志工具进行适配,在类加载时会通过静态代码块按顺序选择合适的日志实现。 + +```java + static { + // 按顺序加载日志实现,如果有某个第三方日志实现可以成功加载,则不继续加载其它实现 + tryImplementation(LogFactory::useSlf4jLogging); + tryImplementation(LogFactory::useCommonsLogging); + tryImplementation(LogFactory::useLog4J2Logging); + tryImplementation(LogFactory::useLog4JLogging); + tryImplementation(LogFactory::useJdkLogging); + tryImplementation(LogFactory::useNoLogging); + } + + /** + * 初始化 logConstructor + * + * @param runnable + */ + private static void tryImplementation(Runnable runnable) { + if (logConstructor == null) { + try { + // 同步执行 + runnable.run(); + } catch (Throwable t) { + // ignore + } + } + } + + /** + * 配置第三方日志实现适配器 + * + * @param implClass + */ + private static void setImplementation(Class implClass) { + try { + Constructor candidate = implClass.getConstructor(String.class); + Log log = candidate.newInstance(LogFactory.class.getName()); + if (log.isDebugEnabled()) { + log.debug("Logging initialized using '" + implClass + "' adapter."); + } + logConstructor = candidate; + } catch (Throwable t) { + throw new LogException("Error setting Log implementation. Cause: " + t, t); + } + } +``` + +`tryImplementation` 按顺序加载第三方日志工具的适配实现,如 `Slf4j` 的适配器 `Slf4jImpl`: + +```java +public Slf4jImpl(String clazz) { + Logger logger = LoggerFactory.getLogger(clazz); + + if (logger instanceof LocationAwareLogger) { + try { + // check for slf4j >= 1.6 method signature + logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class); + log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger); + return; + } catch (SecurityException | NoSuchMethodException e) { + // fail-back to Slf4jLoggerImpl + } + } + + // Logger is not LocationAwareLogger or slf4j version < 1.6 + log = new Slf4jLoggerImpl(logger); +} +``` + +如果 `Slf4jImpl` 能成功执行构造方法,则 `LogFactory` 的 `logConstructor` 被成功赋值,`MyBatis` 就找到了合适的日志实现,可以通过 `getLog` 方法获取 `Log` 对象。 + +### JDBC 日志代理 + +`org.apache.ibatis.logging.jdbc` 包提供了 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法执行的日志记录代理。`BaseJdbcLogger` 在创建时整理了 `PreparedStatement` 执行的相关方法名,并提供容器保存列值: + +```java + /** + * PreparedStatement 接口中的 set* 方法名称集合 + */ + protected static final Set SET_METHODS; + + /** + * PreparedStatement 接口中的 部分执行方法 + */ + protected static final Set EXECUTE_METHODS = new HashSet<>(); + + /** + * 列名-列值 + */ + private final Map columnMap = new HashMap<>(); + + /** + * 列名集合 + */ + private final List columnNames = new ArrayList<>(); + + /** + * 列值集合 + */ + private final List columnValues = new ArrayList<>(); + + static { + SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("set")) + .filter(method -> method.getParameterCount() > 1) + .map(Method::getName) + .collect(Collectors.toSet()); + + EXECUTE_METHODS.add("execute"); + EXECUTE_METHODS.add("executeUpdate"); + EXECUTE_METHODS.add("executeQuery"); + EXECUTE_METHODS.add("addBatch"); + } + + protected void setColumn(Object key, Object value) { + columnMap.put(key, value); + columnNames.add(key); + columnValues.add(value); + } +``` + +`ConnectionLogger`、`PreparedStatementLogger`、`StatementLogger`、`ResultSetLogger` 都继承自 `BaseJdbcLogger`,并实现了 `InvocationHandler` 接口,在运行时通过 `JDK` 动态代理实现代理类,针对相关方法执行打印日志。如下是 `ConnectionLogger` 对 `InvocationHandler` 接口的实现: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] params) + throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, params); + } + if ("prepareStatement".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + // 执行创建PreparedStatement方法,使用PreparedStatementLogger代理 + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("prepareCall".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("createStatement".equals(method.getName())) { + Statement stmt = (Statement) method.invoke(connection, params); + stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else { + return method.invoke(connection, params); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +如果执行 `prepareStatement` 方法创建 `PrepareStatement` 对象,则会使用动态代理创建 `PreparedStatementLogger` 对象增强原有对象。在 `PreparedStatementLogger` 的代理逻辑中,如果执行的是 `executeQuery` 或 `getResultSet` 方法,其返回值 `ResultSet` 也会包装为 `ResultSetLogger` 作为代理,其代理逻辑为如果执行 `ResultSet` 的 `next` 方法,会打印结果集的行。 + +## 资源加载 + +### resources 与 ClassLoaderWrapper + +`MyBatis` 提供工具类 `Resources` 用于工具加载,其底层是通过 `ClassLoaderWrapper` 实现的。`ClassLoaderWrapper` 组合了一系列的 `ClassLoader`: + +```java + ClassLoader[] getClassLoaders(ClassLoader classLoader) { + return new ClassLoader[]{ + // 指定 ClassLoader + classLoader, + // 默认 ClassLoader,默认为 null + defaultClassLoader, + // 当前线程对应的 ClassLoader + Thread.currentThread().getContextClassLoader(), + // 当前类对应的 ClassLoader + getClass().getClassLoader(), + // 默认为 SystemClassLoader + systemClassLoader}; + } +``` + +当加载资源时会按组合的 `ClassLoader` 顺序依次尝试加载资源,例如 `classForName` 方法的实现: + +```java + Class classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException { + // 按组合顺序依次加载 + for (ClassLoader cl : classLoader) { + if (null != cl) { + try { + // 类加载 + Class c = Class.forName(name, true, cl); + if (null != c) { + return c; + } + } catch (ClassNotFoundException e) { + // we'll ignore this until all classloaders fail to locate the class + } + } + } + throw new ClassNotFoundException("Cannot find class: " + name); + } +``` + +### 加载指定包下的类 + +`ResolverUtil` 的 `find` 方法用于按条件加载指定包下的类。 + +```java +public ResolverUtil find(Test test, String packageName) { + // 包名.替换为/ + String path = getPackagePath(packageName); + + try { + // 虚拟文件系统加载文件路径 + List children = VFS.getInstance().list(path); + for (String child : children) { + if (child.endsWith(".class")) { + // 如果指定class文件符合条件,加入容器 + addIfMatching(test, child); + } + } + } catch (IOException ioe) { + log.error("Could not read package: " + packageName, ioe); + } + + return this; +} +``` + +`ResolverUtil` 还提供了一个内部接口 `Test` 用于判断指定类型是否满足条件,在 `ResolverUtil` 有两个默认实现:`IsA` 用于判断是否为指定类型的子类;`AnnotatedWith` 用于判断类上是否有指定注解。 + +```java + /** + * A Test that checks to see if each class is assignable to the provided class. Note + * that this test will match the parent type itself if it is presented for matching. + * + * 判断是否为子类 + */ + public static class IsA implements Test { + private Class parent; + + /** Constructs an IsA test using the supplied Class as the parent class/interface. */ + public IsA(Class parentType) { + this.parent = parentType; + } + + /** Returns true if type is assignable to the parent type supplied in the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && parent.isAssignableFrom(type); + } + } + + /** + * A Test that checks to see if each class is annotated with a specific annotation. If it + * is, then the test returns true, otherwise false. + * + * 判断类上是否有指定注解 + */ + public static class AnnotatedWith implements Test { + private Class annotation; + + /** Constructs an AnnotatedWith test for the specified annotation type. */ + public AnnotatedWith(Class annotation) { + this.annotation = annotation; + } + + /** Returns true if the type is annotated with the class provided to the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && type.isAnnotationPresent(annotation); + } + } +``` + +如果要加载的类符合条件,则将加载的类对象加入容器。 + +```java +protected void addIfMatching(Test test, String fqn) { + try { + String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.'); + ClassLoader loader = getClassLoader(); + if (log.isDebugEnabled()) { + log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]"); + } + // 加载类 + Class type = loader.loadClass(externalName); + if (test.matches(type)) { + // 符合条件,加入容器 + matches.add((Class) type); + } + } catch (Throwable t) { + log.warn("Could not examine class '" + fqn + "'" + " due to a " + + t.getClass().getName() + " with message: " + t.getMessage()); + } +} +``` + +## 数据源实现 + +`MyBatis` 提供了自己的数据源实现,分别为非池化数据源 `UnpooledDataSource` 和池化数据源 `PooledDataSource`。两个数据源都实现了 `javax.sql.DataSource` 接口并分别由 `UnpooledDataSourceFactory` 和 `PooledDataSourceFactory` 工厂类创建。两个工厂类又都实现了 `DataSourceFactory` 接口。 + +![MyBatis DataSource 体系](https://wch853.github.io/img/mybatis/DataSource%E4%BD%93%E7%B3%BB.png) + +### DataSourceFactory 实现 + +`UnpooledDataSourceFactory` 实现了 `DataSourceFactory` 接口的 `setProperties` 和 `getDataSource` 方法,分别用于在创建数据源工厂后配置数据源属性和获取数据源,`PooledDataSourceFactory` 继承了其实现: + +```java + public UnpooledDataSourceFactory() { + this.dataSource = new UnpooledDataSource(); + } + + /** + * 创建数据源工厂后配置数据源属性 + * + * @param props + */ + @Override + public void setProperties(Properties properties) { + Properties driverProperties = new Properties(); + // 数据源对象信息 + MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); + for (Object key : properties.keySet()) { + String propertyName = (String) key; + if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { + // 数据源驱动相关属性 + String value = properties.getProperty(propertyName); + driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); + } else if (metaDataSource.hasSetter(propertyName)) { + // 属性在数据源类中有相应的set方法 + String value = (String) properties.get(propertyName); + // 转换类型 + Object convertedValue = convertValue(metaDataSource, propertyName, value); + // 反射赋值 + metaDataSource.setValue(propertyName, convertedValue); + } else { + throw new DataSourceException("Unknown DataSource property: " + propertyName); + } + } + if (driverProperties.size() > 0) { + metaDataSource.setValue("driverProperties", driverProperties); + } + } + + /** + * 获取数据源 + * + * @return + */ + @Override + public DataSource getDataSource() { + return dataSource; + } +``` + +### 非池化数据源 + +`UnpooledDataSource` 在静态语句块中从数据源驱动管理器 `DriverManager` 获取所有已注册驱动并放入本地容器: + +```java + static { + // 从数据库驱动类中获取所有驱动 + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + registeredDrivers.put(driver.getClass().getName(), driver); + } + } +``` + +此数据源获取连接的实现为调用 `doGetConnection` 方法,每次获取连接时先校验当前驱动是否注册,如果已注册则直接创建新连接,并配置自动提交属性和默认事务隔离级别: + +```java + /** + * 获取连接 + * + * @param properties + * @return + * @throws SQLException + */ + private Connection doGetConnection(Properties properties) throws SQLException { + // 校验当前驱动是否注册,如果未注册,加载驱动并注册 + initializeDriver(); + // 获取数据库连接 + Connection connection = DriverManager.getConnection(url, properties); + // 配置自动提交和默认事务隔离级别属性 + configureConnection(connection); + return connection; + } + + /** + * 校验当前驱动是否注册 + * + * @throws SQLException + */ + private synchronized void initializeDriver() throws SQLException { + if (!registeredDrivers.containsKey(driver)) { + // 当前驱动还未注册 + Class driverType; + try { + // 加载驱动类 + if (driverClassLoader != null) { + driverType = Class.forName(driver, true, driverClassLoader); + } else { + driverType = Resources.classForName(driver); + } + // DriverManager requires the driver to be loaded via the system ClassLoader. + // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html + Driver driverInstance = (Driver)driverType.newInstance(); + // 注册驱动 + DriverManager.registerDriver(new DriverProxy(driverInstance)); + registeredDrivers.put(driver, driverInstance); + } catch (Exception e) { + throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); + } + } + } + + /** + * 配置自动提交和默认事务隔离级别属性 + * + * @param conn + * @throws SQLException + */ + private void configureConnection(Connection conn) throws SQLException { + if (autoCommit != null && autoCommit != conn.getAutoCommit()) { + conn.setAutoCommit(autoCommit); + } + if (defaultTransactionIsolationLevel != null) { + conn.setTransactionIsolation(defaultTransactionIsolationLevel); + } + } +``` + +### 池化数据源 + +数据库连接的创建是十分耗时的,在高并发环境下,频繁地创建和关闭连接会为系统带来很大的开销。而使用连接池实现对数据库连接的重用可以显著提高性能,避免反复创建连接。`MyBatis` 实现的连接池包含了维护连接队列、创建和保存连接、归还连接等功能。 + +#### 池化连接 + +`PooledConnection` 是 `MyBatis` 的池化连接实现。其构造方法中传入了驱动管理器创建的真正连接,并通过 `JDK` 动态代理创建了连接的代理对象: + +```java + public PooledConnection(Connection connection, PooledDataSource dataSource) { + this.hashCode = connection.hashCode(); + // 真正的数据库连接 + this.realConnection = connection; + // 数据源对象 + this.dataSource = dataSource; + // 连接创建时间 + this.createdTimestamp = System.currentTimeMillis(); + // 连接上次使用时间 + this.lastUsedTimestamp = System.currentTimeMillis(); + // 数据源有效标志 + this.valid = true; + // 创建连接代理 + this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); + } +``` + +连接代理的逻辑如下,如果执行 `close` 方法,并不会真正的关闭连接,而是当作空闲连接交给数据源处理,根据连接池的状态选择将连接放入空闲队列或关闭连接;如果执行其它方法,则会判断当前连接是否有效,如果是无效连接会抛出异常: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { + // 调用连接关闭方法代理逻辑:处理空闲连接,放入空闲队列或关闭 + dataSource.pushConnection(this); + return null; + } + try { + if (!Object.class.equals(method.getDeclaringClass())) { + // issue #579 toString() should never fail + // throw an SQLException instead of a Runtime + // 执行其它方法,验证连接是否有效,如果是无效连接,抛出异常 + checkConnection(); + } + return method.invoke(realConnection, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +#### 连接池状态 + +`PoolState` 维护了数据库连接池状态。其内部维护了两个容器,分别为空闲连接集合和活跃连接集合。 + +```java + /** + * 空闲连接集合 + */ + protected final List idleConnections = new ArrayList<>(); + + /** + * 活跃连接集合 + */ + protected final List activeConnections = new ArrayList<>(); +``` + +#### 获取连接 + +池化数据源 `PooledDataSource` 是依赖 `UnpooledDataSource` 实现的。其获取连接的方式是调用 `popConnection` 方法。在获取连接池同步锁后按照以下顺序尝试获取可用连接: + +- 从空闲队列获取连接 +- 活跃连接池未满,创建新连接 +- 检查最早的活跃连接是否超时 +- 等待释放连接 + +```java +private PooledConnection popConnection(String username, String password) throws SQLException { + // 等待连接标志 + boolean countedWait = false; + // 待获取的池化连接 + PooledConnection conn = null; + long t = System.currentTimeMillis(); + int localBadConnectionCount = 0; + + while (conn == null) { + // 循环获取连接 + synchronized (state) { + // 获取连接池的同步锁 + if (!state.idleConnections.isEmpty()) { + // Pool has available connection 连接池中有空闲连接 + conn = state.idleConnections.remove(0); + if (log.isDebugEnabled()) { + log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); + } + } else { + // Pool does not have available connection 连接池无可用连接 + if (state.activeConnections.size() < poolMaximumActiveConnections) { + // Can create new connection 活跃连接数小于设定的最大连接数,创建新的连接(从驱动管理器创建新的连接) + conn = new PooledConnection(dataSource.getConnection(), this); + if (log.isDebugEnabled()) { + log.debug("Created connection " + conn.getRealHashCode() + "."); + } + } else { + // Cannot create new connection 活跃连接数到达最大连接数 + PooledConnection oldestActiveConnection = state.activeConnections.get(0); + // 查询最早入队的活跃连接使用时间(即使用时间最长的活跃连接使用时间) + long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); + if (longestCheckoutTime > poolMaximumCheckoutTime) { + // Can claim overdue connection 超出活跃连接最大使用时间 + state.claimedOverdueConnectionCount++; + // 超时连接累计使用时长 + state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; + state.accumulatedCheckoutTime += longestCheckoutTime; + // 活跃连接队列移除当前连接 + state.activeConnections.remove(oldestActiveConnection); + if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { + try { + // 创建的连接未自动提交,执行回滚 + oldestActiveConnection.getRealConnection().rollback(); + } catch (SQLException e) { + /* + Just log a message for debug and continue to execute the following + statement like nothing happened. + Wrap the bad connection with a new PooledConnection, this will help + to not interrupt current executing thread and give current thread a + chance to join the next competition for another valid/good database + connection. At the end of this loop, bad {@link @conn} will be set as null. + */ + log.debug("Bad connection. Could not roll back"); + } + } + // 包装新的池化连接 + conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); + conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); + conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); + // 设置原连接无效 + oldestActiveConnection.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); + } + } else { + // Must wait + try { + // 存活连接有效 + if (!countedWait) { + state.hadToWaitCount++; + countedWait = true; + } + if (log.isDebugEnabled()) { + log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); + } + long wt = System.currentTimeMillis(); + // 释放锁等待连接,{@link PooledDataSource#pushConnection} 如果有连接空闲,会唤醒等待 + state.wait(poolTimeToWait); + // 记录等待时长 + state.accumulatedWaitTime += System.currentTimeMillis() - wt; + } catch (InterruptedException e) { + break; + } + } + } + } + if (conn != null) { + // ping to server and check the connection is valid or not + if (conn.isValid()) { + // 连接有效 + if (!conn.getRealConnection().getAutoCommit()) { + // 非自动提交的连接,回滚上次任务 + conn.getRealConnection().rollback(); + } + conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); + // 设置连接新的使用时间 + conn.setCheckoutTimestamp(System.currentTimeMillis()); + conn.setLastUsedTimestamp(System.currentTimeMillis()); + // 添加到活跃连接集合队尾 + state.activeConnections.add(conn); + // 连接请求次数+1 + state.requestCount++; + // 请求连接花费的时间 + state.accumulatedRequestTime += System.currentTimeMillis() - t; + } else { + // 未获取到连接 + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); + } + // 因为没有空闲连接导致获取连接失败次数+1 + state.badConnectionCount++; + // 本次请求获取连接失败数+1 + localBadConnectionCount++; + conn = null; + if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { + // 超出获取连接失败的可容忍次数,抛出异常 + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Could not get a good connection to the database."); + } + throw new SQLException("PooledDataSource: Could not get a good connection to the database."); + } + } + } + } + + } + + if (conn == null) { + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + + return conn; +} +``` + +如果暂时获取不到可用连接,则当前线程进入等待,等待新的空闲连接产生唤醒等待或等待超时后重新尝试获取连接。当尝试次数到达指定上限,会抛出异常跳出等待。 + +#### 判断连接有效性 + +如果可以从连接池中获取连接,会调用 `PooledConnection#isValid` 方法判断连接是否有效,其逻辑为 `PooledConnection` 对象自身维护的标志有效且连接存活。判断连接存活的实现如下: + +```java + protected boolean pingConnection(PooledConnection conn) { + boolean result = true; + + try { + // 连接是否关闭 + result = !conn.getRealConnection().isClosed(); + } catch (SQLException e) { + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + result = false; + } + + if (result) { + if (poolPingEnabled) { + // 使用语句检测连接是否可用开关开启 + if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { + // 距上次连接使用经历时长超过设置的阈值 + try { + if (log.isDebugEnabled()) { + log.debug("Testing connection " + conn.getRealHashCode() + " ..."); + } + // 验证连接是否可用 + Connection realConn = conn.getRealConnection(); + try (Statement statement = realConn.createStatement()) { + statement.executeQuery(poolPingQuery).close(); + } + if (!realConn.getAutoCommit()) { + // 未自动提交执行回滚 + realConn.rollback(); + } + result = true; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is GOOD!"); + } + } catch (Exception e) { + log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage()); + try { + // 抛出异常,连接不可用,关闭连接 + conn.getRealConnection().close(); + } catch (Exception e2) { + //ignore + } + result = false; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + } + } + } + } + return result; + } +``` + +默认调用 `Connection#isClosed` 方法判断连接是否存活,如果连接存活,则可以选择执行 `SQL` 语句来进一步判断连接的有效性。是否进一步验证、验证使用的 `SQL` 语句、验证的时间条件,都是可配置的。 + +#### 处理空闲连接 + +在池化连接的代理连接执行关闭操作时,会转为对空闲连接的处理,其实现逻辑如下: + +```java + protected void pushConnection(PooledConnection conn) throws SQLException { + + synchronized (state) { + // 获取连接池状态同步锁,活跃连接队列移除当前连接 + state.activeConnections.remove(conn); + if (conn.isValid()) { + // 连接有效 + if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { + // 空闲连接数小于最大空闲连接数,累计连接使用时长 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 包装成新的代理连接 + PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); + // 将新连接放入空闲队列 + state.idleConnections.add(newConn); + // 设置相关统计时间戳 + newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); + newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); + // 老连接设置失效 + conn.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Returned connection " + newConn.getRealHashCode() + " to pool."); + } + // 唤醒等待连接的线程,通知有新连接可以使用 + state.notifyAll(); + } else { + // 空闲连接数达到最大空闲连接数 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 关闭多余的连接 + conn.getRealConnection().close(); + if (log.isDebugEnabled()) { + log.debug("Closed connection " + conn.getRealHashCode() + "."); + } + // 连接设置失效 + conn.invalidate(); + } + } else { + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection."); + } + // 连接无效次数+1 + state.badConnectionCount++; + } + } + } +``` + +如果当前空闲连接数小于最大空闲连接数,则将空闲连接放入空闲队列,否则关闭连接。 + +#### 处理配置变更 + +在相关配置变更后,`MyBatis` 会调用 `forceCloseAll` 关闭连接池中所有存活的连接: + +```java + public void forceCloseAll() { + synchronized (state) { + // 获取连接池状态同步锁 + expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()); + for (int i = state.activeConnections.size(); i > 0; i--) { + try { + // 移除活跃连接 + PooledConnection conn = state.activeConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + for (int i = state.idleConnections.size(); i > 0; i--) { + try { + // 移除空闲连接 + PooledConnection conn = state.idleConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + } + if (log.isDebugEnabled()) { + log.debug("PooledDataSource forcefully closed/removed all connections."); + } + } +``` + +## 缓存实现 + +`Cache` 是 `MyBatis` 的缓存抽象接口,其要求实现如下方法: + +```java + public interface Cache { + + /** + * 缓存对象 id + * + * @return The identifier of this cache + */ + String getId(); + + /** + * 设置缓存 + * + * @param key Can be any object but usually it is a {@link CacheKey} + * @param value The result of a select. + */ + void putObject(Object key, Object value); + + /** + * 获取缓存 + * + * @param key The key + * @return The object stored in the cache. + */ + Object getObject(Object key); + + /** + * 移除缓存 + * + * @param key The key + * @return Not used + */ + Object removeObject(Object key); + + /** + * 清空缓存 + * + * Clears this cache instance. + */ + void clear(); + + /** + * Optional. This method is not called by the core. + * 获取缓存项数量 + * + * @return The number of elements stored in the cache (not its capacity). + */ + int getSize(); + + /** + * 获取读写锁 + * + * @return A ReadWriteLock + */ + ReadWriteLock getReadWriteLock(); + } +``` + +### 基础实现 + +`PerpetualCache` 类是基础实现类,核心是基于 `HashMap` 作为缓存维护容器。在此基础上,`MyBatis` 实现了多种缓存装饰器,用于满足不同的需求。 + +### 缓存装饰器 + +#### 同步操作 + +`SynchronizedCache` 针对缓存操作方法加上了 `synchronized` 关键字用于进行同步操作。 + +#### 阻塞操作 + +`BlockingCache` 在执行获取缓存操作时对 `key` 加锁,直到写缓存后释放锁,保证了相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 + +```java + /** + * 写缓存完成后释放锁 + */ + @Override + public void putObject(Object key, Object value) { + try { + delegate.putObject(key, value); + } finally { + releaseLock(key); + } + } + + @Override + public Object getObject(Object key) { + // 获取锁 + acquireLock(key); + Object value = delegate.getObject(key); + if (value != null) { + // 缓存不为空则释放锁,否则继续持有锁,在进行数据库操作后写缓存释放锁 + releaseLock(key); + } + return value; + } + + /** + * 删除指定 key 对应的缓存,并释放锁 + * + * @param key The key + * @return + */ + @Override + public Object removeObject(Object key) { + // despite of its name, this method is called only to release locks + releaseLock(key); + return null; + } + + /** + * 获取已有的锁或创建新锁 + * + * @param key + * @return + */ + private ReentrantLock getLockForKey(Object key) { + return locks.computeIfAbsent(key, k -> new ReentrantLock()); + } + + /** + * 根据 key 获取锁 + * + * @param key + */ + private void acquireLock(Object key) { + Lock lock = getLockForKey(key); + if (timeout > 0) { + try { + boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); + if (!acquired) { + throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); + } + } catch (InterruptedException e) { + throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); + } + } else { + lock.lock(); + } + } + + /** + * 释放锁 + * + * @param key + */ + private void releaseLock(Object key) { + ReentrantLock lock = locks.get(key); + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } +``` + +#### 日志记录 + +`LoggingCache` 是缓存日志装饰器。查询缓存时会记录查询日志并统计命中率。 + +```java +/** + * 查询缓存时记录查询日志并统计命中率 + * + * @param key The key + * @return + */ +@Override +public Object getObject(Object key) { + // 查询数+1 + requests++; + final Object value = delegate.getObject(key); + if (value != null) { + // 命中数+1 + hits++; + } + if (log.isDebugEnabled()) { + log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); + } + return value; +} +``` + +#### 定时清理 + +`ScheduledCache` 是缓存定时清理装饰器。在执行缓存相关操作时会根据设置的时间间隔判断是否需要清除全部的缓存。 + +```java + /** + * 操作缓存时判断是否需要清除所有缓存。 + * + * @return + */ + private boolean clearWhenStale() { + if (System.currentTimeMillis() - lastClear > clearInterval) { + clear(); + return true; + } + return false; + } +``` + +#### 序列化与反序列化 + +`SerializedCache` 是缓存序列化装饰器,其在写入时会将值序列化成对象流,并在读取时进行反序列化。 + +#### 事务操作 + +`TransactionalCache` 是事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 + +#### 先进先出 + +`FifoCache` 是先进先出缓存装饰器。其按写缓存顺序维护了一个缓存 `key` 队列,如果缓存项超出指定大小,则删除最先入队的缓存。 + +```java +/** + * 按写缓存顺序维护缓存 key 队列,缓存项超出指定大小,删除最先入队的缓存 + * + * @param key + */ +private void cycleKeyList(Object key) { + keyList.addLast(key); + if (keyList.size() > size) { + Object oldestKey = keyList.removeFirst(); + delegate.removeObject(oldestKey); + } +} +``` + +#### 最近最久未使用 + +`LruCache` 是缓存最近最久未使用装饰器。其基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 + +```java + public void setSize(final int size) { + // LinkedHashMap 在执行 get 方法后会将对应的 entry 移到队尾来维护使用顺序 + keyMap = new LinkedHashMap(size, .75F, true) { + private static final long serialVersionUID = 4267176411845948333L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean tooBig = size() > size; + if (tooBig) { + // 超出缓存项数量限制,获取最近最久未使用的key + eldestKey = eldest.getKey(); + } + return tooBig; + } + }; + } + + /** + * 更新缓存后检查是否需要删除最近最久未使用的缓存项 + */ + @Override + public void putObject(Object key, Object value) { + delegate.putObject(key, value); + cycleKeyList(key); + } + + private void cycleKeyList(Object key) { + keyMap.put(key, key); + if (eldestKey != null) { + delegate.removeObject(eldestKey); + eldestKey = null; + } + } +``` + +#### 软引用缓存 + +`SoftCache` 是缓存软引用装饰器,其使用了软引用 + 强引用队列的方式维护缓存。在写缓存操作中,写入的数据其实时缓存项的软引用包装对象,在 `Full GC` 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被 `GC`,被回收对象进入指定的引用队列。 + +```java + /** + * 引用队列,用于记录已经被 GC 的 SoftEntry 对象 + */ + private final ReferenceQueue queueOfGarbageCollectedEntries; + + /** + * 写入缓存。 + * 不直接写缓存的值,而是写入缓存项对应的软引用 + */ + @Override + public void putObject(Object key, Object value) { + removeGarbageCollectedItems(); + // 在 Full GC 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被回收,被回收对象进入指定的引用队列 + delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); + } + + /** + * 查询已被 GC 的软引用,删除对应的缓存项 + */ + private void removeGarbageCollectedItems() { + SoftEntry sv; + while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { + delegate.removeObject(sv.key); + } + } + + /** + * 封装软引用对象 + */ + private static class SoftEntry extends SoftReference { + private final Object key; + + SoftEntry(Object key, Object value, ReferenceQueue garbageCollectionQueue) { + // 声明 value 为软引用对象 + super(value, garbageCollectionQueue); + // key 为强引用 + this.key = key; + } + } +``` + +在读取缓存时,如果软引用被回收,则删除对应的缓存项;否则将缓存项放入一个强引用队列中,该队列会将最新读取的缓存项放入队首,使得真正的缓存项有了强引用指向,其软引用包装就不会被垃圾回收。队列有数量限制,当超出限制时会删除队尾的缓存项。 + +```java + /** + * 获取缓存。 + * 如果软引用被回收则删除对应的缓存项,如果未回收则加入到有数量限制的 LRU 队列中 + * + * @param key The key + * @return + */ + @Override + public Object getObject(Object key) { + Object result = null; + @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache + SoftReference softReference = (SoftReference) delegate.getObject(key); + if (softReference != null) { + result = softReference.get(); + if (result == null) { + // 软引用已经被回收,删除对应的缓存项 + delegate.removeObject(key); + } else { + // 如果未被回收,增将软引用加入到 LRU 队列 + // See #586 (and #335) modifications need more than a read lock + synchronized (hardLinksToAvoidGarbageCollection) { + // 将对应的软引用 + hardLinksToAvoidGarbageCollection.addFirst(result); + if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { + // 超出数量限制,删除最近最久未使用的软引用对象 + hardLinksToAvoidGarbageCollection.removeLast(); + } + } + } + } + return result; + } +``` + +#### 弱引用缓存 + +`WeakCache` 是缓存弱引用装饰器,使用弱引用 + 强引用队列的方式维护缓存,其实现方式与 `SoftCache` 是一致的。 + +## Binding 模块 + +为了避免因拼写等错误导致在运行期才发现执行方法找不到对应的 `SQL` 语句,`MyBatis` 使用 `Binding` 模块在启动时对执行方法校验,如果找不到对应的语句,则会抛出 `BindingException`。 + +`MyBatis` 一般将执行数据库操作的方法所在的接口称为 `Mapper`,`MapperRegistry` 用来注册 `Mapper` 接口类型与其代理创建工厂的映射,其提供 `addMapper` 和 `addMappers` 接口用于注册。`Mapper` 接口代理工厂是通过 `MapperProxyFactory` 创建,创建过程依赖 `MapperProxy` 提供的 `JDK` 动态代理: + +```java + protected T newInstance(MapperProxy mapperProxy) { + return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); + } + + /** + * 使用 Mapper 代理封装 SqlSession 相关操作 + * + * @param sqlSession + * @return + */ + public T newInstance(SqlSession sqlSession) { + final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); + return newInstance(mapperProxy); + } +``` + +`MapperProxy` 的代理逻辑如下,在 `Mapper` 接口中的方法真正执行时,会为指定的非 `default` 方法创建方法信息和 `SQL` 执行信息缓存: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + // Object中的方法,直接执行 + return method.invoke(this, args); + } else if (isDefaultMethod(method)) { + // 当前方法是接口中的非abstract、非static的public方法,即高版本JDK中的default方法 + return invokeDefaultMethod(proxy, method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + // 缓存 Mapper接口 对应的方法和 SQL 执行信息 + final MapperMethod mapperMethod = cachedMapperMethod(method); + // 执行 SQL + return mapperMethod.execute(sqlSession, args); + } + + /** + * 缓存 Mapper接口 对应的方法和 SQL 执行信息 + * + * @param method + * @return + */ + private MapperMethod cachedMapperMethod(Method method) { + return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); + } +``` + +缓存通过 `MapperMethod` 类来保存,其构造方法创建了 `SqlCommand` 和 `MethodSignature` 对象。 + +```java + public MapperMethod(Class mapperInterface, Method method, Configuration config) { + // SQL 执行信息 + this.command = new SqlCommand(config, mapperInterface, method); + // 获取方法参数和返回值相关信息 + this.method = new MethodSignature(config, mapperInterface, method); + } +``` + +`SqlCommand` 会根据接口和方法名找到对应的 `SQL statement` 对象: + +```java + private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, + Class declaringClass, Configuration configuration) { + // statementId 为接口名与方法名组合 + String statementId = mapperInterface.getName() + "." + methodName; + if (configuration.hasStatement(statementId)) { + // 配置中存在此 statementId,返回对应的 statement + return configuration.getMappedStatement(statementId); + } else if (mapperInterface.equals(declaringClass)) { + // 此方法就是在对应接口中声明的 + return null; + } + // 递归查找父类 + for (Class superInterface : mapperInterface.getInterfaces()) { + if (declaringClass.isAssignableFrom(superInterface)) { + MappedStatement ms = resolveMappedStatement(superInterface, methodName, + declaringClass, configuration); + if (ms != null) { + return ms; + } + } + } + return null; + } +``` + +而 `MethodSignature` 会获取方法相关信息,如返回值类型、是否返回 `void`、是否返回多值等。对于 `Param` 注解的解析也会保存下来(`MyBatis` 使用 `Param` 注解重置参数名)。 + +## 小结 + +`MyBatis` 提供了一系列工具和实现,用于为整个框架提供基础支持。 + +> 类型转换 + +- `org.apache.ibatis.type.TypeHandler`:类型转换器接口,抽象 `JDBC` 类型和 `Java` 类型互转逻辑。 +- `org.apache.ibatis.type.BaseTypeHandler`:`TypeHandler` 的抽象实现,针对 null 和异常处理做了封装,具体逻辑仍由相应的类型转换器实现。 +- `org.apache.ibatis.type.TypeHandlerRegistry`:`TypeHandler` 注册类,维护 `JavaType`、`JdbcType` 和 `TypeHandler` 关系。 + +> 别名注册 + +- `org.apache.ibatis.type.TypeAliasRegistry`:别名注册类。注册常用类型的别名,并提供多种注册别名的方式。 + +> 日志配置 + +- `org.apache.ibatis.logging.Log`:`MyBatis` 日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 +- `org.apache.ibatis.logging.LogFactory`:`MyBatis` 日志工厂,负责适配第三方日志实现。 +- `org.apache.ibatis.logging.jdbc`:`SQL` 执行日志工具包,针对执行 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法,提供日志记录工具。 + +> 资源加载 + +- `org.apache.ibatis.io.Resources`:`MyBatis` 封装的资源加载工具类。 +- `org.apache.ibatis.io.ClassLoaderWrapper`:资源加载底层实现。组合多种 `ClassLoader` 按顺序尝试加载资源。 +- `org.apache.ibatis.io.ResolverUtil`:按条件加载指定包下的类。 + +> 数据源实现 + +- `org.apache.ibatis.datasource.DataSourceFactory`:数据源创建工厂接口。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory`:非池化数据源工厂。 +- `org.apache.ibatis.datasource.pooled.PooledDataSourceFactory`:池化数据源工厂。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSource`:非池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledDataSource`:池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledConnection`:池化连接。 +- `org.apache.ibatis.datasource.pooled.PoolState`:连接池状态。 + +> 事务实现 + +- `org.apache.ibatis.transaction.Transaction`:事务抽象接口 +- `org.apache.ibatis.session.TransactionIsolationLevel`:事务隔离级别。 +- `org.apache.ibatis.transaction.TransactionFactory`:事务创建工厂抽象接口。 +- `org.apache.ibatis.transaction.jdbc.JdbcTransaction`:封装 `JDBC` 数据库事务操作。 +- `org.apache.ibatis.transaction.managed.ManagedTransaction`:数据库事务操作依赖外部管理。 + +> 缓存实现 + +- `org.apache.ibatis.cache.Cache`:缓存抽象接口。 +- `org.apache.ibatis.cache.impl.PerpetualCache`:使用 `HashMap` 作为缓存实现容器的 `Cache` 基本实现。 +- `org.apache.ibatis.cache.decorators.BlockingCache`:缓存阻塞装饰器。保证相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 +- `org.apache.ibatis.cache.decorators.FifoCache`:缓存先进先出装饰器。按写缓存顺序维护缓存 `key` 队列,缓存项超出指定大小,删除最先入队的缓存。 +- `org.apache.ibatis.cache.decorators.LruCache`:缓存最近最久未使用装饰器。基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 +- `org.apache.ibatis.cache.decorators.LoggingCache`:缓存日志装饰器。查询缓存时记录查询日志并统计命中率。 +- `org.apache.ibatis.cache.decorators.ScheduledCache`:缓存定时清理装饰器。 +- `org.apache.ibatis.cache.decorators.SerializedCache`:缓存序列化装饰器。 +- `org.apache.ibatis.cache.decorators.SynchronizedCache`:缓存同步装饰器。在缓存操作方法上使用 `synchronized` 关键字同步。 +- `org.apache.ibatis.cache.decorators.TransactionalCache`:事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 +- `org.apache.ibatis.cache.decorators.SoftCache`:缓存软引用装饰器。使用软引用 + 强引用队列的方式维护缓存。 +- `org.apache.ibatis.cache.decorators.WeakCache`:缓存弱引用装饰器。使用弱引用 + 强引用队列的方式维护缓存。 + +### Binding 模块 + +- `org.apache.ibatis.binding.MapperRegistry`: `Mapper` 接口注册类,管理 `Mapper` 接口类型和其代理创建工厂的映射。 +- `org.apache.ibatis.binding.MapperProxyFactory`:`Mapper` 接口代理创建工厂。 +- `org.apache.ibatis.binding.MapperProxy`:`Mapper` 接口方法代理逻辑,封装 `SqlSession` 相关操作。 +- `org.apache.ibatis.binding.MapperMethod`:封装 `Mapper` 接口对应的方法和 `SQL` 执行信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..fe60300 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,609 @@ +在 `Spring` 与 `MyBatis` 的集成中,通常需要声明一个 `sqlSessionFactory` 用于初始化 `MyBatis`: + +```xml + + + + + + + +``` + +在 `bean` 初始化的时候会调用 `SqlSessionFactoryBean` 的 `afterPropertiesSet` 方法,在此方法中 `MyBatis` 使用 `XMLConfigBuilder` 对配置进行解析。 + +## BaseBuilder 体系 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,继承自 `BaseBuilder`,其为 `MyBatis` 初始化提供了一系列工具方法,如别名转换、类型转换、类加载等。 + +![](http://img.topjavaer.cn/img/202401061526966.png) + +## 全局配置对象 + +`XMLConfigBuilder` 在构造方法中创建了 `Configuration` 对象,这个对象中用于保存 `MyBatis` 相关的全部配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。通过 `XMLConfigBuilder` 的 `parse` 方法可以看出,配置解析的目的就是为了获取 `Configuration` 对象。 + +```java + private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { + // 创建全局配置 + super(new Configuration()); + ErrorContext.instance().resource("SQL Mapper Configuration"); + // 设置自定义配置 + this.configuration.setVariables(props); + // 解析标志 + this.parsed = false; + // 指定环境 + this.environment = environment; + // 包装配置 InputStream 的 XPathParser + this.parser = parser; + } + + public Configuration parse() { + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + parsed = true; + // 读取 configuration 元素并解析 + parseConfiguration(parser.evalNode("/configuration")); + return configuration; + } +``` + +## 解析配置文件 + +配置解析分为多步。`MyBatis` 源码内置 `mybatis-config.xsd` 文件用于定义配置文件书写规则。 + +```java + private void parseConfiguration(XNode root) { + try { + //issue #117 read properties first + // 解析 properties 元素 + propertiesElement(root.evalNode("properties")); + // 加载 settings 配置并验证是否有效 + Properties settings = settingsAsProperties(root.evalNode("settings")); + // 配置自定义虚拟文件系统实现 + loadCustomVfs(settings); + // 配置自定义日志实现 + loadCustomLogImpl(settings); + // 解析 typeAliases 元素 + typeAliasesElement(root.evalNode("typeAliases")); + // 解析 plugins 元素 + pluginElement(root.evalNode("plugins")); + // 解析 objectFactory 元素 + objectFactoryElement(root.evalNode("objectFactory")); + // 解析 objectWrapperFactory 元素 + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + // 解析 reflectorFactory 元素 + reflectorFactoryElement(root.evalNode("reflectorFactory")); + // 将 settings 配置设置到全局配置中 + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + // 解析 environments 元素 + environmentsElement(root.evalNode("environments")); + // 解析 databaseIdProvider 元素 + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + // 解析 typeHandlers 元素 + typeHandlerElement(root.evalNode("typeHandlers")); + // 解析 mappers 元素 + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } + } +``` + +### 解析 properties 元素 + +`properties` 元素用于将自定义配置传递给 `MyBatis`,例如: + +```xml + + + + +``` + +其加载逻辑为将不同配置转为 `Properties` 对象,并设置到全局配置中: + +```java + private void propertiesElement(XNode context) throws Exception { + if (context != null) { + // 获取子元素属性 + Properties defaults = context.getChildrenAsProperties(); + // 读取 resource 属性 + String resource = context.getStringAttribute("resource"); + // 读取 url 属性 + String url = context.getStringAttribute("url"); + if (resource != null && url != null) { + // 不可均为空 + throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); + } + // 加载指定路径文件,转为 properties + if (resource != null) { + defaults.putAll(Resources.getResourceAsProperties(resource)); + } else if (url != null) { + defaults.putAll(Resources.getUrlAsProperties(url)); + } + // 添加创建配置的附加属性 + Properties vars = configuration.getVariables(); + if (vars != null) { + defaults.putAll(vars); + } + parser.setVariables(defaults); + // 设置到全局配置中 + configuration.setVariables(defaults); + } + } +``` + +### 解析 settings 元素 + +`setteings` 元素中的各子元素定义了 `MyBatis` 的运行时行为,例如: + +```xml + + + + + + + + + + ... + +``` + +这些配置在 `Configuration` 类中都有对应的 `setter` 方法。`settings` 元素的解析方法对配置进行了验证: + +```java + private Properties settingsAsProperties(XNode context) { + if (context == null) { + return new Properties(); + } + // 获取子元素配置 + Properties props = context.getChildrenAsProperties(); + // Check that all settings are known to the configuration class + // 获取 Configuration 类的相关信息 + MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); + for (Object key : props.keySet()) { + if (!metaConfig.hasSetter(String.valueOf(key))) { + // 验证对应的 setter 方法存在,保证配置是有效的 + throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); + } + } + return props; + } +``` + +如果不存在对应的配置,会抛出 `BuilderException` 异常,如果自定义配置都是生效的,随后会调用 `settingsElement` 方法将这些运行时行为设置到全局配置中。 + +### 解析 typeAliases 元素 + +`typeAliases` 元素用于定义类别名: + +```xml + + + + + +``` + +如果使用 `package` 元素注册别名,则对应包下的所有类都会注册到 `TypeAliasRegistry` 别名注册容器中;如果使用 `typeAlias` 元素,则会注册指定类到别名容器中。注册逻辑如下,如果没有指定别名,则优先从类的 `Alias` 注解获取别名,如果未在类上定义,则默认使用简单类名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从 Alias 注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } +``` + +### 解析 plugins 元素 + +插件是 `MyBatis` 提供的扩展机制之一,通过添加自定义插件可以实现在 `SQL` 执行过程中的某个时机进行拦截。 `plugins` 元素用于定义调用拦截器: + +```xml + + + + + +``` + +指定的 `interceptor` 需要实现 `org.apache.ibatis.plugin.Interceptor` 接口,在创建对象后被加到全局配置过滤器链中: + +```java + private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + // 获取 interceptor 属性 + String interceptor = child.getStringAttribute("interceptor"); + // 从子元素中读取属性配置 + Properties properties = child.getChildrenAsProperties(); + // 加载指定拦截器并创建实例 + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); + interceptorInstance.setProperties(properties); + // 加入全局配置拦截器链 + configuration.addInterceptor(interceptorInstance); + } + } + } +``` + +`objectFactory`、 `objectWrapperFactory`、`reflectorFactory` 元素的解析方式与 `plugins` 元素类似 ,指定的子类对象创建后被设置到全局对象中。 + +### 解析 environments 元素 + +在实际生产中,一个项目可能会分为多个不同的环境,通过配置`enviroments` 元素可以定义不同的数据环境,并在运行时使用指定的环境: + +```xml + + + + + + + + + + + + + + ... + + +``` + +在解析过程中,只有被 `default` 属性指定的数据环境才会被加载: + +```java + private void environmentsElement(XNode context) throws Exception { + if (context != null) { + if (environment == null) { + // 获取指定的数据源名 + environment = context.getStringAttribute("default"); + } + for (XNode child : context.getChildren()) { + // 环境配置 id + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + // 加载指定环境配置 + // 解析 transactionManager 元素并创建事务工厂实例 + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + // 解析 dataSource 元素并创建数据源工厂实例 + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + // 创建数据源 + DataSource dataSource = dsFactory.getDataSource(); + // 创建环境 + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + // 将环境配置信息设置到全局配置中 + configuration.setEnvironment(environmentBuilder.build()); + } + } + } + } + + /** + * 解析 transactionManager 元素并创建事务工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private TransactionFactory transactionManagerElement(XNode context) throws Exception { + if (context != null) { + // 指定事务工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载事务工厂并创建实例 + TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a TransactionFactory."); + } + + /** + * 解析 dataSource 元素并创建数据源工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private DataSourceFactory dataSourceElement(XNode context) throws Exception { + if (context != null) { + // 指定数据源工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载数据源工厂并创建实例 + DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a DataSourceFactory."); + } +``` + +### 解析 databaseIdProvider 元素 + +`MyBatis` 支持通过 `databaseIdProvider` 元素来指定支持的数据库的 `databaseId`,这样在映射配置文件中指定 `databaseId` 就能够与对应的数据源进行匹配: + +```xml + + + + + +``` + +在根据指定类型解析出对应的 `DatabaseIdProvider` 后,`MyBatis` 会根据数据源获取对应的厂商信息: + +```java + private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + String type = context.getStringAttribute("type"); + // awful patch to keep backward compatibility + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + // 从子元素读取属性配置 + Properties properties = context.getChildrenAsProperties(); + // 加载数据库厂商信息配置类并创建实例 + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + // 获取数据库厂商标识 + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + configuration.setDatabaseId(databaseId); + } + } +``` + +因为 `DB_VENDOR` 被指定为 `VendorDatabaseIdProvider` 的别名,所以默认的获取厂商信息的逻辑如下,当通过 `property` 属性指定了数据库产品名则使用指定的名称,否则使用数据库元信息对应的产品名。 + +```java + /** + * 根据数据源获取对应的厂商信息 + * + * @param dataSource + * @return + */ + @Override + public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + return getDatabaseName(dataSource); + } catch (Exception e) { + LogHolder.log.error("Could not get a databaseId from dataSource", e); + } + return null; + } + + @Override + public void setProperties(Properties p) { + this.properties = p; + } + + /** + * 如果传入的属性配置包含当前数据库产品名,返回指定的值,否则返回数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseName(DataSource dataSource) throws SQLException { + String productName = getDatabaseProductName(dataSource); + if (this.properties != null) { + for (Map.Entry property : properties.entrySet()) { + if (productName.contains((String) property.getKey())) { + return (String) property.getValue(); + } + } + // no match, return null + return null; + } + return productName; + } + + /** + * 获取数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseProductName(DataSource dataSource) throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getDatabaseProductName(); + } finally { + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + // ignored + } + } + } + } +``` + +### 解析 typeHandlers 元素 + +`typeHandlers` 元素用于配置自定义类型转换器: + +```xml + + + +``` + +如果配置的是 `package` 元素,则会将包下的所有类注册为类型转换器;如果配置的是 `typeHandler` 元素,则会根据 `javaType`、`jdbcType`、`handler` 属性注册类型转换器。 + +```java + private void typeHandlerElement(XNode parent) { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包下的类作为类型转换器,如果声明了 MappedTypes 注解则注册为指定 java 类型的转换器 + String typeHandlerPackage = child.getStringAttribute("name"); + typeHandlerRegistry.register(typeHandlerPackage); + } else { + // 获取相关属性 + String javaTypeName = child.getStringAttribute("javaType"); + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + // 加载指定 java 类型类对象 + Class javaTypeClass = resolveClass(javaTypeName); + // 加载指定 JDBC 类型并创建实例 + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + // 加载指定类型转换器类对象 + Class typeHandlerClass = resolveClass(handlerTypeName); + if (javaTypeClass != null) { + // 注册类型转换器 + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } + } +``` + +### 解析 mappers 元素 + +`mappers` 元素用于定义 `Mapper` 映射文件和 `Mapper` 调用接口: + +```xml + + + + + + +``` + +如果定义的是 `mapper` 元素并指定了 `class` 属性,或定义了 `package` 元素,则会将指定类型在 `MapperRegistry` 中注册为 `Mapper` 接口,并使用 `MapperAnnotationBuilder` 对接口方法进行解析;如果定义的是 `mapper` 元素并指定了 `resource`、或 `url` 属性,则会使用 `XMLMapperBuilder` 解析。对于 `Mapper` 接口和映射文件将在下一章进行分析。 + +```java + private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包名下的类为 Mapper 接口 + String mapperPackage = child.getStringAttribute("name"); + configuration.addMappers(mapperPackage); + } else { + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + if (resource != null && url == null && mapperClass == null) { + // 加载指定资源 + ErrorContext.instance().resource(resource); + InputStream inputStream = Resources.getResourceAsStream(resource); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url != null && mapperClass == null) { + // 加载指定 URL + ErrorContext.instance().resource(url); + InputStream inputStream = Resources.getUrlAsStream(url); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url == null && mapperClass != null) { + // 注册指定类为 Mapper 接口 + Class mapperInterface = Resources.classForName(mapperClass); + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } + } +``` + +## 小结 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,通常 `MyBatis` 启动时会使用此类解析配置文件获取运行时行为。 + +- `org.apache.ibatis.builder.BaseBuilder`:为 `MyBatis` 初始化过程提供一系列工具方法。如别名转换、类型转换、类加载等。 +- `org.apache.ibatis.builder.xml.XMLConfigBuilder`:`XML` 配置解析入口。 +- `org.apache.ibatis.session.Configuration`:`MyBatis` 全局配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。 +- `org.apache.ibatis.mapping.VendorDatabaseIdProvider`:根据数据源获取对应的厂商信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..1e51141 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,641 @@ +在上章的配置解析中可以看到 `MyBatis` 在解析完运行时行为相关配置后会继续解析 `Mapper` 映射文件和接口,其中参数映射的解析入口为 `XMLMapperBuilder` 。 + +## 映射文件解析 + +`XMLMapperBuilder` 调用 `parse` 方法解析 `Mapper` 映射文件。 + +```java + public void parse() { + if (!configuration.isResourceLoaded(resource)) { + // 解析 mapper 元素 + configurationElement(parser.evalNode("/mapper")); + // 加入已解析队列 + configuration.addLoadedResource(resource); + // Mapper 映射文件与对应 namespace 的接口进行绑定 + bindMapperForNamespace(); + } + + // 重新引用配置 + parsePendingResultMaps(); + parsePendingCacheRefs(); + parsePendingStatements(); + } +``` + +## 解析 mapper 元素 + +`mapper` 元素通常需要指定 `namespace` 用于唯一区别映射文件,不同映射文件支持通过其它映射文件的 `namespace` 来引用其配置。`mapper` 元素下可以配置二级缓存(`cache`、`cache-ref`)、返回值映射(`resultMap`)、`sql fragments`(`sql`)、`statement`(`select`、`insert`、`update`、`delete`)等,`MyBatis` 源码提供 `mybatis-mapper.xsd` 文件用于规范映射文件书写规则。 + +```java + private void configurationElement(XNode context) { + try { + // 获取元素对应的 namespace 名称 + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + // 设置 Mapper 文件对应的 namespace 名称 + builderAssistant.setCurrentNamespace(namespace); + // 解析 cache-ref 元素 + cacheRefElement(context.evalNode("cache-ref")); + // 解析 cache 元素,会覆盖 cache-ref 配置 + cacheElement(context.evalNode("cache")); + // 解析 parameterMap 元素(废弃) + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + // 解析 resultMap 元素 + resultMapElements(context.evalNodes("/mapper/resultMap")); + // 解析 sql 元素 + sqlElement(context.evalNodes("/mapper/sql")); + // 解析 select|insert|update|delete 元素 + buildStatementFromContext(context.evalNodes("select|insert|update|delete")); + } catch (Exception e) { + throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); + } + } +``` + +## 解析 cache 元素 + +如果要为某命名空间开启二级缓存功能,可以通过配置 `cache` 元素,示例配置如下: + +```xml + + + +``` + +`cache` 元素的解析逻辑如下: + +```java + private void cacheElement(XNode context) { + if (context != null) { + // 获取缓存类型,默认为 PERPETUAL + String type = context.getStringAttribute("type", "PERPETUAL"); + // Configuration 构造方法中已为默认的缓存实现注册别名,从别名转换器中获取类对象 + Class typeClass = typeAliasRegistry.resolveAlias(type); + // 获取失效类型,默认为 LRU + String eviction = context.getStringAttribute("eviction", "LRU"); + Class evictionClass = typeAliasRegistry.resolveAlias(eviction); + // 缓存刷新时间间隔 + Long flushInterval = context.getLongAttribute("flushInterval"); + // 缓存项大小 + Integer size = context.getIntAttribute("size"); + // 是否将序列化成二级制数据 + boolean readWrite = !context.getBooleanAttribute("readOnly", false); + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + boolean blocking = context.getBooleanAttribute("blocking", false); + // 从子元素中加载属性 + Properties props = context.getChildrenAsProperties(); + // 创建缓存配置 + builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); + } + } +``` + +在第三章基础支持模块中已经详细介绍了 `MyBatis` 实现的各种缓存。从 `cache` 元素中获取缓存参数配置后会交由 `MapperBuilderAssistant#useNewCache` 方法处理。`MapperBuilderAssistant` 方法是一个映射文件解析工具,它负责将映射文件各个元素解析的参数生成配置对象,最终设置到全局配置类 `Configuration` 中。 + +```java + public Cache useNewCache(Class typeClass, + Class evictionClass, + Long flushInterval, + Integer size, + boolean readWrite, + boolean blocking, + Properties props) { + Cache cache = new CacheBuilder(currentNamespace) + // 基础缓存配置 + .implementation(valueOrDefault(typeClass, PerpetualCache.class)) + // 失效类型,默认 LRU + .addDecorator(valueOrDefault(evictionClass, LruCache.class)) + // 定时清理缓存时间间隔 + .clearInterval(flushInterval) + // 缓存项大小 + .size(size) + // 是否将缓存系列化成二级制数据 + .readWrite(readWrite) + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + .blocking(blocking) + .properties(props) + .build(); + // 设置到全局配置中 + configuration.addCache(cache); + currentCache = cache; + return cache; + } +``` + +## 解析 cache-ref 元素 + +如果希望引用其它 `namespace` 的缓存配置,可以通过 `cache-ref` 元素配置: + +```xml + +``` + +其解析逻辑是将当前 `namespace` 与引用缓存配置的 `namespace` 在全局配置中进行绑定。 + +```java + private void cacheRefElement(XNode context) { + if (context != null) { + // 当前 namespace - 引用缓存配置的 namespace,在全局配置中进行绑定 + configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); + // 获取缓存配置解析器 + CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); + try { + // 解析获得引用的缓存配置 + cacheRefResolver.resolveCacheRef(); + } catch (IncompleteElementException e) { + // 指定引用的 namespace 缓存还未加载,暂时放入集合,等待全部 namespace 都加载完成后重新引用 + configuration.addIncompleteCacheRef(cacheRefResolver); + } + } + } +``` + +由于存在被引用配置还未被加载,因而无法从全局配置中获取的情况,`MyBatis` 定义了 `IncompleteElementException` 在此时抛出,未解析完成的缓存解析对象会被加入到全局配置中的 `incompleteCacheRefs` 集合中,用于后续处理。 + +```java + public Cache useCacheRef(String namespace) { + if (namespace == null) { + throw new BuilderException("cache-ref element requires a namespace attribute."); + } + try { + unresolvedCacheRef = true; + // 从全局配置中获取缓存配置 + Cache cache = configuration.getCache(namespace); + if (cache == null) { + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); + } + currentCache = cache; + unresolvedCacheRef = false; + return cache; + } catch (IllegalArgumentException e) { + // 可能指定引用的 namespace 缓存还未加载,抛出异常 + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); + } + } +``` + +`MyBatis` 允许 `resultMap`、`cache-ref`、`statement` 元素延迟加载,以 `cache-ref` 重新引用的方法 `parsePendingCacheRefs` 为例,其重新引用逻辑如下: + +```java + private void parsePendingCacheRefs() { + // 从全局配置中获取未解析的缓存引用配置 + Collection incompleteCacheRefs = configuration.getIncompleteCacheRefs(); + synchronized (incompleteCacheRefs) { + Iterator iter = incompleteCacheRefs.iterator(); + while (iter.hasNext()) { + try { + // 逐个重新引用缓存配置 + iter.next().resolveCacheRef(); + // 引用成功,删除集合元素 + iter.remove(); + } catch (IncompleteElementException e) { + // 引用的缓存配置不存在 + // Cache ref is still missing a resource... + } + } + } + } +``` + +## 解析 resultMap 元素 + +`resultMap` 元素用于定义结果集与结果对象(`JavaBean` 对象)之间的映射规则。`resultMap` 下除了 `discriminator` 的其它元素,都会被解析成 `ResultMapping` 对象,其解析过程如下: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 和 case 元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + Discriminator discriminator = null; + List resultMappings = new ArrayList<>(); + resultMappings.addAll(additionalResultMappings); + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } else if ("discriminator".equals(resultChild.getName())) { + // 解析 discriminator 元素 + discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); + } else { + // 解析 resultMap 元素下的其它元素 + List flags = new ArrayList<>(); + if ("id".equals(resultChild.getName())) { + // id 元素增加标志 + flags.add(ResultFlag.ID); + } + // 解析元素映射关系 + resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); + } + } + String id = resultMapNode.getStringAttribute("id", + resultMapNode.getValueBasedIdentifier()); + // extend resultMap id + String extend = resultMapNode.getStringAttribute("extends"); + // 是否设置自动映射 + Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); + // resultMap 解析器 + ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); + try { + // 解析生成 ResultMap 对象并设置到全局配置中 + return resultMapResolver.resolve(); + } catch (IncompleteElementException e) { + // 异常稍后处理 + configuration.addIncompleteResultMap(resultMapResolver); + throw e; + } + } +``` + +以下从不同元素配置的角度分别分析 `MyBatis` 解析规则。 + +### id & result + +```xml + + +``` + +`id` 和 `result` 元素都会将一个列的值映射到一个简单数据类型字段,不同的是 `id` 元素对应对象的标识属性,在比较对象时会用到。此外还可以设置 `typeHandler` 属性用于自定义类型转换逻辑。 + +`buildResultMappingFromContext` 方法负责将 `resultMap` 子元素解析为 `ResultMapping` 对象: + +```java + /** + * 解析 resultMap 子元素映射关系 + */ + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + String property; + if (flags.contains(ResultFlag.CONSTRUCTOR)) { + // constructor 子元素,通过 name 获取参数名 + property = context.getStringAttribute("name"); + } else { + property = context.getStringAttribute("property"); + } + // 列名 + String column = context.getStringAttribute("column"); + // java 类型 + String javaType = context.getStringAttribute("javaType"); + // jdbc 类型 + String jdbcType = context.getStringAttribute("jdbcType"); + // 嵌套的 select id + String nestedSelect = context.getStringAttribute("select"); + // 获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", + processNestedResultMappings(context, Collections.emptyList(), resultType)); + // 获取指定的不为空才创建实例的列 + String notNullColumn = context.getStringAttribute("notNullColumn"); + // 列前缀 + String columnPrefix = context.getStringAttribute("columnPrefix"); + // 类型转换器 + String typeHandler = context.getStringAttribute("typeHandler"); + // 集合的多结果集 + String resultSet = context.getStringAttribute("resultSet"); + // 指定外键对应的列名 + String foreignColumn = context.getStringAttribute("foreignColumn"); + // 是否懒加载 + boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); + // 加载返回值类型 + Class javaTypeClass = resolveClass(javaType); + // 加载类型转换器类型 + Class> typeHandlerClass = resolveClass(typeHandler); + // 加载 jdbc 类型对象 + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); + } +``` + +`ResultMapping` 对象保存了列名与 `JavaBean` 中字段名的对应关系,并明确了 `Java` 类型和 `JDBC` 类型,如果指定了类型转换器则使用指定的转化器将结果集字段映射为 `Java` 对象字段;否则根据 `Java` 类型和 `JDBC` 类型到类型转换器注册类中寻找适合的类型转换器。最终影响映射的一系列因素都被保存到 `ResultMapping` 对象中并加入到全局配置。 + +### constructor + +使用 `constructor` 元素允许返回值对象使用指定的构造方法创建而不是默认的构造方法。 + +```xml + + + + +``` + +在解析 `constructor` 元素时,`MyBatis` 特别指定了将 `constructor` 子元素解析为 `ResultMapping` 对象。 + +```java + ... + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } + ... + } + ... + + /** + * 解析 constructor 元素下的子元素 + */ + private void processConstructorElement(XNode resultChild, Class resultType, List resultMappings) throws Exception { + // 获取子元素 + List argChildren = resultChild.getChildren(); + for (XNode argChild : argChildren) { + List flags = new ArrayList<>(); + // 标明此元素在 constructor 元素中 + flags.add(ResultFlag.CONSTRUCTOR); + if ("idArg".equals(argChild.getName())) { + // 此元素映射 id + flags.add(ResultFlag.ID); + } + // 解析子元素映射关系 + resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); + } + } +``` + +解析 `constructor` 子元素的逻辑与解析 `id`、`result` 元素的逻辑是一致的。 + +##### association + +`association` 用于配置非简单类型的映射关系。其不仅支持在当前查询中做嵌套映射: + +```xml + + + + +``` + +也支持通过 `select` 属性嵌套其它查询: + +```xml + + + + +``` + +在解析 `resultMap` 子元素方法 `buildResultMappingFromContext` 的逻辑中,`MyBatis` 会尝试获取每个子元素的 `resultMap` 属性,如果未指定,则会调用 `processNestedResultMappings` 方法,在此方法中对于 `asociation` 元素来说,如果指定了 `select` 属性,则映射时只需要获取对应 `select` 语句的 `resultMap`;如果未指定,则需要重新调用 `resultMapElement` 解析结果集映射关系。 + +```java + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + ... + // 尝试获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.emptyList(), resultType)); + ... + } + + /** + * 处理嵌套的 resultMap,获取 id + * + * @param context + * @param resultMappings + * @param enclosingType + * @return + * @throws Exception + */ + private String processNestedResultMappings(XNode context, List resultMappings, Class enclosingType) throws Exception { + if ("association".equals(context.getName()) + || "collection".equals(context.getName()) + || "case".equals(context.getName())) { + if (context.getStringAttribute("select") == null) { + // 如果是 association、collection 或 case 元素并且没有 select 属性 + // collection 元素没有指定 resultMap 或 javaType 属性,需要验证 resultMap 父元素对应的返回值类型是否有对当前集合的赋值入口 + validateCollection(context, enclosingType); + ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType); + return resultMap.getId(); + } + } + return null; + } +``` + +在 `resultMapElement` 方法中调用了 `inheritEnclosingType` 针对未定义返回类型的元素的返回值类型解析: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ... + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 等元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + } + + protected Class inheritEnclosingType(XNode resultMapNode, Class enclosingType) { + if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // association 元素没有指定 resultMap 属性 + String property = resultMapNode.getStringAttribute("property"); + if (property != null && enclosingType != null) { + // 根据反射信息确定字段的类型 + MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory()); + return metaResultType.getSetterType(property); + } + } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // case 元素返回值属性与 resultMap 父元素相同 + return enclosingType; + } + return null; + } +``` + +在 `inheritEnclosingType` 方法中,如果未定义 `resultMap` 属性,则会通过反射工具 `MetaClass` 获取父元素 `resultMap` 返回类型的类信息,`association` 元素对应的字段名称的 `setter` 方法的参数就是其返回值类型。由此 `association` 元素必定可以关联到其结果集映射。 + +### collection + +`collection` 元素用于配置集合属性的映射关系,其解析过程与 `association` 元素大致相同,重要的区别是 `collection` 元素使用 `ofType` 属性指定集合元素类型,例如需要映射的 `Java` 集合为 `List users`,则配置示例如下: + +```xml + +``` + +`javaType`熟悉指定的是字段类型,而 `ofType` 属性指定的才是需要映射的集合存储的类型。 + +### discriminator + +`discriminator` 支持对一个查询可能出现的不同结果集做鉴别,根据具体的条件为 `resultMap` 动态选择返回值类型。 + +```xml + + + + +``` + +`discriminator` 元素的解析是通过 `processDiscriminatorElement` 方法完成的: + +```java + private Discriminator processDiscriminatorElement(XNode context, Class resultType, List resultMappings) throws Exception { + // 获取需要鉴别的字段的相关信息 + String column = context.getStringAttribute("column"); + String javaType = context.getStringAttribute("javaType"); + String jdbcType = context.getStringAttribute("jdbcType"); + String typeHandler = context.getStringAttribute("typeHandler"); + Class javaTypeClass = resolveClass(javaType); + Class> typeHandlerClass = resolveClass(typeHandler); + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + Map discriminatorMap = new HashMap<>(); + // 解析 discriminator 的 case 子元素 + for (XNode caseChild : context.getChildren()) { + // 解析不同列值对应的不同 resultMap + String value = caseChild.getStringAttribute("value"); + String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType)); + discriminatorMap.put(value, resultMap); + } + return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); + } +``` + +### 创建 ResultMap + +在 `resultMap` 各子元素解析完成,`ResultMapResolver` 负责将生成的 `ResultMapping` 集合解析为 `ResultMap` 对象: + +```java + public ResultMap addResultMap(String id, Class type, String extend, Discriminator discriminator, List resultMappings, Boolean autoMapping) { + id = applyCurrentNamespace(id, false); + extend = applyCurrentNamespace(extend, true); + + if (extend != null) { + if (!configuration.hasResultMap(extend)) { + throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'"); + } + // 获取继承的 ResultMap 对象 + ResultMap resultMap = configuration.getResultMap(extend); + List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings()); + extendedResultMappings.removeAll(resultMappings); + // Remove parent constructor if this resultMap declares a constructor. + boolean declaresConstructor = false; + for (ResultMapping resultMapping : resultMappings) { + if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { + // 当前 resultMap 指定了构造方法 + declaresConstructor = true; + break; + } + } + if (declaresConstructor) { + // 移除继承的 ResultMap 的构造器映射对象 + extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)); + } + resultMappings.addAll(extendedResultMappings); + } + ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build(); + configuration.addResultMap(resultMap); + return resultMap; + } +``` + +如果当前 `ResultMap` 指定了 `extend` 属性,`MyBatis` 会从全局配置中获取被继承的 `ResultMap` 的相关映射关系,加入到当前映射关系中。但是如果被继承的 `ResultMap` 指定了构造器映射关系,当前 `ResultMap` 会选择移除。 + +## 解析 sql 元素 + +`sql` 元素用于定义可重用的 `SQL` 代码段,这些代码段可以通过 `include` 元素进行引用。 + +```xml + + id, name, value + + + + ${alias} + + + +``` + +对 `sql` 元素的解析逻辑如下,符合 `databaseId` 要求的 `sql` 元素才会被加载到全局配置中。 + +```java +private void sqlElement(List list) { + if (configuration.getDatabaseId() != null) { + sqlElement(list, configuration.getDatabaseId()); + } + sqlElement(list, null); +} + + +/** + * 解析 sql 元素,将对应的 sql 片段设置到全局配置中 + */ +private void sqlElement(List list, String requiredDatabaseId) { + for (XNode context : list) { + String databaseId = context.getStringAttribute("databaseId"); + String id = context.getStringAttribute("id"); + id = builderAssistant.applyCurrentNamespace(id, false); + if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { + // 符合当前 databaseId 的 sql fragment,加入到全局配置中 + sqlFragments.put(id, context); + } + } +} + +/** + * 判断 sql 元素是否满足加载条件 + * + * @param id + * @param databaseId + * @param requiredDatabaseId + * @return + */ +private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { + if (requiredDatabaseId != null) { + // 如果指定了当前数据源的 databaseId + if (!requiredDatabaseId.equals(databaseId)) { + // 被解析 sql 元素的 databaseId 需要符合 + return false; + } + } else { + if (databaseId != null) { + // 全局未指定 databaseId,不会加载指定了 databaseId 的 sql 元素 + return false; + } + // skip this fragment if there is a previous one with a not null databaseId + if (this.sqlFragments.containsKey(id)) { + XNode context = this.sqlFragments.get(id); + if (context.getStringAttribute("databaseId") != null) { + return false; + } + } + } + return true; +} +``` + +## 小结 + +`MyBatis` 能够轻松实现列值转换为 Java 对象依靠的是其强大的参数映射功能,能够支持集合、关联类型、嵌套等复杂场景的映射。同时缓存配置、`sql` 片段配置,也为开发者方便的提供了配置入口。 + +- `org.apache.ibatis.builder.annotation.MapperAnnotationBuilder`:解析 `Mapper` 接口。 +- `org.apache.ibatis.builder.xml.XMLMapperBuilder`:解析 `Mapper` 文件。 +- `org.apache.ibatis.builder.MapperBuilderAssistant`:`Mapper` 文件解析工具。生成元素对象并设置到全局配置中。 +- `org.apache.ibatis.builder.CacheRefResolver`:缓存引用配置解析器,应用其它命名空间缓存配置到当前命名空间下。 +- `org.apache.ibatis.builder.IncompleteElementException`:当前映射文件引用了其它命名空间下的配置,而该配置还未加载到全局配置中时会抛出此异常。 +- `org.apache.ibatis.mapping.ResultMapping`:返回值字段映射关系对象。 +- `org.apache.ibatis.builder.ResultMapResolver`:`ResultMap` 解析器。 +- `org.apache.ibatis.mapping.ResultMap`:返回值映射对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" new file mode 100644 index 0000000..57c2231 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" @@ -0,0 +1,1382 @@ +`Mapper` 映射文件解析的最后一步是解析所有 `statement` 元素,即 `select`、`insert`、`update`、`delete` 元素,这些元素中可能会包含动态 `SQL`,即使用 `${}` 占位符或 `if`、`choose`、`where` 等元素动态组成的 `SQL`。动态 `SQL` 功能正是 `MyBatis` 强大的所在,其解析过程也是十分复杂的。 + +## 解析工具 + +为了方便 `statement` 的解析,`MyBatis` 提供了一些解析工具。 + +### Token 解析 + +`MyBatis` 支持使用 `${}` 或 `#{}` 类型的 `token` 作为动态参数,不仅文本中可以使用 `token`,`xml` 元素中的属性等也可以使用。 + +#### GenericTokenParser + +`GenericTokenParser` 是 `MyBatis` 提供的通用 `token` 解析器,其解析逻辑是根据指定的 `token` 前缀和后缀搜索 `token`,并使用传入的 `TokenHandler` 对文本进行处理。 + +```java + public String parse(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + // search open token 搜索 token 前缀 + int start = text.indexOf(openToken); + if (start == -1) { + // 没有 token 前缀,返回原文本 + return text; + } + char[] src = text.toCharArray(); + // 当前解析偏移量 + int offset = 0; + // 已解析文本 + final StringBuilder builder = new StringBuilder(); + // 当前占位符内的表达式 + StringBuilder expression = null; + while (start > -1) { + if (start > 0 && src[start - 1] == '\\') { + // 如果待解析属性前缀被转义,则去掉转义字符,加入已解析文本 + // this open token is escaped. remove the backslash and continue. + builder.append(src, offset, start - offset - 1).append(openToken); + // 更新解析偏移量 + offset = start + openToken.length(); + } else { + // found open token. let's search close token. + if (expression == null) { + expression = new StringBuilder(); + } else { + expression.setLength(0); + } + // 前缀前面的部分加入已解析文本 + builder.append(src, offset, start - offset); + // 更新解析偏移量 + offset = start + openToken.length(); + // 获取对应的后缀索引 + int end = text.indexOf(closeToken, offset); + while (end > -1) { + if (end > offset && src[end - 1] == '\\') { + // 后缀被转义,加入已解析文本 + // this close token is escaped. remove the backslash and continue. + expression.append(src, offset, end - offset - 1).append(closeToken); + offset = end + closeToken.length(); + // 寻找下一个后缀 + end = text.indexOf(closeToken, offset); + } else { + // 找到后缀,获取占位符内的表达式 + expression.append(src, offset, end - offset); + offset = end + closeToken.length(); + break; + } + } + if (end == -1) { + // 找不到后缀,前缀之后的部分全部加入已解析文本 + // close token was not found. + builder.append(src, start, src.length - start); + offset = src.length; + } else { + // 能够找到后缀,追加 token 处理器处理后的文本 + builder.append(handler.handleToken(expression.toString())); + // 更新解析偏移量 + offset = end + closeToken.length(); + } + } + // 寻找下一个前缀,重复解析表达式 + start = text.indexOf(openToken, offset); + } + if (offset < src.length) { + // 将最后的部分加入已解析文本 + builder.append(src, offset, src.length - offset); + } + // 返回解析后的文本 + return builder.toString(); + } +``` + +由于 `GenericTokenParser` 的 `token` 前后缀和具体解析逻辑都是可指定的,因此基于 `GenericTokenParser` 可以实现对不同 `token` 的定制化解析。 + +#### TokenHandler + +`TokenHandler` 是 `token` 处理器抽象接口。实现此接口可以定义 `token` 以何种方式被解析。 + +```java +public interface TokenHandler { + + /** + * 对 token 进行解析 + * + * @param content 待解析 token + * @return + */ + String handleToken(String content); +} +``` + +#### PropertyParser + +`PropertyParser` 是 `token` 解析的一种具体实现,其指定对 `${}` 类型 `token` 进行解析,具体解析逻辑由其内部类 `VariableTokenHandler` 实现: + +```java + /** + * 对 ${} 类型 token 进行解析 + * + * @param string + * @param variables + * @return + */ + public static String parse(String string, Properties variables) { + VariableTokenHandler handler = new VariableTokenHandler(variables); + GenericTokenParser parser = new GenericTokenParser("${", "}", handler); + return parser.parse(string); + } + + /** + * 根据配置属性对 ${} token 进行解析 + */ + private static class VariableTokenHandler implements TokenHandler { + + /** + * 预先设置的属性 + */ + private final Properties variables; + + /** + * 是否运行使用默认值,默认为 false + */ + private final boolean enableDefaultValue; + + /** + * 默认值分隔符号,即如待解析属性 ${key:default},key 的默认值为 default + */ + private final String defaultValueSeparator; + + private VariableTokenHandler(Properties variables) { + this.variables = variables; + this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); + this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); + } + + private String getPropertyValue(String key, String defaultValue) { + return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue); + } + + @Override + public String handleToken(String content) { + if (variables != null) { + String key = content; + if (enableDefaultValue) { + // 如待解析属性 ${key:default},key 的默认值为 default + final int separatorIndex = content.indexOf(defaultValueSeparator); + String defaultValue = null; + if (separatorIndex >= 0) { + key = content.substring(0, separatorIndex); + defaultValue = content.substring(separatorIndex + defaultValueSeparator.length()); + } + if (defaultValue != null) { + // 使用默认值 + return variables.getProperty(key, defaultValue); + } + } + if (variables.containsKey(key)) { + // 不使用默认值 + return variables.getProperty(key); + } + } + // 返回原文本 + return "${" + content + "}"; + } + } +``` + +`VariableTokenHandler` 实现了 `TokenHandler` 接口,其构造方法允许传入一组 `Properties` 用于获取 `token` 表达式的值。如果开启了使用默认值,则表达式 `${key:default}` 会在 `key` 没有映射值的时候使用 `default` 作为默认值。 + +### 特殊容器 + +#### StrictMap + +`Configuration` 中的 `StrictMap` 继承了 `HashMap`,相对于 `HashMap`,其存取键值的要求更为严格。`put` 方法不允许添加相同的 `key`,并获取最后一个 `.` 后的部分作为 `shortKey`,如果 `shortKey` 也重复了,其会向容器中添加一个 `Ambiguity` 对象,当使用 `get` 方法获取这个 `shortKey` 对应的值时,就会抛出异常。`get` 方法对于不存在的 `key` 也会抛出异常。 + +```java + public V put(String key, V value) { + if (containsKey(key)) { + // 重复 key 异常 + throw new IllegalArgumentException(name + " already contains value for " + key + + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value))); + } + if (key.contains(".")) { + // 获取最后一个 . 后的部分作为 shortKey + final String shortKey = getShortName(key); + // shortKey 不允许重复,否则在获取时异常 + if (super.get(shortKey) == null) { + super.put(shortKey, value); + } else { + super.put(shortKey, (V) new Ambiguity(shortKey)); + } + } + return super.put(key, value); + } + + public V get(Object key) { + V value = super.get(key); + if (value == null) { + // key 不存在抛异常 + throw new IllegalArgumentException(name + " does not contain value for " + key); + } + // 重复的 key 抛异常 + if (value instanceof Ambiguity) { + throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name + + " (try using the full name including the namespace, or rename one of the entries)"); + } + return value; + } +``` + +#### ContextMap + +`ContextMap` 是 `DynamicContext` 的静态内部类,用于保存 `sql` 上下文中的绑定参数。 + +```java +static class ContextMap extends HashMap { + private static final long serialVersionUID = 2977601501966151582L; + + /** + * 参数对象 + */ + private MetaObject parameterMetaObject; + + public ContextMap(MetaObject parameterMetaObject) { + this.parameterMetaObject = parameterMetaObject; + } + + @Override + public Object get(Object key) { + // 先根据 key 查找原始容器 + String strKey = (String) key; + if (super.containsKey(strKey)) { + return super.get(strKey); + } + + // 再进入参数对象查找 + if (parameterMetaObject != null) { + // issue #61 do not modify the context when reading + return parameterMetaObject.getValue(strKey); + } + + return null; + } +} +``` + +### OGNL 工具 + +#### OgnlCache + +`OGNL` 工具支持通过字符串表达式调用 `Java` 方法,但是其实现需要对 `OGNL` 表达式进行编译,为了提高性能,`MyBatis` 提供 `OgnlCache` 工具类用于对 `OGNL` 表达式编译结果进行缓存。 + +```java + /** + * 根据 ognl 表达式和参数计算值 + * + * @param expression + * @param root + * @return + */ + public static Object getValue(String expression, Object root) { + try { + Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null); + return Ognl.getValue(parseExpression(expression), context, root); + } catch (OgnlException e) { + throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); + } + } + + /** + * 编译 ognl 表达式并放入缓存 + * + * @param expression + * @return + * @throws OgnlException + */ + private static Object parseExpression(String expression) throws OgnlException { + Object node = expressionCache.get(expression); + if (node == null) { + // 编译 ognl 表达式 + node = Ognl.parseExpression(expression); + // 放入缓存 + expressionCache.put(expression, node); + } + return node; + } +``` + +#### ExpressionEvaluator + +`ExpressionEvaluator` 是 `OGNL` 表达式计算工具,`evaluateBoolean` 和 `evaluateIterable` 方法分别根据传入的表达式和参数计算出一个 `boolean` 值或一个可迭代对象。 + +```java + /** + * 计算 ognl 表达式 true / false + * + * @param expression + * @param parameterObject + * @return + */ + public boolean evaluateBoolean(String expression, Object parameterObject) { + // 根据 ognl 表达式和参数计算值 + Object value = OgnlCache.getValue(expression, parameterObject); + // true / false + if (value instanceof Boolean) { + return (Boolean) value; + } + // 不为 0 + if (value instanceof Number) { + return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; + } + // 不为 null + return value != null; + } + + /** + * 计算获得一个可迭代的对象 + * + * @param expression + * @param parameterObject + * @return + */ + public Iterable evaluateIterable(String expression, Object parameterObject) { + Object value = OgnlCache.getValue(expression, parameterObject); + if (value == null) { + throw new BuilderException("The expression '" + expression + "' evaluated to a null value."); + } + if (value instanceof Iterable) { + // 已实现 Iterable 接口 + return (Iterable) value; + } + if (value.getClass().isArray()) { + // 数组转集合 + // the array may be primitive, so Arrays.asList() may throw + // a ClassCastException (issue 209). Do the work manually + // Curse primitives! :) (JGB) + int size = Array.getLength(value); + List answer = new ArrayList<>(); + for (int i = 0; i < size; i++) { + Object o = Array.get(value, i); + answer.add(o); + } + return answer; + } + if (value instanceof Map) { + // Map 获取 entry + return ((Map) value).entrySet(); + } + throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable."); + } +``` + +## 解析逻辑 + +`MyBastis` 中调用 `XMLStatementBuilder#parseStatementNode` 方法解析单个 `statement` 元素。此方法中除了逐个获取元素属性,还对 `include` 元素、`selectKey` 元素进行解析,创建了 `sql` 生成对象 `SqlSource`,并将 `statement` 的全部信息聚合到 `MappedStatement` 对象中。 + +```java + public void parseStatementNode() { + // 获取 id + String id = context.getStringAttribute("id"); + // 自定义数据库厂商信息 + String databaseId = context.getStringAttribute("databaseId"); + + if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { + // 不符合当前数据源对应的数据厂商信息的语句不加载 + return; + } + + // 获取元素名 + String nodeName = context.getNode().getNodeName(); + // 元素名转为对应的 SqlCommandType 枚举 + SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); + // 是否为查询 + boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + // 获取 flushCache 属性,查询默认为 false,其它默认为 true + boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); + // 获取 useCache 属性,查询默认为 true,其它默认为 false + boolean useCache = context.getBooleanAttribute("useCache", isSelect); + // 获取 resultOrdered 属性,默认为 false + boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); + + // Include Fragments before parsing + XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); + // 解析 include 属性 + includeParser.applyIncludes(context.getNode()); + + // 参数类型 + String parameterType = context.getStringAttribute("parameterType"); + Class parameterTypeClass = resolveClass(parameterType); + + // 获取 Mapper 语法类型 + String lang = context.getStringAttribute("lang"); + // 默认使用 XMLLanguageDriver + LanguageDriver langDriver = getLanguageDriver(lang); + + // Parse selectKey after includes and remove them. + // 解析 selectKey 元素 + processSelectKeyNodes(id, parameterTypeClass, langDriver); + + // 获取 KeyGenerator + // Parse the SQL (pre: and were parsed and removed) + KeyGenerator keyGenerator; + String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; + keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); + if (configuration.hasKeyGenerator(keyStatementId)) { + // 获取解析完成的 KeyGenerator 对象 + keyGenerator = configuration.getKeyGenerator(keyStatementId); + } else { + // 如果开启了 useGeneratedKeys 属性,并且为插入类型的 sql 语句、配置了 keyProperty 属性,则可以批量自动设置属性 + keyGenerator = context.getBooleanAttribute("useGeneratedKeys", + configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) + ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; + } + + // 生成有效 sql 语句和参数绑定对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + // sql 类型 + StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 分批获取数据的数量 + Integer fetchSize = context.getIntAttribute("fetchSize"); + // 执行超时时间 + Integer timeout = context.getIntAttribute("timeout"); + // 参数映射 + String parameterMap = context.getStringAttribute("parameterMap"); + // 返回值类型 + String resultType = context.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + // 返回值映射 map + String resultMap = context.getStringAttribute("resultMap"); + // 结果集类型 + String resultSetType = context.getStringAttribute("resultSetType"); + ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); + // 插入、更新生成键值的字段 + String keyProperty = context.getStringAttribute("keyProperty"); + // 插入、更新生成键值的列 + String keyColumn = context.getStringAttribute("keyColumn"); + // 指定多结果集名称 + String resultSets = context.getStringAttribute("resultSets"); + + // 新增 MappedStatement + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); + } +``` + +### 语法驱动 + +`LanguageDriver` 是 `statement` 创建语法驱动,默认实现为 `XMLLanguageDriver`,其提供 `createSqlSource` 方法用于使用 `XMLScriptBuilder` 创建 `sql` 生成对象。 + +### 递归解析 include + +`include` 元素是 `statement` 元素的子元素,通过 `refid` 属性可以指向在别处定义的 `sql fragments`。 + +```java + public void applyIncludes(Node source) { + Properties variablesContext = new Properties(); + Properties configurationVariables = configuration.getVariables(); + // 拷贝全局配置中设置的额外配置属性 + Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll); + applyIncludes(source, variablesContext, false); + } + + /** + * 递归解析 statement 元素中的 include 元素 + * + * Recursively apply includes through all SQL fragments. + * @param source Include node in DOM tree + * @param variablesContext Current context for static variables with values + */ + private void applyIncludes(Node source, final Properties variablesContext, boolean included) { + if (source.getNodeName().equals("include")) { + // include 元素,从全局配置中找对应的 sql 节点并 clone + Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); + // 读取 include 子元素中的 property 元素,获取全部属性 + Properties toIncludeContext = getVariablesContext(source, variablesContext); + applyIncludes(toInclude, toIncludeContext, true); + if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { + toInclude = source.getOwnerDocument().importNode(toInclude, true); + } + source.getParentNode().replaceChild(toInclude, source); + while (toInclude.hasChildNodes()) { + toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); + } + toInclude.getParentNode().removeChild(toInclude); + } else if (source.getNodeType() == Node.ELEMENT_NODE) { + if (included && !variablesContext.isEmpty()) { + // replace variables in attribute values + // include 指向的 sql clone 节点,逐个对属性进行解析 + NamedNodeMap attributes = source.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); + } + } + // statement 元素中可能包含 include 子元素 + NodeList children = source.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + applyIncludes(children.item(i), variablesContext, included); + } + } else if (included && source.getNodeType() == Node.TEXT_NODE + && !variablesContext.isEmpty()) { + // replace variables in text node + // 替换元素值,如果使用了 ${} 占位符,会对 token 进行解析 + source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); + } + } + + /** + * 从全局配置中找对应的 sql fragment + * + * @param refid + * @param variables + * @return + */ + private Node findSqlFragment(String refid, Properties variables) { + // 解析 refid + refid = PropertyParser.parse(refid, variables); + // namespace.refid + refid = builderAssistant.applyCurrentNamespace(refid, true); + try { + // 从全局配置中找对应的 sql fragment + XNode nodeToInclude = configuration.getSqlFragments().get(refid); + return nodeToInclude.getNode().cloneNode(true); + } catch (IllegalArgumentException e) { + // sql fragments 定义在全局配置中的 StrictMap 中,获取不到会抛出异常 + throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e); + } + } + + private String getStringAttribute(Node node, String name) { + return node.getAttributes().getNamedItem(name).getNodeValue(); + } + + /** + * Read placeholders and their values from include node definition. + * + * 读取 include 子元素中的 property 元素 + * @param node Include node instance + * @param inheritedVariablesContext Current context used for replace variables in new variables values + * @return variables context from include instance (no inherited values) + */ + private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) { + Map declaredProperties = null; + // 解析 include 元素中的 property 子元素 + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node n = children.item(i); + if (n.getNodeType() == Node.ELEMENT_NODE) { + // include 运行包含 property 元素 + String name = getStringAttribute(n, "name"); + // Replace variables inside + String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext); + if (declaredProperties == null) { + declaredProperties = new HashMap<>(); + } + if (declaredProperties.put(name, value) != null) { + // 不允许添加同名属性 + throw new BuilderException("Variable " + name + " defined twice in the same include definition"); + } + } + } + if (declaredProperties == null) { + return inheritedVariablesContext; + } else { + // 聚合属性配置 + Properties newProperties = new Properties(); + newProperties.putAll(inheritedVariablesContext); + newProperties.putAll(declaredProperties); + return newProperties; + } + } +``` + +在开始解析前,从全局配置中获取全部的属性配置,如果 `include` 元素中有 `property` 元素,解析并获取键值,放入 `variablesContext` 中,在后续处理中针对可能出现的 `${}` 类型 `token` 使用 `PropertyParser` 进行解析。 + +因为解析 `statement` 元素前已经加载过 `sql` 元素,因此会根据 `include` 元素的 `refid` 属性查找对应的 `sql fragments`,如果全局配置中无法找到就会抛出异常;如果能够找到则克隆 `sql` 元素并插入到当前 `xml` 文档中。 + +### 解析 selectKey + +`selectKey` 用于指定 `sql` 在 `insert` 或 `update` 语句执行前或执行后生成或获取列值,在 `MyBatis` 中 `selectKey` 也被当做 `statement` 语句进行解析并设置到全局配置中。单个 `selectKey` 元素会以`SelectKeyGenerator` 对象的形式进行保存用于后续调用。 + +```java + private void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameterTypeClass, LanguageDriver langDriver, String databaseId) { + // 返回值类型 + String resultType = nodeToHandle.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 对应字段名 + String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); + // 对应列名 + String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); + // 是否在父sql执行前执行 + boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); + + //defaults + boolean useCache = false; + boolean resultOrdered = false; + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + Integer fetchSize = null; + Integer timeout = null; + boolean flushCache = false; + String parameterMap = null; + String resultMap = null; + ResultSetType resultSetTypeEnum = null; + + // 创建 sql 生成对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); + SqlCommandType sqlCommandType = SqlCommandType.SELECT; + + // 将 KeyGenerator 生成 sql 作为 MappedStatement 加入全局对象 + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); + + id = builderAssistant.applyCurrentNamespace(id, false); + + MappedStatement keyStatement = configuration.getMappedStatement(id, false); + // 包装为 SelectKeyGenerator 对象 + configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); + } +``` + +在 `selectKey` 解析完成后,按指定的 `namespace` 规则从全局配置中获取 `SelectKeyGenerator` 对象,等待创建 `MappedStatement` 对象。如果未指定 `selectKey` 元素,但是全局配置中开启了 `useGeneratedKeys`,并且指定 `insert` 元素的 `useGeneratedKeys` 属性为 `true`,则 `MyBatis` 会指定 `Jdbc3KeyGenerator` 作为 `useGeneratedKeys` 的默认实现。 + +### 创建 sql 生成对象 + +#### SqlSource + +`SqlSource` 是 `sql` 生成抽象接口,其提供 `getBoundSql` 方法用于根据参数生成有效 `sql` 语句和参数绑定对象 `BoundSql`。在生成 `statement` 元素的解析结果 `MappedStatement` 对象前,需要先创建 `sql` 生成对象,即 `SqlSource` 对象。 + +```java +public interface SqlSource { + + /** + * 根据参数生成有效 sql 语句和参数绑定对象 + * + * @param parameterObject + * @return + */ + BoundSql getBoundSql(Object parameterObject); + +} +``` + +#### SqlNode + +`SqlNode` 是 `sql` 节点抽象接口。`sql` 节点指的是 `statement` 中的组成部分,如果简单文本、`if` 元素、`where` 元素等。`SqlNode` 提供 `apply` 方法用于判断当前 `sql` 节点是否可以加入到生效的 `sql` 语句中。 + +```java +public interface SqlNode { + + /** + * 根据条件判断当前 sql 节点是否可以加入到生效的 sql 语句中 + * + * @param context + * @return + */ + boolean apply(DynamicContext context); +} +``` + +![SqlNode 体系](https://wch853.github.io/img/mybatis/SqlNode%E4%BD%93%E7%B3%BB.png) + +#### DynamicContext + +`DynamicContext` 是动态 `sql` 上下文,用于保存绑定参数和生效 `sql` 节点。`DynamicContext` 使用 `ContextMap` 作为参数绑定容器。由于动态 `sql` 是根据参数条件组合生成 `sql`,`DynamicContext` 还提供了对 `sqlBuilder` 修改和访问方法,用于添加有效 `sql` 节点和生成 `sql` 文本。 + +```java + /** + * 生效的 sql 部分,以空格相连 + */ + private final StringJoiner sqlBuilder = new StringJoiner(" "); + + public void appendSql(String sql) { + sqlBuilder.add(sql); + } + + public String getSql() { + return sqlBuilder.toString().trim(); + } +``` + +#### 节点解析 + +将 `statement` 元素转为 `sql` 生成对象依赖于 `LanguageDriver` 的 `createSqlSource` 方法,此方法中创建 `XMLScriptBuilder` 对象,并调用 `parseScriptNode` 方法对 `sql` 组成节点逐个解析并进行组合。 + +```java + public SqlSource parseScriptNode() { + // 递归解析各 sql 节点 + MixedSqlNode rootSqlNode = parseDynamicTags(context); + SqlSource sqlSource; + if (isDynamic) { + // 动态 sql + sqlSource = new DynamicSqlSource(configuration, rootSqlNode); + } else { + // 原始文本 sql + sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); + } + return sqlSource; + } + + /** + * 处理 statement 各 SQL 组成部分,并进行组合 + */ + protected MixedSqlNode parseDynamicTags(XNode node) { + // SQL 各组成部分 + List contents = new ArrayList<>(); + // 遍历子元素 + NodeList children = node.getNode().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + XNode child = node.newXNode(children.item(i)); + if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { + // 解析 sql 文本 + String data = child.getStringBody(""); + TextSqlNode textSqlNode = new TextSqlNode(data); + if (textSqlNode.isDynamic()) { + // 判断是否为动态 sql,包含 ${} 占位符即为动态 sql + contents.add(textSqlNode); + isDynamic = true; + } else { + // 静态 sql 元素 + contents.add(new StaticTextSqlNode(data)); + } + } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 + // 如果是子元素 + String nodeName = child.getNode().getNodeName(); + // 获取支持的子元素语法处理器 + NodeHandler handler = nodeHandlerMap.get(nodeName); + if (handler == null) { + throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); + } + // 根据子元素标签类型使用对应的处理器处理子元素 + handler.handleNode(child, contents); + // 包含标签元素,认定为动态 SQL + isDynamic = true; + } + } + return new MixedSqlNode(contents); + } +``` + +`parseDynamicTags` 方法会对 `sql` 各组成部分进行分解,如果 `statement` 元素包含 `${}` 类型 `token` 或含有标签子元素,则认为当前 `statement` 是动态 `sql`,随后 `isDynamic` 属性会被设置为 `true`。对于文本节点,如 `sql` 纯文本和仅含 `${}` 类型 `token` 的文本,会被包装为 `StaticTextSqlNode` 或 `TextSqlNode` 加入到 `sql` 节点容器中,而其它元素类型的 `sql` 节点会经过 `NodeHandler` 的 `handleNode` 方法处理过之后才能加入到节点容器中。`nodeHandlerMap` 定义了不同动态 `sql` 元素节点与 `NodeHandler` 的关系: + +```java + private void initNodeHandlerMap() { + nodeHandlerMap.put("trim", new TrimHandler()); + nodeHandlerMap.put("where", new WhereHandler()); + nodeHandlerMap.put("set", new SetHandler()); + nodeHandlerMap.put("foreach", new ForEachHandler()); + nodeHandlerMap.put("if", new IfHandler()); + nodeHandlerMap.put("choose", new ChooseHandler()); + nodeHandlerMap.put("when", new IfHandler()); + nodeHandlerMap.put("otherwise", new OtherwiseHandler()); + nodeHandlerMap.put("bind", new BindHandler()); + } +``` + +##### MixedSqlNode + +`MixedSqlNode` 中定义了一个 `SqlNode` 集合,用于保存 `statement` 中包含的全部 `sql` 节点。其生成有效 `sql` 的逻辑为逐个判断节点是否有效。 + +```java + /** + * 组合 SQL 各组成部分 + * + * @author Clinton Begin + */ + public class MixedSqlNode implements SqlNode { + + /** + * SQL 各组装成部分 + */ + private final List contents; + + public MixedSqlNode(List contents) { + this.contents = contents; + } + + @Override + public boolean apply(DynamicContext context) { + // 逐个判断各个 sql 节点是否能生效 + contents.forEach(node -> node.apply(context)); + return true; + } + } +``` + +##### StaticTextSqlNode + +`StaticTextSqlNode` 中仅包含静态 `sql` 文本,在组装时会直接追加到 `sql` 上下文的有效 `sql` 中: + +```java + @Override + public boolean apply(DynamicContext context) { + context.appendSql(text); + return true; + } +``` + +##### TextSqlNode + +`TextSqlNode` 中的 `sql` 文本包含 `${}` 类型 `token`,使用 `GenericTokenParser` 搜索到 `token` 后会使用 `BindingTokenParser` 对 `token` 进行解析,解析后的文本会被追加到生效 `sql` 中。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 搜索 ${} 类型 token 节点 + GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); + // 解析 token 并追加解析后的文本到生效 sql 中 + context.appendSql(parser.parse(text)); + return true; + } + + private GenericTokenParser createParser(TokenHandler handler) { + return new GenericTokenParser("${", "}", handler); + } + + private static class BindingTokenParser implements TokenHandler { + + private DynamicContext context; + private Pattern injectionFilter; + + public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { + this.context = context; + this.injectionFilter = injectionFilter; + } + + @Override + public String handleToken(String content) { + // 获取绑定参数 + Object parameter = context.getBindings().get("_parameter"); + if (parameter == null) { + context.getBindings().put("value", null); + } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { + context.getBindings().put("value", parameter); + } + // 计算 ognl 表达式的值 + Object value = OgnlCache.getValue(content, context.getBindings()); + String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" + checkInjection(srtValue); + return srtValue; + } + } +``` + +##### IfSqlNode + +`if` 标签用于在 `test` 条件生效时才追加标签内的文本。 + +```xml + ... + + AND user_id = #{userId} + +``` + +`IfSqlNode` 保存了 `if` 元素下的节点内容和 `test` 表达式,在生成有效 `sql` 时会根据 `OGNL` 工具计算 `test` 表达式是否生效。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 根据 test 表达式判断当前节点是否生效 + if (evaluator.evaluateBoolean(test, context.getBindings())) { + contents.apply(context); + return true; + } + return false; + } +``` + +##### TrimSqlNode + +`trim` 标签用于解决动态 `sql` 中由于条件不同不能拼接正确语法的问题。 + +```xml + SELECT * FROM test + + + a = #{a} + + + OR b = #{b} + + + AND c = #{c} + + +``` + +如果没有 `trim` 标签,这个 `statement` 的有效 `sql` 最终可能会是这样的: + +```sql +SELECT * FROM test OR b = #{b} +``` + +但是加上 `trim` 标签,生成的 `sql` 语法是正确的: + +```sql +SELECT * FROM test WHERE b = #{b} +``` + +`prefix` 属性用于指定 `trim` 节点生成的 `sql` 语句的前缀,`prefixOverrides` 则会指定生成的 `sql` 语句的前缀需要去除的部分,多个需要去除的前缀可以使用 `|` 隔开。`suffix` 与 `suffixOverrides` 的功能类似,但是作用于后缀。 + +`TrimSqlNode` 首先调用 `parseOverrides` 对 `prefixOverrides` 和 `suffixOverrides` 进行解析,通过 `|` 分隔,分别加入字符串集合。 + +```java + private static List parseOverrides(String overrides) { + if (overrides != null) { + // 解析 token,按 | 分隔 + final StringTokenizer parser = new StringTokenizer(overrides, "|", false); + final List list = new ArrayList<>(parser.countTokens()); + while (parser.hasMoreTokens()) { + // 保存为字符串集合 + list.add(parser.nextToken().toUpperCase(Locale.ENGLISH)); + } + return list; + } + return Collections.emptyList(); + } +``` + +在调用包含的 `SqlNode` 的 `apply` 方法后还会调用 `FilteredDynamicContext` 的 `applyAll` 方法处理前缀和后缀。 + +```java + @Override + public boolean apply(DynamicContext context) { + FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); + boolean result = contents.apply(filteredDynamicContext); + // 加上前缀和和后缀,并去除多余字段 + filteredDynamicContext.applyAll(); + return result; + } +``` + +对于已经生成的 `sql` 文本,分别根据规则加上和去除指定前缀和后缀。 + +```java + public void applyAll() { + sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); + String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); + if (trimmedUppercaseSql.length() > 0) { + // 加上前缀和和后缀,并去除多余字段 + applyPrefix(sqlBuffer, trimmedUppercaseSql); + applySuffix(sqlBuffer, trimmedUppercaseSql); + } + delegate.appendSql(sqlBuffer.toString()); + } + + private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { + if (!prefixApplied) { + prefixApplied = true; + if (prefixesToOverride != null) { + // 文本最前去除多余字段 + for (String toRemove : prefixesToOverride) { + if (trimmedUppercaseSql.startsWith(toRemove)) { + sql.delete(0, toRemove.trim().length()); + break; + } + } + } + // 在文本最前插入前缀和空格 + if (prefix != null) { + sql.insert(0, " "); + sql.insert(0, prefix); + } + } + } + + private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { + if (!suffixApplied) { + suffixApplied = true; + if (suffixesToOverride != null) { + // 文本最后去除多余字段 + for (String toRemove : suffixesToOverride) { + if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) { + int start = sql.length() - toRemove.trim().length(); + int end = sql.length(); + sql.delete(start, end); + break; + } + } + } + // 文本最后插入空格和后缀 + if (suffix != null) { + sql.append(" "); + sql.append(suffix); + } + } + } +``` + +##### WhereSqlNode + +`where` 元素与 `trim` 元素的功能类似,区别在于 `where` 元素不提供属性配置可以处理的前缀和后缀。 + +```xml + ... + + ... + +``` + +`WhereSqlNode` 继承了 `TrimSqlNode`,并指定了需要添加和删除的前缀。 + +```java +public class WhereSqlNode extends TrimSqlNode { + + private static List prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); + + public WhereSqlNode(Configuration configuration, SqlNode contents) { + // 默认添加 WHERE 前缀,去除 AND、OR 等前缀 + super(configuration, contents, "WHERE", prefixList, null, null); + } + +} +``` + +因此,生成的 `sql` 语句会自动在最前加上 `WHERE`,并去除前缀中包含的 `AND`、`OR` 等字符串。 + +##### SetSqlNode + +`set` 标签用于 `update` 语句中。 + +```xml + UPDATE test + + + a = #{a}, + + + b = #{b} + + +``` + +`SetSqlNode` 同样继承自 `TrimSqlNode`,并指定默认添加 `SET` 前缀,去除 `,` 前缀和后缀。 + +```java +public class SetSqlNode extends TrimSqlNode { + + private static final List COMMA = Collections.singletonList(","); + + public SetSqlNode(Configuration configuration,SqlNode contents) { + // 默认添加 SET 前缀,去除 , 前缀和后缀 + super(configuration, contents, "SET", COMMA, null, COMMA); + } + +} +``` + +##### ForEachSqlNode + +`foreach` 元素用于指定对集合循环添加 `sql` 语句。 + +```xml +... + + AND itm = #{item} AND idx #{index} + +``` + +`ForEachSqlNode` 解析生成有效 `sql` 的逻辑如下,除了计算 `collection` 表达式的值、添加前缀、后缀外,还将参数与索引进行了绑定。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 获取绑定参数 + Map bindings = context.getBindings(); + // 计算 ognl 表达式获取可迭代对象 + final Iterable iterable = evaluator.evaluateIterable(collectionExpression, bindings); + if (!iterable.iterator().hasNext()) { + return true; + } + boolean first = true; + // 添加动态语句前缀 + applyOpen(context); + // 迭代索引 + int i = 0; + for (Object o : iterable) { + DynamicContext oldContext = context; + // 首个元素 + if (first || separator == null) { + context = new PrefixedContext(context, ""); + } else { + context = new PrefixedContext(context, separator); + } + int uniqueNumber = context.getUniqueNumber(); + // Issue #709 + if (o instanceof Map.Entry) { + // entry 集合项索引为 key,集合项为 value + @SuppressWarnings("unchecked") + Map.Entry mapEntry = (Map.Entry) o; + applyIndex(context, mapEntry.getKey(), uniqueNumber); + applyItem(context, mapEntry.getValue(), uniqueNumber); + } else { + // 绑定集合项索引关系 + applyIndex(context, i, uniqueNumber); + // 绑定集合项关系 + applyItem(context, o, uniqueNumber); + } + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); + if (first) { + first = !((PrefixedContext) context).isPrefixApplied(); + } + context = oldContext; + i++; + } + // 添加动态语句后缀 + applyClose(context); + // 移除原始的表达式 + context.getBindings().remove(item); + context.getBindings().remove(index); + return true; + } + + /** + * 绑定集合项索引关系 + * + * @param context + * @param o + * @param i + */ + private void applyIndex(DynamicContext context, Object o, int i) { + if (index != null) { + context.bind(index, o); + context.bind(itemizeItem(index, i), o); + } + } + + /** + * 绑定集合项关系 + * + * @param context + * @param o + * @param i + */ + private void applyItem(DynamicContext context, Object o, int i) { + if (item != null) { + context.bind(item, o); + context.bind(itemizeItem(item, i), o); + } + } +``` + +对于循环中的 `#{}` 类型 `token`,`ForEachSqlNode` 在内部类 `FilteredDynamicContext` 中定义了解析规则: + +```java + @Override + public void appendSql(String sql) { + GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); + if (itemIndex != null && newContent.equals(content)) { + newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); + } + return "#{" + newContent + "}"; + }); + + delegate.appendSql(parser.parse(sql)); + } +``` + +类似 `idx = #{index} AND itm = #{item}` 会被替换为 `idx = #{__frch_index_1} AND itm = #{__frch_item_1}`,而 `ForEachSqlNode` 也做了参数与索引的绑定,因此在替换时可以快速绑定参数。 + +##### ChooseSqlNode + +`choose` 元素用于生成带默认 `sql` 文本的语句,当 `when` 元素中的条件都不生效,就可以使用 `otherwise` 元素的默认文本。 + +```xml + ... + + + AND a = #{a} + + + AND b = #{b} + + +``` + +`ChooseSqlNode` 是由 `choose` 节点和 `otherwise` 节点组合而成的,在生成有效 `sql` 于语句时会逐个计算 `when` 节点的 `test` 表达式,如果返回 `true` 则生效当前 `when` 语句中的 `sql`。如果均不生效则使用 `otherwise` 语句对应的默认 `sql` 文本。 + +```java +@Override +public boolean apply(DynamicContext context) { + // when 节点根据 test 表达式判断是否生效 + for (SqlNode sqlNode : ifSqlNodes) { + if (sqlNode.apply(context)) { + return true; + } + } + + // when 节点如果都未生效,且存在 otherwise 节点,则使用 otherwise 节点 + if (defaultSqlNode != null) { + defaultSqlNode.apply(context); + return true; + } + return false; +} +``` + +##### VarDeclSqlNode + +`bind` 元素用于绑定一个 `OGNL` 表达式到一个动态 `sql` 变量中。 + +```xml + +``` + +`VarDeclSqlNode` 会计算表达式的值并将参数名和值绑定到参数容器中。 + +```java +@Override +public boolean apply(DynamicContext context) { + // 解析 ognl 表达式 + final Object value = OgnlCache.getValue(expression, context.getBindings()); + // 绑定参数 + context.bind(name, value); + return true; +} +``` + +### 创建解析对象与生成可执行 sql + +`statemen` 解析完毕后会创建 `MappedStatement` 对象,`statement` 的相关属性以及生成的 `sql` 创建对象都会被保存到该对象中。`MappedStatement` 还提供了 `getBoundSql` 方法用于获取可执行 sql 和参数绑定对象,即 `BoundSql` 对象。 + +```java +public BoundSql getBoundSql(Object parameterObject) { + // 生成可执行 sql 和参数绑定对象 + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + // 获取参数映射 + List parameterMappings = boundSql.getParameterMappings(); + if (parameterMappings == null || parameterMappings.isEmpty()) { + boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); + } + + // check for nested result maps in parameter mappings (issue #30) + // 检查是否有嵌套的 resultMap + for (ParameterMapping pm : boundSql.getParameterMappings()) { + String rmId = pm.getResultMapId(); + if (rmId != null) { + ResultMap rm = configuration.getResultMap(rmId); + if (rm != null) { + hasNestedResultMaps |= rm.hasNestedResultMaps(); + } + } + } + + return boundSql; +} +``` + +`BoundSql` 对象由 `DynamicSqlSource` 的 `getBoundSql` 方法生成,在验证各个 `sql` 节点,生成了有效 `sql` 后会继续调用 `SqlSourceBuilder` 将 `sql` 解析为 `StaticSqlSource`,即可执行 `sql`。 + +```java + @Override + public BoundSql getBoundSql(Object parameterObject) { + DynamicContext context = new DynamicContext(configuration, parameterObject); + // 验证各 sql 节点,生成有效 sql + rootSqlNode.apply(context); + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); + // 将生成的 sql 文本解析为 StaticSqlSource + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + context.getBindings().forEach(boundSql::setAdditionalParameter); + return boundSql; + } +``` + +此时的 `sql` 文本中仍包含 `#{}` 类型 `token`,需要通过 `ParameterMappingTokenHandler` 进行解析。 + +```java + public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { + ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); + // 创建 #{} 类型 token 搜索对象 + GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); + // 解析 token + String sql = parser.parse(originalSql); + // 创建静态 sql 生成对象,并绑定参数 + return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); + } +``` + +`token` 的具体解析逻辑为根据表达式的参数名生成对应的参数映射对象,并将表达式转为预编译 `sql` 的占位符 `?`。 + +```java + @Override + public String handleToken(String content) { + // 创建参数映射对象 + parameterMappings.add(buildParameterMapping(content)); + // 将表达式转为预编译 sql 占位符 + return "?"; + } +``` + +最终解析完成的 `sql` 与参数映射关系集合包装为 `StaticSqlSource` 对象,该对象在随后的逻辑中通过构造方法创建了 `BoundSql` 对象。 + +## 接口解析 + +除了使用 `xml` 方式配置 `statement`,`MyBatis` 同样支持使用 `Java` 注解配置。但是相对于 `xml` 的映射方式,将动态 `sql` 写在 `Java` 代码中是不合适的。如果在配置文件中指定了需要注册 `Mapper` 接口的类或包,`MyBatis` 会扫描相关类进行注册;在 `Mapper` 文件解析完成后也会尝试加载 `namespace` 的同名类,如果存在,则注册为 `Mapper` 接口。 + +无论是绑定还是直接注册 `Mapper` 接口,都是调用 `MapperAnnotationBuilder#parse` 方法来解析的。此方法中的解析方式与上述 `xml` 解析方式大致相同,区别只在于相关配置参数是从注解中获取而不是从 `xml` 元素属性中获取。 + +```java + public void addMapper(Class type) { + if (type.isInterface()) { + if (hasMapper(type)) { + // 不允许相同接口重复注册 + throw new BindingException("Type " + type + " is already known to the MapperRegistry."); + } + boolean loadCompleted = false; + try { + knownMappers.put(type, new MapperProxyFactory<>(type)); + // It's important that the type is added before the parser is run + // otherwise the binding may automatically be attempted by the + // mapper parser. If the type is already known, it won't try. + MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); + parser.parse(); + loadCompleted = true; + } finally { + if (!loadCompleted) { + knownMappers.remove(type); + } + } + } + } +``` + +## 小结 + +`statement` 解析的最终目的是为每个 `statement` 创建一个 `MappedStatement` 对象保存相关定义,在 `sql` 执行时根据传入参数动态获取可执行 `sql` 和参数绑定对象。 + +- `org.apache.ibatis.builder.xml.XMLStatementBuilder`:解析 `Mapper` 文件中的 `select|insert|update|delete` 元素。 +- `org.apache.ibatis.parsing.GenericTokenParser.GenericTokenParser`:搜索指定格式 `token` 并进行解析。 +- `org.apache.ibatis.parsing.TokenHandler`:`token` 处理器抽象接口。定义 `token` 以何种方式被解析。 +- `org.apache.ibatis.parsing.PropertyParser`:`${}` 类型 `token` 解析器。 +- `org.apache.ibatis.session.Configuration.StrictMap`:封装 `HashMap`,对键值存取有严格要求。 +- `org.apache.ibatis.builder.xml.XMLIncludeTransformer`:`include` 元素解析器。 +- `org.apache.ibatis.mapping.SqlSource`:`sql` 生成抽象接口。根据传入参数生成有效 `sql` 语句和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.XMLScriptBuilder`:解析 `statement` 各个 `sql` 节点并进行组合。 +- `org.apache.ibatis.scripting.xmltags.SqlNode`:`sql` 节点抽象接口。用于判断当前 `sql` 节点是否可以加入到生效的 sql 语句中。 +- `org.apache.ibatis.scripting.xmltags.DynamicContext`:动态 `sql` 上下文。用于保存绑定参数和生效 `sql` 节点。 +- `org.apache.ibatis.scripting.xmltags.OgnlCache`:`ognl` 缓存工具,缓存表达式编译结果。 +- `org.apache.ibatis.scripting.xmltags.ExpressionEvaluator`:`ognl` 表达式计算工具。 +- `org.apache.ibatis.scripting.xmltags.MixedSqlNode`:`sql` 节点组合对象。 +- `org.apache.ibatis.scripting.xmltags.StaticTextSqlNode`:静态 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TextSqlNode`:`${}` 类型 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.IfSqlNode`:`if` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TrimSqlNode`:`trim` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.WhereSqlNode`:`where` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.SetSqlNode`:`set` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ForEachSqlNode`:`foreach` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ChooseSqlNode`:`choose` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.VarDeclSqlNode`:`bind` 元素 `sql` 节点对象。 +- `org.apache.ibatis.mapping.MappedStatement`:`statement` 解析对象。 +- `org.apache.ibatis.mapping.BoundSql`:可执行 `sql` 和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.DynamicSqlSource`:根据参数动态生成有效 `sql` 和绑定参数。 +- `org.apache.ibatis.builder.SqlSourceBuilder`:解析 `#{}` 类型 `token` 并绑定参数对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" new file mode 100644 index 0000000..54b81e8 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" @@ -0,0 +1,765 @@ +执行器 `Executor` 是 `MyBatis` 的核心接口之一,接口层提供的相关数据库操作,都是基于 `Executor` 的子类实现的。 + +![Executor 体系](https://wch853.github.io/img/mybatis/Executor%E4%BD%93%E7%B3%BB.png) + +## 创建执行器 + +在创建 `sql` 会话时,`MyBatis` 会调用 `Configuration#newExecutor` 方法创建执行器。枚举类 `ExecutorType` 定义了三种执行器类型,即 `SIMPLE`、`REUSE` 和 `Batch`,这些执行器的主要区别在于: + +- `SIMPLE` 在每次执行完成后都会关闭 `statement` 对象; +- `REUSE` 会在本地维护一个容器,当前 `statement` 创建完成后放入容器中,当下次执行相同的 `sql` 时会复用 `statement` 对象,执行完毕后也不会关闭; +- `BATCH` 会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作。 + +```java +public Executor newExecutor(Transaction transaction, ExecutorType executorType) { + // 默认类型为 simple + executorType = executorType == null ? defaultExecutorType : executorType; + executorType = executorType == null ? ExecutorType.SIMPLE : executorType; + Executor executor; + if (ExecutorType.BATCH == executorType) { + executor = new BatchExecutor(this, transaction); + } else if (ExecutorType.REUSE == executorType) { + executor = new ReuseExecutor(this, transaction); + } else { + executor = new SimpleExecutor(this, transaction); + } + if (cacheEnabled) { + // 如果全局缓存打开,使用 CachingExecutor 代理执行器 + executor = new CachingExecutor(executor); + } + // 应用插件 + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + +执行器创建后,如果全局缓存配置是有效的,则会将执行器装饰为 `CachingExecutor`。 + +## 基础执行器 + +`SimpleExecutor`、`ReuseExecutor`、`BatchExecutor` 均继承自 `BaseExecutor`。`BaseExecutor` 实现了 `Executor` 的全部方法,对缓存、事务、连接处理等提供了一些模板方法,但是针对具体的数据库操作留下了四个抽象方法交由子类实现。 + +```java + /** + * 更新 + */ + protected abstract int doUpdate(MappedStatement ms, Object parameter) + throws SQLException; + + /** + * 刷新 statement + */ + protected abstract List doFlushStatements(boolean isRollback) + throws SQLException; + + /** + * 查询 + */ + protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) + throws SQLException; + + /** + * 查询获取游标对象 + */ + protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) + throws SQLException; +``` + +基础执行器的查询逻辑如下: + +```java + @Override + public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + if (queryStack == 0 && ms.isFlushCacheRequired()) { + // 非嵌套查询且设置强制刷新时清除缓存 + clearLocalCache(); + } + List list; + try { + queryStack++; + list = resultHandler == null ? (List) localCache.getObject(key) : null; + if (list != null) { + // 缓存不为空,组装存储过程出参 + handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); + } else { + // 无本地缓存,执行数据库查询 + list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); + } + } finally { + queryStack--; + } + if (queryStack == 0) { + for (DeferredLoad deferredLoad : deferredLoads) { + deferredLoad.load(); + } + // issue #601 + deferredLoads.clear(); + if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { + // issue #482 + // 全局配置语句不共享缓存 + clearLocalCache(); + } + } + return list; + } + + /** + * 查询本地缓存,组装存储过程结果集 + */ + private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) { + if (ms.getStatementType() == StatementType.CALLABLE) { + // 存储过程类型,查询缓存 + final Object cachedParameter = localOutputParameterCache.getObject(key); + if (cachedParameter != null && parameter != null) { + final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter); + final MetaObject metaParameter = configuration.newMetaObject(parameter); + for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { + if (parameterMapping.getMode() != ParameterMode.IN) { + // 参数类型为 OUT 或 INOUT 的,组装结果集 + final String parameterName = parameterMapping.getProperty(); + final Object cachedValue = metaCachedParameter.getValue(parameterName); + metaParameter.setValue(parameterName, cachedValue); + } + } + } + } + } + + /** + * 查询数据库获取结果集 + */ + private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + List list; + // 放一个 placeHolder 标志 + localCache.putObject(key, EXECUTION_PLACEHOLDER); + try { + // 执行查询 + list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); + } finally { + localCache.removeObject(key); + } + // 查询结果集放入本地缓存 + localCache.putObject(key, list); + if (ms.getStatementType() == StatementType.CALLABLE) { + // 如果是存储过程查询,将存储过程结果集放入本地缓存 + localOutputParameterCache.putObject(key, parameter); + } + return list; + } +``` + +执行查询时 `MyBatis` 首先会根据 `CacheKey` 查询本地缓存,`CacheKey` 由本次查询的参数生成,本地缓存由 `PerpetualCache` 实现,这就是 `MyBatis` 的一级缓存。一级缓存维护对象 `localCache` 是基础执行器的本地变量,因此只有相同 `sql` 会话的查询才能共享一级缓存。当一级缓存中没有对应的数据,基础执行器最终会调用 `doQuery` 方法交由子类去获取数据。 + +而执行 `update` 等其它操作时,则会首先清除本地的一级缓存再交由子类执行具体的操作: + +```java + @Override + public int update(MappedStatement ms, Object parameter) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + // 清空本地缓存 + clearLocalCache(); + // 调用子类执行器逻辑 + return doUpdate(ms, parameter); + } +``` + +## 简单执行器 + +简单执行器是 `MyBatis` 的默认执行器。其封装了对 `JDBC` 的操作,对于查询方法 `doQuery` 的实现如下,其主要包括创建 `statement` 处理器、创建 `statement`、执行查询、关闭 `statement`。 + +```java + @Override + public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + // 创建 statement 处理器 + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); + // 创建 statement + stmt = prepareStatement(handler, ms.getStatementLog()); + // 执行查询 + return handler.query(stmt, resultHandler); + } finally { + // 关闭 statement + closeStatement(stmt); + } + } +``` + +### 创建 statement 处理器 + +全局配置类 `Configuration` 提供了方法 `newStatementHandler` 用于创建 `statement` 处理器: + +```java + /** + * 创建 statement 处理器 + */ + public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // StatementHandler 包装对象,根据 statement 类型创建代理处理器,并将实际操作委托给代理处理器处理 + StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); + // 应用插件 + statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); + return statementHandler; + } +``` + +实际每次创建的 `statement` 处理器对象都是由 `RoutingStatementHandler` 创建的,`RoutingStatementHandler` 根据当前 `MappedStatement` 的类型创建具体的 `statement` 类型处理器。`StatementType` 定义了 `3` 个 `statement` 类型枚举,分别对应 `JDBC` 的普通语句、预编译语句和存储过程语句。 + +```java + public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 根据 statement 类型选择对应的 statementHandler + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } + } +``` + +### 创建 statement + +简单执行器中的 `Statement` 对象是根据上述步骤中生成的 `statement` 处理器获取的。 + +```java + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + // 获取代理连接对象 + Connection connection = getConnection(statementLog); + // 创建 statement 对象 + stmt = handler.prepare(connection, transaction.getTimeout()); + // 设置 statement 参数 + handler.parameterize(stmt); + return stmt; + } + + @Override + public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { + ErrorContext.instance().sql(boundSql.getSql()); + Statement statement = null; + try { + // 从连接中创建 statement 对象 + statement = instantiateStatement(connection); + // 设置超时时间 + setStatementTimeout(statement, transactionTimeout); + // 设置分批获取数据数量 + setFetchSize(statement); + return statement; + } catch (SQLException e) { + closeStatement(statement); + throw e; + } catch (Exception e) { + closeStatement(statement); + throw new ExecutorException("Error preparing statement. Cause: " + e, e); + } + } +``` + +### 执行查询 + +创建 `statement` 对象完成即可通过 `JDBC` 的 `API` 执行数据库查询,并从 `statement` 对象中获取查询结果,根据配置进行转换。 + +```java + @Override + public List query(Statement statement, ResultHandler resultHandler) throws SQLException { + PreparedStatement ps = (PreparedStatement) statement; + // 执行查询 + ps.execute(); + // 处理结果集 + return resultSetHandler.handleResultSets(ps); + } + + /** + * 处理结果集 + */ + @Override + public List handleResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); + // 多结果集 + final List multipleResults = new ArrayList<>(); + int resultSetCount = 0; + ResultSetWrapper rsw = getFirstResultSet(stmt); + // statement 对应的所有 ResultMap 对象 + List resultMaps = mappedStatement.getResultMaps(); + int resultMapCount = resultMaps.size(); + // 验证结果集不为空时,ResultMap 数量不能为 0 + validateResultMapsCount(rsw, resultMapCount); + while (rsw != null && resultMapCount > resultSetCount) { + // 逐个获取 ResultMap + ResultMap resultMap = resultMaps.get(resultSetCount); + // 转换结果集,放到 multipleResults 容器中 + handleResultSet(rsw, resultMap, multipleResults, null); + // 获取下一个待处理的结果集 + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + + // statement 配置的多结果集类型 + String[] resultSets = mappedStatement.getResultSets(); + if (resultSets != null) { + while (rsw != null && resultSetCount < resultSets.length) { + ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); + if (parentMapping != null) { + String nestedResultMapId = parentMapping.getNestedResultMapId(); + ResultMap resultMap = configuration.getResultMap(nestedResultMapId); + handleResultSet(rsw, resultMap, null, parentMapping); + } + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + } + + return collapseSingleResultList(multipleResults); + } +``` + +### 关闭连接 + +查询完成后 `statement` 对象会被关闭。 + +```java + /** + * 关闭 statement + * + * @param statement + */ + protected void closeStatement(Statement statement) { + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + // ignore + } + } + } +``` + +简单执行器中的其它数据库执行方法与 `doQuery` 方法实现类似。 + +## 复用执行器 + +`ReuseExecutor` 相对于 `SimpleExecutor` 实现了对 `statment` 对象的复用,其在本地维护了 `statementMap` 用于保存 `sql` 语句和 `statement` 对象的关系。当调用 `prepareStatement` 方法获取 `statement` 对象时首先会查找本地是否有对应的 `statement` 对象,如果有则进行复用,负责重新创建并将 `statement` 对象放入本地缓存。 + +```java + /** + * 创建 statement 对象 + * + * @param handler + * @param statementLog + * @return + * @throws SQLException + */ + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + BoundSql boundSql = handler.getBoundSql(); + String sql = boundSql.getSql(); + if (hasStatementFor(sql)) { + // 如果本地容器中包含当前 sql 对应的 statement 对象,进行复用 + stmt = getStatement(sql); + applyTransactionTimeout(stmt); + } else { + Connection connection = getConnection(statementLog); + stmt = handler.prepare(connection, transaction.getTimeout()); + putStatement(sql, stmt); + } + handler.parameterize(stmt); + return stmt; + } + + private boolean hasStatementFor(String sql) { + try { + return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed(); + } catch (SQLException e) { + return false; + } + } + + private Statement getStatement(String s) { + return statementMap.get(s); + } + + private void putStatement(String sql, Statement stmt) { + statementMap.put(sql, stmt); + } +``` + +提交或回滚会导致执行器调用 `doFlushStatements` 方法,复用执行器会因此批量关闭本地的 `statement` 对象。 + +```java + /** + * 批量关闭 statement 对象 + * + * @param isRollback + * @return + */ + @Override + public List doFlushStatements(boolean isRollback) { + for (Statement stmt : statementMap.values()) { + closeStatement(stmt); + } + statementMap.clear(); + return Collections.emptyList(); + } +``` + +## 批量执行器 + +`BatchExecutor` 相对于 `SimpleExecutor` ,其 `update` 操作是批量执行的。 + +```java + @Override + public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { + final Configuration configuration = ms.getConfiguration(); + final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); + final BoundSql boundSql = handler.getBoundSql(); + final String sql = boundSql.getSql(); + final Statement stmt; + if (sql.equals(currentSql) && ms.equals(currentStatement)) { + // 如果当前 sql 与上次传入 sql 相同且为相同的 MappedStatement,复用 statement 对象 + int last = statementList.size() - 1; + // 获取最后一个 statement 对象 + stmt = statementList.get(last); + // 设置超时时间 + applyTransactionTimeout(stmt); + // 设置参数 + handler.parameterize(stmt);//fix Issues 322 + // 获取批量执行结果对象 + BatchResult batchResult = batchResultList.get(last); + batchResult.addParameterObject(parameterObject); + } else { + // 创建新的 statement 对象 + Connection connection = getConnection(ms.getStatementLog()); + stmt = handler.prepare(connection, transaction.getTimeout()); + handler.parameterize(stmt); //fix Issues 322 + currentSql = sql; + currentStatement = ms; + statementList.add(stmt); + batchResultList.add(new BatchResult(ms, sql, parameterObject)); + } + // 执行 JDBC 批量添加 sql 语句操作 + handler.batch(stmt); + return BATCH_UPDATE_RETURN_VALUE; + } + + /** + * 批量执行 sql + * + * @param isRollback + * @return + * @throws SQLException + */ + @Override + public List doFlushStatements(boolean isRollback) throws SQLException { + try { + // 批量执行结果 + List results = new ArrayList<>(); + if (isRollback) { + return Collections.emptyList(); + } + for (int i = 0, n = statementList.size(); i < n; i++) { + Statement stmt = statementList.get(i); + applyTransactionTimeout(stmt); + BatchResult batchResult = batchResultList.get(i); + try { + // 设置执行影响行数 + batchResult.setUpdateCounts(stmt.executeBatch()); + MappedStatement ms = batchResult.getMappedStatement(); + List parameterObjects = batchResult.getParameterObjects(); + KeyGenerator keyGenerator = ms.getKeyGenerator(); + if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) { + Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator; + // 设置数据库生成的主键 + jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects); + } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141 + for (Object parameter : parameterObjects) { + keyGenerator.processAfter(this, ms, stmt, parameter); + } + } + // Close statement to close cursor #1109 + closeStatement(stmt); + } catch (BatchUpdateException e) { + StringBuilder message = new StringBuilder(); + message.append(batchResult.getMappedStatement().getId()) + .append(" (batch index #") + .append(i + 1) + .append(")") + .append(" failed."); + if (i > 0) { + message.append(" ") + .append(i) + .append(" prior sub executor(s) completed successfully, but will be rolled back."); + } + throw new BatchExecutorException(message.toString(), e, results, batchResult); + } + results.add(batchResult); + } + return results; + } finally { + for (Statement stmt : statementList) { + closeStatement(stmt); + } + currentSql = null; + statementList.clear(); + batchResultList.clear(); + } + } +``` + +执行器提交或回滚事务时会调用 `doFlushStatements`,从而批量执行提交的 `sql` 语句并最终批量关闭 `statement` 对象。 + +## 缓存执行器与二级缓存 + +`CachingExecutor` 对基础执行器进行了装饰,其作用就是为查询提供二级缓存。所谓的二级缓存是由 `CachingExecutor` 维护的,相对默认内置的一级缓存而言的缓存。二者区别如下: + +- 一级缓存由基础执行器维护,且不可关闭。二级缓存的配置是开发者可干预的,在 `xml` 文件或注解中针对 `namespace` 的缓存配置就是二级缓存配置。 +- 一级缓存在执行器中维护,即不同 `sql` 会话不能共享一级缓存。二级缓存则是根据 `namespace` 维护,不同 `sql` 会话是可以共享二级缓存的。 + +`CachingExecutor` 中的方法大多是通过直接调用其代理的执行器来实现的,而查询操作则会先查询二级缓存。 + +```java + /** + * 缓存事务管理器 + */ + private final TransactionalCacheManager tcm = new TransactionalCacheManager(); + + @Override + public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) + throws SQLException { + // 查询二级缓存配置 + Cache cache = ms.getCache(); + if (cache != null) { + flushCacheIfRequired(ms); + if (ms.isUseCache() && resultHandler == null) { + // 当前 statement 配置使用二级缓存 + ensureNoOutParams(ms, boundSql); + @SuppressWarnings("unchecked") + List list = (List) tcm.getObject(cache, key); + if (list == null) { + // 二级缓存中没用数据,调用代理执行器 + list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + // 将查询结果放入二级缓存 + tcm.putObject(cache, key, list); // issue #578 and #116 + } + return list; + } + } + // 无二级缓存配置,调用代理执行器获取结果 + return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + } + + private void flushCacheIfRequired(MappedStatement ms) { + Cache cache = ms.getCache(); + if (cache != null && ms.isFlushCacheRequired()) { + // 存在 namespace 对应的缓存配置,且当前 statement 配置了刷新缓存,执行清空缓存操作 + // 非 select 语句配置了默认刷新 + tcm.clear(cache); + } + } +``` + +如果对应的 `statement` 的二级缓存配置有效,则会先通过缓存事务管理器 `TransactionalCacheManager` 查询二级缓存,如果没有命中则查询一级缓存,仍没有命中才会执行数据库查询。 + +### 缓存事务管理器 + +缓存执行器对二级缓存的维护是基于缓存事务管理器 `TransactionalCacheManager` 的,其内部维护了一个 `Map` 容器,用于保存 `namespace` 缓存配置与事务缓存对象的映射关系。 + +```java +public class TransactionalCacheManager { + + /** + * 缓存配置 - 缓存事务对象 + */ + private final Map transactionalCaches = new HashMap<>(); + + /** + * 清除缓存 + * + * @param cache + */ + public void clear(Cache cache) { + getTransactionalCache(cache).clear(); + } + + /** + * 获取缓存 + */ + public Object getObject(Cache cache, CacheKey key) { + return getTransactionalCache(cache).getObject(key); + } + + /** + * 写缓存 + */ + public void putObject(Cache cache, CacheKey key, Object value) { + getTransactionalCache(cache).putObject(key, value); + } + + /** + * 缓存提交 + */ + public void commit() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.commit(); + } + } + + /** + * 缓存回滚 + */ + public void rollback() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.rollback(); + } + } + + /** + * 获取或新建事务缓存对象 + */ + private TransactionalCache getTransactionalCache(Cache cache) { + return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); + } + +} +``` + +缓存配置映射的事务缓存对象就是前文中提到过的事务缓存装饰器 `TransactionalCache`。`getTransactionalCache` 会从维护容器中查找对应的事务缓存对象,如果找不到就创建一个事务缓存对象,即通过事务缓存对象装饰当前缓存配置。 + +查询缓存时,如果缓存未命中,则将对应的 `key` 放入未命中队列,执行数据库查询完毕后写缓存时并不是立刻写到缓存配置的本地容器中,而是暂时放入待提交队列中,当触发事务提交时才将提交队列中的缓存数据写到缓存配置中。如果发生回滚,则提交队列中的数据会被清空,从而保证了数据的一致性。 + +```java + @Override + public Object getObject(Object key) { + // issue #116 + Object object = delegate.getObject(key); + if (object == null) { + // 放入未命中缓存的 key 的队列 + entriesMissedInCache.add(key); + } + // issue #146 + if (clearOnCommit) { + return null; + } else { + return object; + } + } + + @Override + public void putObject(Object key, Object object) { + // 缓存先写入待提交容器 + entriesToAddOnCommit.put(key, object); + } + + /** + * 事务提交 + */ + public void commit() { + if (clearOnCommit) { + delegate.clear(); + } + // 提交缓存 + flushPendingEntries(); + reset(); + } + + /** + * 事务回滚 + */ + public void rollback() { + unlockMissedEntries(); + reset(); + } + + /** + * 事务提交,提交待提交的缓存。 + */ + private void flushPendingEntries() { + for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { + delegate.putObject(entry.getKey(), entry.getValue()); + } + for (Object entry : entriesMissedInCache) { + if (!entriesToAddOnCommit.containsKey(entry)) { + delegate.putObject(entry, null); + } + } + } + + /** + * 事务回滚,清理未命中缓存。 + */ + private void unlockMissedEntries() { + for (Object entry : entriesMissedInCache) { + try { + delegate.removeObject(entry); + } catch (Exception e) { + log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + + "Consider upgrading your cache adapter to the latest version. Cause: " + e); + } + } + } +``` + +### 二级缓存与一级缓存的互斥性 + +使用二级缓存要求无论是否配置了事务自动提交,在执行完成后, `sql` 会话必须手动提交事务才能触发事务缓存管理器维护缓存到缓存配置中,否则二级缓存无法生效。而缓存执行器在触发事务提交时,不仅会调用事务缓存管理器提交,还会调用代理执行器提交事务: + +```java + @Override + public void commit(boolean required) throws SQLException { + // 代理执行器提交 + delegate.commit(required); + // 事务缓存管理器提交 + tcm.commit(); + } +``` + +代理执行器的事务提交方法继承自 `BaseExecutor`,其 `commit` 方法中调用了 `clearLocalCache` 方法清除本地一级缓存。因此二级缓存和一级缓存的使用是互斥的。 + +```java + @Override + public void commit(boolean required) throws SQLException { + if (closed) { + throw new ExecutorException("Cannot commit, transaction is already closed"); + } + // 清除本地一级缓存 + clearLocalCache(); + flushStatements(); + if (required) { + transaction.commit(); + } + } +``` + +## 小结 + +`MyBatis` 提供若干执行器封装底层 `JDBC` 操作和结果集转换,并嵌入 `sql` 会话维度的一级缓存和 `namespace` 维度的二级缓存。接口层可以通过调用不同类型的执行器来完成 `sql` 相关操作。 + +- `org.apache.ibatis.executor.Executor`:数据库操作执行器抽象接口。 +- `org.apache.ibatis.executor.BaseExecutor`:执行器基础抽象实现。 +- `org.apache.ibatis.executor.SimpleExecutor`:简单类型执行器。 +- `org.apache.ibatis.executor.ReuseExecutor`:`statement` 复用执行器。 +- `org.apache.ibatis.executor.BatchExecutor`:批量执行器。 +- `org.apache.ibatis.executor.CachingExecutor`:二级缓存执行器。 +- `org.apache.ibatis.executor.statement.StatementHandler`:`statement` 处理器抽象接口。 +- `org.apache.ibatis.executor.statement.BaseStatementHandler`:`statement` 处理器基础抽象实现。 +- `org.apache.ibatis.executor.statement.RoutingStatementHandler`:`statement` 处理器路由对象。 +- `org.apache.ibatis.executor.statement.SimpleStatementHandler`:简单 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.PreparedStatementHandler`:预编译 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.CallableStatementHandler`:存储过程 `statement` 处理器。 +- `org.apache.ibatis.cache.TransactionalCacheManager`:缓存事务管理器。 + diff --git a/docs/source/spring-mvc/1-overview.md b/docs/source/spring-mvc/1-overview.md new file mode 100644 index 0000000..5dc737d --- /dev/null +++ b/docs/source/spring-mvc/1-overview.md @@ -0,0 +1,1444 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 先从DispatcherServlet说起 + +有Servlet基础的同学应该都知道,前端的每一个请求都会由一个Servlet来处理。在最原始的Java Web开发中,我们需要写自己的Servlet,然后再标注上这个Servlet是处理什么URL路径的,这样当一个HTTP请求到来时,就可以根据请求的路径找到我们的Servlet,再调用这个Servlet来处理请求。 + +在SpringMVC的开发中,我们需要配一个特殊的Servlet叫`DispatcherServlet`(这个东西是Spring帮我们写好的),然后我们再为`DispatcherServlet`配置映射的URL路径为`/`。其中`/`是个通配符,代表它能处理所有URL的请求。这代表所有的HTTP请求都会打到了`DispatcherServlet`上,那么此时我们的开发就由: + +![](http://img.topjavaer.cn/img/202311230846421.png) + +转为了: + +![](http://img.topjavaer.cn/img/202311230846478.png) + + + +其中上图的**后端处理**就是我们自己写的Controller里的每个方法。 + +这样就有了一个问题,**为什么要那么做?为什么要将所有的请求都打到一个Servlet上再由这个Servlet分发?** + +原因很简单,**就是Spring想掌控一切**。Spring想拦截所有的HTTP请求并对每个请求做一定的处理,**让我们能够尽量少的关心HTTP请求和响应,尽可能多的关心业务**。 + +比如,我们往往会写这样一个创建用户的请求: + +```java +@RestController +public class UserController{ + @PostMapping("/user") + public User createUser(@RequestBody User user){ + //... + } +} +``` + +上述Controller中,我们的参数User对象是如何从HTTP请求中解析到的,又是如何将返回给前端的数据写进HTTP响应的,这些我们都不需要关心,这些其实都是`DispatcherServlet`这个类帮我们做了。 + +有了上面这些思想后,我们来看下`DispatcherServlet`类的继承结构: + +![](http://img.topjavaer.cn/img/202311230847898.png) + + + +可以看到`DispatcherServlet`是一个Servlet实现类。而我们又知道处理Http请求的Servlet必须是`HttpServlet`,`HttpServlet`内规定了`doGet()、doPost()、doPut()`等方法,其中`FrameworkServlet`类(上图的倒数第二个类)重写了这些方法,它对这些方法的重写都一模一样: + +```java +public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { + //... + @Override + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + @Override + protected final void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + @Override + protected final void doPut(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + //... +} +``` + +都是调用`processRequest()`方法,而`processRequest()`方法又会调用`doService()`方法。`doService()`是`FrameworkServlet`的抽象方法,这个方法由`DispatcherServlet`实现,`DispatcherServlet#doService()`又调用了`DispatcherServlet#doDispatch()`方法。其时序图如下: + +![image-20220807223638836](http://img.topjavaer.cn/img/202311230846720.png) + + + +其中,**`DispatcherServlet#doDispatch()`**是最核心的方法,**对于SpringMVC处理请求的源码分析往往也是对`DispatcherServlet#doDispatch()`的分析**。 + +## 2. doDispatch做了什么 + +简单来讲,`doDispatch()`做了四件事: + +1. 根据HTTP请求的信息找到我们的处理方法(包括方法的适配器,我们一会说) +2. 使用适配器,根据HTTP请求信息和我们的处理方法,解析出请求参数,并执行我们的处理方法。 +3. 将处理方法的返回结果进行处理 +4. 视图解析 + +其中我们的处理方法就是Controller层里面每一个标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法。 + +`doDispatch()`源码中这四步流程的具体执行为: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +我们将`doDispatch()`中比较重要的源码拿了出来,这样看起来更加清晰明了,这些重要的源码就分别对应我们上面说的4个步骤。下面我们会一一说一下每个步骤SpringMVC都做了什么。 + +## 3. 第一步 找到处理方法 + +要想处理请求,必然要先找到能处理这个请求的方法。SpringMVC分为两步来寻找,**第一步是找到我们的Controller里自己写的那个方法,称作handler(处理器)。第二步是通过这个handler找到对应的适配器,称作handlerAdapter。** + +### 3.1 找到handler + +#### 3.1.1 HandlerMapping + +先介绍一个接口`HandlerMapping`,它翻译过来是请求映射处理器。我们可以这样理解: + +对于后端来讲,我们把HTTP请求分为几种:比如请求静态资源的,请求动态处理的,请求欢迎页的等。每一种类型的HTTP请求都需要一个专门的处理器来处理,这些处理器就是`HandlerMapping`。换句话说一个`HandlerMapping`实现类就是一个场景下的HTTP请求处理器。 + +`DispatcherServlet`类内部有一个属性`handlerMappings`,这个属性里面装了一些`HandlerMapping`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerMappings; + //... +} +``` + +默认情况下`handlerMappings`里有5个实现类,分别是`RequestMappingHandlerMapping`、`BeanNameUrlHandlerMapping`、`RouterFunctionMapping`、`SimpleUrlHandlerMapping`和`WelcomePageHandlerMapping`。 + +![image-20220808000313545](http://img.topjavaer.cn/img/202311230846759.png) + + + +看名字大概也可以猜到这些`HandlerMapping`的应用场景,比如`SimpleUrlHandlerMapping`是用来处理静态页面请求的,`WelcomePageHandlerMapping`是用来处理欢迎页请求的。而**对于动态请求的处理,也即执行我们Controller里的方法(handler)的请求,是由`RequestMappingHandlerMapping`实现的**。 + +#### 3.1.2 DispatcherServlet#getHandler() + +了解了这些后,我们再来看SpringMVC是如何找到我们的hander的,根据上面的源码我们知道首先会执行`getHandler()`方法,`getHandler()`就是遍历`DispatcherServlet`类内`handlerMappings`的所有`HandlerMapping`,挨个调用它们的`getHandler()`方法,谁先有返回就代表找到了(这里返回的是`HandlerExecutionChain`,而非`HandlerMapping`,主要原因是`HandlerExecutionChain`封装了一些拦截器,我们讲到拦截器时再说,大家可以简单认为`HandlerExecutionChain`和`HandlerMapping`是一样的)。 + +```java + protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + //遍历handlerMappings里的每个HandlerMapping实现类,判断哪个HandlerMapping能处理这个HTTP请求 + for (HandlerMapping mapping : this.handlerMappings) { + //一旦HandlerMapping的getHandler()有返回,就代表这个找到了能处理的handler + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + } + return null; + } +``` + +我们之前说了对于HTTP的动态请求的是由`RequestMappingHandlerMapping`来处理的,那也就是`RequestMappingHandlerMapping#getHandler()`有返回,那么我们看下它是怎么处理的。 + +#### 3.1.3 RequestMappingHandlerMapping#getHandler() + +MappingRegistry与HandlerMethod + +这里又需要插入一个知识,我们要先讲一个叫做`MappingRegistry`的东西。`MappingRegistry`翻译过来叫做映射器注册中心。也即所有的映射器都需要注册到这里,它就像是一个大集合,装了所有的映射器。那什么是映射器呢?就是我们的handler,也即我们自己写的Controller里的方法。也就是说在项目启动的时候,Spring会扫描我们这个项目下所有标注了`@Controller`注解的类,然后再扫描这些类里面标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法,并将这些方法封装注册到`MappingRegistry`中。**Spring将我们这些方法统一封装成`HandlerMethod`对象。**`HandlerMethod`内的属性挺丰富的,比如: + +我们之前说了`MappingRegistry`是一个大的集合,这个集合装了所有Controller类内标了`@RequestMapping`的方法。因此我们必然能根据HTTP请求从`MappingRegistry`中找到我们的处理方法。`MappingRegistry`内部有两个非常重要的Map + +```java +class MappingRegistry { + + private final Map> registry = new HashMap<>(); + + private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); + //... +} +``` + +首先`MappingRegistration`是一个包装类,它包装了我们上面说的`HandlerMethod`,部分信息如下: + +```java +static class MappingRegistration { + + private final T mapping; + //可以看到包装了我们的HandlerMethod + private final HandlerMethod handlerMethod; + + //... +} +``` + +`pathLookup`这个Map,key是URL路径,而value是能处理这个URL路径的方法描述信息(注意是方法描述信息,不是方法本身)。这个Map比较特殊的是一个`MultiValueMap`,也即**可以根据一个key得到多个value**。这个很正常,比如: + +```java +@RestController +public class UserController { + @GetMapping("/user") + public Object getUser(){ + //... + } + + @PostMapping("/user") + public User postUser(@RequestBody User user){ + //... + } + + @DeleteMapping("/user") + public String deleteUser(){ + //... + } +} +``` + +我们一个URL路径`/user`可以对应上述Controller里的三个方法。 + +而`registry`这个Map的key是方法描述信息 (就是pathLookup的value),`registry`的value是`MappingRegistration`,也即封装了具体的执行方法。 + +AbstractHandlerMethodMapping#getHandlerInternal() + +了解了`MappingRegistry`后,我们再回来看`RequestMappingHandlerMapping`,`RequestMappingHandlerMapping`类的父类是`AbstractHandlerMethodMapping`,这个类内持有`MappingRegistry`实例: + +```java +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean { + //... + private final MappingRegistry mappingRegistry = new MappingRegistry(); + //... +} +``` + +也即我们刚才分析的`MappingRegistry`会被`AbstractHandlerMethodMapping`持有,那也就代表**我们的`RequestMappingHandlerMapping`这个类拥有项目所有的`HandlerMethod`**。 + +对于`RequestMappingHandlerMapping#getHandler()`的调用最终会调用到`AbstractHandlerMethodMapping#getHandlerInternal()`上,其关键源码如下: + +```java +protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + //先根据HTTP请求得到请求URL路径,比如/user + String lookupPath = initLookupPath(request); + //读写锁,获得读锁,暂时可以不必关心 + this.mappingRegistry.acquireReadLock(); + try { + //根据URL路径和HTTP请求信息从自己的属性MappingRegistry中拿到匹配的HandlerMethod + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + //找到后将匹配的HandlerMethod返回 + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); + } + finally { + //释放读锁 + this.mappingRegistry.releaseReadLock(); + } +} +``` + +可以看到上述信息核心的地方在`lookupHandlerMethod()`,其部分关键源码如下: + +```java +protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + //match是符合的,匹配到的HandlerMethod,一会我们找到的HandlerMethod会放到这个里面 + List matches = new ArrayList<>(); + //根据URL路径先找匹配的方法信息(注意是方法信息,不是方法本身) + //我们上面说过,根据URL路径寻找可能会找到多个符合的方法,比如GET/user POST/user DELETE/user + List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); + if (directPathMatches != null) { + //再根据HTTP请求的其他信息(比如请求类型(GET,POST等)),进一步寻找符合的处理方法 + //并将找到的方法放入matches里面 + addMatchingMappings(directPathMatches, matches, request); + } + //下面就是一些异常判断了 + //比如如果matchers是空的话,也即没找到能处理的方法要怎么办啦 + //如果找到不只一个能处理的方法就抛出异常啦之类的 + //正常情况下我们只会找到一个能处理的方法,将这个方法返回即可 + //... +} +``` + +这里很容易想到`this.mappingRegistry.getMappingsByDirectPath(lookupPath)`其实就是从`MappingRegistry`中的`pathLookup`这个Map里面去取信息(还记得吗,我们刚说过`MappingRegistry`内部的两个重要map)。 + +根据URL路径匹配完以后,肯定还得结合HTTP请求,再详细的判断这个方法是否满足需求(比如我要的是GET,你的方法是POST,那肯定就不行)做一层筛选,这样筛选下来得到的方法描述信息,再作为key值,从`MappingRegistry`中的`registry`这个Map里面去取具体的方法。而这个就是`addMatchingMappings()`方法的实现细节。感兴趣的可以自己点进去这两个方法细节,实际很简单,就几行代码,这里不再贴出。 + +#### 3.1.4 总结 + +走完上面的流程,我们已经拿到了能处理请求的`HandlerMethod`,我们可以总结: + +首先`DispatcherServlet`内部有5个`HandlerMapping`,它会挨个询问哪个`HandlerMapping`能处理这个请求,结果是我们的`RequestMappingHandlerMapping`能处理。`RequestMappingHandlerMapping`这个对象的内部有一个叫`MappingRegistry`的大集合,这个集合里面装了我们项目所有的自己编写的Controller里处理请求的方法,它将我们的方法封装为`HandlerMethod`,当`RequestMappingHandlerMapping`去处理的时候实际上就是根据HTTP 信息(比如请求类型和URL路径)去`MappingRegistry`里面找符合的`HandlerMethod`,将找到的`HandlerMethod`返回。 + +流程大致如下图: + +![image-20220808171735041](http://img.topjavaer.cn/img/202311230846114.png) + + + +#### 3.1.5 一些补充 + +handlerMappings内的元素是有顺序的 + +我们上面看到`DispatcherServlet`里的`handlerMappings`有5个实现类,都放在一个List里面,它们其实是有顺序的(父类`AbstractHandlerMapping`继承接口`Ordered`),`RequestMappingHandlerMapping`在最前面,这有什么影响呢?举个例子: + +假设我的SpringBoot项目现在有一个静态文件叫hello.html + +![](http://img.topjavaer.cn/img/202311230846876.png) + +默认情况下,我通过访问`localhost:8080/hello.html`,SpringBoot就会将这个静态文件返回给浏览器。但如果这个时候我有个Controller,它是这样写的 + +```java +@RestController +public class HelloController { + @GetMapping("/hello.html") + public String hello(){ + return "hello world"; + } + +} +``` + +那么请问此时再访问`localhost:8080/hello.html`还会返回静态文件吗,还是会走到我们的Controller返回`"hello world"`字符串呢? + +答案是会走进Controller返回`"hello world"`字符串。原因也很简单,因为`RequestMappingHandlerMapping`在`SimpleUrlHandlerMapping`前面,`DispatcherServlet`会先问`RequestMappingHandlerMapping`能不能处理。在我们没写这个Controller时,`RequestMappingHandlerMapping`肯定是没法从`MappingRegistry`找到匹配的处理方法的,所以就处理不了,这时才会轮到后面的`SimpleUrlHandlerMapping`来处理,然后返回静态页面。但当我们写了这个Controller,自然`RequestMappingHandlerMapping`能找到处理方法,也就没后面`SimpleUrlHandlerMapping`什么事了。 + +MappingRegistry里的读写锁 + +刚才我们在源码里看到了`MappingRegistry`的读写锁,这个原因也挺简单的,首先`MappingRegistry`是一个注册中心,自然就允许有人往里注册或注销东西,比如MappingRegistry的部分功能如下: + +```java +class MappingRegistry { + public void register(T mapping, Object handler, Method method) { + //... + } + public void unregister(T mapping) { + //... + } +} +``` + +所谓注册和注销,本质上就是在操作`MappingRegistry`里面的那些装了我们方法的集合。同时`MappingRegistry`又会被大量的查询(每次HTTP请求来了以后都会向它查询能处理的方法),因此这是一个典型的**读多写少**的场景,为保证并发安全且尽可能的提高性能,读写锁就是很好的选择。读锁之间共享,写锁独占,同时写的时候不能读。 + +从`MappingRegistry`的API,我们也可以大胆猜测,在项目运行期间,我们也可以向其注册和删除`HandlerMethod`。 + +### 3.2 找到handlerAdapter + +#### 3.2.1 HandlerAdapter + +在说寻找handlerAdapter之前,我们先来说下为什么需要handlerAdapter。 + +走到这一步的时候,想想我们拿到了什么?拿到了能处理请求的`HandlerMethod`,以及拥有HTTP请求的所有信息和HTTP响应。那正常来说就是根据HTTP请求的信息,调用我们的`HandlerMethod`来处理请求,处理完后将处理结果写进HTTP响应。但是我们知道HTTP请求是五花八门的,比如参数放在请求头或请求体,以form形式提交,以JSON格式提交等等。同时我们自己的Controller层函数写法也是各种各样,加`@RequestParam`注解的,加Cookie、Session信息的,想返回页面的,想返回数据的等等。 + +我们将HTTP请求类比为插头,`HandlerMethod`类比为插座,为了让这些插头和插座能够结合,就需要一个适配器,插头插在适配器上,适配器插在插座上,这样任何类型的请求,任何不同的处理方法,都会有一个适配器来处理。 + +**Spring将这种适配器称作`HandlerAdapter`。** + +与`handlerMappings`相同,`DispatcherServlet`内部也有一个属性叫`handlerAdapters`,这个属性里面也装了一些`HandlerAdapter`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerAdapters; + //... +} +``` + +默认情况下,这个List集合里有4个实现类,分别是`RequestMappingHandlerAdapter`、`HandlerFunctionAdapter`、`HttpRequestHandlerAdapter`和`SimpleControllerHandlerAdapter`。 + +![image-20220804185151491](http://img.topjavaer.cn/img/202311230846149.png) + + + +根据名字可以看出,这些适配器与`HandlerMapping`是有对应关系的。比如`RequestMappingHandlerMapping`与`RequestMappingHandlerAdapter`是对应的,说明`RequestMappingHandlerAdapter`是用于适配`RequestMappingHandlerMapping`映射器的。 + +#### 3.2.2 DispatcherServlet#getHandlerAdapter() + +根据之前的源码,我们知道`getHandlerAdapter()`函数就是用来获得适配器的,它的源码如下: + +```java +protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + for (HandlerAdapter adapter : this.handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); +} +``` + +可以看到,十分简单,就是挨个问`handlerAdapters`里的每一个`HandlerAdapter`,谁支持适配这个请求处理,谁支持就返回谁。 + +我们刚才知道`RequestMappingHandlerMapping`的适配器是`RequestMappingHandlerAdapter`,那也就代表,这里正常会返回`RequestMappingHandlerAdapter`,我们不妨走进`RequestMappingHandlerAdapter#support()`,看看它是如何判断的。 + +#### 3.2.3 RequestMappingHandlerAdapter#support() + +`RequestMappingHandlerAdapter#support()`会进入`AbstractHandlerMethodAdapter#supports()`,其中`AbstractHandlerMethodAdapter`是`RequestMappingHandlerAdapter`的父类,其源码如下: + +```java +public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered { + //... + public final boolean supports(Object handler) { + return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)); + } + //... +} +``` + +`supportsInternal()`由子类`RequestMappingHandlerAdapter`实现: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + @Override + protected boolean supportsInternal(HandlerMethod handlerMethod) { + return true; + } + //... +} +``` + +可以看到,`RequestMappingHandlerAdapter`的判断逻辑很简单,就是问找到的handler是不是`HandlerMethod`类型,通过前面的源码分析,我们很清楚handler就是`HandlerMethod`。 + +至此我们就找到了我们的`HandlerMethod`和`HandlerAdapter`,完成了`DispatcherServlet#doDispatch()`的第一步:根据HTTP请求的信息找到我们的处理方法(包括方法的适配器)。 + +#### 3.2.4 总结 + +我们在3.1节中已经找到了`HandlerMethod`,但考虑到HTTP请求信息比较多,如何将HTTP请求的信息与我们的`HandlerMethod`参数信息进行匹配,以及又如何将`HandlerMethod`处理完的结果返回给HTTP响应,这就需要适配器,我们需要找到合适的适配器来进行后续的工作。 + +与`handlerMappings`相同,`DispatcherServlet`内的`handlerAdapters`装了一些`HandlerAdapter`的实现类。处理动态请求会由`RequestMappingHandlerAdapter`适配器来处理,而`RequestMappingHandlerAdapter`判断能处理的逻辑也很简单,就是看上一步得到的处理器是不是`HandlerMethod`。拿到适配器后,我们就可以将HTTP请求信息与我们的`HandlerMethod`进行适配了。 + +## 4. 第二步 解析参数 + +在讲参数解析前,我们先回顾下目前已经走了哪些步骤,得到了哪些东西: + +首先我们根据HTTP请求,从`MappingRegistry`中得到了能处理请求的`HandlerMethod`,其次为将HTTP请求与我们的`HandlerMethod`适配,又根据`HandlerMethod`找到了适配器`RequestMappingHandlerAdapter`,现在我们拥有的就是 + +- 实际的处理器`HandlerMethod` +- 适配器`RequestMappingHandlerAdapter` +- HTTP请求和HTTP响应 + +我们之前说了由于HTTP请求和`HandlerMethod`五花八门,因此需要适配器来帮忙执行HTTP请求。那适配器到底帮了哪些忙呢? + +实际上就两点:**参数解析和返回结果的处理**。适配器会首先根据HTTP请求信息和我们的`HandlerMethod`,解析HTTP请求,将它转为我们`HandlerMethod`上的参数。举个例子,前端通过form表单发送HTTP请求: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +SpringMVC会自动将表单中的字符串信息转为我们的User参数对象。 + +得到所有参数后,适配器会直接调用`HandlerMethod`处理请求。这时我们又得到了处理的返回结果。这个结果的意义也是众多的,它可以是一个对象然后用JSON写出,也可以是一个页面,还可以是一个跳转或重定向,不同的返回情况都需要被特殊适配处理,这些东西也是适配器帮我们做的。 + +为了让大家回想起`DispatcherServlet#doDispatch()`的四个步骤,我们再将源码贴过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +其中`mv = ha.handle(processedRequest, response, mappedHandler.getHandler());`就是适配器做的工作:**参数解析,执行方法并处理执行返回**。 + +在本章中我们先学习参数解析,下一章讲返回结果的处理。 + +### 4.1 先从一个设计模式说起 + +为便于理解参数解析的设计思想,我们先从一个设计模式讲起,理解这个设计模式对于SpringMVC的学习很关键。 + +我们假设现在你是一个包工头,你的手底下有很多技术工,有能刷墙的,能砌砖的,能设计图纸的还有能开挖掘机的。很多房地产开发商有活的时候会找你。比如今天恒太房地产跟你说他现在手里有一个刷墙的活问你能不能干。你二话不说接了这个活并派出了刷墙技术工来干活。 + +针对上面这个场景,我们可以抽象为代码,首先我们有个工人接口。刷墙工,砌砖工等都属于这个接口的实现类。 + +```java +public interface Worker{} +``` + +每个工人首先需要明确自己自己能干哪些活,比如刷墙工只能干刷墙的活,他干不了砌砖。怎么明确呢,只能问。比如现在来了一个活,我得问下刷墙工能不能干。因此就需要一个`support(Object work)`方法,表明当前工人能不能干这个活。同时如果能干,还得有个实际干活的功能,因此我们的`Worker`接口如下: + +```java +public interface Worker{ + /** + * 判断当前工人是否能干这个活 + */ + boolean support(Object work); + /** + * 如果能干就实际的干 + */ + void doWork(Object work); +} +``` + +由于你是个包工头,因此你手里应该有一批工人,我们可以使用类`Boss`描述包工头: + +```java +public class Boss{ + List workerList; +} +``` + +`workerList`就表示你手里拥有的众多工人。 + +现在,某客户有活过来了,你得判断谁能接这个活,然后交给他干。比较简单的办法就是挨个问手底下的工人,谁能干就让谁干。 + +```java +public class Boss{ + List workerList; + + //遍历询问谁能干,谁能干就让谁干 + public void doWork(Object work){ + for(Worker worker : workerList){ + if(worker.support(work)){ + worker.doWork(work); + return; + } + } + //都不能干就抛出异常 + throw new RuntimeException("抱歉,我们这接不了这种活"); + } +} +``` + +这样,其实我们就完成了一个设计模式,叫**策略模式**。 + +什么是策略模式?**我们可以将上面的工人都理解为某种策略,当有任务来的时候,就需要选择一个正确的策略来处理这个任务。我们将策略抽象为接口(或抽象类),然后自己持有这个接口(或抽象类)的集合**。类似于: + +```java +//A接口是策略 +public interface A{} +public class B{ + //B对象持有A的实现类集合 + List aList; +} +``` + +后面源码的分析中,**策略模式会使用的非常频繁**。其实如果你细心观察会发现之前的`DispatcherServlet`里的`handlerMappings`和`handlerAdapters`也是策略模式,每个`HandlerMapping`或`HandlerAdapter`的实现类都是一种策略,然后我们的`DispatcherServlet`持有这些策略,当有HTTP请求到来时,就遍历这些策略判断哪个能处理,找到那个能处理的策略来处理任务。 + +### 4.2 参数解析 + +了解了策略模式,我们来看下参数解析的源码。 + +``` +ha.handle()`的具体执行会走到`RequestMappingHandlerAdapter#handleInternal()`,而`handleInternal()`又会调用`RequestMappingHandlerAdapter#invokeHandlerMethod() +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + @Override + protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ModelAndView mav; + //... + //一些校验和特殊情况的判断,比如加session锁 + //最终会执行invokeHandlerMethod + mav = invokeHandlerMethod(request, response, handlerMethod); + //... + return mav; + } +} +``` + +因此我们先来分析下`RequestMappingHandlerAdapter#invokeHandlerMethod()` + +#### 4.2.1 RequestMappingHandlerAdapter#invokeHandlerMethod() + +HandlerMethodArgumentResolver + +本着不大篇幅的贴源码,尽量深入浅出的思想,在讲源码前我们先来说点别的。首先从一个接口`HandlerMethodArgumentResolver`说起。 + +我们都知道SpringMVC会根据HTTP请求和我们写的`HandlerMethod`来解析参数。但是之前也说过,解析参数的情况太多了,比如用`@RequestParam`从请求头中解析,使用`@RequestBody`从请求体中解析等。面对如此多的情况,我们不妨使用刚刚学习的策略模式,**将每种情况的处理都认为是一个策略,然后使用一个List将这些策略汇总起来。那么当要解析一个参数的时候,就从集合中取出一个合适的策略来解析这个参数**。SpringMVC就是那么做的。其中对于解析参数的这一策略接口叫做**`HandlerMethodArgumentResolver`**,我们称为参数解析器。接口的源码为: + +```java +public interface HandlerMethodArgumentResolver { + + boolean supportsParameter(MethodParameter parameter); + + @Nullable + Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; + +} +``` + +可以看到一个`supportsParameter()`一个`resolveArgument()`,和我们之前讲的包工头例子一模一样。那么自然而言我们需要一个集合来保存`HandlerMethodArgumentResolver`接口的实现类。`RequestMappingHandlerAdapter`内有一个`argumentResolvers`属性: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodArgumentResolverComposite argumentResolvers; + //... +} +``` + +**其中`HandlerMethodArgumentResolverComposite`就是`HandlerMethodArgumentResolver`的集合**,其内部属性如下: + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + + private final List argumentResolvers = new ArrayList<>(); + //... +} +``` + +默认情况下`argumentResolvers`内包含27个实现类,内容如下: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845819.png) + + + +ModelAndView + +我们再来说第二个重要的类`ModelAndViewContainer`,这个类的作用类似于上下文,它保存着整个请求处理过程中的**Model**和**View**。 + +首先根据MVC思想我们知道,对于一个MVC项目,我们可以给请求响应两种东西:数据和页面。除此以外,在转发或重定向时,往往需要将我们的数据也转发或重定向到下一级请求(转发是同一个HTTP请求,这里只是表述方便)。这时就需要一种数据结构,用来装我们返回的结果,这个结果可以是返回给请求的,也可以是转发到下一级请求处理的(转发和重定向),这就是`ModelAndView`。因此我们可以理解为`ModelAndView`就是用来装一次HTTP处理结果的容器,这个容器里主要装了Model和View两个信息。 + +其实我们在之前的源码中已经看到过`ModelAndView`了,它实际上贯穿于整个`doDispatch()`方法,我们不妨将之前`DispatcherServelt`的重要源码再拿过来看: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + //这里就是先声明了一个ModelAndView + ModelAndView mv = null; + + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //可以看到适配器的处理结果就是返回一个ModelAndView + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析,需要从ModelAndView中拿到视图信息 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +`ModelAndViewContainer`可以理解为`ModelAndView`的包装类,这个类在`RequestMappingHandlerAdapter#invokeHandlerMethod()`处理过程中被创建(我们一会儿会看到),主要是装`HandlerMethod`的处理结果(如果参数有Map或者Model还会装参数里的Map和Model信息)。 + +**也即`ModelAndViewContainer`就是适配器在处理一次HTTP请求的上下文信息,这个上下文里面装的是处理过程中的Model和View。** +`ModelAndViewContainer`源码中几个重要的属性信息如下: + +```java +public class ModelAndViewContainer { + //视图信息,可以是一个View,也可能只是一个视图名String + private Object view; + //数据,本质是个Map,我们在Controller内的函数上写的Map或Model其实都是它 + private final ModelMap defaultModel = new BindingAwareModelMap(); + //重定向的数据,也是个Map + private ModelMap redirectModel; +} +``` + +可以看到`ModelAndViewContainer`内部维护了一个View和两个Model,分别是默认的Model和重定向的Model。 + +WebDataBinderFactory + +翻译过来就是数据绑定工厂,我们拿之前表单提交的例子来说: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +这里很明显的是,SpringMVC将form提交的每一个参数信息**绑定**到了我们的User对象上,而这个绑定操作就是`WebDataBinderFactory`干的工作。 + +RequestMappingHandlerAdapter#invokeHandlerMethod() + +了解了上面那么多以后,我们就可以看下`RequestMappingHandlerAdapter#invokeHandlerMethod()`,其部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //webRequest是HttpServletRequest和HttpServletResponse的包装类 + ServletWebRequest webRequest = new ServletWebRequest(request, response); + //... + //ServletInvocableHandlerMethod是一个大的包装器,下面的一系列set操作都是对ServletInvocableHandlerMethod的丰富 + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + if (this.returnValueHandlers != null) { + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + invocableMethod.setDataBinderFactory(binderFactory); + invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); + + //创建ModelAndViewContainer + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + //做一些ModelAndViewContainer的初始化工作 + + //... + + //包装之后的核心执行,包含参数解析,处理器执行和返回结果的处理 + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //... + + //处理完后返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + +} +``` + +`ServletInvocableHandlerMethod`是`HandlerMethod`的包装类(实际上它继承自`HandlerMethod`类),它里面装了很多信息,比如装了我们之前说的参数解析器,装了我们的`handlerMethod`,还装了`WebDataBinderFactory`等。另外还有一个`returnValueHandlers`,它是我们的返回结果处理器,还记得我们之前说的,handlerAdpter实际上帮我们做的事是**参数解析和返回结果的处理**。这里的`returnValueHandlers`就是用于返回结果处理的 + +`ServletWebRequest`也是个包装类,就是将`HttpServletRequest`和`HttpServletResponse`合并到了一个类里。`ModelAndViewContainer`我们在之前已经介绍过了,当都准备好这些信息后,就要开始执行 + +```java +invocableMethod.invokeAndHandle(webRequest, mavContainer); +``` + +因此下一步我们就需要追溯:`ServletInvocableHandlerMethod#invokeAndHandle()`的源码 + +#### 4.2.2 ServletInvocableHandlerMethod#invokeAndHandle() + +其部分源码如下: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...后面是返回结果的处理,我们在第五章讲 + //... +} +``` + +其中`invokeForRequest()`是参数解析以及执行`HandlerMethod`然后得到返回结果。 + +`invokeForRequest()`源码如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //根据HTTP请求解析参数 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + + //...一些日志打印 + //... + //得到参数后,反射执行HandlerMethod + return doInvoke(args); +} +``` + +因此参数解析的核心代码就是`getMethodArgumentValues()`函数 + +#### 4.2.3 getMethodArgumentValues() + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //拿到所有的参数信息 + MethodParameter[] parameters = getMethodParameters(); + + //... + //args就是装我们所有的参数,这里先声明出来 + Object[] args = new Object[parameters.length]; + //遍历所有的参数信息 + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + + //...一些参数处理 + //使用参数解析器来解析参数 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + //... + //一些异常处理 + //... + } + return args; +} +``` + +可以看到参数解析就是**以我们的`HandlerMethod`的参数信息为模板,使用解析器,从HTTP请求中拿到信息赋值到我们的参数上**,这样循环遍历`HandlerMethod`中的每个参数,就可以解析得到所有的参数对象。 + +那么我们就要看下参数解析器到底是如何解析参数的。 + +#### 4.2.4 HandlerMethodArgumentResolverComposite#resolveArgument() + +`HandlerMethodArgumentResolverComposite#resolveArgument()`源码很好理解,就是找到能解析这个参数的参数解析器,然后调用这个找到的解析器解析它。 + +```java +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //得到参数解析器 + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + if (resolver == null) { + throw new IllegalArgumentException("Unsupported parameter type [" + + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); + } + //解析参数 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +其中获得参数解析器代码为: + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + //先从缓存中寻找,找不到再遍历寻找 + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + //遍历每一个参数解析器,判断谁能处理这个参数 + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + //找到能处理的参数后就将它放入缓存,保证下次不用再遍历 + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +这里需要提一嘴的是`argumentResolverCache`属性,`argumentResolverCache`作用很简单,就是缓存的功能。一开始项目启动的时候缓存里面没有任何东西,这样解析每个参数就都需要遍历所有的参数解析器,判断谁能支持处理这个参数,一旦找到这个参数解析器就需要将它缓存起来,这样下次再请求的时候就可以直接从缓存拿避免再遍历,节省了下次HTTP请求的时间。 + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + //... + private final Map argumentResolverCache = + new ConcurrentHashMap<>(256); + + //... +} +``` + +可以看到`argumentResolverCache`本质就是一个`ConcurrentHashMap`,它的key是`MethodParameter`也即参数信息(可以推测出`MethodParameter`一定重写了`equals()`和`hashCode()`),Value是能处理这个参数的参数解析器。 + +这里我们又看到了策略模式的使用,使用`argumentResolvers`将所有参数解析器汇总起来,在真正需要进行参数解析的时候就会挨个问这些参数解析器有没有人能处理这个参数,谁能处理就交由谁处理。 + +最后的一步就是调用参数解析器解析参数: + +```java +resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +``` + +首先,参数解析器解析参数需要parameter,mavContainer,webRequest,binderFactory四个参数才能解析,每个参数的作用我们一一来说(虽然我们刚才已经讲了一些): + +- parameter 参数模板信息。我现在是参数解析器,我要解析这个参数肯定得有参数信息吧,这个参数是不是标了`@RequestBody`注解啊,是不是一个复杂类型比如User对象啊等等,如果是的话我得拿到它的Class,反射创建和赋值吧。只有获得这些信息才能从HTTP请求中对应的位置找到参数,按照参数模板信息来生成参数。 + +- mavContainer 我们之前说过,mavContainer 是一次HTTP请求的上下文,用来装返回的Model和View,既然是装返回的信息,那为啥解析参数的时候还需要它呢?在写Controller方法的时候,有一种场景可以如下: + + ```java + @GetMapping("/user") + public User getUser(Model model){ + model.addAttribute("key1","value1"); + //... + } + ``` + + 我们往参数Model里设置的任何信息,都会被放进返回结果处理,比如可以转发给下一级HTTP请求(或者返给前端)。 + + 那这个参数Model是哪里来的?**就是mavContainer里的defaultModel这个属性**。我们会在具体的例子中再详细的看相关的源码,这里大家只需要大概知道一下就可以。 + +- webRequest 封装了HTTP请求和HTTP响应的类。既然要从HTTP中解析参数,自然就需要HTTP请求和响应的信息,这都是原始数据。 + +- binderFactory 我们之前讲过,数据绑定工厂,将HTTP请求信息与我们的参数的值绑定上的东西,我们会在后面具体使用场景中分析这个东西,大家别急。 + +可以看到,要解析一个`HandlerMethod`上的参数,就需要一个特定的参数解析器,而参数解析器在进行解析的时候需要parameter,mavContainer,webRequest,binderFactory的帮助才能解析出来参数。我们上面也看到Spring为我们提供了27种参数解析器(SpringBoot 2.7.2版本下是27个),这27种参数解析器就是为了应付各种场景下的HTTP请求和参数处理: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845947.png) + + + +通过上面的名字也很容易看出一些参数解析器的作用,比如`@RequestParamMethodArgumentResolver`是用来解析标了`@RequestParam`注解的参数;`@PathVariableMethodArgumentRResolver`是解析标了`@PathVariable`注解的参数;`@RequestResponseBodyMethodArgumentResolver`是用来解析标了`@RequestBody`注解的参数。 + +**每个参数解析器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些参数解析器的源码。** + +#### 4.2.5 doInvoke() + +最后提一嘴`HandlerMethod`的执行,我们在[4.2.2](https://www.coderzoe.com/archives/28/#))的源码中已经看到了一段话 + +```java +return doInvoke(args); +``` + +也即解析完所有的参数后就可以执行`HandlerMethod`了,`doInvoke()`的源码也很简单,就是单纯的调用`method.invoke()`: + +```java +protected Object doInvoke(Object... args) throws Exception { + Method method = getBridgedMethod(); + try { + //...一些异常处理,主要是针对Kotlin + return method.invoke(getBean(), args); + } + //...一些异常处理 +} +``` + +拿到方法,填入bean,填入参数,然后反射执行。 + +#### 4.2.6 总结 + +首先SpringMvc对不同情况下的参数解析定义了一个参数解析器接口,每个参数解析器接口的实现类就是一种情况下的参数解析,借助于策略模式,我们的`HandlerAdapter`内会有一些参数解析器的实现类。在要执行一个`HandlerMethod`前肯定要先把这个方法的所有参数解析出来,因此我们就需要遍历这个方法上的每一个参数,为每一个参数寻找一个合适的参数解析器来解析它。寻找合适参数解析器的方法很简单,就是挨个问`HandlerAdapter`持有的那些参数解析器是否支持解析,一旦有支持的就代表找到了,找到后就调用参数解析器解析参数。参数解析器在解析参数的时候,需要四个帮手才能真正的解析参数,分别是: + +- 参数模板,告知这个参数的一些元数据信息 +- ModelAndView上下文容器,用来处理多参数返回和HTTP请求见传递信息的作用。 +- 原生HTTP请求和HTTP响应 +- 数据绑定工厂,用来将从HTTP请求中解析出的信息绑定到参数对象上 + +目前SpringBoot2.7.2版本有27个参数解析器,他们用于不同的场景。 + +#### 4.2.7 一些补充 + +在获得某个参数的参数解析器后,我们使用`argumentResolverCache`将这一信息缓存起来,这里就会有个问题:这个缓存的生命周期是怎样的?它会在什么时候销毁?同样两次HTTP请求,后一次会使用前一次保存的缓存解析器信息吗? + +要回答这个问题,我们需要追溯源码看下这个缓存的创建时间和执行HTTP请求时的传递过程: + +首先`argumentResolverCache`是`HandlerMethodArgumentResolverComposite`内的属性,而`RequestMappingHandlerAdapter`的属性`argumentResolvers`正是`HandlerMethodArgumentResolverComposite`: + +通过`WebMvcAutoConfiguration`和`RequestMappingHandlerAdapter`源码: + +```java +public class WebMvcAutoConfiguration { + //... + //内部类 + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware{ + //... + //@Bean创建RequestMappingHandlerAdapter实例 + @Bean + @Override + public RequestMappingHandlerAdapter requestMappingHandlerAdapter( + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcValidator") Validator validator) { + RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager, + conversionService, validator); + adapter.setIgnoreDefaultModelOnRedirect( + this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect()); + return adapter; + } + } + //... + +} + +//RequestMappingHandlerAdapter部分源码: +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + + //... + //后置的属性设置,主要包含初始化argumentResolvers属性 + @Override + public void afterPropertiesSet() { + // Do this first, it may add ResponseBody advice beans + initControllerAdviceCache(); + //初始化argumentResolvers + if (this.argumentResolvers == null) { + List resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.initBinderArgumentResolvers == null) { + List resolvers = getDefaultInitBinderArgumentResolvers(); + this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.returnValueHandlers == null) { + List handlers = getDefaultReturnValueHandlers(); + this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); + } + } +} +``` + +可以知道`RequestMappingHandlerAdapter`的创建是在项目启动的时候创建了,同样在项目启动的时候,我们的`argumentResolverCache`就被创建好了。 + +在执行HTTP请求的时候,invokeHandlerMethod()方法部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + + //... + + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //.. +} +``` + +每次HTTP请求的时候都会创建一个`ServletInvocableHandlerMethod`对象,但往`ServletInvocableHandlerMethod`对象内设置的`argumentResolvers`是由`RequestMappingHandlerAdapter#argumentResolvers`属性传进去的,`RequestMappingHandlerAdapter`是单例的,也即`RequestMappingHandlerAdapter#argumentResolvers`只有一份,即使每次HTTP请求都创建`ServletInvocableHandlerMethod`,但每个对象内持有的`argumentResolvers`都是同一份,再往下追寻: + +在`ServletInvocableHandlerMethod`对象内部执行参数解析操作 + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + //... + if (!this.resolvers.supportsParameter(parameter)) { + throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); + } + try { + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + } + //... +} +``` + +参数解析的时候就会将找到的参数放进`argumentResolverCache`缓存中,既然所有的`ServletInvocableHandlerMethod`对象都持有一份`argumentResolvers`,那自然也持有同一份`argumentResolverCache`,都是`RequestMappingHandlerAdapter`里的那一份。因此上一次HTTP请求做的缓存保存,自然可以被下一次HTTP请求使用。 + +这里也解释了`argumentResolverCache`是`ConcurrentHashMap`,而非HashMap,因为请求肯定是并发的,有人往Map里写有人从Map中拿,要保证线程安全。 + +因此可以得出结论`argumentResolverCache`是与项目生命周期基本相同的,同一HTTP请求可以被缓存,供以后使用。 + +## 5. 第三步 执行方法并处理返回 + +### 5.1 HandlerMethodReturnValueHandler + +我们上面说了,对于`HandlerMethod`的返回情况也是多种多样的,比如可以返回视图,可以返回一个对象,对象可以被解析为JSON,还可以被解析为XML返回,可以跳转/重定向等等。针对那么多种情况,就需要对返回的结果进行适配处理。 + +与参数处理器解析器类似,对于返回结果的处理,Spring也抽象为了一个接口叫`HandlerMethodReturnValueHandler`,翻译为返回值处理器,因此一个`HandlerMethodReturnValueHandler`的实现类就是一个场景下的返回结果处理。 + +同样与参数解析器类似,SpringMVC再次采用策略模式,将所有返回值处理器的实现类保存起来,当需要处理返回值的时候就从这些实现类中选择一个来处理返回。 + +`RequestMappingHandlerAdapter`类中有一个重要的属性`returnValueHandlers`: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodReturnValueHandlerComposite returnValueHandlers; + //... +} +``` + +其中`HandlerMethodReturnValueHandlerComposite`就是返回值处理器的集合: + +```java +public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { + //... + private final List returnValueHandlers = new ArrayList<>(); + //... +} +``` + +因此`RequestMappingHandlerAdapter`对象中持有多个`HandlerMethodReturnValueHandler`实现类,默认情况下`RequestMappingHandlerAdapter`持有15个返回值处理器(SpringBoot2.7.2版本) + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845634.png) + + + +### 5.2 ServletInvocableHandlerMethod#invokeAndHandle() + +`ServletInvocableHandlerMethod`类我们之前已经讲过,它就是`HandlerMethod`的一个包装类,里面装了很多东西,其中上面说的返回值处理器也被装在了里面: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + if (this.returnValueHandlers != null) { + //将返回值处理器设置进包装类 + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + //... + } + //... +} +``` + +因此`ServletInvocableHandlerMethod`在解析完参数并执行完`HandlerMethod`拿到结果后就开始使用返回值处理器来处理结果: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //解析参数并执行HandlerMethod,得到返回结果 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...一些返回信息的校验 + + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + //...一些异常处理 +} +``` + +可以看到直接使用`returnValueHandlers`来处理返回值,因此对返回值的处理就在代码 + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +我们继续深入。 + +### 5.3 HandlerMethodReturnValueHandlerComposite#handleReturnValue() + +其中`HandlerMethodReturnValueHandlerComposite#handleReturnValue()`代码也与我们在参数解析器里看到的思路基本相同,从众多返回值处理器实现类中获得能处理的返回值处理器,用这个得到的实现类来处理返回值。 + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + //得到返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + + // .. 异常处理 + + //调用得到的返回值处理器处理结果 + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` + +而`selectHandler()`也很简单,就是遍历挨个问返回值处理器,谁支持处理,谁支持就让谁处理。 + +```java +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + //... + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + //... + //判断是否支持处理 + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +我们刚才已经看到在SpringBoot2.7.2版本中已经默认带了15个返回值处理器 + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845673.png) + + + +其中通过类名不难看出,`@RequestResponseBodyMethodProcessor`是处理返回结果加了`@ResponseBody`注解的情况(准确来说是`HandelrMethod`上或者Controller类上加了`@ResponseBody`注解),`ViewNameMethodReturnValueHandler`是处理返回结果是视图名的等。 + +**每个返回值处理器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些返回值处理器的源码。** + +### 5.4 RequestMappingHandlerAdapter#getModelAndView() + +执行完获得结果以后,就需要将`ModelAndView`返回,在上面的源码中我们也看到,`HandlerAdapter#handle()`的返回是`ModelAndView`。 + +返回`ModelAndView`的源码为: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + //返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + } +} +``` + +`ModelAndView`的获取很简单,就是从mavContainer中得到Model和View信息,返回即可。 + +```java +private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, + ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { + + //...一些更新操作 + + //设置Model和View + ModelMap model = mavContainer.getModel(); + ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); + //设置View,针对返回不是视图名的情况 + if (!mavContainer.isViewReference()) { + mav.setView((View) mavContainer.getView()); + } + + //....重定向的特殊处理 + + return mav; +} +``` + +## 6. 第四步 视图解析 + +走完了上面那些就到了第四步:视图解析。视图解析不是必须的,只有在需要SpringMVC判断返回结果包含视图的时候才会进行视图解析(也即ModelAndView中的view信息不为空),举个例子: + +在配置了 + +```yaml +spring: + mvc: + view: + prefix: / + suffix: .html +``` + +Controller层写为 + +```java +@Controller +public class HelloController { + @GetMapping("/hello") + public String hello(){ + return "hello"; + } +} +``` + +且静态资源路径下存在hello.html时,我们请求`/hello`便会返回hello.html页面。 + +为了最后再加强大家对于SpringMVC执行流程的记忆,我们最后再将`DispatcherServlet#doDispatch()`四个步骤的重要源码拿过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +可以看到,第四步的视图解析是`processDispatchResult()`函数。 + +### 6.1 DispatcherServlet#processDispatchResult() + +其主要源码如下: + +```java +private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, + @Nullable Exception exception) throws Exception { + + //... 异常校验 + + // Did the handler return a view to render? + if (mv != null && !mv.wasCleared()) { + //解析视图 + render(mv, request, response); + //... + } + //... 一些别的处理,如记录日志和拦截器的triggerAfterCompletion执行 +} +``` + +可以看到SpringMVC会先判断ModelAndView不为空且视图信息不为空(较低版本SpringBoot这里是`&&mv.view!=null`)。 + +因此视图解析的所有核心源码在`render()`函数中: + +```java +protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { + //...国际化处理 + + View view; + String viewName = mv.getViewName(); + if (viewName != null) { + // We need to resolve the view name. + view = resolveViewName(viewName, mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + //... 处理viewName为空的异常 + + try { + if (mv.getStatus() != null) { + //设置HTTP响应状态码 + request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus()); + response.setStatus(mv.getStatus().value()); + } + //视图渲染 + view.render(mv.getModelInternal(), request, response); + } + + //...异常处理 +} +``` + +这里需要讲的一点是`View`对象,对于视图这一信息,SpringMVC抽象出了一个接口叫`View`。`View`接口下的实现类非常多,每个不同的实现类都是一种视图场景。 + +![image-20220823223018574](http://img.topjavaer.cn/img/202311230845196.png) + +比如处理重定向页面的`RedirectView`,处理内部视图资源的`InternalResourceView`。 + +因此上述`render()`函数的核心是**先解析视图名得到`View`对象,再通过`View`对象做视图渲染**。 + +我们先看第一个函数,通过视图名得到`View`对象`resolveViewName()` + +### 6.2 DispatcherServlet#resolveViewName() + +#### 6.2.1 ViewResolver + +SpringMVC再次借助策略模式,将解析视图名返回View对象这一能力作为策略,策略的接口为`ViewResolver`。`ViewResolver`接口下只有一个方法: + +```java +public interface ViewResolver { + @Nullable + View resolveViewName(String viewName, Locale locale) throws Exception; + +} +``` + +就是解析视图名称返回`View`对象。 + +同样的,大家应该猜到了`DispatcherServlet`内部持有多个`ViewResolver`实现类: + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + private List viewResolvers; + //... +} +``` + +#### 6.2.2 DispatcherServlet#resolveViewName() + +有了这些信息后,我们再来看`resolveViewName`函数: + +```java +protected View resolveViewName(String viewName, @Nullable Map model, + Locale locale, HttpServletRequest request) throws Exception { + + if (this.viewResolvers != null) { + for (ViewResolver viewResolver : this.viewResolvers) { + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + return view; + } + } + } + return null; +} +``` + +就是拿到所有的实现类,挨个遍历调用`viewResolver.resolveViewName()`来解析视图,谁解析出来了(`View!=null`)就直接返回。 + +默认情况下`DispatcherServlet`内部持有4个视图解析器实现类: + +![image-20220823224512517](http://img.topjavaer.cn/img/202311230845917.png) + + + +其实第一个实现类内部持有其他三个实现类,这点我们会以后在具体场景中再讲。 + +### 6.3 view.render() + +视图解析器解析出的View对象后,调用`view.render()`就可以渲染视图,我们刚才已经看到了`View`接口下有很多实现类且每个实现类应用场景不同,因此具体的`render()`我们后续会以具体的场景来分析。 + +## 7. 总结 + +其实从上到下看过来,发现一切都是一个策略模式(其中还有一个适配器模式),首先根据HTTP请求处理的不同,SpringMVC抽象出了`HandlerMapping`接口,并且`DispatcherServlet`对象持有多个`HandlerMapping`实现类。 + +一般请求到我们Controller层的由实现类`RequestHandlerMapping`来处理,SpringMVC会在项目一启动的扫描我们所有Controller下的标了`@RequestMapping`注解的处理 函数,并将这个处理函数封装成`HandlerMethod`,使用一个大的集合管理这些`HandlerMethod`(就是`MappingRegistry`),因此`RequestHandlerMapping`会根据请求路径和请求方式从集合中找到合适的`HandlerMethod`来处理请求。 + +但是HTTP请求五花八门,我们写的Controller层方法各有不同,为了能比较通用的执行HTTP请求,就需要适配,因此再次借助策略模式,SpringMVC将适配器抽象为接口`HandlerAdapter`,并且`DispatcherServlet`对象持有多个`HandlerAdapter`实现类。 + +需要适配Controller层方法,也即`HandlerMethod`的适配器叫做`RequestMappingHandlerAdapter`,`RequestMappingHandlerAdapter`作为适配器主要做了两件事:参数解析和返回结果处理。所谓参数解析就是将HTTP请求中的内容解析为我们`HandlerMethod`上的参数,返回结果处理就是对一些返回的结果做特殊处理并写出到HTTP输出流中,比如将返回的Java对象转为JSON写进输出流。 + +`RequestMappingHandlerAdapter`的适配工作也依赖于了策略模式,解析参数的时候抽象出一个接口叫`HandlerMethodArgumentResolver`,同时自己持有多个`HandlerMethodArgumentResolver`的实现类。`RequestMappingHandlerAdapter`会遍历我们`HandlerMethod`上的每一个参数,然后对每个参数都挨个问`HandlerMethodArgumentResolver`能否解析,如果能解析就解析参数(实际会有个缓存,防止以后再挨个问)。同理返回处理的时候也使用了策略模式,将返回处理抽象为接口`HandlerMethodReturnValueHandler`,自己持有多个`HandlerMethodReturnValueHandler`的实现类,在解析完参数并调用`MethodHandler`执行完处理得到结果后,就开始结果处理,同样挨个调用`HandlerMethodReturnValueHandler`的实现类,问问它能不能处理,要是能就处理。 + +在上述这些流程都完成后,最后一步就是视图解析,视图解析一般分为两步,视图名解析和视图渲染。我们一般返回的信息是视图名,SpringMVC将所有视图抽象为接口`View`,因此就需要根据返回的视图名寻找对应的解析器将视图名解析为`View`对象,针对这一解析过程,使用策略模式,抽象为接口`ViewResolver`,`DispatcherServlet`内持有多个`ViewResolver`的实现类,也是一样的遍历每个实现类看看能不能解。在得到`View`对象后,调用`View.render()`对视图进行渲染,就完成了视图解析。 \ No newline at end of file diff --git a/docs/source/spring-mvc/2-guide.md b/docs/source/spring-mvc/2-guide.md new file mode 100644 index 0000000..8902e33 --- /dev/null +++ b/docs/source/spring-mvc/2-guide.md @@ -0,0 +1,93 @@ + +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +在分析SpringMVC源码之前,先来分享一下:如何优雅高效地阅读源码 + +### 3.1 官方文档 + +https://docs.spring.io/spring/docs/4.3.7.RELEASE/spring-framework-reference/htmlsingle/#spring-introduction + +理论以官方文档和源码为准 + +### 3.2 下载源码 + +在阅读源码前,请大家下载源码,可以在Maven中下载,请大家自行百度如何下载源码。源码有丰富的注释 + +下面是DispatcherServlet.java源码截图,方法和变量都有详细的注释 + +![](http://img.topjavaer.cn/img/202310070843881.png) + +### 3.3 常用快捷键 + +记住快捷键会让你事半功倍 + +ctrl+n:快速进入类 + +ctrl+shift+n:进入普通文件 + +ctrl+f12:查看该类方法 + +在阅读源码的时候特别方便,因为你不可能每个方法都细细品读 + +![](http://img.topjavaer.cn/img/202310070843416.png) + +ctrl+alt+u:查看类结构图,这些类都可以点击进入,我比较喜欢用这个 + +![](http://img.topjavaer.cn/img/202310070843077.png) + +ctrl+shift+alt+u:查看类结构图,这些类不能进入 + +alt+f7:查看方法引用位置,以doDispatch()为例,可以看到DispatcherServlet 897行被引用,858行注释被引用 + +![](http://img.topjavaer.cn/img/202310070844708.png) + +ctrl+alt+b:跳转到方法实现处,对者接口方法点击,会弹出来在哪里实现。 + +![](http://img.topjavaer.cn/img/202310070844974.png) + +接下来是我不得不说的idea的神器——书签(bookmark),可以对代码行进行标记,并进行快速切换 + +ctrl+f11:显示bookmark标记情况,土黄色代表该字符已被占用,输入或者点击1代表在此位置书签为1 + +![](http://img.topjavaer.cn/img/202310070844271.png) + +我们以processDispatchResult()方法为例 + +![](http://img.topjavaer.cn/img/202310070844048.png) + +ctrl+标记编号 快速回到标记处,如我刚才在这留下了书签,ctrl+1,DispatcherServlet 1018行 + +shift+f11:显示所有书签,左栏是我打过书签的类、行信息,右边是代码详情 + +![](http://img.topjavaer.cn/img/202310070845135.png) + +当你所有书签都用完,0-9,a-z全部用完,可以直接ctrl+f11,记录普通书签,虽然无法用ctrl快速跳转,在shift+f11还是可以找到 + +![](http://img.topjavaer.cn/img/202310070845600.png) + +alt+f8:启用Evaluate窗口 + +当我们想看返回值,无法声明变量查看该变量的时候(源码不可更改) + +![](http://img.topjavaer.cn/img/202310070845830.png) + +可以使用Evaluate表达式 + +![](http://img.topjavaer.cn/img/202310070845794.png) + + + +以上就是导读篇的内容,下篇文章我们将进入源码分析部分。 \ No newline at end of file diff --git a/docs/source/spring-mvc/3-scene.md b/docs/source/spring-mvc/3-scene.md new file mode 100644 index 0000000..e7d8aa7 --- /dev/null +++ b/docs/source/spring-mvc/3-scene.md @@ -0,0 +1,1982 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,参数解析器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +上篇中我们已经讲了SpringMVC处理HTTP请求的整体流程,其间我们讲到了很多接口,如参数解析器,返回结果处理器等,但我们都没有深入的进去查看这些解析器或处理器是如何处理的。本章就带大家进入真实的案例场景,看看这些接口在具体场景下是如何发挥作用的 + +## 0. 参数解析器 + +为了方便后面具体案例的分析,我们先来回顾下之前在上一篇中讲到的参数解析器。 + +当我们发送HTTP请求 时,根据之前的框架分析,我们知道这个请求会由`RequestMappingHandlerAdapter`来处理,在`RequestMappingHandlerAdapter`来处理的时候,会将自己的很多信息封装到`ServletInvocableHandlerMethod`中,包括自己的参数解析器和返回值处理器。`ServletInvocableHandlerMethod`做处理的代码我们之前看过了,这里再拿过来: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析与HandlerMethod的执行 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + //... 一些返回的处理 +} +``` + +其中`invokeForRequest()`内容如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + //...日志 + //函数执行 + return doInvoke(args); +} +``` + +`getMethodArgumentValues()`内容如下: + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + MethodParameter[] parameters = getMethodParameters(); + if (ObjectUtils.isEmpty(parameters)) { + return EMPTY_ARGS; + } + + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); + args[i] = findProvidedArgument(parameter, providedArgs); + if (args[i] != null) { + continue; + } + //判断解析器是否支持解析 + //本质是循环遍历 + if (!this.resolvers.supportsParameter(parameter)) { + throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); + } + try { + //找到能解析的解析器就解析参数 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + } + //...异常处理 + } + return args; +} +``` + +这里的`supportsParameter()`源码如下: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return getArgumentResolver(parameter) != null; +} +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + //遍历判断每个参数解析器是否支持解析当前参数 + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +而`resolveArgument()`源码如下: + +```java +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + //异常处理 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +我们之前虽然大体上走完了每个流程,但其实并没有深入到每个接口的实现类里去看,这一主要原因是SpringMVC要处理的情况太多了,只能结合实际的场景来说源码。因此下面我们将会一个一个场景的讲源码处理,首先从参数解析器的源码场景说起。 + +## 1. @PathVariable + +`@PathVariable`注解是Web开发中十分常见的一个注解,我们可以从路径中获取信息作为参数。 + +举个例子如下: + +```java +@RestController +public class PathVariableController { + @GetMapping("/user/{id}") + public String getUser(@PathVariable("id") long id){ + return "hello user"; + } +} +``` + +当我们执行HTTP请求 `GET /user/1`的时候,上面的参数id值就被映射为了1。根据上面的源码我们知道,要想解析出参数id,必然有一个参数解析器做了这个工作。 + +首先打断点到`supportsParameter()`,查看到底是哪个参数解析器支持这样的处理。通过打断点可以很容易知道是`PathVariableMethodArgumentResolver`参数解析器支持解析这个参数,那我们就需要问两个问题了: + +1. 为什么`PathVariableMethodArgumentResolver`可以支持这个参数的解析,它是如何判定的 +2. `PathVariableMethodArgumentResolver`是如何解析参数,从HTTP请求中将信息抠出赋给id字段的呢? + +这两个问题的答案其实也是`PathVariableMethodArgumentResolver`的两个实现方法:`supportsParameter()`和`resolveArgument()` + +我们先来看第一个方法的源码: + +```java +public boolean supportsParameter(MethodParameter parameter) { + //不带PathVariable注解直接返回false + if (!parameter.hasParameterAnnotation(PathVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class); + return (pathVariable != null && StringUtils.hasText(pathVariable.value())); + } + return true; +} +``` + +这个函数的判断逻辑比较简单,首先是判断如果参数上不带`@PathVariable`注解直接返回false。其次带了注解,判断我们的参数类型是不是Map类型的,如果是的话就获取`@PathVariable`注解信息并要求其value内容不能为空。最后如果不是Map但是有`@PathVariable`注解就直接返回true。我们的参数是long型,且标了`@PathVariable`,因此会直接返回true。 + +再看第二个源码(其实是`PathVariableMethodArgumentResolver`的父类`AbstractNamedValueMethodArgumentResolver`实现的): + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //拿到我们的参数名,在上例中,我们的参数名是id(其实就是@PathVariable注解里的value值) + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + + //... + + //拿到参数名后,从HTTP请求中解析出这个参数名位置的值,比如 GET /user/1 此时arg就是"1" + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... 一些校验和处理 + if (binderFactory != null) { + //创建数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); + try { + //使用数据绑定器进行数据转化(如果需要的话) + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); + } + + //... 一些异常处理 + } + + handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); + + return arg; +} +``` + +首先我们先看下参数解析器是如何从请求信息中拿到参数值,也即:`resolveName(resolvedName.toString(), nestedParameter, webRequest);`的实现: + +```java +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + Map uriTemplateVars = (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); +} +``` + +可以看到思路很简单,在走到当前步骤之前SpringMVC就做了一个处理,将我们Controller上写的URL与实际请求的URL做了一个映射处理,比如 Controller层为:`/user/{id}/{age}` ,实际请求为:`/user/1/27`。这时SpringMVC会根据路径匹配得到K,V,分别是id ->1和age ->27。并将这个Map存储在Request的请求域中,且将它的key值设为`HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE`。这样我们走到这里的时候再从请求域中根据key值就可以拿到这个信息,信息本身是个Map,再传入resolvedName就可以拿到value,因此很容易通过传入id得到1这个信息。(关心这个Map是何时构建的可以查阅源码的`RequestMappingInfoHandlerMapping#extractMatchDetails()`函数,它会在`RequestMappingInfoHandlerMapping#getHandlerInternal()`里被调用,只不过这里面的内容有些多,需要断点打的深一点) + +其次我们又看到了数据绑定器,我们前一篇中已经见过它了,当时我们说从HTTP请求中解析出数据后,得能将这些数据绑定到我们的参数上的,这个工作就是数据绑定器干的工作。由于我们的当前例子的参数比较简单(只是一个long id),所以很难发挥数据绑定器的作用,不过我们这里可以先简单的说一下: + +我们知道HTTP协议是文本协议,这代表从HTTP请求中解析出的东西都是文本(二进制除外),所谓文本也即字符串,因此上面的`resolveName()`函数得到的其实是字符串"1",但我们的HandlerMethod参数是long类型,这就需要一个字符串向long类型的转化,而这其实就是数据绑定器做的工作。 + +如果大家源码打的比较深的话会发现,实际进行转化的核心代码如下: + +```java +public class GenericConversionService implements ConfigurableConversionService { + //... + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + //... + //拿到转化器进行转化 + GenericConverter converter = getConverter(sourceType, targetType); + if (converter != null) { + Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); + } + //... + } + //... + +} +``` + +而获取转化器的代码如下: + +```java +protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); + GenericConverter converter = this.converterCache.get(key); + if (converter != null) { + return (converter != NO_MATCH ? converter : null); + } + + converter = this.converters.find(sourceType, targetType); + if (converter == null) { + converter = getDefaultConverter(sourceType, targetType); + } + + if (converter != null) { + this.converterCache.put(key, converter); + return converter; + } + + this.converterCache.put(key, NO_MATCH); + return null; +} +``` + +是不是又想到了策略模式?自己持有多个转化器,在需要转化的时候就找到适合的转化器来转化。 + +在SpringBoot2.7.2版本中,默认情况下的转化器共有124个,部分内容如下: + +![](http://img.topjavaer.cn/img/202311262224580.png) + + + +![image-20220906212651245](http://img.topjavaer.cn/img/202311262224620.png) + + + +![image-20220906212707262](http://img.topjavaer.cn/img/202311262224059.png) + +可以看到这些参数解析器都是将一种数据类型转为另一种数据类型,针对于我们的情况就需要一个将String类型转为Long类型的转化器。 + +另外需要提一嘴的是,这里的策略模式与之前参数解析器或返回值处理器的设计不同,之前策略接口都都有个类似于`support()`的功能,主类会挨个问每个策略是否支持解析,支持了再调用它来解析。但是这里是用HashMap来做的。也即我们将情况设计为key值,解决方案设计为value值。这样直接输入key值就能得到策略,无需遍历询问。通过源码也很容易看到: + +```java +private final Map converters = new ConcurrentHashMap<>(256); +``` + +`converters`是一个Map。其Key是源类型+目标类型 + +```java +final class ConvertiblePair { + + private final Class sourceType; + + private final Class targetType; +} +``` + +另外,SpringMVC支持让我们自定义一些类型转化器的,可以按照自己的规则来做类型转化,我们下一章就会说到。 + +## 2. 表单提交 + +### 2.1. 源码分析 + +举例如下: + +```html +
+
+
+
+
+ +
+``` + + + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +其中我们的POJO类,User和Pet信息如下: + +```java +public class User { + private String name; + private int age; + private Pet pet; + //省略getter setter +} +public class Pet { + private String name; + private int age; + //省略getter setter +} +``` + +当我们点击输入如下信息: + +![image-20220906214620559](http://img.topjavaer.cn/img/202311262225025.png) + +点击提交给后端,我们就能通过前端的参数将这些信息赋值到User对象上 + +![](http://img.topjavaer.cn/img/202311262225164.png) + +很明显,这是SpringMVC帮我们做的,依据之前的源码经验,我们知道必然有一个参数解析器帮我们做了这个工作,同样通过源码 + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +很快可以定位到帮我们解析参数的参数解析器是`ServletModelAttributeMethodProcessor`,因此这个参数解析器就是处理表单提交的,老规矩我们先看下它为什么能够处理表单请求再看下它是如何解析表单参数的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(ModelAttribute.class) || + (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); +} +``` + +其中`parameter.hasParameterAnnotation(ModelAttribute.class)`是指当前参数上包含注解`@ModelAttribute`,我们这里没有,因此是false。`this.annotationNotRequired`在当前对象中恒为true(构造方法中构造时就写死的true),最后一个`BeanUtils.isSimpleProperty(parameter.getParameterType())`是判断当前参数是否是基本类型,我们的参数是User对象,不是基本类型,取反后为true,因此整体返回true。 + +下面我们再看下`ServletModelAttributeMethodProcessor`是如何解析参数的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //... + //...获得参数名 + String name = ModelFactory.getNameForParameter(parameter); + //... + + Object attribute = null; + BindingResult bindingResult = null; + //... + //构造参数实例,其实就是调用默认构造方法,得到一个空的对象 + attribute = createAttribute(name, parameter, binderFactory, webRequest); + //...异常处理 + + if (bindingResult == null) { + //获得数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); + if (binder.getTarget() != null) { + if (!mavContainer.isBindingDisabled(name)) { + //使用数据绑定器,从HTTP请求中取出信息做绑定操作,核心方法 + bindRequestParameters(binder, webRequest); + } + //... + } + //... + } + //将信息加入到ModelAndViewContainer中 + Map bindingResultModel = bindingResult.getModel(); + mavContainer.removeAttributes(bindingResultModel); + mavContainer.addAllAttributes(bindingResultModel); + //返回结果 + return attribute; +} +``` + +源码中的流程会比较长,这里只截取了主要的部分,其思路比较清晰: + +由于我们知道当前参数不是一个简单类型,因此需要先构造出来,通过 + +```java +attribute = createAttribute(name, parameter, binderFactory, webRequest); +``` + +就构造出了一个空的对象(如果点进去源码的话会发现其实就是反射拿到默认构造方法,然后调用默认的构造方法创建对象),比如如果是我们的User对象,执行完这句后就会得到: + +![](http://img.topjavaer.cn/img/202311262225846.png) + +我们不妨叫一个壳对象,然后构造数据绑定器,数据绑定器会解析HTTP请求,将HTTP请求的信息绑定到我们的壳对象上,这样我们就得到了一个有意义的对象,其中数据的绑定操作对应源码: + +```java +bindRequestParameters(binder, webRequest); +``` + +执行完成后,我们的User对象就变成了 + +![](http://img.topjavaer.cn/img/202311262225889.png) + +因此`bindRequestParameters()`方法是解析出来参数。 + +`bindRequestParameters()`源码如下: + +```java +protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { + ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + Assert.state(servletRequest != null, "No ServletRequest"); + ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; + servletBinder.bind(servletRequest); +} +``` + +而`servletBinder.bind()`源码如下: + +```java +public void bind(ServletRequest request) { + MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); + //... 一些特殊处理 + addBindValues(mpvs, request); + doBind(mpvs); +} +``` + +十分关键的是`MutablePropertyValues`对象,这里我们已经将form表单提交的参数进行了初步解析,解析为了一个`MutablePropertyValues`对象,`MutablePropertyValues`内部有个List,装着解析后的每个参数信息: + +![](http://img.topjavaer.cn/img/202311262226515.png) + +这个解析其实不难,我们在提交form表单的时候,HTTP的参数原始数据如下: + +`name=tom&age=18&pet.name=myDog&pet.age=2`,因此我们可以很容易的按`&`和`=`进行拆分,得到上述`MutablePropertyValues`信息。 + +这个信息会非常的关键,我们在后面的数据绑定中就是以这个信息作为信息源来与我们的User对象进行绑定的。 + +多次源码深入后会走到 `DataBinder#applyPropertyValues()`方法,其源码如下: + +```java +protected void applyPropertyValues(MutablePropertyValues mpvs) { + try { + // Bind request parameters onto target object. + getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); + } + //...异常处理 +} +``` + +可以看到就是拿到属性访问器,设置属性。所谓属性访问器,大家不用想的多高大上,其实就是一个对象包装器,通过它可以反射的将一些属性设置值。这里的属性访问器就是`BeanWrapperImpl`,了解Spring的同学肯定对它很熟悉(其实我们三期网管的协议解析器就用了这个对象,我们可以简单将它理解为一个工具类,这个工具类可以反射设置对象里的属性值)。因此SpringMVC就是通过`BeanWrapperImpl`将HTTP请求信息绑定到`User`对象上的。 + +`BeanWrapperImpl#setPropertyValues()`函数源码如下: + +```java +public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) + throws BeansException { + + List propertyAccessExceptions = null; + //获得每个属性的信息,在上面我们已经看到了pvs中是有一个List的,从HTTP请求中解析出的 + List propertyValues = (pvs instanceof MutablePropertyValues ? + ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); + //... + try { + //for循环遍历每个属性值,然后设置 + for (PropertyValue pv : propertyValues) { + try { + setPropertyValue(pv); + } + //... 异常处理 + } + } + //... +} +``` + +后面一层层的源码会很深,我们可以讲一些比较核心的部分: + +在进行`setPropertyValue()`设置时会走进`AbstractNestablePropertyAccessor#processLocalProperty()`函数(`BeanWrapperImpl`继承自`AbstractNestablePropertyAccessor`): + +`AbstractNestablePropertyAccessor#processLocalProperty()`函数中比较重要的一行信息是: + +```java +valueToApply = convertForProperty( + tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); +``` + +其中`convertForProperty()`内容如下: + +```java +protected Object convertForProperty( + String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) + throws TypeMismatchException { + + return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); +} +``` + +走到这里的时候就可以看到与`@PathVariable`的一些共同之处,都需要使用类型转化器来转化数据。 + +这其实也容易理解,HTTP请求提交age=18,解出来的是字符串"18",自然就需要转为int类型。 + +因此大体的流程为:通过HTTP请求解析form提交的信息,根据KV值解析出来多条,如: + +```java +[ + "name": "张三", + "age": "18", + "pet.name": "狗子", + "pet.age": "18" +] +``` + +接着调用默认构造方法,new出空壳的参数对象,再借用`BeanWrapperImpl`,将解析出的多条KV信息绑定到参数对象上。在绑定的过程中可能需要类型转化,比如字符串转整型,这时就需要借助类型转化器来转化数据,将转化后的数据再绑定到属性上。 + +另外,`BeanWrapperImpl`其实也是持有124个类型转化器的: + +![](http://img.topjavaer.cn/img/202311262226320.png) + + + +### 2.2 自定义类型转化器 + +可以看到,类型转化器会将HTTP请求信息转化为我们参数上对应的数据类型,我们之前也说了SpringBoot2.7.2版本中默认包含124个类型转化器,这些类型转化器大都是基本类型转化器,如String转Integer等。我们可以自定义类型转化器,来扩展Spring自带的转化器功能,举例如下: + +```html +
+
+
+
+ +
+``` + +上例中,我们将`pet`信息写为**"狗子,18"**,也即我们想将**"狗子,18"**这一信息转为`Pet`对象,再或者说,我们想将字符串类型转为`Pet`对象。我们知道SpringBoot默认的数据转化器是没有这种功能的,此时就需要自定义类型转化器: + +```java +public class MyPetConverter implements Converter { + @Override + public Pet convert(String source) { + Pet pet = new Pet(); + String[] split = source.split(","); + pet.setName(split[0]); + pet.setAge(Integer.parseInt(split[1])); + return pet; + } +} +``` + +自定义类型转化器需要继承自`Convert`接口,我们这里的转化做的比较粗糙,大家明白就好。 + +接着我们就需要将自己的自定义转化器注册到SpringBoot中,**目前对于SpringMvc功能的增强可以通过自定义一个WebMvcConfigure Bean 或者继承WebMvcConfigure接口实现自己的对象注册到Bean中**。 + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new MyPetConverter()); + } +} +``` + +这样SpringMVC在进行类型转化的时候,根据form表单提交的信息`pet=myDog,2`,SpringMVC根据key值pet找到User对象中属性是pet的属性,发现类型是Pet类型,然后就是将`myDog,2`字符串转为Pet类型,此时转化的源是String,目的是Pet类型,根据这一信息作为key值从converts这个map中获取转化器,很自然的就拿到了我们自己写的转化器,然后调用我们自己写的转化器将字符串转为Pet对象。 + +### 2.3 一些补充 + +我在B站看这块视频的时候看到很多弹幕会提一个问题,我们在2.1节举的例子中,为什么Controller层的方法没加`@RequestBody`注解? + +也即 + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +这段代码中的参数User对象,为什么没有标@RequestBody注解。很多同学会认为只要是Post请求提交的对象,后端都应该是加`@RequestBody`注解的。 + +首先`@RequestBody`注解使用的场景是请求参数在请求体中并且是JSON格式(如果不了解你需要先学习基本的Spring MVC应用知识),而表单提交提交的数据虽然在请求体中,但不是JSON格式的数据,而是param1=value1¶m2=value2格式的数据,因此此处如果加`@RequestBody`注解会无法进来,从这也可以看出,这两种情况是使用不同的参数解析器来解析的。 + +注:上面说的不是很贴切,`@RequestBody`注解虽然拿的是请求体中的数据,但并不一定是JSON,你完全可以这样写: + +```java +@PostMapping("/user") +public String postUser(@RequestBody String json){ + return json; +} +``` + +这代表直接将请求体中所有的数据拿到作为一个字符串,这时请求体中到底传过来的是不是JSON都无所谓了。 + +但如果写出 + +```java +@PostMapping("/user") +public String postUser(@RequestBody User user){ + System.out.println(user); + return user.toString(); +} +``` + +则请求体中一定得是JSON格式的。 + +## 3. @RequestBody + +### 3.1 源码分析 + +上面我们已经提了一嘴`@RequestBody`注解,这个注解主要是将HTTP请求协议体中的JSON数据转为我们的Java对象,举例如下: + +```java +@RestController +public class RequestBodyController { + + @PostMapping("/user/2") + public Object addUse(@RequestBody User user){ + return user; + } +} +``` + +我们使用postman模拟发送请求如下: + +![](http://img.topjavaer.cn/img/202311262227870.png) + +很明显又是SpringMVC将HTTP请求体中的数据取出来,转为了我们的User对象并设置赋给了我们的参数,那么就来看看是哪个参数解析器做的工作: + +打断点定位是哪个参数解析器的工作我们不再重复了,实际上是`RequestResponseBodyMethodProcessor`参数解析器,我们还是看两个内容,为什么它支持解析这种情况,以及它是如何解析的: + +`supportsParameter()`源码如下: + +```java +@Override +public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestBody.class); +} +``` + +可以看到很简单,就是判断参数上是否有`@RequestBody`注解,凡是有这个注解的就支持,我们的User对象上有这个注解,所以很明显,当前参数解析器能解析这种情况。 + +下面我们就看下`RequestResponseBodyMethodProcessor`是如何解析参数的: + +```java +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + //核心的参数解析 + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + //... + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +`readWithMessageConverters()`函数会将HTTP请求体中的JSON转为我们的Java对象,其源码如下: + +```java +@Nullable +protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { + + //拿到请求类型 + MediaType contentType; + boolean noContentType = false; + try { + contentType = inputMessage.getHeaders().getContentType(); + } + //... + + //拿到我们的参数类型和Controller类型 + Class contextClass = parameter.getContainingClass(); + Class targetClass = (targetType instanceof Class ? (Class) targetType : null); + //... + + //拿到HTTP请求类型 + HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null); + Object body = NO_VALUE; + + EmptyBodyCheckingHttpInputMessage message = null; + try { + message = new EmptyBodyCheckingHttpInputMessage(inputMessage); + + //开始遍历所有的HttpMessageConverter,看看谁支持将当前http请求内容转为我们的参数 + for (HttpMessageConverter converter : this.messageConverters) { + Class> converterType = (Class>) converter.getClass(); + GenericHttpMessageConverter genericConverter = + (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : + //核心就是这个canRead,与我们之前的参数解析器的support()功能一致 + (targetClass != null && converter.canRead(targetClass, contentType))) { + if (message.hasBody()) { + HttpInputMessage msgToUse = + getAdvice().beforeBodyRead(message, parameter, targetType, converterType); + //找到HttpMessageConverter后调用read方法进行转化 + body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : + ((HttpMessageConverter) converter).read(targetClass, msgToUse)); + body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); + } + else { + body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); + } + break; + } + } + } + //...异常捕捉与日志记录 + + return body; +} +``` + +能够看到,这里依然是策略模式的使用,策略的接口是`HttpMessageConverter`,翻译过来即Http消息转化器。`RequestResponseBodyMethodProcessor`根据请求的**content-type**和我们的参数以及Controller对象类型等信息挨个询问每个消息转化器是否支持解析当前HTTP消息。在这里我们的content-type是**application/json**。 + +`RequestResponseBodyMethodProcessor`对象持有多个`HttpMessageConverter`,其中属性`messageConverters`是个`List`。SpringBoot2.7.2版本默认情况下`messageConverters`内有10个`HttpMessageConverter`实现对象 + +![](http://img.topjavaer.cn/img/202311262227622.png) + +通过名字不难看出`ByteArrayHttpMessageConverter`是用来解析byte数组的,`StringHttpMessageConverter`是用来解析字符串的,`MappingJackson2HttpMessageConverter`对象是用来转化JSON数据的,`Jaxb2RootElementHttpMessageConverter`是用来解析xml的。 + +打断点不难发现,`MappingJackson2HttpMessageConverter`解析器可以解析我们的请求,其判断自己是否能解析的代码如下: + +```java +public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + //先判断是不是自己能支持的mediaType,这里我们的mediaType是application/json + if (!canRead(mediaType)) { + return false; + } + //得到参数类型,其实就是我们的User.class + JavaType javaType = getJavaType(type, contextClass); + + //得到ObjectMapper,jackson的核心组件 + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + //判断当前对象是否能序列化 + if (objectMapper.canDeserialize(javaType, causeRef)) { + return true; + } + logWarningIfNecessary(javaType, causeRef.get()); + return false; +} +``` + +`MappingJackson2HttpMessageConverter`支持两种mediaType:**application/json**和**application/\*+json** + +![](http://img.topjavaer.cn/img/202311262227182.png) + +我们发的HTTP请求content-type是**application/json**,同时我们的Java对象还支持序列化,因此自然返回true。 + +知道了`MappingJackson2HttpMessageConverter`为什么能够解析,再看`MappingJackson2HttpMessageConverter`是如何解析的: + +```java +public Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + //拿到我们的参数类型 + JavaType javaType = getJavaType(type, contextClass); + //传参Java类型和Http输入信息 + return readJavaType(javaType, inputMessage); +} +``` + + + +```java +private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { + //拿到http请求的content-type和charset + MediaType contentType = inputMessage.getHeaders().getContentType(); + Charset charset = getCharset(contentType); + + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType); + //... + + //判断是不是unicode编码类型 + boolean isUnicode = ENCODINGS.containsKey(charset.name()) || + "UTF-16".equals(charset.name()) || + "UTF-32".equals(charset.name()); + try { + InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody()); + //.... + + //由于我们的请求类型是UTF-8,因此会进入if分支 + if (isUnicode) { + //直接调用objectMapper将HTTP输入流转为我们的java对象 + return objectMapper.readValue(inputStream, javaType); + } + //... + } + //...异常捕捉 +} +``` + +其实就是拿到`ObjectMappper`然后调用`ObjectMappper`来解析JSON。 + +### 3.2 一些总结 + +这样其实我们就基本分析完了参数解析器`RequestResponseBodyMethodProcessor`处理流程的源码,首先`RequestResponseBodyMethodProcessor`会解析所有标注了`@RequestBody`注解的参数,其次在解析的时候,`RequestResponseBodyMethodProcessor`内部持有多个`HttpMessageConverter`,`RequestResponseBodyMethodProcessor`会挨个遍历每个`HttpMessageConverter`询问其是否能够解析,`HttpMessageConverter`一般会根据请求的**content-type**和要转化的Java对象来判断自己是否能解析,如我们的`MappingJackson2HttpMessageConverter`只能解析请求的content-type是**application/json**和**application/\*+json**的。拿到`HttpMessageConverter`就可以直接进行解析了。**可以看到我们之前对于`@RequestBody`注解的理解比较片面,认为前端必须要传入JSON,然后它就会被解析为Java对象,现在看了源码会发现前端能传很多格式可以被`@RequestBody`解析,比如xml。** + +### 3.3 自定义HttpMessageConverter + +与自定义类型转化器一样,我们也可以自定义消息转化器。一个消息转化器往往是解析一种(或多种)mediaType类型下的HTTP请求,不同的mediaType的HTTP请求内容格式也不相同,比如application/json格式就是JSON类型,application-xml格式就是xml类型。 + +既然要自定义`HttpMessageConverter`,就使用自定义的media-type:**application-dabin** + +![](http://img.topjavaer.cn/img/202311262228041.png) + +同时要求这种media-type下前端传过来的参数只有value,没有key,且value之间逗号隔开,比如前端传过来的请求体是: + +![](http://img.topjavaer.cn/img/202311262228829.png) + +这时为保证我们的Controller层依然能够正常接收前端的请求,就需要自定义一个消息转化器,来专门解析**application-dabin**这种mediaType的请求: + + + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +我们主要实现了`canRead()`和`read()`方法,实现的源码比较简单,这里不再解释。 + +然后我们将这个自定义的消息转化器加入到Spring中: + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } +} +``` + +根据前面的postman截图发送消息,打断点可以看到`RequestResponseBodyMethodProcessor`的`messageConverters`已经有了11个实现类,其中就包含我们自定义的消息转化器。 + +![](http://img.topjavaer.cn/img/202311262228331.png) + + + +同样,我们的Controller层依然可以正常接收到参数: + +![](http://img.topjavaer.cn/img/202311262228197.png) + + + +## 4. @ResponseBody + +### 4.1 源码分析 + +前面我们看的都是参数解析器的源码,现在我们看下返回值处理器的源码,从我们使用最频繁的`@ResponseBody`说起,很多同学知道的是`@ResponseBody`会将我们返回给请求的对象转为JSON写出到HTTP响应。现在我们来看这一功能是如何实现的。 + +我们知道在执行完HandlerMethod拿到返回值的时候,SpringMVC会使用返回值处理器来处理返回值: + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +而处理返回的源码为: + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + if (handler == null) { + throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); + } + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} + + +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + boolean isAsyncValue = isAsyncReturnValue(value, returnType); + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { + continue; + } + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +这些内容我们之前都看过,策略模式,选择一个能处理的`HandlerMethodReturnValueHandler`进行处理。 + +为了源码分析的顺序,我们依然举个例子: + +编写后端: + +```java +@RestController +public class ResponseBodyController { + @GetMapping("/user/3") + public User getUser(){ + User user = new User(); + user.setName("Tom"); + user.setAge(18); + Pet pet = new Pet(); + pet.setName("myDog"); + pet.setAge(2); + user.setPet(pet); + return user; + } +} +``` + +通过PostMan发送请求: + +![](http://img.topjavaer.cn/img/202311262229456.png) + + + +可以看到我们后端返回的是一个Java对象,前端拿到的是一个JSON。这肯定是SpringMVC帮我们做了处理,根据前面的源码,很容易打断点定位到能处理这个返回的是`RequestResponseBodyMethodProcessor`返回值处理器(是的,又是它,我们在看`@RequestBody`注解源码的时候也是它,它既是参数解析器也是返回值处理器)。 + +#### 4.1.1 RequestResponseBodyMethodProcessor + +同样,我们先看它为什么能处理这个返回,再看它是如何处理返回的: + +`supportsReturnType()`源码如下: + +```java +@Override +public boolean supportsReturnType(MethodParameter returnType) { + return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || + returnType.hasMethodAnnotation(ResponseBody.class)); +} +``` + +可以看到就是判断方法所在的类上是否包含`@ResponseBody`注解和方法本身上是否包含`@ResponseBody`注解,由于`@RestController`是个复合注解,由`@Controller`与`@ResponseBody`组成,因此我们的返回值可以被`RequestResponseBodyMethodProcessor`处理。下面我们就看下`RequestResponseBodyMethodProcessor`是如何处理返回值的: + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + mavContainer.setRequestHandled(true); + ServletServerHttpRequest inputMessage = createInputMessage(webRequest); + ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); + + // Try even with null return value. ResponseBodyAdvice could get involved. + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); +} +``` + +这里会走进`writeWithMessageConverters()`函数: + +`writeWithMessageConverters()`的源码会相对比较多且复杂,在讲解源码前我们需要先讲一个东西叫**内容协商**。 + +#### 4.1.2 内容协商 + +我们之前在HTTP请求头中已经看到了**content-type**信息,这代表请求方会告诉服务端自己发来的数据是什么类型的数据,除此以外,HTTP还有另一个重要信息**accept**。 + +在发送HTTP请求的时候,浏览器会告诉服务器自己**支持**哪种数据的返回,这一信息会放在HTTP请求头的Accept字段。比如我们上面的Post表单提交,其Accept信息是: + +![](http://img.topjavaer.cn/img/202311262229278.png) + +上述代表当前浏览器可以接收(逗号分隔) + +- text/html +- application/xhtml+xml +- application/xml;q=0.9 +- image/webp +- image/apng +- */*;q=0.8 +- application/signed-exchange;v=b3;q=0.9 + +这些类型的返回,其中q代表权重(关于权重我们一会再说)。 + +但是我们的**服务器往往也需要判断自己能够返回哪些类型,然后对服务器能返回的且浏览器能接收的这两个类型集合做交集,得到的结果就是服务器可以返回给浏览器的数据类型,这就是内容协商**。 + +#### 4.1.3 writeWithMessageConverters() + +下面我们看下`writeWithMessageConverters()`的部分源码: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + Object body; + Class valueType; + Type targetType; + + //... + //得到返回结果的值和返回结果的类型 + body = value; + valueType = getReturnValueType(body, returnType); + targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); + + //一些IO的处理... + + //首先说明MediaType类型翻译过来是媒体类型,其实就是我们前面说的Http Accept和Content-Type里写的那些东西 + //也就是我们前面说的返回给(浏览器支持的)Http的数据类型,下面有很多MediaType 不再赘述 + //selectedMediaType就是被选中的那个输出类型 + MediaType selectedMediaType = null; + //判断用户是否自己设置了content-type,所谓自己设置了content-type也即我们自己指定的输出的mediaType + MediaType contentType = outputMessage.getHeaders().getContentType(); + boolean isContentTypePreset = contentType != null && contentType.isConcrete(); + if (isContentTypePreset) { + //... 日志 + //如果自己设置了输出的content-type就按用户自己设置的来,否则才内容协商 + selectedMediaType = contentType; + } + else { + //这整个else都是内容协商,目的就是找到需要返回的mediaType + HttpServletRequest request = inputMessage.getServletRequest(); + //acceptableTypes是浏览器可以接收的数据类型,也即从请求头里的Accept解析出的内容 + List acceptableTypes; + try { + //解析并获得浏览器可以接收的数据类型 + acceptableTypes = getAcceptableMediaTypes(request); + } + // ...异常处理 + + //这里是获得服务器可以输出的数据类型 + List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); + + //...异常处理 + + //mediaTypesToUse是producibleTypes和acceptableType的交集,也即内容协商的结果 + //两层for循环遍历将两边都支持的数据类型放入mediaTypesToUse + List mediaTypesToUse = new ArrayList<>(); + for (MediaType requestedType : acceptableTypes) { + for (MediaType producibleType : producibleTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + //...找不到可用media的异常处理 + + //按权重排序 + MediaType.sortBySpecificityAndQuality(mediaTypesToUse); + //遍历交集,从中确定一个输出的数据类型 + for (MediaType mediaType : mediaTypesToUse) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } + else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; + } + } + //... + } + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + //下面的代码我们一会再说 + if (selectedMediaType != null) { + selectedMediaType = selectedMediaType.removeQualityValue(); + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +可以看到,得到selectedMediaType,也即得到了确定的Http输出类型(MediaType)后,就该将我们的输出值转为这个对应的类型数据了,但现在问题来了,要怎样转化呢? + +`AbstractMessageConverterMethodArgumentResolver`(`RequestResponseBodyMethodProcessor`的父类)内部有一个属性叫 + +```java +protected final List> messageConverters; +``` + +`HttpMessageConverter`的集合,这个接口我们上面讲过了HTTP消息转化器,但上面说的是将HTTP消息转为我们的Java对象,这里的作用是将Java对象转为我们的HTTP消息。对于`HttpMessageConverter`而言,HTTP转Java属于read,Java转HTTP属于write。 + +`HttpMessageConverter`接口源码如下: + +```java +public interface HttpMessageConverter { + + boolean canRead(Class clazz, @Nullable MediaType mediaType); + + boolean canWrite(Class clazz, @Nullable MediaType mediaType); + + List getSupportedMediaTypes(); + + default List getSupportedMediaTypes(Class clazz) { + return (canRead(clazz, null) || canWrite(clazz, null) ? + getSupportedMediaTypes() : Collections.emptyList()); + } + + T read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + + void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; +} +``` + +其中`canRead`和`canWrite()`代表是否支持读取和写入。`read()`和`write()`代表进行读取和写入。 + +在默认情况下,`messageConverters`内有10个已经初始化的`HttpMessageConverter`(我们之前已经看到过了): + +![](http://img.topjavaer.cn/img/202311262229615.png) + + + +它们的功能通过名字也很容易看出来,比如`ByteArrayHttpMessageConvert`是将byte数组转为Http数据输出,`MappingJackson2HttpMessageConverter`是将Java对象转为JSON然后通过HTTP输出。 + +我们的返回处理器会挨个遍历这10个消息转化器,询问它们是否支持写入,如果支持写入,那么就用这个消息处理器来写入,对应的源码便是刚才的下半部分: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + //...上面的源码我们已经说过了,这里不再贴出 + + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + if (selectedMediaType != null) { + //移除权重 + selectedMediaType = selectedMediaType.removeQualityValue(); + //遍历10个消息转化器 + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + //判断它们是否支持写入 + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + //如果支持写入就调用write方法写入(写入到Http的响应体中) + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +通过上面说的内容协商,我们已经知道`selectedMediaType`是**application/json**。然后打断点会得到`MappingJackson2HttpMessageConverter`消息转化器支持处理我们的返回结果。我们这里自然需要看两点内容了,为什么它支持写出,它又是如何写出的。 + +#### 4.1.4 MappingJackson2HttpMessageConverter + +`canWrite()`源码如下: + +```java +public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { + if (!canWrite(mediaType)) { + return false; + } + if (mediaType != null && mediaType.getCharset() != null) { + Charset charset = mediaType.getCharset(); + if (!ENCODINGS.containsKey(charset.name())) { + return false; + } + } + ObjectMapper objectMapper = selectObjectMapper(clazz, mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + if (objectMapper.canSerialize(clazz, causeRef)) { + return true; + } + logWarningIfNecessary(clazz, causeRef.get()); + return false; +} +``` + +可以看到与`canRead()`代码基本相同,就是判断要写出的mediaType自己是否支持,以及要写出的对象是否可以序列化。 + +`write()`源码如下: + +```java +public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + final HttpHeaders headers = outputMessage.getHeaders(); + addDefaultHeaders(headers, t, contentType); + //... + //调用子类的writeInternal() + writeInternal(t, type, outputMessage); + //通过输出流写出 + outputMessage.getBody().flush(); +} + +protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + //得到输出的mediaType和编码格式 + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + + //得到返回的对象类型 + Class clazz = (object instanceof MappingJacksonValue ? + ((MappingJacksonValue) object).getValue().getClass() : object.getClass()); + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(clazz, contentType); + + //... + + //拿到HTTP请求输出流 + OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody()); + + //下面的内容都是使用jackson将参数写出 + //笔者不太熟悉jackson的api,因此只能写到这里 + try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding)) { + writePrefix(generator, object); + + Object value = object; + Class serializationView = null; + FilterProvider filters = null; + JavaType javaType = null; + + if (object instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + if (type != null && TypeUtils.isAssignable(type, value.getClass())) { + javaType = getJavaType(type, null); + } + + ObjectWriter objectWriter = (serializationView != null ? + objectMapper.writerWithView(serializationView) : objectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + if (javaType != null && javaType.isContainerType()) { + objectWriter = objectWriter.forType(javaType); + } + SerializationConfig config = objectWriter.getConfig(); + if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && + config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + objectWriter = objectWriter.with(this.ssePrettyPrinter); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); + } + //异常处理... +} +``` + +核心思想还是拿到jackson的ObjectMapper将Java对象转为JSON再写出到HTTP输出流。 + +### 4.2 XML + +看源码的过程中我们其实是看到SpringMVC是包含有xml消息转化器的,但xml消息转化器要想生效,还需要导入一个依赖包。 + +```xml + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + +``` + +此时我们的Controller层方法不变 + +```java +@PostMapping("/user") +public User postUser(@RequestBody User user){ + return user; +} +``` + +但这时的Http响应体返回为: + +![](http://img.topjavaer.cn/img/202311262230595.png) + +可以看到是一个xml类型的数据。 + +我们之前说过,内容协商是根据浏览器能处理的请求和我们服务器能返回的请求共同再根据权重排序共同得出来的结果。上面的浏览器请求中,我们返回的是`application/json`格式的数据,这是匹配到的浏览器的`*/*`类型,但这种类型只有0.8的权重,如果我们的服务器支持xml类型的返回结果,`application/xml;q=0.9`是0.9的权重,此时就会按xml转化返回。 + +并且由于我们支持xml类型的输出了,因此在内容协商的时候得到了服务端更多可支持的输出类型。 + +`getProducibleMediaTypes()`获得服务端支持的输出类型由之前的4种: + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + + + +![](http://img.topjavaer.cn/img/202311262230521.png) + + + +变为了7种(虽然是10个,但有3个重复的) + +![](http://img.topjavaer.cn/img/202311262230388.png) + + + +多出来的`application/xml`优先级肯定高于之前的`application/json`这种,因此SpringBoot会按xml去解析。 + +这时我们就要问一句,为什么多出来了几种解析方式,我们点进`getProducibleMediaTypes()`函数源码: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +其源码思路比较简单,就是遍历每个`HttpMessageConverter`,判断其是否支持写入,如果支持写入的话,顺便也将其支持的mediaType拿到,所有支持写入的`HttpMessageConverter`对应的mediaType集合就是服务器支持的medisType。 + +不同的是由于xml解析依赖的导入,现在SpringBoot的`messageConverters`集合多了两种类型(同时也少了一个): + +![](http://img.topjavaer.cn/img/202311262230063.png) + +这俩是同一个类,都是`MappingJackson2XmlHttpMessageConverter`,它们是用于支持xml写出的,它们支持的mediaType为: + +```java +public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType("application", "xml", StandardCharsets.UTF_8), + new MediaType("text", "xml", StandardCharsets.UTF_8), + new MediaType("application", "*+xml", StandardCharsets.UTF_8)); + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); +} +``` + +共三个mediaType: + +- `application/xml;charset=UTF-8` +- `text/xml;charset=UTF-8` +- `application/*+xml;charset=UTF-8` + +也就是我们上面截图多出来的那三个。 + +根据排序后,由于xml优先级高于json,自然`selectMedia`就是`application/xhtml+xml` + +能处理`application/xhtml+xml`类型的消息解析器自然是新加进来的`MappingJackson2XmlHttpMessageConverter`。 + +在这里就可以看到虽然后端业务代码同样返回的是User对象,但由于HTTP请求Accept字段的不同,就可以解析为不同的格式,有人想要json就将Accept写为**application/json**,有人想要xml就将Accept写为**application/xhtml+xml** + +这一做法的应用场景还是非常多的,比如不同的客户端想要不同的数据结构,浏览器想要json格式,而app想要xml格式,客户端只需要在自己的请求头的accept字段修改接收的数据类型或给要接收的数据类型排较高权重即可实现自适应返回数据。 + +还有类似的应用场景,比如我们自己写一个消息转化器,将返回的结果转为excel表格作为导出。同一个查询接口,前端可以根据请求accepet的不同,将json设置最高,可以查回来json结构直接展示。也可以将Accept设置为excel文件(自定义的mediaType),此时后端就会使用自定义消息转化器按文件输出。 + +刚才我们说了请求端可以修改accept的属性来决定返回类型,但有时修改accept会比较麻烦,比如对于表单提交。SpringBoot针对这种情况给出了可以通过请求参数来获得客户端想返回的数据类型,通过属性`spring.mvc.contentnegotiation.favorParameter`来开启这一功能 + +```yaml +spring: + mvc: + content negotiation: + favor-parameter: true +``` + +此时我们只需要在请求参数中加上format参数,即可指定客户端想要的数据类型,如: + +`ip:port/user?format=xml` 代表以xml形式返回 + +`ip:port/user?format=json`代表以json形式返回 + +### 4.3 为什么导入XML依赖就多了xml的消息转化器 + +这里再补充一个细节,我们之前看到,在项目启动的时候,`messageConverters`内就已经加载了很多实现好的消息转化器,并且当我们导入`jackson-dataformat-xml`依赖时,又会自动增加xml的消息转化器,这是怎么做到的? + +首先根据SpringBoot的自动装配我们知道,有关Spring Mvc的所有装配都在`WebMvcAutoConfiguration`下,在这个类下。在这个类下有一个继承自`WebMvcConfigurer`的方法 + +```java +@Override +public void configureMessageConverters(List> converters) { + this.messageConvertersProvider + .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); +} +``` + +在这里SpringBoot装配进了一些消息转化器,其中`customConverters.getConverters()`源码为: + +```java +public class HttpMessageConverters implements Iterable> { + //... + public List> getConverters() { + return this.converters; + } + //... +} +``` + +这里的`converters`属性是在构造方法中传进来的,也即一开始new的时候构造好的,构造方法如下: + +```java +public class HttpMessageConverters implements Iterable> { + //... + + public HttpMessageConverters(HttpMessageConverter... additionalConverters) { + this(Arrays.asList(additionalConverters)); + } + public HttpMessageConverters(Collection> additionalConverters) { + this(true, additionalConverters); + } + public HttpMessageConverters(boolean addDefaultConverters, Collection> converters) { + List> combined = getCombinedConverters(converters, + addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); + combined = postProcessConverters(combined); + this.converters = Collections.unmodifiableList(combined); + } + //... +} +``` + +可以看到构造方法会执行一个叫`getDefaultConverters()`方法,这个方法会获得默认的`HttpMessageConverter` + +```javascript +public class HttpMessageConverters implements Iterable> { + //... + private List> getDefaultConverters() { + List> converters = new ArrayList<>(); + if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport",null)) { + converters.addAll(new WebMvcConfigurationSupport() { + public List> defaultMessageConverters() { + return super.getMessageConverters(); + } + }.defaultMessageConverters()); + } else { + converters.addAll(new RestTemplate().getMessageConverters()); + } + reorderXmlConvertersToEnd(converters); + return converters; + } + //... +} +``` + +其中`getDefaultConverters()`又会调用`super.getMessageConverters();` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final List> getMessageConverters() { + if (this.messageConverters == null) { + this.messageConverters = new ArrayList<>(); + configureMessageConverters(this.messageConverters); + if (this.messageConverters.isEmpty()) { + addDefaultHttpMessageConverters(this.messageConverters); + } + extendMessageConverters(this.messageConverters); + } + return this.messageConverters; + } + //... +} +``` + +上面的代码又会调用`addDefaultHttpMessageConverters(this.messageConverters);` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final void addDefaultHttpMessageConverters(List> messageConverters) { + messageConverters.add(new ByteArrayHttpMessageConverter()); + messageConverters.add(new StringHttpMessageConverter()); + messageConverters.add(new ResourceHttpMessageConverter()); + messageConverters.add(new ResourceRegionHttpMessageConverter()); + if (!shouldIgnoreXml) { + try { + messageConverters.add(new SourceHttpMessageConverter<>()); + } + catch (Throwable ex) { + // Ignore when no TransformerFactory implementation is available... + } + } + messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + + if (romePresent) { + messageConverters.add(new AtomFeedHttpMessageConverter()); + messageConverters.add(new RssChannelHttpMessageConverter()); + } + + if (!shouldIgnoreXml) { + if (jackson2XmlPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); + } + else if (jaxb2Present) { + messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + } + } + + if (kotlinSerializationJsonPresent) { + messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); + } + if (jackson2Present) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); + } + else if (gsonPresent) { + messageConverters.add(new GsonHttpMessageConverter()); + } + else if (jsonbPresent) { + messageConverters.add(new JsonbHttpMessageConverter()); + } + + if (jackson2SmilePresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build())); + } + if (jackson2CborPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); + } + } + //... +} +``` + +从上面的代码可以看到默认情况下导入了`ByteArrayHttpMessageConverter`、`StringHttpMessageConverter`、`ResourceHttpMessageConverter`、`ResourceRegionHttpMessageConverter`、`AllEncompassingFormHttpMessageConverter`,这些我们都在之前见到了,还有一些需要根据条件判断是否应该导入的,比如`MappingJackson2HttpMessageConverter`、`MappingJackson2XmlHttpMessageConverter`。 + +我们之前是导入了xml解析包,就自动添加了`MappingJackson2XmlHttpMessageConverter`消息转化器,这是因为此时`jackson2XmlPresent`属性为true,而`jackson2XmlPresent`属性的判断逻辑是: + +```java +jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); +``` + +很简单,就是判断类`com.fasterxml.jackson.dataformat.xml.XmlMapper`是否存在,如果存在`jackson2XmlPresent`就为true,从而`MappingJackson2XmlHttpMessageConverter`就会被创建和加载。 + +### 4.4 自定义HttpMessageConverter + +我们在上一章`@RequestBody`中已经讲了自定义HttpMessageConverter,那时我们自定义了一个消息转化器,只不过它是用来解析HTTP请求的,现在我们需要自定义一个解析器是处理返回HTTP响应的。我们可以看下之前定的自定义消息转化器: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +上面的`canWrite()`、`write()`和`getSupportedMediaTypes()`都是写出的时候需要实现的,我们现在就来实现: + +同样我们自定义一种mediaType叫`application/dabin`,然后自定义消息转化器,将Java对象按`application/dabin`的格式写出: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return mediaType== null ||this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public List getSupportedMediaTypes() { + return Collections.singletonList(mediaType); + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + if(o instanceof User){ + User user = (User) o; + String stringBuilder = user.getName() + "," + + user.getAge() + "," + + user.getPet().getName() + "," + + user.getPet().getAge(); + outputMessage.getBody().write(stringBuilder.getBytes()); + } + } +} +``` + +同理,加这个convert加入到`HttpMessageConverters`中: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } + } +} +``` + +我们依然使用`application/dabin`的content-type方式提交,但同时将Accept也设置为`application/dabin`: + +此时,我们的返回结果为 + +![](http://img.topjavaer.cn/img/202311262232506.png) + +可以看到数据确实按照我们想要的结果类型返回了,也就是根据我们能处理的MedisType走到了我们自定义的消息转化器。 + +如果不想修改Http头的Accept信息,而是像我们之前那样,从参数中的format来决定Media类型,之前通过yml配置开启的方式在这种情况下已经不适用,因为那种方式只支持format=json和format=xml两种类型,我们现在想要类似于format=dabin这种类型,此时就需要自定义内容协商策略。 + +```java + @Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + configurer.strategies(Collections.singletonList(myContentNegotiationStrategy)); + } + } +} +``` + +这种情况下,我们就可以通过`ip:port/user?format=dabin`的形式走到自定义消息转化器了 + +但这种方式有个问题,就是我们自定义的消息转化器会覆盖了SpringBoot自带的消息转化器,那么此时在协议头中的Accept等信息都无法处理了,这肯定不是我们想看到的,一种比较危险的办法是我们自己再把它new出来加进去: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + ArrayList list = new ArrayList<>(); + list.add(myContentNegotiationStrategy); + list.add(headerContentNegotiationStrategy); + configurer.strategies(list); + } + } +} +``` + +还一种比较简单,没有心里负担的做法: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("dabin",new MediaType("application","dabin")); + } + } +} +``` + +这种情况也是可以的,不过需要我们开启`spring.mvc.contentnegotiation.favorParameter` + +这种就是在默认的`ParameterContentNegotiationStrategy`中添加支持的mediaType,在原来支持的xml和json里又加入dabin。 + +![](http://img.topjavaer.cn/img/202311262232459.png) + +还有一种更简单的方案,`ContentNegotiationConfigurer`中的`mediaTypes`支持yml配置,也即我们只需要: + +```java +spring: + mvc: + content negotiation: + favor-parameter: true + media-types: {dabin: application/dabin} +``` + +即可。 + +### 4.5 源码优化 + +在前面的源码分析中,其实是可以看到一个SpringMVC的优化点的,在说如何优化前我们先说回内容协商: + +内容协商的时候,会执行 + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + +语句,这条语句会得到当前服务器端支持返回的mediaType。其源码如下: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +可以看到就是遍历所有的`HttpMessageConverter`实例,判断其是否支持将当前返回值写出,如果支持就将这个`HttpMessageConverter`对应的mediaType记录起来,然后汇总返回,这个汇总结果就是当前服务器端支持的返回类型。 + +在内容协商后,我们拿到了要输出的mediaType理论上就该使用`HttpMessageConverter`将信息写出,此时SpringMVC的做法如下: + +```java +for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } +} +``` + +依然是遍历所有的`HttpMessageConverter`,判断其是否支持写出,支持再调用写出函数转化和写出结果。 + +其实这里很多人已经看明白了,上一步内容协商的时候已经遍历过所有的`HttpMessageConverter`了,这其中有些是支持写出,有些是不支持的,将支持写出的`HttpMessageConverter`对应的mediaType汇总起来。再从这些汇总后候选的mediaType中选出一个合适的mediaType作为写出类型,再选择一个能处理这个mediaType的`HttpMessageConverter`写出。这时第二遍就不需要再遍历所有的mediaType,直接遍历第一遍支持写出的`HttpMessageConverter`结果就可以了,相当于第一遍做个初筛选,第二遍做可以在初筛选的结果上再遍历得到具体的那个`HttpMessageConverter`做转化,而无需在第二次时再遍历所有的mediaType。甚至如果mediaType与`HttpMessageConverter`具有映射关系,可以将第一步的初筛结果转为map,key是支持的mediaType,value是`HttpMessageConverter`,内容协商后可以直接通过key拿到`HttpMessageConverter`,时间复杂度O(1),无需二次遍历。 \ No newline at end of file diff --git a/docs/source/spring-mvc/4-fileupload-interceptor.md b/docs/source/spring-mvc/4-fileupload-interceptor.md new file mode 100644 index 0000000..285e5a2 --- /dev/null +++ b/docs/source/spring-mvc/4-fileupload-interceptor.md @@ -0,0 +1,631 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,文件上传,拦截器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 文件上传 + +我们先从文件上传说起,相信大家都写过这样的代码: + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +其中参数`MultipartFile`是SpringMVC为我们提供的,SpringMVC会将HTTP请求传入的文件转为`MultipartFile`对象,我们直接操作这个对象即可。但我们不禁要问,SpringMVC是如何做的呢?在文件上传的时候,SpringMVC帮我们做了哪些事情呢?要回答这个问题还是得回到梦开始的地方:`DispatcherServlet#doDispatch()` + +我们之前已经在前面两篇文章讲了`DispatcherServlet#doDispatch()`的大体流程,这里不妨再拿过来,不过这里我们需要细化一点关于文件上传的东西: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + //文件上传的处理 + boolean multipartRequestParsed = false; + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + ModelAndView mv = null; + //找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //找到处理方法的适配器 + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... + + //文件资源的释放 + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } +} +``` + +可以看到,这里面最核心的代码是`checkMultipart(request)`,我们不妨点进源码: + +```java +protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + //... + return this.multipartResolver.resolveMultipart(request); + //... + } + // If not returned before: return original request. + return request; +} +``` + +首先`multipartResolver`就是文件解析器,`DispatcherServlet`对象持有`multipartResolver`属性,if语句会先判断当前HTTP请求是否是文件,如果是文件则用文件解析器来解析HTTP请求。 + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + @Nullable + private MultipartResolver multipartResolver; + //... +} +``` + +其中`multipartResolver`的具体实现类为`StandardServletMultipartResolver`,因此`StandardServletMultipartResolver`的`isMultipart()`判断内容如下: + +```java +public boolean isMultipart(HttpServletRequest request) { + return StringUtils.startsWithIgnoreCase(request.getContentType(), + (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); +} +``` + +简单来讲就是判断当前HTTP请求的`contentType`是否是以`multipart/`开头的(严格模式下是以`multipart/form-data`开头),我们知道,如果前端传文件,则HTTP请求的请求往往是`multipart/form-data`,因此 如果请求的参数包含文件,这个if语句肯定是判断为true的 + +再往下走就是使用文件解析器来解析HTTP请求,也即: + +```java +return this.multipartResolver.resolveMultipart(request); +``` + +很明显,上面的代码就是最核心的文件解析,`StandardServletMultipartResolver#resolveMultipart()`源码内容如下: + +```java +public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { + return new StandardMultipartHttpServletRequest(request, this.resolveLazily); +} +``` + + + +```java +public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) + throws MultipartException { + + super(request); + if (!lazyParsing) { + parseRequest(request); + } +} +``` + +其中`resolveLazily`默认是false,`resolveLazily`代表是否懒解析,也即是否到参数需要文件的时候再解析,默认为false。因此我们会在这里直接解析,解析的核心也很简单,就是将当前HTTP请求对象包装为一个更复杂的的HTTP请求对象:`StandardMultipartHttpServletRequest`,这个对象在我们原来的HTTP请求对象的基础上多了一个关键的属性:`multipartFiles` + +```java +public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper + implements MultipartHttpServletRequest { + + @Nullable + private MultiValueMap multipartFiles; + //... +} +``` + +`AbstractMultipartHttpServletRequest`是`StandardMultipartHttpServletRequest`的抽象父类。 + +`multipartFiles`属性是个`Map`,`Map`的key为文件名,value为`MultipartFile`对象,`MultipartFile`对象大家应该已经很熟悉了。补充一句的是这是个`MultiValueMap`,也即允许一个key对应多个value,也即允许多个同名的文件上传。 + +很明显我们也可以得出结论,所谓文件解析,就是构造那么一个`StandardMultipartHttpServletRequest`对象,并从当前HTP请求中将文件解出来赋值给`multipartFiles`属性,这样在`HandlerAdapter`做参数解析的时候就可以直接从`StandardMultipartHttpServletRequest`中拿出文件信息赋值给我们的参数了。 + +那么就让我们看下文件解析器是如何解析文件的: + +```java +private void parseRequest(HttpServletRequest request) { + try { + //从原始的HTTP请求中得到Part,Part其实就可以认为是文件对象了 + Collection parts = request.getParts(); + this.multipartParameterNames = new LinkedHashSet<>(parts.size()); + //files对象就是用来存储解析结果的 + MultiValueMap files = new LinkedMultiValueMap<>(parts.size()); + //遍历parts + for (Part part : parts) { + //下面三行代码是要拿到文件名 + String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); + ContentDisposition disposition = ContentDisposition.parse(headerValue); + String filename = disposition.getFilename(); + //如果文件名存在 + if (filename != null) { + //做一下文件名的特殊处理 + if (filename.startsWith("=?") && filename.endsWith("?=")) { + filename = MimeDelegate.decode(filename); + } + //将文件加入到files中,其中key为文件名,value就是StandardMultipartFile对象 + files.add(part.getName(), new StandardMultipartFile(part, filename)); + } + else { + this.multipartParameterNames.add(part.getName()); + } + } + //将files对象赋值给multipartFiles属性 + setMultipartFiles(files); + } + catch (Throwable ex) { + handleParseFailure(ex); + } +} +//做了一个map的不可修改 +protected final void setMultipartFiles(MultiValueMap multipartFiles) { + this.multipartFiles = + new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles)); +} +``` + +很明显,最最核心的代码其实就是 + +```java +Collection parts = request.getParts(); +``` + +这句话其实就是从HTTP请求中解析出来`Part`对象,然后取Part对象的各种属性以及将它包装为`StandardMultipartFile`,最终就是赋值到`multipartFiles`属性上。 + +`request.getParts()`的内容非常长,也非常深,就不带大家一起看了,感兴趣的同学可以自己去看下,这里补充一句的是,有时我们对HTTP请求文件的一些设置,如 + +```yaml +spring: + servlet: + multipart: + max-file-size: 1GB +``` + +其实都是在这里校验的,如果不满足都是在这里抛出来的异常。 + +下面我们再看下参数解析器是如何将我们解析出来的`multipartFiles`属性赋值到我们的参数上的。 + +在我们一开始示例 + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +这种写法中,使用的参数解析器为`RequestParamMethodArgumentResolver`,那么我们必然就需要看下为什么是这个参数解析器,以及这个参数解析器是如何解析的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); + return (requestParam != null && StringUtils.hasText(requestParam.name())); + } + else { + return true; + } + } + else { + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return false; + } + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; + } + else if (this.useDefaultResolution) { + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); + } + else { + return false; + } + } +} +``` + +对于文件上传请求,代码会走入上述第16行代码,也即: + +```java +if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; +} +``` + +判断我们当前的参数是否是一个`Multipart`参数,判断的方法如下: + +```java +public static boolean isMultipartArgument(MethodParameter parameter) { + Class paramType = parameter.getNestedParameterType(); + return (MultipartFile.class == paramType || + isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) || + (Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter))); +} +``` + +拿到参数的class,如果是`MultipartFile`类型,或者是`MultipartFile`集合类型,再或者是`MultipartFile`数组类型,亦或者是`Part`类型、`Part`集合,`Part`数组类型都可以。 + +很明显我们的参数是符合的,因此返回true,也就代表`RequestParamMethodArgumentResolver`可以解析这种情况,那我们再看下它是如何解析的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + + //... + + //拿到参数名 + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + //... + + //按参数名解析 + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... +} + + + +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + + if (servletRequest != null) { + //按照文件的形式解析参数 + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; + } + } + + //... +} +``` + +可以看到上面参数解析,最核心的内容便是`MultipartResolutionDelegate.resolveMultipartArgument()` + +```java +public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) + throws Exception { + + //将当前HTTP请求按MultipartHttpServletRequest解析 + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + //判断当前HTTP请求是真的是Multipart,也即传文件的 + //这里可能很多人有疑问,直接看前半部分multipartRequest != null不等于空不就行了吗,为什么还要后半部分 + //因为我们前面提到了懒解析,如果是懒解析,当前HTTP请求对象不是MultipartHttpServletRequest类型 + //但HTTP请求的contentType依然是multipart + boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); + + //如果当前参数类型是MultipartFile类型 + if (MultipartFile.class == parameter.getNestedParameterType()) { + //但当前HTTP请求不是multipart,也即没传来文件,那直接返回null + if (!isMultipart) { + return null; + } + //当前HTTP请求没有解析成MultipartHttpServletRequest + //很明显这是懒解析的场景,因为上面的isMultipart为true才能走到这里,既然HTTP请求是文件请求,但没解析为 + //MultipartHttpServletRequest对象,那只能是懒解析,刚才没解析,那就这里解析好了 + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + //从MultipartHttpServletRequest中将文件取出来返回 + return multipartRequest.getFile(name); + } + //下面内容都是类似,不再赘述。 + else if (isMultipartFileCollection(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files : null); + } + else if (isMultipartFileArray(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null); + } + else if (Part.class == parameter.getNestedParameterType()) { + if (!isMultipart) { + return null; + } + return request.getPart(name); + } + else if (isPartCollection(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts : null); + } + else if (isPartArray(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); + } + else { + return UNRESOLVABLE; + } +} +``` + + + +```java +public MultipartFile getFile(String name) { + return getMultipartFiles().getFirst(name); +} +protected MultiValueMap getMultipartFiles() { + if (this.multipartFiles == null) { + initializeMultipart(); + } + return this.multipartFiles; +} +``` + +这样,其实我们就把SpringMVC解析文件上传讲完了。 + +## 2. 拦截器 + +拦截器是我们在Spring MVC开发中使用的非常频繁的功能,如果我们需要做一些统一处理的时候往往就可以使用拦截器,比如鉴权,日志记录等。 + +在讲拦截器前我们先回顾一下Spring MVC的处理流程: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012203622.png) + + + +Spring MVC提供的拦截器接口如下: + +```java +public interface HandlerInterceptor { + default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return true; + } + default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + } + + default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable Exception ex) throws Exception { + } + +} +``` + +拦截器里的这些方法都切入到MVC的处理过程中: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012209775.png) + +也即`preHandle`的切入点是在获得适配器以后但实际执行请求处理以前,`postHandle`是在实际执行处理之后但在渲染返回结果之前,`afterCompletion`是在渲染返回结果之后,实际返回给请求方之前。 + +> **注:上图的拦截器切入流程是不准确的,因为很多时候拦截器是可以短路(拦截)整个处理的,上图我们只是画出了正常情况下的处理,对于更通用的情况会在我们看源码的时候详细的说。** + +它们对应的源码内容如下: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //拦截器 preHandle方法的执行 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + //拦截器 postHandle方法的执行 + mappedHandler.applyPostHandle(processedRequest, response, mv); + //... + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + //拦截器 afterCompletion的执行 + //这里的顺序虽然是对的,但triggerAfterCompletion函数会在多处多种情况下被调用,我们下面会说 + mappedHandler.triggerAfterCompletion(request, response, null); + //... + +} +``` + + + +### 2.1 源码分析 + +通过上面的方法,我们看到`preHandle()`方法在找到handler与handlerAdapter之后但实际执行`HandlerMethod`之前之前被调用。`preHandle()`接口方法如下: + +```java +default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return true; +} +``` + +函数会返回一个`boolean`值,如果为true则代表前置处理成功,可以放行执行`HandlerMethod`,否则就代表执行被拦截,不再实际执行`HandlerMethod`,其中参数`Object handler`在请求是动态请求(会打到Controller上,由`RequestMappingHandlerMapping`处理的请求)时,往往实际的类型就是`HandlerMethod`对象,因此这里其实可以做强转。 + +很明显,`preHandle()`更适合做统一拦截,如鉴权的时候判断用户是否有权限,如果没有权限就直接拦截驳回即可。 + +现在我们看下,`DispatcherServlet`中对它的调用源码: + +首先我们需要先知道`applyPreHandle()`的`HandlerExecutionChain`内部方法: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HandlerExecutionChain mappedHandler = null; + mappedHandler = getHandler(processedRequest); + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } +} +``` + +`HandlerExecutionChain`内部有几个重要的属性: + +```java +public class HandlerExecutionChain { + + private final Object handler; + + private final List interceptorList = new ArrayList<>(); + + private int interceptorIndex = -1; +} +``` + +其中`handler`往往就是我们的`HandlerMethod`,而`interceptorList`就是属于当前`handler`的拦截器对象,**一个请求可以有多个拦截器,它们是有序的**。`interceptorIndex`属性很关键,它指向的是**当前执行到的拦截器位置**,这在我们讲整个拦截器处理流程中异常重要。 + +现在我们看下`applyPreHandle()`的源码: + +```java +boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + for (int i = 0; i < this.interceptorList.size(); i++) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + return true; +} +``` + +可以看到比较简单,就是遍历`interceptorList`,挨个执行这些拦截器的`preHandle()`方法。但这里有两点十分重要的内容: + +1. 如果当前拦截器的`preHandle()`返回true,则interceptorIndex记录遍历的拦截器位置。 +2. 如果拦截器的`preHandle()`有一个返回false,就直接执行`triggerAfterCompletion()`方法,然后整个处理流程就结束了。 + +为什么要记录执行到的拦截器的位置?我们看下`triggerAfterCompletion()`的源码就明白了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +可以看到`triggerAfterCompletion()`就是遍历执行拦截器的`afterCompletion()`方法,但它的执行顺序有些不同,它是从`interceptorIndex`开始,倒序的方式执行。 + +假设我们现在写了3个拦截器,它们的顺序分别是A、B、C。假设现在A、B的`preHandle()`返回的是`true`,但C返回的是`false`。一个请求过来后,会先执行A的`preHandle()`方法,此时`interceptorIndex`更新为0(A在数组的第一个位置);然后执行B,将`interceptorIndex`更新为1;最后执行C的`preHandle()`,返回为`false`,终止执行。调用`triggerAfterCompletion()`,此时`triggerAfterCompletion()`会从B开始执行B的`afterCompletion()`,然后再执行A的`afterCompletion()`。 + +看明白了吗?`interceptorIndex`是用于**记录当前拦截器是在执行到第几个出问题的,一旦出了问题,就从出问题前的那一个拦截器开始倒序执行`afterCompletion()`方法。** + +如果都没有问题(`preHandle()`均返回的`true`),那`interceptorIndex`就更新为最后那个拦截器的位置。然后使用handlerAdapter来实际的执行`HandlerMethod`,如果执行没有异常,则会执行拦截器的`postHandle()`方法。其源码如下: + +```java +void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) + throws Exception { + + for (int i = this.interceptorList.size() - 1; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + interceptor.postHandle(request, response, this.handler, mv); + } +} +``` + +需要先明确的是,会执行到`postHandle()`则代表: + +1. 当前方法所属所有拦截器的`preHandle()`都返回的`true` +2. `HandlerMethod`的执行没有出错 + +`applyPostHandle()`的执行也比较简单,就是挨个遍历每个拦截器,然后执行它们的`postHandle()`方法,但是需要注意的是,这里的执行是倒序执行的,也即对于拦截器A、B、C,它们的`preHandle()`执行顺序是A、B、C,但`postHandle()`是C、B、A的顺序执行。 + +最后,当所有处理均执行完(做完数据和视图的处理了)或者**整个`doDispatch()`的执行出现任何异常**,都会调用`DispatcherServlet#triggerAfterCompletion()`,尝试执行拦截器的`afterCompletion`方法。 + +这里的异常包括但不限于: + +1. `getHandler()`找`HandlerMethod`出现异常(更具体地说是找`HandlerExecutionChain`) +2. `getHandlerAdapter()`找`HandlerAdapter`出现异常 +3. 执行拦截器的`preHandle()`出现异常 +4. 实际执行`HandlerMethod`出现异常 +5. 执行拦截器的`postHandle()`出现异常 +6. 数据和视图的解析渲染出现异常 + +而`DispatcherServlet#triggerAfterCompletion()`的源码如下: + +```java +private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { + + if (mappedHandler != null) { + mappedHandler.triggerAfterCompletion(request, response, ex); + } + throw ex; +} +``` + +可以看到会先判断是否找到了`HandlerExecutionChain`(因为我们上面说了`getHandler()`也可能会出现异常),如果找到了就调用它的`triggerAfterCompletion()`。 + +`HandlerExecutionChain#triggerAfterCompletion()`的源码我们其实上面已经看过了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +就是从`interceptorIndex`开始倒序执行拦截器的`afterCompletion()`方法。 + +### 2.2 总结 + +拦截器的执行源码其实还是很简单的,大家需要自己手动的翻一翻看一看。总的来说就是会先正序的执行每个拦截器的`preHandle()`,如果有任何异常(包括返回`false`),就直接从那个异常的拦截器之前开始,倒序执行它们的`afterCompletion()`方法。如果没有异常,且使用适配器执行完了处理方法,就再倒叙的执行每个拦截器的`postHandle()`方法。然后执行`ModelAndView`的解析和渲染,最终如果均没有异常或者再任何一步出现异常,都会倒序的执行拦截器的`afterCompletion()`方法。 \ No newline at end of file diff --git a/docs/source/spring/1-architect.md b/docs/source/spring/1-architect.md new file mode 100644 index 0000000..c9cc97f --- /dev/null +++ b/docs/source/spring/1-architect.md @@ -0,0 +1,88 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Spring 动态代理,Bean生命周期,自动装配,Spring注解,Spring事务,Async注解,Spring架构 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 + +## spring的整体架构 + +Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如下图所示: + +![](http://img.topjavaer.cn/img/202309161608576.png) + +从上图spring framework整体架构图可以看到,这些模块被总结为以下几个部分: + +### 1. Core Container + +Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块 + +Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 + +Core模块主要包含Spring框架基本的核心工具类 + +Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control/Dependency Injection(Ioc/DI)操作相关的所有类 + +Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。 + +ApplicationContext接口是Context模块的关键 + +Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操纵对象,该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的IoC容器中根据名称检索对象 + + + +### 2. Data Access/Integration + +JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码,这个模块包含了Spring对JDBC数据访问进行封装的所有类 + +ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层,利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射,如前边提到的简单声明性事务管理 + +OXM模块提供了一个Object/XML映射实现的抽象层,Object/XML映射实现抽象层包括JAXB,Castor,XMLBeans,JiBX和XStream +JMS(java Message Service)模块主要包含了一些制造和消费消息的特性 + +Transaction模块支持编程和声明式事物管理,这些事务类必须实现特定的接口,并且对所有POJO都适用 + + + +### 3. Web + +Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以Spring框架支持与Jakarta Struts的集成。 + +Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web、Porlet模块 + +Web模块:提供了基础的面向Web的集成特性,例如,多文件上传、使用Servlet listeners初始化IoC容器以及一个面向Web的应用上下文,它还包含了Spring远程支持中Web的相关部分 + +Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现,Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性基础在一起 + +Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起 + +Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现 + + + +### 4. AOP + +AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中 + +Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务,通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中 + + + +### 5. Test + +Test模块支持使用Junit和TestNG对Spring组件进行测试 + + + diff --git a/docs/source/spring/10-bean-initial.md b/docs/source/spring/10-bean-initial.md new file mode 100644 index 0000000..3bd63c8 --- /dev/null +++ b/docs/source/spring/10-bean-initial.md @@ -0,0 +1,348 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean初始化,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +一个 bean 经历了 `createBeanInstance()` 被创建出来,然后又经过一番属性注入,依赖处理,历经千辛万苦,千锤百炼,终于有点儿 bean 实例的样子,能堪大任了,只需要经历最后一步就破茧成蝶了。这最后一步就是初始化,也就是 `initializeBean()`,所以这篇文章我们分析 `doCreateBean()` 中最后一步:初始化 bean。 +我回到之前的doCreateBean方法中,如下 + +![](http://img.topjavaer.cn/img/202309232326239.png) + +在populateBean方法下面有一个initializeBean(beanName, exposedObject, mbd)方法,这个就是用来执行用户设定的初始化操作。我们看下方法体: + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + // 激活 Aware 方法 + invokeAwareMethods(beanName, bean); + return null; + }, getAccessControlContext()); + } + else { + // 对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware + invokeAwareMethods(beanName, bean); + } + + Object wrappedBean = bean; + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + } + + try { + // 激活用户自定义的 init 方法 + invokeInitMethods(beanName, wrappedBean, mbd); + } + catch (Throwable ex) { + throw new BeanCreationException( + (mbd != null ? mbd.getResourceDescription() : null), + beanName, "Invocation of init method failed", ex); + } + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + } + return wrappedBean; +} +``` + +初始化 bean 的方法其实就是三个步骤的处理,而这三个步骤主要还是根据用户设定的来进行初始化,这三个过程为: + +1. 激活 Aware 方法 +2. 后置处理器的应用 +3. 激活自定义的 init 方法 + +## 激Aware方法 + +我们先了解一下Aware方法的使用。Spring中提供了一些Aware接口,比如BeanFactoryAware,ApplicationContextAware,ResourceLoaderAware,ServletContextAware等,实现这些Aware接口的bean在被初始化后,可以取得一些相对应的资源,例如实现BeanFactoryAware的bean在初始化之后,Spring容器将会注入BeanFactory实例,而实现ApplicationContextAware的bean,在bean被初始化后,将会被注入ApplicationContext实例等。我们先通过示例方法了解下Aware的使用。 + +定义普通bean,如下代码: + +```java +public class HelloBean { + public void say() + { + System.out.println("Hello"); + } +} +``` + +定义beanFactoryAware类型的bean + +```java +public class MyBeanAware implements BeanFactoryAware { + private BeanFactory beanFactory; + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + public void testAware() + { + //通过hello这个bean id从beanFactory获取实例 + HelloBean hello = (HelloBean)beanFactory.getBean("hello"); + hello.say(); + } +} +``` + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +进行测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); + MyBeanAware test = (MyBeanAware)ctx.getBean("myBeanAware"); + test.testAware(); + } +} + + + + + + + + + + +``` + +输出 + +``` +Hello +``` + +上面的方法我们获取到Spring中BeanFactory,并且可以根据BeanFactory获取所有的bean,以及进行相关设置。还有其他Aware的使用都是大同小异,看一下Spring的实现方式: + +```java +private void invokeAwareMethods(final String beanName, final Object bean) { + if (bean instanceof Aware) { + if (bean instanceof BeanNameAware) { + ((BeanNameAware) bean).setBeanName(beanName); + } + if (bean instanceof BeanClassLoaderAware) { + ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); + } + if (bean instanceof BeanFactoryAware) { + ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); + } + } +} +``` + + + +## 处理器的应用 + +BeanPostPrecessor我们经常看到Spring中使用,这是Spring开放式架构的一个必不可少的亮点,给用户充足的权限去更改或者扩展Spring,而除了BeanPostProcessor外还有很多其他的PostProcessor,当然大部分都以此为基础,集成自BeanPostProcessor。BeanPostProcessor在调用用户自定义初始化方法前或者调用自定义初始化方法后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterinitialization方法,使用户可以根据自己的业务需求就行相应的处理。 + +```java +public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessBeforeInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} + +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + + +## 激活自定义的init方法 + +客户定制的初始化方法除了我们熟知的使用配置init-method外,还有使自定义的bean实现InitializingBean接口,并在afterPropertiesSet中实现自己的初始化业务逻辑。 + +init-method与afterPropertiesSet都是在初始化bean时执行,执行顺序是afterPropertiesSet先执行,而init-method后执行。 +在invokeInitMethods方法中就实现了这两个步骤的初始化调用。 + +```java +protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) + throws Throwable { + + // 是否实现 InitializingBean + // 如果实现了 InitializingBean 接口,则只掉调用bean的 afterPropertiesSet() + boolean isInitializingBean = (bean instanceof InitializingBean); + if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); + } + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + ((InitializingBean) bean).afterPropertiesSet(); + return null; + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + // 直接调用 afterPropertiesSet() + ((InitializingBean) bean).afterPropertiesSet(); + } + } + + if (mbd != null && bean.getClass() != NullBean.class) { + // 判断是否指定了 init-method(), + // 如果指定了 init-method(),则再调用制定的init-method + String initMethodName = mbd.getInitMethodName(); + if (StringUtils.hasLength(initMethodName) && + !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.isExternallyManagedInitMethod(initMethodName)) { + // 利用反射机制执行 + invokeCustomInitMethod(beanName, bean, mbd); + } + } +} +``` + +首先检测当前 bean 是否实现了 InitializingBean 接口,如果实现了则调用其 `afterPropertiesSet()`,然后再检查是否也指定了 `init-method()`,如果指定了则通过反射机制调用指定的 `init-method()`。 + +### init-method() + +```java +public class InitializingBeanTest { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setOtherName(){ + System.out.println("InitializingBeanTest setOtherName..."); + + this.name = "dabin"; + } +} + +// 配置文件 + + + +``` + +执行结果: + +``` +dabin +``` + +我们可以使用 `` 标签的 `default-init-method` 属性来统一指定初始化方法,这样就省了需要在每个 `` 标签中都设置 `init-method` 这样的繁琐工作了。比如在 `default-init-method` 规定所有初始化操作全部以 `initBean()` 命名。如下: + +![](http://img.topjavaer.cn/img/202309232330774.png) + +我们看看 invokeCustomInitMethod 方法: + +```java +protected void invokeCustomInitMethod(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + + String initMethodName = mbd.getInitMethodName(); + Assert.state(initMethodName != null, "No init method set"); + Method initMethod = (mbd.isNonPublicAccessAllowed() ? + BeanUtils.findMethod(bean.getClass(), initMethodName) : + ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + + if (initMethod == null) { + if (mbd.isEnforceInitMethod()) { + throw new BeanDefinitionValidationException("Could not find an init method named '" + + initMethodName + "' on bean with name '" + beanName + "'"); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No default init method named '" + initMethodName + + "' found on bean with name '" + beanName + "'"); + } + // Ignore non-existent default lifecycle methods. + return; + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); + } + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod); + + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(methodToInvoke); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + methodToInvoke.invoke(bean), getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + InvocationTargetException ex = (InvocationTargetException) pae.getException(); + throw ex.getTargetException(); + } + } + else { + try { + ReflectionUtils.makeAccessible(initMethod); + initMethod.invoke(bean); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +} +``` + + + +我们看出最后是使用反射的方式来执行初始化方法。 \ No newline at end of file diff --git a/docs/source/spring/11-application-refresh.md b/docs/source/spring/11-application-refresh.md new file mode 100644 index 0000000..e1d4995 --- /dev/null +++ b/docs/source/spring/11-application-refresh.md @@ -0,0 +1,1203 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,容器刷新,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +在之前的博文中我们一直以BeanFactory接口以及它的默认实现类XmlBeanFactory为例进行分析,但是Spring中还提供了另一个接口ApplicationContext,用于扩展BeanFactory中现有的功能。 + +ApplicationContext和BeanFactory两者都是用于加载Bean的,但是相比之下,ApplicationContext提供了更多的扩展功能,简而言之:ApplicationContext包含BeanFactory的所有功能。通常建议比优先使用ApplicationContext,除非在一些限制的场合,比如字节长度对内存有很大的影响时(Applet),绝大多数“典型的”企业应用系统,ApplicationContext就是需要使用的。 + +那么究竟ApplicationContext比BeanFactory多了哪些功能?首先我们来看看使用两个不同的类去加载配置文件在写法上的不同如下代码: + +```java +//使用BeanFactory方式加载XML. +BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml")); + +//使用ApplicationContext方式加载XML. +ApplicationContext bf = new ClassPathXmlApplicationContext("beanFactoryTest.xml"); +``` + +接下来我们就以ClassPathXmlApplicationContext作为切入点,开始对整体功能进行分析。首先看下其构造函数: + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +public ClassPathXmlApplicationContext() { +} + +public ClassPathXmlApplicationContext(ApplicationContext parent) { + super(parent); +} + +public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); +} + +public ClassPathXmlApplicationContext(String... configLocations) throws BeansException { + this(configLocations, true, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) + throws BeansException { + + this(configLocations, true, parent); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } +} +``` + +设置路径是必不可少的步骤,ClassPathXmlApplicationContext中可以将配置文件路径以数组的方式传入,ClassPathXmlApplicationContext可以对数组进行解析并进行加载。而对于解析及功能实现都在refresh()中实现。 + +## 设置配置路径 + +在ClassPathXmlApplicationContext中支持多个配置文件以数组方式同时传入,以下是设置配置路径方法代码: + +```java +public void setConfigLocations(@Nullable String... locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not be null"); + this.configLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } +} +``` + +其中如果给定的路径中包含特殊符号,如${var},那么会在方法resolvePath中解析系统变量并替换 + +## 扩展功能 + +设置了路径之后,便可以根据路径做配置文件的解析以及各种功能的实现了。可以说refresh函数中包含了几乎ApplicationContext中提供的全部功能,而且此函数中逻辑非常清晰明了,使我们很容易分析对应的层次及逻辑,我们看下方法代码: + +```java +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + //准备刷新的上下文 环境 + prepareRefresh(); + //初始化BeanFactory,并进行XML文件读取 + /* + * ClassPathXMLApplicationContext包含着BeanFactory所提供的一切特征,在这一步骤中将会复用 + * BeanFactory中的配置文件读取解析及其他功能,这一步之后,ClassPathXmlApplicationContext + * 实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基础操作了。 + */ + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + //对beanFactory进行各种功能填充 + prepareBeanFactory(beanFactory); + try { + //子类覆盖方法做额外处理 + /* + * Spring之所以强大,为世人所推崇,除了它功能上为大家提供了便利外,还有一方面是它的 + * 完美架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式 + * 的设计在Spring中随处可见,例如在本例中就提供了一个空的函数实现postProcessBeanFactory来 + * 方便程序猿在业务上做进一步扩展 + */ + postProcessBeanFactory(beanFactory); + //激活各种beanFactory处理器 + invokeBeanFactoryPostProcessors(beanFactory); + //注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用实在getBean时候 + registerBeanPostProcessors(beanFactory); + //为上下文初始化Message源,即不同语言的消息体,国际化处理 + initMessageSource(); + //初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + initApplicationEventMulticaster(); + //留给子类来初始化其它的Bean + onRefresh(); + //在所有注册的bean中查找Listener bean,注册到消息广播器中 + registerListeners(); + //初始化剩下的单实例(非惰性的) + finishBeanFactoryInitialization(beanFactory); + //完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + destroyBeans(); + cancelRefresh(ex); + throw ex; + } + finally { + resetCommonCaches(); + } + } +} +``` + +我们简单的分析下代码的步骤: + +(1)初始化前的准备工作,例如对系统属性或者环境变量进行准备及验证。 + +在某种情况下项目的使用需要读取某些系统变量,而这个变量的设置很可能会影响着系统的正确性,那么ClassPathXmlApplicationContext为我们提供的这个准备函数就显得非常必要,他可以在spring启动的时候提前对必须的环境变量进行存在性验证。 + +(2)初始化BeanFactory,并进行XML文件读取。 + +之前提到ClassPathXmlApplicationContext包含着对BeanFactory所提供的一切特征,那么这一步中将会复用BeanFactory中的配置文件读取解析其他功能,这一步之后ClassPathXmlApplicationContext实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基本操作了。 + +(3)对BeanFactory进行各种功能填充 + +@Qualifier和@Autowired应该是大家非常熟悉的注解了,那么这两个注解正是在这一步骤中增加支持的。 + +(4)子类覆盖方法做额外处理。 + +spring之所以强大,为世人所推崇,除了它功能上为大家提供了遍历外,还有一方面是它完美的架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式的设计在spring中随处可见,例如本利中就提供了一个空的函数实现postProcessBeanFactory来方便程序员在业务上做进一步的扩展。 + +(5)激活各种BeanFactory处理器 + +(6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean时候 + +(7)为上下文初始化Message源,及对不同语言的小西天进行国际化处理 + +(8)初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + +(9)留给子类来初始化其他的bean + +(10)在所有注册的bean中查找listener bean,注册到消息广播器中 + +(11)初始化剩下的单实例(非惰性的) + +(12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。 + +接下来我们就详细的讲解每一个过程 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## prepareRefresh刷新上下文的准备工作 + +```java +/** + * 准备刷新上下文环境,设置它的启动日期和活动标志,以及执行任何属性源的初始化。 + * Prepare this context for refreshing, setting its startup date and + * active flag as well as performing any initialization of property sources. + */ +protected void prepareRefresh() { + this.startupDate = System.currentTimeMillis(); + this.closed.set(false); + this.active.set(true); + + // 在上下文环境中初始化任何占位符属性源。(空的方法,留给子类覆盖) + initPropertySources(); + + // 验证需要的属性文件是否都已放入环境中 + getEnvironment().validateRequiredProperties(); + + // 允许收集早期的应用程序事件,一旦有了多播器,就可以发布…… + this.earlyApplicationEvents = new LinkedHashSet<>(); +} +``` + + + +## obtainFreshBeanFactory->读取xml并初始化BeanFactory + +obtainFreshBeanFactory方法从字面理解是获取beanFactory.ApplicationContext是对BeanFactory的扩展,在其基础上添加了大量的基础应用,obtainFreshBeanFactory正式实现beanFactory的地方,经过这个函数后ApplicationContext就有了BeanFactory的全部功能。我们看下此方法的代码: + +```java +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + //初始化BeanFactory,并进行XML文件读取,并将得到的BeanFactory记录在当前实体的属性中 + refreshBeanFactory(); + //返回当前实体的beanFactory属性 + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} +``` + +继续深入到refreshBeanFactory方法中,方法的实现是在AbstractRefreshableApplicationContext中: + +```java +@Override +protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + //创建DefaultListableBeanFactory + /* + * 以前我们分析BeanFactory的时候,不知道是否还有印象,声明方式为:BeanFactory bf = + * new XmlBeanFactory("beanFactoryTest.xml"),其中的XmlBeanFactory继承自DefaulltListableBeanFactory; + * 并提供了XmlBeanDefinitionReader类型的reader属性,也就是说DefaultListableBeanFactory是容器的基础。必须 + * 首先要实例化。 + */ + DefaultListableBeanFactory beanFactory = createBeanFactory(); + //为了序列化指定id,如果需要的话,让这个BeanFactory从id反序列化到BeanFactory对象 + beanFactory.setSerializationId(getId()); + //定制beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖以及设置 + //@Autowired和Qualifier注解解析器QualifierAnnotationAutowireCandidateResolver + customizeBeanFactory(beanFactory); + //加载BeanDefiniton + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + //使用全局变量记录BeanFactory实例。 + //因为DefaultListableBeanFactory类型的变量beanFactory是函数内部的局部变量, + //所以要使用全局变量记录解析结果 + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); + } +} +``` + +### 加载BeanDefinition + +在第一步中提到了将ClassPathXmlApplicationContext与XMLBeanFactory创建的对比,除了初始化DefaultListableBeanFactory外,还需要XmlBeanDefinitionReader来读取XML,那么在loadBeanDefinitions方法中首先要做的就是初始化XmlBeanDefinitonReader,我们跟着到loadBeanDefinitions(beanFactory)方法体中,我们看到的是在AbstractXmlApplicationContext中实现的,具体代码如下: + +```java +@Override +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); +} +``` + +在初始化了DefaultListableBeanFactory和XmlBeanDefinitionReader后,就可以进行配置文件的读取了。继续进入到loadBeanDefinitions(beanDefinitionReader)方法体中,代码如下: + +``` +protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } +} +``` + +因为在XmlBeanDefinitionReader中已经将之前初始化的DefaultListableBeanFactory注册进去了,所以XmlBeanDefinitionReader所读取的BeanDefinitionHolder都会注册到DefinitionListableBeanFactory中,也就是经过这个步骤,DefaultListableBeanFactory的变量beanFactory已经包含了所有解析好的配置。 + +## 功能扩展 + +如上图所示prepareBeanFactory(beanFactory)就是在功能上扩展的方法,而在进入这个方法前spring已经完成了对配置的解析,接下来我们详细分析下次函数,进入方法体: + +```java +protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { + // Tell the internal bean factory to use the context's class loader etc. + //设置beanFactory的classLoader为当前context的classloader + beanFactory.setBeanClassLoader(getClassLoader()); + //设置beanFactory的表达式语言处理器,Spring3增加了表达式语言的支持, + //默认可以使用#{bean.xxx}的形式来调用相关属性值 + beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); + //为beanFactory增加了一个的propertyEditor,这个主要是对bean的属性等设置管理的一个工具 + beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); + + // Configure the bean factory with context callbacks. + beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); + //设置了几个忽略自动装配的接口 + beanFactory.ignoreDependencyInterface(EnvironmentAware.class); + beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); + beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); + beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); + beanFactory.ignoreDependencyInterface(MessageSourceAware.class); + beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); + + // BeanFactory interface not registered as resolvable type in a plain factory. + // MessageSource registered (and found for autowiring) as a bean. + //设置了几个自动装配的特殊规则 + beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); + beanFactory.registerResolvableDependency(ResourceLoader.class, this); + beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); + beanFactory.registerResolvableDependency(ApplicationContext.class, this); + + // Register early post-processor for detecting inner beans as ApplicationListeners. + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); + + // Detect a LoadTimeWeaver and prepare for weaving, if found. + //增加对AspectJ的支持 + if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + // Set a temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + + // Register default environment beans. + //添加默认的系统环境bean + if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); + } + if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties()); + } + if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment()); + } +} +``` + +详细分析下代码发现上面函数主要是在以下方法进行了扩展: + +(1)对SPEL语言的支持 + +(2)增加对属性编辑器的支持 + +(3)增加对一些内置类的支持,如EnvironmentAware、MessageSourceAware的注入 + +(4)设置了依赖功能可忽略的接口 + +(5)注册一些固定依赖的属性 + +(6)增加了AspectJ的支持 + +(7)将相关环境变量及属性以单例模式注册 + + + +### 增加对SPEL语言的支持 + +Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts 2x中使用的OGNL语言,SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用 + +SpEL使用#{…}作为定界符,所有在大框号中的字符都将被认为是SpEL,使用格式如下: + +```xml + + + + + + + + +``` + +上面只是列举了其中最简单的使用方式,SpEL功能非常强大,使用好可以大大提高开发效率。在源码中通过代码beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver()),注册语言解析器,就可以对SpEL进行解析了,那么之后是在什么地方调用这个解析器的呢? + +之前说beanFactory中说过Spring在bean进行初始化的时候会有属性填充的一步,而在这一步中Spring会调用AbstractAutowireCapabelBeanFactory类的applyPropertyValues来进行属性值得解析。同时这个步骤中一般通过AbstractBeanFactory中的evaluateBeanDefinitionString方法进行SpEL解析,方法代码如下: + +```java +protected Object evaluateBeanDefinitionString(String value, BeanDefinition beanDefinition) { + if (this.beanExpressionResolver == null) { + return value; + } + Scope scope = (beanDefinition != null ? getRegisteredScope(beanDefinition.getScope()) : null); + return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope)); +} +``` + + + +## BeanFactory的后处理 + +BeanFactory作为spring中容器功能的基础,用于存放所有已经加载的bean,为例保证程序上的高可扩展性,spring针对BeanFactory做了大量的扩展,比如我们熟悉的PostProcessor就是在这里实现的。接下来我们就深入分析下BeanFactory后处理 + +### 激活注册的BeanFactoryPostProcessor + +在正是介绍BeanFactoryPostProcessor的后处理前我们先简单的了解下其用法,BeanFactoryPostProcessor接口跟BeanPostProcessor类似,都可以对bean的定义(配置元数据)进行处理,也就是说spring IoC容器允许BeanFactoryPostProcessor在容器实际实例化任何其他的bean之前读取配置元数据,并可能修改他。也可以配置多个BeanFactoryPostProcessor,可以通过order属性来控制BeanFactoryPostProcessor的执行顺序(此属性必须当BeanFactoryPostProcessor实现了Ordered的接口时才可以赊账,因此在实现BeanFactoryPostProcessor时应该考虑实现Ordered接口) + +如果想改变世界的bean实例(例如从配置元数据创建的对象),那最好使用BeanPostProcessor。同样的BeanFactoryPostProcessor的作用域范围是容器级别的,它只是和你锁使用的容器有关。如果你在容器中定义了一个BeanFactoryPostProcessor,它仅仅对此容器中的bean进行后置处理。BeanFactoryPostProcessor不会对定义在另一个容器中的bean进行后置处理,即使这两个容器都在同一层次上。在spring中存在对于BeanFactoryPostProcessor的典型应用,如PropertyPlaceholderConfigurer。 + + + +#### BeanFactoryPostProcessor的典型应用:PropertyPlaceholderConfigurer + +有时候我们在阅读spring的配置文件中的Bean的描述时,会遇到类似如下情况: + +```xml + + + + +``` + +这其中出现了变量:user.name、user.name、{user.birthday},这是spring的分散配置,可以在另外的配置文件中为user.name、user.birthday指定值,例如在bean.properties文件中定义: + +``` +user.name = xiaoming +user.birthday = 2019-04-19 +``` + +当访问名为user的bean时,其name属性就会被字符串xiaoming替换,那spring框架是怎么知道存在这样的配置文件呢,这个就是PropertyPlaceholderConfigurer,需要在配置文件中添加一下代码: + +```xml + + + + classpath:bean.properties + + + +``` + +在这个bean中指定了配置文件的位置。其实还是有个问题,这个userHandler只不过是spring框架管理的一个bean,并没有被别的bean或者对象引用,spring的beanFactory是怎么知道这个需要从这个bean中获取配置信息呢?我们看下PropertyPlaceholderConfigurer这个类的层次结构,如下图: + +![](http://img.topjavaer.cn/img/202309241051602.png) + +从上图中我们可以看到PropertyPlaceholderConfigurer间接的继承了BeanFactoryPostProcessor接口,这是一个很特别的A接口,当spring加载任何实现了这个接口的bean的配置时,都会在bean工厂载入所有bean的配置之后执行postProcessBeanFactory方法。在PropertyResourceConfigurer类中实现了postProcessBeanFactory方法,在方法中先后调用了mergeProperties、convertProperties、processProperties这三个方法,分别得到配置,将得到的配置转换为合适的类型,最后将配置内容告知BeanFactory。 + +正是通过实现BeanFactoryPostProcessor接口,BeanFactory会在实例化任何bean之前获得配置信息,从而能够正确的解析bean描述文件中的变量引用。 + +```java +public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + try { + Properties mergedProps = this.mergeProperties(); + this.convertProperties(mergedProps); + this.processProperties(beanFactory, mergedProps); + } catch (IOException var3) { + throw new BeanInitializationException("Could not load properties", var3); + } +} +``` + + + +### 自定义BeanFactoryPostProcessor + +编写实现了BeanFactoryPostProcessor接口的MyBeanFactoryPostProcessor的容器后处理器,如下代码: + +```java +public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + System.out.println("对容器进行后处理。。。。"); + } +} +``` + +然后在配置文件中注册这个bean,如下: + +```xml + +``` + +最后编写测试代码: + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + + User user = (User)context.getBean("user"); + System.out.println(user.getName()); + + } +} +``` + + + +### 激活BeanFactoryPostProcessor + +在了解BeanFactoryPostProcessor的用法后我们便可以深入的研究BeanFactoryPostProcessor的调用过程了,其是在方法invokeBeanFactoryPostProcessors(beanFactory)中实现的,进入到方法内部: + +```java +public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { + + // Invoke BeanDefinitionRegistryPostProcessors first, if any. + // 1、首先调用BeanDefinitionRegistryPostProcessors + Set processedBeans = new HashSet<>(); + + // beanFactory是BeanDefinitionRegistry类型 + if (beanFactory instanceof BeanDefinitionRegistry) { + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + // 定义BeanFactoryPostProcessor + List regularPostProcessors = new ArrayList<>(); + // 定义BeanDefinitionRegistryPostProcessor集合 + List registryProcessors = new ArrayList<>(); + + // 循环手动注册的beanFactoryPostProcessors + for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { + // 如果是BeanDefinitionRegistryPostProcessor的实例话,则调用其postProcessBeanDefinitionRegistry方法,对bean进行注册操作 + if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { + // 如果是BeanDefinitionRegistryPostProcessor类型,则直接调用其postProcessBeanDefinitionRegistry + BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor) postProcessor; + registryProcessor.postProcessBeanDefinitionRegistry(registry); + registryProcessors.add(registryProcessor); + } + // 否则则将其当做普通的BeanFactoryPostProcessor处理,直接加入regularPostProcessors集合,以备后续处理 + else { + regularPostProcessors.add(postProcessor); + } + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // Separate between BeanDefinitionRegistryPostProcessors that implement + // PriorityOrdered, Ordered, and the rest. + List currentRegistryProcessors = new ArrayList<>(); + + // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered(有限排序接口)的BeanDefinitionRegistryPostProcessors + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered(普通排序接口)的BeanDefinitionRegistryPostProcessors + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. + // 最后,调用其他的BeanDefinitionRegistryPostProcessors + boolean reiterate = true; + while (reiterate) { + reiterate = false; + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + reiterate = true; + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用其他的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + } + + // Now, invoke the postProcessBeanFactory callback of all processors handled so far. + // 调用所有BeanDefinitionRegistryPostProcessor(包括手动注册和通过配置文件注册) + // 和BeanFactoryPostProcessor(只有手动注册)的回调函数-->postProcessBeanFactory + invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); + invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); + } + + // 2、如果不是BeanDefinitionRegistry的实例,那么直接调用其回调函数即可-->postProcessBeanFactory + else { + // Invoke factory processors registered with the context instance. + invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // 3、上面的代码已经处理完了所有的BeanDefinitionRegistryPostProcessors和手动注册的BeanFactoryPostProcessor + // 接下来要处理通过配置文件注册的BeanFactoryPostProcessor + // 首先获取所有的BeanFactoryPostProcessor(注意:这里获取的集合会包含BeanDefinitionRegistryPostProcessors) + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); + + // Separate between BeanFactoryPostProcessors that implement PriorityOrdered, Ordered, and the rest. + // 这里,将实现了PriorityOrdered,Ordered的处理器和其他的处理器区分开来,分别进行处理 + // PriorityOrdered有序处理器 + List priorityOrderedPostProcessors = new ArrayList<>(); + // Ordered有序处理器 + List orderedPostProcessorNames = new ArrayList<>(); + // 无序处理器 + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + // 判断processedBeans是否包含当前处理器(processedBeans中的处理器已经被处理过);如果包含,则不做任何处理 + if (processedBeans.contains(ppName)) { + // skip - already processed in first phase above + } + else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + // 加入到PriorityOrdered有序处理器集合 + priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + // 加入到Ordered有序处理器集合 + orderedPostProcessorNames.add(ppName); + } + else { + // 加入到无序处理器集合 + nonOrderedPostProcessorNames.add(ppName); + } + } + + // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered接口的处理器 + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); + + // Next, invoke the BeanFactoryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered接口的处理器 + List orderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : orderedPostProcessorNames) { + orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + sortPostProcessors(orderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); + + // Finally, invoke all other BeanFactoryPostProcessors. + // 最后,调用无序处理器 + List nonOrderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : nonOrderedPostProcessorNames) { + nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); + + // Clear cached merged bean definitions since the post-processors might have + // modified the original metadata, e.g. replacing placeholders in values... + // 清理元数据 + beanFactory.clearMetadataCache(); +} +``` + +循环遍历 BeanFactoryPostProcessor 中的 postProcessBeanFactory 方法 + +```java +private static void invokeBeanFactoryPostProcessors( + Collection postProcessors, ConfigurableListableBeanFactory beanFactory) { + + for (BeanFactoryPostProcessor postProcessor : postProcessors) { + postProcessor.postProcessBeanFactory(beanFactory); + } +} +``` + +## 注册BeanPostProcessor + +在上文中提到了BeanFactoryPostProcessor的调用,接下来我们就探索下BeanPostProcessor。但这里并不是调用,而是注册,真正的调用其实是在bean的实例化阶段进行的,这是一个很重要的步骤,也是很多功能BeanFactory不知道的重要原因。spring中大部分功能都是通过后处理器的方式进行扩展的,这是spring框架的一个特写,但是在BeanFactory中其实并没有实现后处理器的自动注册,所以在调用的时候如果没有进行手动注册其实是不能使用的。但是ApplicationContext中却添加了自动注册功能,如自定义一个后处理器: + +```java +public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + System.out.println("before"); + return null; + } +} +``` + +然后在配置文件中添加bean的配置: + +```xml + +``` + +这样的话再使用BeanFactory的方式进行加载的bean在加载时不会有任何改变的,而在使用ApplicationContext方式获取的bean时就会打印出“before”,而这个特性就是咋registryBeanPostProcessor方法中完成的。 + +我们继续深入分析registryBeanPostProcessors的方法实现: + +```java +protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this); +} +public static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { + + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); + + /* + * BeanPostProcessorChecker是一个普通的信息打印,可能会有些情况当spring的配置中的后 + * 处理器还没有被注册就已经开了bean的初始化,这时就会打印出BeanPostProcessorChecker中 + * 设定的信息 + */ + int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; + beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + + //使用PriorityOrdered来保证顺序 + List priorityOrderedPostProcessors = new ArrayList<>(); + List internalPostProcessors = new ArrayList<>(); + //使用Ordered来保证顺序 + List orderedPostProcessorNames = new ArrayList<>(); + //无序BeanPostProcessor + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + priorityOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + orderedPostProcessorNames.add(ppName); + } + else { + nonOrderedPostProcessorNames.add(ppName); + } + } + + //第一步,注册所有实现了PriorityOrdered的BeanPostProcessor + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); + + //注册实现了Ordered的BeanPostProcessor + List orderedPostProcessors = new ArrayList<>(); + for (String ppName : orderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + orderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + sortPostProcessors(orderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, orderedPostProcessors); + + //注册所有的无序的BeanPostProcessor + List nonOrderedPostProcessors = new ArrayList<>(); + for (String ppName : nonOrderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + nonOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); + + //注册所有的内部BeanFactoryProcessor + sortPostProcessors(internalPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, internalPostProcessors); + + // Re-register post-processor for detecting inner beans as ApplicationListeners, + //添加ApplicationListener探测器 + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext)); +} +``` + +我们可以看到先从容器中获取所有类型为 **BeanPostProcessor.class** 的Bean的name数组,然后通过 **BeanPostProcessor pp** **= beanFactory.getBean(ppName, BeanPostProcessor.class****);** 获取Bean的实例,最后通过 **registerBeanPostProcessors(beanFactory, orderedPostProcessors);**将获取到的**BeanPostProcessor**实例添加到容器的属性中,如下 + +```java +private static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, List postProcessors) { + + for (BeanPostProcessor postProcessor : postProcessors) { + beanFactory.addBeanPostProcessor(postProcessor); + } +} + +@Override +public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) { + Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null"); + // Remove from old position, if any + this.beanPostProcessors.remove(beanPostProcessor); + // Track whether it is instantiation/destruction aware + if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) { + this.hasInstantiationAwareBeanPostProcessors = true; + } + if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) { + this.hasDestructionAwareBeanPostProcessors = true; + } + // Add to end of list + this.beanPostProcessors.add(beanPostProcessor); +} +``` + +可以看到将 **beanPostProcessor 实例添加到容器的** **beanPostProcessors 属性中** + + + +## 初始化Message资源 + +该方法不是很重要,留在以后分析吧。 + + + +## 初始事件广播器 + +初始化ApplicationEventMulticaster是在方法initApplicationEventMulticaster()中实现的,进入到方法体,如下: + +```java +protected void initApplicationEventMulticaster() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + // 1、默认使用内置的事件广播器,如果有的话. + // 我们可以在配置文件中配置Spring事件广播器或者自定义事件广播器 + // 例如: + if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { + this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); + } + // 2、否则,新建一个事件广播器,SimpleApplicationEventMulticaster是spring的默认事件广播器 + else { + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); + beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); + } +} +``` + +通过源码可以看到其实现逻辑与initMessageSource基本相同,其步骤如下: + +(1)查找是否有name为applicationEventMulticaster的bean,如果有放到容器里,如果没有,初始化一个系统默认的SimpleApplicationEventMulticaster放入容器 + +(2)查找手动设置的applicationListeners,添加到applicationEventMulticaster里 + +(3)查找定义的类型为ApplicationListener的bean,设置到applicationEventMulticaster + +(4)初始化完成、对earlyApplicationEvents里的事件进行通知(此容器仅仅是广播器未建立的时候保存通知信息,一旦容器建立完成,以后均直接通知) + +(5)在系统操作时候,遇到的各种bean的通知事件进行通知 + +可以看到的是applicationEventMulticaster是一个标准的观察者模式,对于他内部的监听者applicationListeners,每次事件到来都会一一获取通知。 + + + +## 注册监听器 + +```java +protected void registerListeners() { + // Register statically specified listeners first. + // 首先,注册指定的静态事件监听器,在spring boot中有应用 + for (ApplicationListener listener : getApplicationListeners()) { + getApplicationEventMulticaster().addApplicationListener(listener); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let post-processors apply to them! + // 其次,注册普通的事件监听器 + String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); + for (String listenerBeanName : listenerBeanNames) { + getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); + } + + // Publish early application events now that we finally have a multicaster... + // 如果有早期事件的话,在这里进行事件广播 + // 因为前期SimpleApplicationEventMulticaster尚未注册,无法发布事件, + // 因此早期的事件会先存放在earlyApplicationEvents集合中,这里把它们取出来进行发布 + // 所以早期事件的发布时间节点是早于其他事件的 + Set earlyEventsToProcess = this.earlyApplicationEvents; + // 早期事件广播器是一个Set集合,保存了无法发布的早期事件,当SimpleApplicationEventMulticaster + // 创建完之后随即进行发布,同事也要将其保存的事件释放 + this.earlyApplicationEvents = null; + if (earlyEventsToProcess != null) { + for (ApplicationEvent earlyEvent : earlyEventsToProcess) { + getApplicationEventMulticaster().multicastEvent(earlyEvent); + } + } +} +``` + +我们来看一下Spring的事件监昕的简单用法 + + + +### 定义监听事件 + +```java +public class TestEvent extends ApplicationonEvent { + public String msg; + public TestEvent (Object source ) { + super (source ); + } + public TestEvent (Object source , String msg ) { + super(source); + this.msg = msg ; + } + public void print () { + System.out.println(msg) ; + } +} +``` + + + +### 定义监昕器 + +```java +public class TestListener implement ApplicationListener { + public void onApplicationEvent (ApplicationEvent event ) { + if (event instanceof TestEvent ) { + TestEvent testEvent = (TestEvent) event ; + testEvent print () ; + } + } +} +``` + + + + + +### 添加配置文件 + +```xml + +``` + +### 测试 + +```java +@Test +public void MyAopTest() { + ApplicationContext ac = new ClassPathXmlApplicationContext("spring-aop.xml"); + TestEvent event = new TestEvent (“hello” ,”msg”) ; + context.publishEvent(event); +} +``` + + + +### 源码分析 + +```java +protected void publishEvent(Object event, ResolvableType eventType) { + Assert.notNull(event, "Event must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Publishing event in " + getDisplayName() + ": " + event); + } + + // Decorate event as an ApplicationEvent if necessary + ApplicationEvent applicationEvent; + //支持两种事件1、直接继承ApplicationEvent,2、其他时间,会被包装为PayloadApplicationEvent,可以使用getPayload获取真实的通知内容 + if (event instanceof ApplicationEvent) { + applicationEvent = (ApplicationEvent) event; + } + else { + applicationEvent = new PayloadApplicationEvent(this, event); + if (eventType == null) { + eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType(); + } + } + + // Multicast right now if possible - or lazily once the multicaster is initialized + if (this.earlyApplicationEvents != null) { + //如果有预制行添加到预制行,预制行在执行一次后被置为null,以后都是直接执行 + this.earlyApplicationEvents.add(applicationEvent); + } + else { + //广播event事件 + getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); + } + + // Publish event via parent context as well... + //父bean同样广播 + if (this.parent != null) { + if (this.parent instanceof AbstractApplicationContext) { + ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); + } + else { + this.parent.publishEvent(event); + } + } +} +``` + +查找所有的监听者,依次遍历,如果有线程池,利用线程池进行发送,如果没有则直接发送,如果针对比较大的并发量,我们应该采用线程池模式,将发送通知和真正的业务逻辑进行分离 + +```java +public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + for (final ApplicationListener listener : getApplicationListeners(event, type)) { + Executor executor = getTaskExecutor(); + if (executor != null) { + executor.execute(new Runnable() { + @Override + public void run() { + invokeListener(listener, event); + } + }); + } + else { + invokeListener(listener, event); + } + } +} +``` + +调用invokeListener + +```java +protected void invokeListener(ApplicationListener listener, ApplicationEvent event) { + ErrorHandler errorHandler = getErrorHandler(); + if (errorHandler != null) { + try { + listener.onApplicationEvent(event); + } + catch (Throwable err) { + errorHandler.handleError(err); + } + } + else { + try { + listener.onApplicationEvent(event); + } + catch (ClassCastException ex) { + // Possibly a lambda-defined listener which we could not resolve the generic event type for + LogFactory.getLog(getClass()).debug("Non-matching event type for listener: " + listener, ex); + } + } +} +``` + +初始化其他的单例Bean(非延迟加载的) + +```java +protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + // 判断有无ConversionService(bean属性类型转换服务接口),并初始化 + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) + && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + // 如果beanFactory中不包含EmbeddedValueResolver,则向其中添加一个EmbeddedValueResolver + // EmbeddedValueResolver-->解析bean中的占位符和表达式 + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + // 初始化LoadTimeWeaverAware类型的bean + // LoadTimeWeaverAware-->加载Spring Bean时织入第三方模块,如AspectJ + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // Stop using the temporary ClassLoader for type matching. + // 释放临时类加载器 + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + // 冻结缓存的BeanDefinition元数据 + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 初始化其他的非延迟加载的单例bean + beanFactory.preInstantiateSingletons(); +} +``` + + + +我们重点看 **beanFactory.preInstantiateSingletons();** + +```java +@Override +public void preInstantiateSingletons() throws BeansException { + if (logger.isTraceEnabled()) { + logger.trace("Pre-instantiating singletons in " + this); + } + + // Iterate over a copy to allow for init methods which in turn register new bean definitions. + // While this may not be part of the regular factory bootstrap, it does otherwise work fine. + List beanNames = new ArrayList<>(this.beanDefinitionNames); + + // Trigger initialization of all non-lazy singleton beans... + for (String beanName : beanNames) { + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof FactoryBean) { + final FactoryBean factory = (FactoryBean) bean; + boolean isEagerInit; + if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { + isEagerInit = AccessController.doPrivileged((PrivilegedAction) + ((SmartFactoryBean) factory)::isEagerInit, + getAccessControlContext()); + } + else { + isEagerInit = (factory instanceof SmartFactoryBean && + ((SmartFactoryBean) factory).isEagerInit()); + } + if (isEagerInit) { + getBean(beanName); + } + } + } + else { + getBean(beanName); + } + } + } + +} +``` + +完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知 + +```java +protected void finishRefresh() { + // Clear context-level resource caches (such as ASM metadata from scanning). + // 清空资源缓存 + clearResourceCaches(); + + // Initialize lifecycle processor for this context. + // 初始化生命周期处理器 + initLifecycleProcessor(); + + // Propagate refresh to lifecycle processor first. + // 调用生命周期处理器的onRefresh方法 + getLifecycleProcessor().onRefresh(); + + // Publish the final event. + // 推送容器刷新事件 + publishEvent(new ContextRefreshedEvent(this)); + + // Participate in LiveBeansView MBean, if active. + LiveBeansView.registerApplicationContext(this); +} +``` + + + +分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ + +![](http://img.topjavaer.cn/image/image-20211127150136157.png) + +![](http://img.topjavaer.cn/image/image-20220316234337881.png) + +需要的小伙伴可以自行**下载**: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd \ No newline at end of file diff --git a/docs/source/spring/12-aop-custom-tag.md b/docs/source/spring/12-aop-custom-tag.md new file mode 100644 index 0000000..bc8ec1a --- /dev/null +++ b/docs/source/spring/12-aop-custom-tag.md @@ -0,0 +1,354 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,AOP自定义标签,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +我们知道在面向对象OOP编程存在一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志,安全检测等,我们只有在每个对象里引入公共行为,这样程序中就产生了大量的重复代码,所以有了面向对象编程的补充,面向切面编程(AOP),AOP所关注的方向是横向的,不同于OOP的纵向。接下来我们就详细分析下spring中的AOP。首先我们从动态AOP的使用开始。 + +> [最全面的Java面试网站](https://topjavaer.cn) + +## AOP的使用 + +在开始前,先引入Aspect。 + +```xml + + + org.aspectj + aspectjweaver + ${aspectj.version} + +``` + +### 创建用于拦截的bean + +```java +public class TestBean { + private String message = "test bean"; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void test(){ + System.out.println(this.message); + } +} +``` + + + +### 创建Advisor + +Spring中摒弃了最原始的繁杂配置方式而采用@AspectJ注解对POJO进行标注,使AOP的工作大大简化,例如,在AspectJTest类中,我们要做的就是在所有类的test方法执行前在控制台beforeTest。而在所有类的test方法执行后打印afterTest,同时又使用环绕的方式在所有类的方法执行前后在此分别打印before1和after1,以下是AspectJTest的代码: + +```java +@Aspect +public class AspectJTest { + @Pointcut("execution(* *.test(..))") + public void test(){ + } + + @Before("test()") + public void beforeTest(){ + System.out.println("beforeTest"); + } + + @Around("test()") + public Object aroundTest(ProceedingJoinPoint p){ + System.out.println("around.....before"); + Object o = null; + try{ + o = p.proceed(); + }catch(Throwable e){ + e.printStackTrace(); + } + System.out.println("around.....after"); + return o; + } + + @After("test()") + public void afterTest() + { + System.out.println("afterTest"); + } + } +``` + + + +### 创建配置文件 + +要在Spring中开启AOP功能,,还需要在配置文件中作如下声明: + +```xml + + + + + + + + + +``` + + + +### 测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml"); + TestBean bean = (TestBean)bf.getBean("test"); + bean.test(); + } +} +``` + + + +执行后输出如下: + +![](http://img.topjavaer.cn/img/202309241751697.png) + + + +Spring实现了对所有类的test方法进行增强,使辅助功能可以独立于核心业务之外,方便与程序的扩展和解耦。 + +那么,Spring是如何实现AOP的呢?首先我们知道,SPring是否支持注解的AOP是由一个配置文件控制的,也就是``,当在配置文件中声明了这句配置的时候,Spring就会支持注解的AOP,那么我们的分析就从这句注解开始。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + + + +## AOP自定义标签 + +之前讲过Spring中的自定义注解,如果声明了自定义的注解,那么就一定会在程序中的某个地方注册了对应的解析器。我们搜索 **aspectj-autoproxy** 这个代码,尝试找到注册的地方,全局搜索后我们发现了在org.springframework.aop.config包下的AopNamespaceHandler中对应着这样一段函数: + +```java +@Override +public void init() { + // In 2.0 XSD as well as in 2.1 XSD. + registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); + registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); + registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); + + // Only in 2.0 XSD: moved to context namespace as of 2.1 + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); +} +``` + +这里我们就不再对spring中的自定义注解方式进行讨论了。从这段代码中我们可以得知,在解析配置文件的时候,一旦遇到了aspectj-autoproxy注解的时候会使用解析器AspectJAutoProxyBeanDefinitionParser进行解析,接下来我们就详细分析下其内部实现。 + +### 注册AnnotationAwareAspectJAutoProxyCreator + +所有解析器,因为都是对BeanDefinitionParser接口的统一实现,入口都是从parse函数开始的,AspectJAutoProxyBeanDefinitionParser的parse函数如下: + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + // 注册AnnotationAwareAspectJAutoProxyCreator + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + // 对于注解中子类的处理 + extendBeanDefinition(element, parserContext); + return null; +} +``` + +通过代码可以了解到函数的具体逻辑是在registerAspectJAnnotationAutoProxyCreatorIfecessary方法中实现的,继续进入到函数体内: + +```java +/** + * 注册AnnotationAwareAspectJAutoProxyCreator + * @param parserContext + * @param sourceElement + */ +public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + // 注册或升级AutoProxyCreator定义beanName为org.springframework.aop.config.internalAutoProxyCreator的BeanDefinition + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + // 对于proxy-target-class以及expose-proxy属性的处理 + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + // 注册组件并通知,便于监听器做进一步处理 + registerComponentIfNecessary(beanDefinition, parserContext); +} +``` + +在registerAspectJAnnotationAutoProxyCreatorIfNeccessary方法中主要完成了3件事情,基本上每行代码都是一个完整的逻辑。接下来我们详细分析每一行代码。 + +#### 注册或升级AnnotationAwareAspectJAutoProxyCreator + +对于AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成,它可以根据@Point注解定义的切点来自动代理相匹配的bean。但是为了配置简便,Spring使用了自定义配置来帮助我们自动注册AnnotationAwareAspectJAutoProxyCreator,其注册过程就是在这里实现的。我们继续跟进到方法内部: + +```java +public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); +} + +public static final String AUTO_PROXY_CREATOR_BEAN_NAME = "org.springframework.aop.config.internalAutoProxyCreator"; + +private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, + @Nullable Object source) { + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + //如果已经存在了自动代理创建器且存在的自动代理创建器与现在的不一致那么需要根据优先级来判断到底需要使用哪个 + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { + //改变bean最重要的就是改变bean所对应的className属性 + apcDefinition.setBeanClassName(cls.getName()); + } + } + return null; + } + //注册beanDefinition,Class为AnnotationAwareAspectJAutoProxyCreator.class,beanName为internalAutoProxyCreator + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); + return beanDefinition; +} +``` + +以上代码实现了自动注册AnnotationAwareAspectJAutoProxyCreator类的功能,同时这里还涉及了一个优先级的问题,如果已经存在了自动代理创建器,而且存在的自动代理创建器与现在的不一致,那么需要根据优先级来判断到底需要使用哪个。 + +### 处理proxy-target-class以及expose-proxy属性 + +useClassProxyingIfNecessary实现了proxy-target-class属性以及expose-proxy属性的处理,进入到方法内部: + +```java +private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { + if (sourceElement != null) { + //实现了对proxy-target-class的处理 + boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + //对expose-proxy的处理 + boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE)); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } +} +``` + +在上述代码中用到了两个强制使用的方法,强制使用的过程其实也是一个属性设置的过程,两个函数的方法如下: + +```java +public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } +} + +public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } +} +``` + + + +proxy-target-class:Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理),如果被代理的目标对象实现了至少一个接口, 則会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。如果你希望强制使用CGLIB代理,(例如希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下两个问题。 + +1. 无法通知(advise) Final方法,因为它们不能被覆写。 +2. 你需要将CGLIB二进制发行包放在classpath下面。 + +与之相较,JDK本身就提供了动态代理,强制使用CGLIB代理需要将的 proxy-target-class 厲性设为 true: + +``` +... +``` + +当需要使用CGLIB代理和@AspectJ自动代理支持,可以按照以下方式设罝的 proxy-target-class 属性: + +``` + +``` + +- JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。 +- CGIJB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM (开源的Java字节码编辑类库)操作字节码实现的,性能比JDK强。 +- expose-proxy:有时候目标对象内部的自我调用将无法实施切面中的增强,如下示例: + +```java +public interface AService { + public void a(); + public void b(); +} + +@Service() +public class AServicelmpll implements AService { + @Transactional(propagation = Propagation.REQUIRED) + public void a() { + this.b{); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void b() { + } +} +``` + +此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强, 因此 b 方法的事务定义“@Transactional(propagation = Propagation.REQUIRES_NEW)” 将不会实施,为了解决这个问题,我们可以这样做: + +``` + +``` + +然后将以上代码中的 “this.b();” 修改为 “((AService) AopContext.currentProxy()).b();” 即可。 通过以上的修改便可以完成对a和b方法的同时增强。 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/13-aop-proxy-advisor.md b/docs/source/spring/13-aop-proxy-advisor.md new file mode 100644 index 0000000..c8bf06b --- /dev/null +++ b/docs/source/spring/13-aop-proxy-advisor.md @@ -0,0 +1,841 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +在上一篇的博文中我们讲解了通过自定义配置完成了对AnnotationAwareAspectJAutoProxyCreator类型的自动注册,那么这个类到底做了什么工作来完成AOP的操作呢?首先我们看看AnnotationAwareAspectJAutoProxyCreator的层次结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309262317016.png) + +> 内容摘自我的学习网站:topjavaer.cn + +从上图的类层次结构图中我们看到这个类实现了BeanPostProcessor接口,而实现BeanPostProcessor后,当Spring加载这个Bean时会在实例化前调用其postProcesssAfterIntialization方法,而我们对于AOP逻辑的分析也由此开始。 +首先看下其父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法: + +```java +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { + if (bean != null) { + //根据给定的bean的class和name构建出个key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + //如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +在上面的代码中用到了方法wrapIfNecessary,继续跟踪到方法内部: + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果已经处理过 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 无需增强 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 给定的bean类是否代表一个基础设施类,基础设施类不应代理,或者配置了指定bean不需要自动代理 + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 如果存在增强方法则创建代理 + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果获取到了增强则需要针对增强创建代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; +} +``` + +函数中我们已经看到了代理创建的雏形。当然,真正开始之前还需要经过一些判断,比如是否已经处理过或者是否是需要跳过的bean,而真正创建代理的代码是从getAdvicesAndAdvisorsForBean开始的。 + +创建代理主要包含了两个步骤: + +(1)获取增强方法或者增强器; + +(2)根据获取的增强进行代理。 + +其中逻辑复杂,我们首先来看看获取增强方法的实现逻辑。是在AbstractAdvisorAutoProxyCreator中实现的,代码如下: + +**AbstractAdvisorAutoProxyCreator** + +```java +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + +对于指定bean的增强方法的获取一定是包含两个步骤的,获取所有的增强以及寻找所有增强中使用于bean的增强并应用,那么**findCandidateAdvisors**与**findAdvisorsThatCanApply**便是做了这两件事情。当然,如果无法找到对应的增强器便返回DO_NOT_PROXY,其中DO_NOT_PROXY=null。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## 获取增强器 + +由于我们分析的是使用注解进行的AOP,所以对于findCandidateAdvisors的实现其实是由AnnotationAwareAspectJAutoProxyCreator类完成的,我们继续跟踪AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors方法。代码如下 + +```java +@Override +protected List findCandidateAdvisors() { + // Add all the Spring advisors found according to superclass rules. + // 当使用注解方式配置AOP的时候并不是丢弃了对XML配置的支持, + // 在这里调用父类方法加载配置文件中的AOP声明 + List advisors = super.findCandidateAdvisors(); + // Build Advisors for all AspectJ aspects in the bean factory. + if (this.aspectJAdvisorsBuilder != null) { + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + } + return advisors; +} +``` + +AnnotationAwareAspectJAutoProxyCreator间接继承了AbstractAdvisorAutoProxyCreator,在实现获取增强的方法中除了保留父类的获取配置文件中定义的增强外,同时添加了获取Bean的注解增强的功能,那么其实现正是由this.aspectJAdvisorsBuilder.buildAspectJAdvisors()来实现的。 + +在真正研究代码之前读者可以尝试着自己去想象一下解析思路,看看自己的实现与Spring是否有差别呢? + +(1)获取所有beanName,这一步骤中所有在beanFactory中注册的Bean都会被提取出来。 + +(2)遍历所有beanName,并找出声明AspectJ注解的类,进行进一步的处理。 + +(3)对标记为AspectJ注解的类进行增强器的提取。 + +(4)将提取结果加入缓存。 + +```java +public List buildAspectJAdvisors() { + List aspectNames = this.aspectBeanNames; + + if (aspectNames == null) { + synchronized (this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List advisors = new ArrayList<>(); + aspectNames = new ArrayList<>(); + // 获取所有的beanName + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Object.class, true, false); + // 循环所有的beanName找出对应的增强方法 + for (String beanName : beanNames) { + // 不合法的bean则略过,由子类定义规则,默认返回true + if (!isEligibleBean(beanName)) { + continue; + } + // We must be careful not to instantiate beans eagerly as in this case they + // would be cached by the Spring container but would not have been weaved. + // 获取对应的bean的Class类型 + Class beanType = this.beanFactory.getType(beanName); + if (beanType == null) { + continue; + } + // 如果存在Aspect注解 + if (this.advisorFactory.isAspect(beanType)) { + aspectNames.add(beanName); + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + // 解析标记Aspect注解中的增强方法 + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + //将增强器存入缓存中,下次可以直接取 + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); + } + else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); + this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + } + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } + // 记录在缓存中 + List advisors = new ArrayList<>(); + for (String aspectName : aspectNames) { + List cachedAdvisors = this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } + else { + MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + return advisors; +} +``` + +至此,我们已经完成了Advisor的提取,在上面的步骤中最为重要也最为繁杂的就是增强器的获取,而这一切功能委托给了getAdvisors方法去实现(this.advisorFactory.getAdvisors(factory))。 + +我们先来看看 **this.advisorFactory.isAspect(beanType)** + +```java +@Override +public boolean isAspect(Class clazz) { + return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); +} +private boolean hasAspectAnnotation(Class clazz) { + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); +} + +@Nullable +private static A findAnnotation(Class clazz, Class annotationType, Set visited) { + try { + //判断此Class 是否存在Aspect.class注解 + A annotation = clazz.getDeclaredAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + for (Annotation declaredAnn : getDeclaredAnnotations(clazz)) { + Class declaredType = declaredAnn.annotationType(); + if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) { + annotation = findAnnotation(declaredType, annotationType, visited); + if (annotation != null) { + return annotation; + } + } + } + } +} +``` + +如果 bean 存在 Aspect.class注解,就可以获取此bean中的增强器了,接着我们来看看 `List classAdvisors = this.advisorFactory.getAdvisors(factory)`; + +```java +@Override +public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + // 获取标记为AspectJ的类 + Class aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + // 获取标记为AspectJ的name + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); + validate(aspectClass); + + // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator + // so that it will only instantiate once. + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = + new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + + List advisors = new ArrayList<>(); + + // 对aspectClass的每一个带有注解的方法进行循环(带有PointCut注解的方法除外),取得Advisor,并添加到集合里。 + // (这是里应该是取得Advice,然后取得我们自己定义的切面类中PointCut,组合成Advisor) + for (Method method : getAdvisorMethods(aspectClass)) { + //将类中的方法封装成Advisor + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + // If it's a per target aspect, emit the dummy instantiating aspect. + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + + // Find introduction fields. + for (Field field : aspectClass.getDeclaredFields()) { + Advisor advisor = getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; +} +``` + + + +### 普通增强器的获取 + +普通增强器的获取逻辑通过getAdvisor方法实现,实现步骤包括对切点的注解的获取以及根据注解信息生成增强。我们先来看看 getAdvisorMethods(aspectClass),这个方法,通过很巧妙的使用接口,定义一个匿名回调,把带有注解的Method都取得出来,放到集合里 + +```java +private List getAdvisorMethods(Class aspectClass) { + final List methods = new LinkedList(); + ReflectionUtils.doWithMethods(aspectClass, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException { + // Exclude pointcuts + // 将没有使用Pointcut.class注解的方法加入到集合中 + if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { + methods.add(method); + } + } + }); + Collections.sort(methods, METHOD_COMPARATOR); + return methods; +} + +public static void doWithMethods(Class clazz, MethodCallback mc, @Nullable MethodFilter mf) { + // Keep backing up the inheritance hierarchy. + // 通过反射获取类中所有的方法 + Method[] methods = getDeclaredMethods(clazz); + //遍历所有的方法 + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + //调用函数体 + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } +} +``` + +普通增强器的获取逻辑通过 **getAdvisor** 方法来实现,实现步骤包括对切点的注解以及根据注解信息生成增强。 + +```java +public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aif, + int declarationOrderInAspect, String aspectName) { + + validate(aif.getAspectMetadata().getAspectClass()); + // 获取PointCut信息(主要是PointCut里的表达式) + // 把Method对象也传进去的目的是,比较Method对象上的注解,是不是下面注解其中一个 + // 如果不是,返回null;如果是,就把取得PointCut内容包装返回 + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJExpressionPointcut ajexp = + getPointcut(candidateAdviceMethod, aif.getAspectMetadata().getAspectClass()); + if (ajexp == null) { + return null; + } + // 根据PointCut信息生成增强器 + return new InstantiationModelAwarePointcutAdvisorImpl( + this, ajexp, aif, candidateAdviceMethod, declarationOrderInAspect, aspectName); +} +``` + +**(1)切点信息的获取** + +所谓获取切点信息就是指定注解的表达式信息的获取,如@Before("test()")。 + +```java +private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + // 获取方法上的注解 + // 比较Method对象上的注解,是不是下面注解其中一个,如果不是返回null + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + // 使用AspectJExpressionPointcut 实例封装获取的信息 + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]); + + // 提取得到的注解中的表达式如: + // @Pointcut("execution(* test.TestBean.*(..))") + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + return ajexp; +} +``` + +详细看下上面方法中使用到的方法findAspectJAnnotationOnMethod + +```java +protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + // 设置要查找的注解类,看看方法的上注解是不是这些注解其中之一 + Class[] classesToLookFor = new Class[] { + Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class}; + for (Class c : classesToLookFor) { + AspectJAnnotation foundAnnotation = findAnnotation(method, (Class) c); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + return null; +} +``` + +在上面方法中又用到了方法findAnnotation,继续跟踪代码: + +```java +// 获取指定方法上的注解并使用 AspectJAnnotation 封装 +private static AspectJAnnotation findAnnotation(Method method, Class toLookFor) { + A result = AnnotationUtils.findAnnotation(method, toLookFor); + if (result != null) { + return new AspectJAnnotation(result); + } + else { + return null; + } +} +``` + +此方法的功能是获取指定方法上的注解并使用AspectJAnnotation封装。 + +**(2)根据切点信息生成增强类** + +所有的增强都有Advisor实现类InstantiationModelAwarePontcutAdvisorImpl进行统一封装的。我们看下其构造函数: + +```java +public InstantiationModelAwarePointcutAdvisorImpl(AspectJAdvisorFactory af, AspectJExpressionPointcut ajexp, + MetadataAwareAspectInstanceFactory aif, Method method, int declarationOrderInAspect, String aspectName) { + + this.declaredPointcut = ajexp; + this.method = method; + this.atAspectJAdvisorFactory = af; + this.aspectInstanceFactory = aif; + this.declarationOrder = declarationOrderInAspect; + this.aspectName = aspectName; + + if (aif.getAspectMetadata().isLazilyInstantiated()) { + // Static part of the pointcut is a lazy type. + Pointcut preInstantiationPointcut = + Pointcuts.union(aif.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); + + // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. + // If it's not a dynamic pointcut, it may be optimized out + // by the Spring AOP infrastructure after the first evaluation. + this.pointcut = new PerTargetInstantiationModelPointcut(this.declaredPointcut, preInstantiationPointcut, aif); + this.lazy = true; + } + else { + // A singleton aspect. + // 初始化Advice + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + this.pointcut = declaredPointcut; + this.lazy = false; + } +} +``` + +通过对上面构造函数的分析,发现封装过程只是简单地将信息封装在类的实例中,所有的额信息单纯地复制。在实例初始化的过程中还完成了对于增强器的初始化。因为不同的增强所体现的逻辑是不同的,比如@Before(“test()”)与After(“test()”)标签的不同就是增强器增强的位置不同,所以就需要不同的增强器来完成不同的逻辑,而根据注解中的信息初始化对应的额增强器就是在instantiateAdvice函数中实现的,继续跟踪代码: + +```java +private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) { + Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, + this.aspectInstanceFactory, this.declarationOrder, this.aspectName); + return (advice != null ? advice : EMPTY_ADVICE); +} + + @Override +@Nullable +public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + validate(candidateAspectClass); + + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + // If we get here, we know we have an AspectJ method. + // Check that it's an AspectJ-annotated class + if (!isAspect(candidateAspectClass)) { + throw new AopConfigException("Advice must be declared inside an aspect type: " + + "Offending method '" + candidateAdviceMethod + "' in class [" + + candidateAspectClass.getName() + "]"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found AspectJ method: " + candidateAdviceMethod); + } + + AbstractAspectJAdvice springAdvice; + // 根据不同的注解类型封装不同的增强器 + switch (aspectJAnnotation.getAnnotationType()) { + case AtBefore: + springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfter: + springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfterReturning: + springAdvice = new AspectJAfterReturningAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterReturningAnnotation.returning())) { + springAdvice.setReturningName(afterReturningAnnotation.returning()); + } + break; + case AtAfterThrowing: + springAdvice = new AspectJAfterThrowingAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { + springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); + } + break; + case AtAround: + springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtPointcut: + if (logger.isDebugEnabled()) { + logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); + } + return null; + default: + throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); + } + + // Now to configure the advice... + springAdvice.setAspectName(aspectName); + springAdvice.setDeclarationOrder(declarationOrder); + String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + if (argNames != null) { + springAdvice.setArgumentNamesFromStringArray(argNames); + } + springAdvice.calculateArgumentBindings(); + + return springAdvice; +} +``` + +从上述函数代码中可以看到,Spring会根据不同的注解生成不同的增强器,正如代码switch (aspectJAnnotation.getAnnotationType()),根据不同的类型来生成。例如AtBefore会对应AspectJMethodBeforeAdvice。在AspectJMethodBeforeAdvice中完成了增强逻辑,这里的 **AspectJMethodBeforeAdvice 最后会被适配器封装成\**MethodBeforeAdviceInterceptor,\****下一篇文章中我们具体分析,具体看下其代码: + +**MethodBeforeAdviceInterceptor** + +```java +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { + + private MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +其中的MethodBeforeAdvice代表着前置增强的AspectJMethodBeforeAdvice,跟踪before方法: + +**AspectJMethodBeforeAdvice.java** + +```java +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + + public AspectJMethodBeforeAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + //直接调用增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + +} + +protected Object invokeAdviceMethod( + @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) + throws Throwable { + + return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); +} + +protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterCount() == 0) { + actualArgs = null; + } + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + // TODO AopUtils.invokeJoinpointUsingReflection + // 通过反射调用AspectJ注解类中的增强方法 + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + + this.aspectJAdviceMethod + "]; pointcut expression [" + + this.pointcut.getPointcutExpression() + "]", ex); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } +} +``` + +invokeAdviceMethodWithGivenArgs方法中的aspectJAdviceMethod正是对与前置增强的方法,在这里实现了调用。 + +后置增强与前置增强有稍许不一致的地方。回顾之前讲过的前置增强,大致的结构是在拦截器链中放置MethodBeforeAdviceInterceptor,而在MethodBeforeAdviceInterceptor中又放置了AspectJMethodBeforeAdvice,并在调用invoke时首先串联调用。但是在后置增强的时候却不一样,没有提供中间的类,而是直接在拦截器中使用了中间的AspectJAfterAdvice,也就是直接实现了**MethodInterceptor**。 + +```java +public class AspectJAfterAdvice extends AbstractAspectJAdvice + implements MethodInterceptor, AfterAdvice, Serializable { + + public AspectJAfterAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + finally { + // 激活增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + +} +``` + + + + + +## 寻找匹配的增强器 + +前面的函数中已经完成了所有增强器的解析,但是对于所有增强器来讲,并不一定都适用于当前的bean,还要挑取出适合的增强器,也就是满足我们配置的通配符的增强器。具体实现在findAdvisorsThatCanApply中。 + +```java +protected List findAdvisorsThatCanApply( + List candidateAdvisors, Class beanClass, String beanName) { + + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + try { + // 过滤已经得到的advisors + return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } + finally { + ProxyCreationContext.setCurrentProxiedBeanName(null); + } +} +``` + +继续看findAdvisorsThatCanApply: + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} +``` + +findAdvisorsThatCanApply函数的主要功能是寻找增强器中适用于当前class的增强器。引介增强与普通的增强的处理是不一样的,所以分开处理。而对于真正的匹配在canApply中实现。 + +``` +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} + +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + //通过Pointcut的条件判断此类是否能匹配 + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //根据匹配原则判断该方法是否能匹配Pointcut中的规则,如果有一个方法能匹配,则返回true + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + + + +首先判断bean是否满足切点的规则,如果能满足,则获取bean的所有方法,判断是否有方法能匹配规则,有方法匹配规则,就代表此 Advisor 能作用于该bean,然后将该Advisor加入 eligibleAdvisors 集合中。 + +我们以注解的规则来看看bean中的method是怎样匹配 Pointcut中的规则 + +**AnnotationMethodMatcher** + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); +} + +private boolean matchesMethod(Method method) { + //可以看出判断该Advisor是否使用于bean中的method,只需看method上是否有Advisor的注解 + return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) : + method.isAnnotationPresent(this.annotationType)); +} +``` + + + +至此,我们在后置处理器中找到了所有匹配Bean中的增强器,下一篇讲解如何使用找到切面,来创建代理。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/14-aop-proxy-create.md b/docs/source/spring/14-aop-proxy-create.md new file mode 100644 index 0000000..19e87b3 --- /dev/null +++ b/docs/source/spring/14-aop-proxy-create.md @@ -0,0 +1,673 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理生成,AOP代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +> 本文已经收录到大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +**正文** + +在获取了所有对应bean的增强后,便可以进行代理的创建了。回到AbstractAutoProxyCreator的wrapIfNecessary方法中,如下所示: + +```java + protected static final Object[] DO_NOT_PROXY = null; + + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } +``` + +我们上一篇文章分析完了第16行,获取到了所有对应bean的增强器,并获取到了此目标bean所有匹配的 Advisor,接下来我们要从第17行开始分析,如果 specificInterceptors 不为空,则要为当前bean创建代理类,接下来我们来看创建代理类的方法 **createProxy:** + +> [最全面的Java面试网站](https://topjavaer.cn) + +```java +protected Object createProxy(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource) { + + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + // 获取当前类中相关属性 + proxyFactory.copyFrom(this); + + if (!proxyFactory.isProxyTargetClass()) { + // 决定对于给定的bean是否应该使用targetClass而不是他的接口代理, + // 检査 proxyTargetClass 设置以及 preserveTargetClass 属性 + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + // 加入增强器 + proxyFactory.addAdvisors(advisors); + // 设置要代理的目标类 + proxyFactory.setTargetSource(targetSource); + // 定制代理 + customizeProxyFactory(proxyFactory); + // 用来控制代理工厂被配置之后,是否还允许修改通知。 + // 缺省值是false (即在代理被配置之后,不允许修改代理的配置)。 + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + //真正创建代理的方法 + return proxyFactory.getProxy(getProxyClassLoader()); +} + +@Override +public void setTargetSource(@Nullable TargetSource targetSource) { + this.targetSource = (targetSource != null ? targetSource : EMPTY_TARGET_SOURCE); +} + +public void addAdvisors(Collection advisors) { + if (isFrozen()) { + throw new AopConfigException("Cannot add advisor: Configuration is frozen."); + } + if (!CollectionUtils.isEmpty(advisors)) { + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + Assert.notNull(advisor, "Advisor must not be null"); + this.advisors.add(advisor); + } + updateAdvisorArray(); + adviceChanged(); + } +} +``` + +从上面代码我们看到对于代理类的创建及处理spring是委托给了ProxyFactory处理的 + +## 创建代理 + +```java +public Object getProxy(@Nullable ClassLoader classLoader) { + return createAopProxy().getProxy(classLoader); +} +``` + +在上面的getProxy方法中createAopProxy方法,其实现是在DefaultAopProxyFactory中,这个方法的主要功能是,根据optimize、ProxyTargetClass等参数来决定生成Jdk动态代理,还是生成Cglib代理。我们进入到方法内: + +```java +protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + activate(); + } + // 创建代理 + return getAopProxyFactory().createAopProxy(this); +} + +public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + //手动设置创建Cglib代理类后,如果目标bean是一个接口,也要创建jdk代理类 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + //创建Cglib代理 + return new ObjenesisCglibAopProxy(config); + } + else { + //默认创建jdk代理 + return new JdkDynamicAopProxy(config); + } +} +``` + + + +我们知道对于Spring的代理是通过JDKProxy的实现和CglibProxy实现。Spring是如何选取的呢? + +从if的判断条件中可以看到3个方面影响这Spring的判断。 + +- optimize:用来控制通过CGLIB创建的代理是否使用激进的优化策略,除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。目前这个属性仅用于CGLIB 代理,对于JDK动态代理(缺省代理)无效。 +- proxyTargetClass:这个属性为true时,目标类本身被代理而不是目标类的接口。如果这个属性值被设为true,CGLIB代理将被创建,设置方式:**。** +- hasNoUserSuppliedProxylnterfaces:是否存在代理接口 + +下面是对JDK与Cglib方式的总结。 + +- **如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。** +- **如果目标对象实现了接口,可以强制使用CGLIB实现AOP。** +- **如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理 和CGLIB之间转换。** + +如何强制使用CGLIB实现AOP? + +(1)添加 CGLIB 库,Spring_HOME/cglib/*.jar。 + +(2)在 Spring 配置文件中加人。 + +JDK动态代理和CGLIB字节码生成的区别? + +- JDK动态代理只能对实现了接口的类生成代理,而不能针对类。 +- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。 + +## 获取代理 + +Spring的AOP实现其实也是用了**Proxy和InvocationHandler**这两个东西的。 + +我们再次来回顾一下使用JDK代理的方式,在整个创建过程中,对于InvocationHandler的创建是最为核心的,在自定义的InvocationHandler中需要重写3个函数。 + +- 构造函数,将代理的对象传入。 +- invoke方法,此方法中实现了 AOP增强的所有逻辑。 +- getProxy方法,此方法千篇一律,但是必不可少。 + +那么,我们看看Spring中的JDK代理实现是不是也是这么做的呢?我们来看看简化后的 JdkDynamicAopProxy 。 + +```java + final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { + + private final AdvisedSupport advised; + + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + Assert.notNull(config, "AdvisedSupport must not be null"); + if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { + throw new AopConfigException("No advisors and no TargetSource specified"); + } + this.advised = config; + } + + + @Override + public Object getProxy() { + return getProxy(ClassUtils.getDefaultClassLoader()); + } + + @Override + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); + } + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + // The target does not implement the hashCode() method itself. + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + // There is only getDecoratedClass() declared -> dispatch to proxy config. + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + // Service invocations on ProxyConfig with the proxy config... + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + // Get as late as possible to minimize the time we "own" the target, + // in case it comes from a pool. + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // Get the interception chain for this method. + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + // Check whether we have any advice. If we don't, we can fallback on direct + // reflective invocation of the target, and avoid creating a MethodInvocation. + if (chain.isEmpty()) { + // We can skip creating a MethodInvocation: just invoke the target directly + // Note that the final invoker must be an InvokerInterceptor so we know it does + // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + MethodInvocation invocation = + new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + retVal = invocation.proceed(); + } + + // Massage return value if necessary. + Class returnType = method.getReturnType(); + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this" and the return type of the method + // is type-compatible. Note that we can't help if the target sets + // a reference to itself in another returned object. + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + } +``` + + + +我们看到JdkDynamicAopProxy 也是和我们自定义的**InvocationHandler**一样,实现了InvocationHandler接口,并且提供了一个getProxy方法创建代理类,重写invoke方法。 + +我们重点看看代理类的调用。了解Jdk动态代理的话都会知道,在实现Jdk动态代理功能,要实现InvocationHandler接口的invoke方法(这个方法是一个回调方法)。 被代理类中的方法被调用时,实际上是调用的invoke方法,我们看一看这个方法的实现。 + +```java + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + MethodInvocation invocation; + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // 获取当前方法的拦截器链 + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + if (chain.isEmpty()) { + // 如果没有发现任何拦截器那么直接调用切点方法 + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + // 将拦截器封装在ReflectiveMethodInvocation, + // 以便于使用其proceed进行链接表用拦截器 + invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + // 执行拦截器链 + retVal = invocation.proceed(); + } + + Class returnType = method.getReturnType(); + // 返回结果 + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } +``` + +我们先来看看第37行,获取目标bean中目标method中的增强器,并将增强器封装成拦截器链 + +```java + @Override + public List getInterceptorsAndDynamicInterceptionAdvice( + Advised config, Method method, @Nullable Class targetClass) { + + // This is somewhat tricky... We have to process introductions first, + // but we need to preserve order in the ultimate list. + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + Advisor[] advisors = config.getAdvisors(); + List interceptorList = new ArrayList<>(advisors.length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + Boolean hasIntroductions = null; + + //获取bean中的所有增强器 + for (Advisor advisor : advisors) { + if (advisor instanceof PointcutAdvisor) { + // Add it conditionally. + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; + if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + boolean match; + if (mm instanceof IntroductionAwareMethodMatcher) { + if (hasIntroductions == null) { + hasIntroductions = hasMatchingIntroductions(advisors, actualClass); + } + match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions); + } + else { + //根据增强器中的Pointcut判断增强器是否能匹配当前类中的method + //我们要知道目标Bean中并不是所有的方法都需要增强,也有一些普通方法 + match = mm.matches(method, actualClass); + } + if (match) { + //如果能匹配,就将advisor封装成MethodInterceptor加入到interceptorList中 + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + if (mm.isRuntime()) { + // Creating a new object instance in the getInterceptors() method + // isn't a problem as we normally cache created chains. + for (MethodInterceptor interceptor : interceptors) { + interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); + } + } + else { + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + } + } + else if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + else { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + + return interceptorList; + } +``` + + + +我们知道目标Bean中并不是所有的方法都需要增强,所以我们要遍历所有的 Advisor ,根据**Pointcut判断增强器是否能匹配当前类中的method,取出能匹配的增强器,封装成** **MethodInterceptor,加入到拦截器链中**,我们来看看第34行 + +```java +@Override +public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { + List interceptors = new ArrayList<>(3); + Advice advice = advisor.getAdvice(); + if (advice instanceof MethodInterceptor) { + interceptors.add((MethodInterceptor) advice); + } + //这里遍历三个适配器,将对应的advisor转化成Interceptor + //这三个适配器分别是MethodBeforeAdviceAdapter,AfterReturningAdviceAdapter,ThrowsAdviceAdapter + for (AdvisorAdapter adapter : this.adapters) { + if (adapter.supportsAdvice(advice)) { + interceptors.add(adapter.getInterceptor(advisor)); + } + } + if (interceptors.isEmpty()) { + throw new UnknownAdviceTypeException(advisor.getAdvice()); + } + return interceptors.toArray(new MethodInterceptor[0]); +} + +private final List adapters = new ArrayList<>(3); + +/** + * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters. + */ +public DefaultAdvisorAdapterRegistry() { + registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); + registerAdvisorAdapter(new AfterReturningAdviceAdapter()); + registerAdvisorAdapter(new ThrowsAdviceAdapter()); +} + +@Override +public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); +} +``` + +由于Spring中涉及过多的拦截器,增强器,增强方法等方式来对逻辑进行增强,在上一篇文章中我们知道创建的几个增强器,AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice这几个增强器都实现了 MethodInterceptor 接口,AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice 并没有实现 MethodInterceptor 接口,因此AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice不能满足MethodInterceptor 接口中的invoke方法,所以这里使用适配器模式将AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice转化成能满足需求的MethodInterceptor实现类。 + +遍历adapters,通过adapter.supportsAdvice(advice)找到advice对应的适配器,adapter.getInterceptor(advisor)将advisor转化成对应的interceptor + +接下来我们看看MethodBeforeAdviceAdapter和AfterReturningAdviceAdapter这两个适配器,这两个适配器是将MethodBeforeAdvice和AfterReturningAdvice适配成对应的Interceptor + +**MethodBeforeAdviceAdapter** + +```java +class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + //判断是否是MethodBeforeAdvice类型的advice + return (advice instanceof MethodBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); + //将advice封装成MethodBeforeAdviceInterceptor + return new MethodBeforeAdviceInterceptor(advice); + } +} + +//MethodBeforeAdviceInterceptor实现了MethodInterceptor接口,实现了invoke方法,并将advice作为属性 +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable { + + private final MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +**AfterReturningAdviceAdapter** + +```java +class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof AfterReturningAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice(); + return new AfterReturningAdviceInterceptor(advice); + } +} + +public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { + + private final AfterReturningAdvice advice; + + public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } + +} +``` + +至此我们获取到了一个拦截器链,链中包括AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice、MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor + +接下来 ReflectiveMethodInvocation 类进行了链的封装,而在ReflectiveMethodInvocation类的proceed方法中实现了拦截器的逐一调用,那么我们继续来探究,在proceed方法中是怎么实现前置增强在目标方法前调用后置增强在目标方法后调用的逻辑呢? + +我们先来看看ReflectiveMethodInvocation的构造器,只是简单的进行属性赋值,不过我们要注意有一个特殊的变量currentInterceptorIndex,这个变量代表执行Interceptor的下标,从-1开始,Interceptor执行一个,先++this.currentInterceptorIndex + +```java +protected ReflectiveMethodInvocation( + Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, + @Nullable Class targetClass, List interceptorsAndDynamicMethodMatchers) { + + this.proxy = proxy; + this.target = target; + this.targetClass = targetClass; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments); + this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers; +} + +private int currentInterceptorIndex = -1; +``` + +下面是ReflectiveMethodInvocation类Proceed方法: + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + + + +下一篇文章讲解目标方法和增强方法是如何执行的。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git a/docs/source/spring/15-aop-advice-create.md b/docs/source/spring/15-aop-advice-create.md new file mode 100644 index 0000000..22df32a --- /dev/null +++ b/docs/source/spring/15-aop-advice-create.md @@ -0,0 +1,193 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,目标方法,增强方法,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇博文中我们讲了代理类的生成,这一篇主要讲解剩下的部分,当代理类调用时,目标方法和代理方法是如何执行的,我们还是接着上篇的ReflectiveMethodInvocation类Proceed方法来看。[最全面的Java面试网站](https://topjavaer.cn) + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + +我们先来看一张方法调用顺序图 + +![](http://img.topjavaer.cn/img/202310011108435.png) + +我们看到链中的顺序是AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor,这些拦截器是按顺序执行的,那我们来看看第一个拦截器AspectJAfterThrowingAdvice中的invoke方法 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +## **AspectJAfterThrowingAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + //从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身 + //这里又直接调用了ReflectiveMethodInvocation的proceed()方法 + return mi.proceed(); + } + catch (Throwable ex) { + if (shouldInvokeOnThrowing(ex)) { + invokeAdviceMethod(getJoinPointMatch(), null, ex); + } + throw ex; + } + } +``` + +第一个拦截器AspectJAfterThrowingAdvice的invoke方法中,直接调用mi.proceed();,从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身,所以又会执行proceed()方法,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AfterReturningAdviceInterceptor,并调用拦截器中的invoke方法,,此时第一个拦截器在invoke()方法的第七行卡住了,接下来我们看第二个拦截器的执行 + +## **AfterReturningAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //直接调用MethodInvocation的proceed方法 + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } +``` + +AfterReturningAdviceInterceptor还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时AfterReturningAdviceInterceptor方法卡在第四行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AspectJAfterAdvice,并调用AspectJAfterAdvice中的invoke方法 + +## **AspectJAfterAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + return mi.proceed(); + } + finally { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } +``` + +AspectJAfterAdvice还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时**AspectJAfterAdvice**方法卡在第五行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器MethodBeforeAdviceInterceptor,并调用MethodBeforeAdviceInterceptor中的invoke方法 + +## **MethodBeforeAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //终于开始做事了,调用增强器的before方法,明显是通过反射的方式调用 + //到这里增强方法before的业务逻辑执行 + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + //又调用了调用MethodInvocation的proceed方法 + return mi.proceed(); + } +``` + +第5行代码终于通过反射调用了切面里面的before方法,接着又调用mi.proceed(),我们知道这是最后一个拦截器了,此时this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1应该为true了,那么就会执行 return invokeJoinpoint();,也就是执行bean中的目标方法,接着我们来看看目标方法的执行 + +```java +@Nullable +protected Object invokeJoinpoint() throws Throwable { + return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); +} + + @Nullable +public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(method); + //直接通过反射调用目标bean中的method + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + // Invoked method threw a checked exception. + // We must rethrow it. The client won't see the interceptor. + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + + method + "] on target [" + target + "]", ex); + } + catch (IllegalAccessException ex) { + throw new AopInvocationException("Could not access method [" + method + "]", ex); + } +} +``` + +before方法执行完后,就通过反射的方式执行目标bean中的method,并且返回结果,接下来我们想想程序该怎么执行呢? + +1 、MethodBeforeAdviceInterceptor执行完了后,开始退栈,AspectJAfterAdvice中invoke卡在第5行的代码继续往下执行, 我们看到在AspectJAfterAdvice的invoke方法中的finally中第8行有这样一句话 invokeAdviceMethod(getJoinPointMatch(), null, null);,就是通过反射调用AfterAdvice的方法,意思是切面类中的 @After方法不管怎样都会执行,因为在finally中 + + 2、AspectJAfterAdvice中invoke方法发执行完后,也开始退栈,接着就到了AfterReturningAdviceInterceptor的invoke方法的第4行开始恢复,但是此时如果目标bean和前面增强器中出现了异常,此时AfterReturningAdviceInterceptor中第5行代码就不会执行了,直接退栈;如果没有出现异常,则执行第5行,也就是通过反射执行切面类中@AfterReturning注解的方法,然后退栈 + +3、AfterReturningAdviceInterceptor退栈后,就到了AspectJAfterThrowingAdvice拦截器,此拦截器中invoke方法的第7行开始恢复,我们看到在 catch (Throwable ex) { 代码中,也就是第11行 invokeAdviceMethod(getJoinPointMatch(), null, ex);,如果目标bean的method或者前面的增强方法中出现了异常,则会被这里的catch捕获,也是通过反射的方式执行@AfterThrowing注解的方法,然后退栈\ + + + +## 总结 + +这个代理类调用过程,我们可以看到是一个递归的调用过程,通过ReflectiveMethodInvocation类中Proceed方法递归调用,顺序执行拦截器链中AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor这几个拦截器,在拦截器中反射调用增强方法 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/16-transactional.md b/docs/source/spring/16-transactional.md new file mode 100644 index 0000000..e63dd5a --- /dev/null +++ b/docs/source/spring/16-transactional.md @@ -0,0 +1,430 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +面的几个章节已经分析了spring基于`@AspectJ`的源码,那么接下来我们分析一下Aop的另一个重要功能,事物管理。[最全面的Java面试网站](https://topjavaer.cn) + +## 事务的介绍 + +### 1.数据库事物特性 + +- 原子性 + 多个数据库操作是不可分割的,只有所有的操作都执行成功,事物才能被提交;只要有一个操作执行失败,那么所有的操作都要回滚,数据库状态必须回复到操作之前的状态 +- 一致性 + 事物操作成功后,数据库的状态和业务规则必须一致。例如:从A账户转账100元到B账户,无论数据库操作成功失败,A和B两个账户的存款总额是不变的。 +- 隔离性 + 当并发操作时,不同的数据库事物之间不会相互干扰(当然这个事物隔离级别也是有关系的) +- 持久性 + 事物提交成功之后,事物中的所有数据都必须持久化到数据库中。即使事物提交之后数据库立刻崩溃,也需要保证数据能能够被恢复。 + +### 2.事物隔离级别 + +当数据库并发操作时,可能会引起脏读、不可重复读、幻读、第一类丢失更新、第二类更新丢失等现象。 + +- 脏读 + 事物A读取事物B尚未提交的更改数据,并做了修改;此时如果事物B回滚,那么事物A读取到的数据是无效的,此时就发生了脏读。 +- 不可重复读 + 一个事务执行相同的查询两次或两次以上,每次都得到不同的数据。如:A事物下查询账户余额,此时恰巧B事物给账户里转账100元,A事物再次查询账户余额,那么A事物的两次查询结果是不一致的。 +- 幻读 + A事物读取B事物提交的新增数据,此时A事物将出现幻读现象。幻读与不可重复读容易混淆,如何区分呢?幻读是读取到了其他事物提交的新数据,不可重复读是读取到了已经提交事物的更改数据(修改或删除) + +对于以上问题,可以有多个解决方案,设置数据库事物隔离级别就是其中的一种,数据库事物隔离级别分为四个等级,通过一个表格描述其作用。 + +| 隔离级别 | 脏读 | 不可重复读 | 幻象读 | +| ---------------- | ------ | ---------- | ------ | +| READ UNCOMMITTED | 允许 | 允许 | 允许 | +| READ COMMITTED | 脏读 | 允许 | 允许 | +| REPEATABLE READ | 不允许 | 不允许 | 允许 | +| SERIALIZABLE | 不允许 | 不允许 | 不允许 | + +### 3.Spring事物支持核心接口 + +![](http://img.topjavaer.cn/img/202310011145573.png) + +- TransactionDefinition-->定义与spring兼容的事务属性的接口 + +```java +public interface TransactionDefinition { + // 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中。 + int PROPAGATION_REQUIRED = 0; + // 支持当前事物,如果当前没有事物,则以非事物方式执行。 + int PROPAGATION_SUPPORTS = 1; + // 使用当前事物,如果当前没有事物,则抛出异常。 + int PROPAGATION_MANDATORY = 2; + // 新建事物,如果当前已经存在事物,则挂起当前事物。 + int PROPAGATION_REQUIRES_NEW = 3; + // 以非事物方式执行,如果当前存在事物,则挂起当前事物。 + int PROPAGATION_NOT_SUPPORTED = 4; + // 以非事物方式执行,如果当前存在事物,则抛出异常。 + int PROPAGATION_NEVER = 5; + // 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 + int PROPAGATION_NESTED = 6; + // 使用后端数据库默认的隔离级别。 + int ISOLATION_DEFAULT = -1; + // READ_UNCOMMITTED 隔离级别 + int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED; + // READ_COMMITTED 隔离级别 + int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED; + // REPEATABLE_READ 隔离级别 + int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ; + // SERIALIZABLE 隔离级别 + int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; + // 默认超时时间 + int TIMEOUT_DEFAULT = -1; + // 获取事物传播特性 + int getPropagationBehavior(); + // 获取事物隔离级别 + int getIsolationLevel(); + // 获取事物超时时间 + int getTimeout(); + // 判断事物是否可读 + boolean isReadOnly(); + // 获取事物名称 + @Nullable + String getName(); +} +``` + +1. Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + +1. Spring事物隔离级别表: + +| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 | +| ---------------------------- | ---- | ---------- | ---- | +| 读未提交(read-uncommitted) | 是 | 是 | 是 | +| 不可重复读(read-committed) | 否 | 是 | 是 | +| 可重复读(repeatable-read) | 否 | 否 | 是 | +| 串行化(serializable) | 否 | 否 | 否 | + +MySQL默认的事务隔离级别为 **可重复读repeatable-read** + +  3.PlatformTransactionManager-->Spring事务基础结构中的中心接口 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +```java +public interface PlatformTransactionManager { + // 根据指定的传播行为,返回当前活动的事务或创建新事务。 + TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; + // 就给定事务的状态提交给定事务。 + void commit(TransactionStatus status) throws TransactionException; + // 执行给定事务的回滚。 + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +Spring将事物管理委托给底层的持久化框架来完成,因此,Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现。列举几个Spring自带的事物管理器: + +| 事物管理器 | 说明 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| org.springframework.jdbc.datasource.DataSourceTransactionManager | 提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理 | +| org.springframework.orm.jpa.JpaTransactionManager | 提供对单个javax.persistence.EntityManagerFactory事务支持,用于集成JPA实现框架时的事务管理 | +| org.springframework.transaction.jta.JtaTransactionManager | 提供对分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器 | + + + +- TransactionStatus-->事物状态描述 + +1. TransactionStatus接口 + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +1. SavepointManager接口 + +```java +public interface SavepointManager { + // 创建一个新的保存点。 + Object createSavepoint() throws TransactionException; + // 回滚到给定的保存点。 + // 注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点, + // 可以通过调用releaseSavepoint方法释放保存点。 + void rollbackToSavepoint(Object savepoint) throws TransactionException; + // 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点) + void releaseSavepoint(Object savepoint) throws TransactionException; +} +``` + + + +## Spring编程式事物 + +- 表 + +```sql +CREATE TABLE `account` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `balance` int(11) DEFAULT NULL COMMENT '账户余额', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='--账户表' +``` + +- 实现 + +```java + import org.apache.commons.dbcp.BasicDataSource; + import org.springframework.dao.DataAccessException; + import org.springframework.jdbc.core.JdbcTemplate; + import org.springframework.jdbc.datasource.DataSourceTransactionManager; + import org.springframework.transaction.TransactionDefinition; + import org.springframework.transaction.TransactionStatus; + import org.springframework.transaction.support.DefaultTransactionDefinition; + + import javax.sql.DataSource; + + public class MyTransaction { + + private JdbcTemplate jdbcTemplate; + private DataSourceTransactionManager txManager; + private DefaultTransactionDefinition txDefinition; + private String insert_sql = "insert into account (balance) values ('100')"; + + public void save() { + + // 1、初始化jdbcTemplate + DataSource dataSource = getDataSource(); + jdbcTemplate = new JdbcTemplate(dataSource); + + // 2、创建物管理器 + txManager = new DataSourceTransactionManager(); + txManager.setDataSource(dataSource); + + // 3、定义事物属性 + txDefinition = new DefaultTransactionDefinition(); + txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + + // 3、开启事物 + TransactionStatus txStatus = txManager.getTransaction(txDefinition); + + // 4、执行业务逻辑 + try { + jdbcTemplate.execute(insert_sql); + //int i = 1/0; + jdbcTemplate.execute(insert_sql); + txManager.commit(txStatus); + } catch (DataAccessException e) { + txManager.rollback(txStatus); + e.printStackTrace(); + } + + } + + public DataSource getDataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("com.mysql.jdbc.Driver"); + dataSource.setUrl("jdbc:mysql://localhost:3306/my_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8"); + dataSource.setUsername("root"); + dataSource.setPassword("dabin1991@"); + return dataSource; + } + + } +``` + + + +- 测试类及结果 + +```java +public class MyTest { + @Test + public void test1() { + MyTransaction myTransaction = new MyTransaction(); + myTransaction.save(); + } +} +``` + +运行测试类,在抛出异常之后手动回滚事物,所以数据库表中不会增加记录。 + + + +## 基于@Transactional注解的声明式事物 + +其底层建立在 AOP 的基础之上,对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事物,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。 + +- 接口 + +```java +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(propagation = Propagation.REQUIRED) +public interface AccountServiceImp { + void save() throws RuntimeException; +} +``` + +- 实现 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +public class AccountServiceImpl implements AccountServiceImp { + + private JdbcTemplate jdbcTemplate; + + private static String insert_sql = "insert into account(balance) values (100)"; + + + @Override + public void save() throws RuntimeException { + System.out.println("==开始执行sql"); + jdbcTemplate.update(insert_sql); + System.out.println("==结束执行sql"); + + System.out.println("==准备抛出异常"); + throw new RuntimeException("==手动抛出一个异常"); + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} +``` + +- 配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- 测试 + +```java +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class MyTest { + + @Test + public void test1() { + // 基于tx标签的声明式事物 + ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml"); + AccountServiceImp studentService = ctx.getBean("accountService", AccountServiceImp.class); + studentService.save(); + } +} +``` + +- 测试 + +```java +==开始执行sql +==结束执行sql +==准备抛出异常 + +java.lang.RuntimeException: ==手动抛出一个异常 + + at com.lyc.cn.v2.day09.AccountServiceImpl.save(AccountServiceImpl.java:24) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) +``` + +测试方法中手动抛出了一个异常,Spring会自动回滚事物,查看数据库可以看到并没有新增记录。 + +注意:默认情况下Spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果。 + + + +下一篇我们分析基于@Transactional注解的声明式事物的的源码实现。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/17-spring-transaction-aop.md b/docs/source/spring/17-spring-transaction-aop.md new file mode 100644 index 0000000..8888543 --- /dev/null +++ b/docs/source/spring/17-spring-transaction-aop.md @@ -0,0 +1,687 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +此篇文章需要有SpringAOP基础,知道AOP底层原理可以更好的理解Spring的事务处理。[最全面的Java面试网站](https://topjavaer.cn) + + + +## 自定义标签 + +对于Spring中事务功能的代码分析,我们首先从配置文件开始人手,在配置文件中有这样一个配置:``。可以说此处配置是事务的开关,如果没有此处配置,那么Spring中将不存在事务的功能。那么我们就从这个配置开始分析。 + +根据之前的分析,我们因此可以判断,在自定义标签中的解析过程中一定是做了一些辅助操作,于是我们先从自定义标签入手进行分析。使用Idea搜索全局代码,关键字annotation-driven,最终锁定类TxNamespaceHandler,在TxNamespaceHandler中的 init 方法中: + +```java +@Override +public void init() { + registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); +} +``` + +在遇到诸如tx:annotation-driven为开头的配置后,Spring都会使用AnnotationDrivenBeanDefinitionParser类的parse方法进行解析。 + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { + registerJtaTransactionAspect(element, parserContext); + } + } + else { + // mode="proxy" + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + + +在解析中存在对于mode属性的判断,根据代码,如果我们需要使用AspectJ的方式进行事务切入(Spring中的事务是以AOP为基础的),那么可以使用这样的配置: + +``` + +``` + +## 注册 InfrastructureAdvisorAutoProxyCreator + +我们以默认配置为例进行分析,进人AopAutoProxyConfigurer类的configureAutoProxyCreator: + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +```java +public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { + //向IOC注册InfrastructureAdvisorAutoProxyCreator这个类型的Bean + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + + String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; + if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { + Object eleSource = parserContext.extractSource(element); + + // Create the TransactionAttributeSource definition. + // 创建AnnotationTransactionAttributeSource类型的Bean + RootBeanDefinition sourceDef = new RootBeanDefinition("org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the TransactionInterceptor definition. + // 创建TransactionInterceptor类型的Bean + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(element, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + // 创建BeanFactoryTransactionAttributeSourceAdvisor类型的Bean + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + // 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + // 将上面TransactionInterceptor类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); + // 将上面三个Bean注册进IOC中 + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); + parserContext.registerComponent(compositeDef); + } +} +``` + +这里分别是注册了三个Bean,和一个`InfrastructureAdvisorAutoProxyCreator`,其中三个Bean支撑了整个事务的功能。 + +我们首先需要回顾一下AOP的原理,AOP中有一个 Advisor 存放在代理类中,而Advisor中有advise与pointcut信息,每次执行被代理类的方法时都会执行代理类的invoke(如果是JDK代理)方法,而invoke方法会根据advisor中的pointcut动态匹配这个方法需要执行的advise链,遍历执行advise链,从而达到AOP切面编程的目的。 + +- `BeanFactoryTransactionAttributeSourceAdvisor`:首先看这个类的继承结构,可以看到这个类其实是一个Advisor,其实由名字也能看出来,类中有几个关键地方注意一下,在之前的注册过程中,将两个属性注入进这个Bean中: + +```java +// 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); +// 将上面TransactionInterceptor类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); +``` + +那么它们被注入成什么了呢?进入`BeanFactoryTransactionAttributeSourceAdvisor`一看便知。 + +```java +@Nullable +private TransactionAttributeSource transactionAttributeSource; +``` + +在其父类中有属性: + +```java +@Nullable +private String adviceBeanName; +``` + +也就是说,这里先将上面的TransactionInterceptor的BeanName传入到Advisor中,然后将AnnotationTransactionAttributeSource这个Bean注入到Advisor中,那么这个Source Bean有什么用呢?可以继续看看BeanFactoryTransactionAttributeSourceAdvisor的源码。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +看到这里应该明白了,这里的Source是提供了pointcut信息,作为存放事务属性的一个类注入进Advisor中,到这里应该知道注册这三个Bean的作用了吧?首先注册pointcut、advice、advisor,然后将pointcut和advice注入进advisor中,在之后动态代理的时候会使用这个Advisor去寻找每个Bean是否需要动态代理(取决于是否有开启事务),因为Advisor有pointcut信息。 + +![](http://img.topjavaer.cn/img/202310031553121.png) + +- InfrastructureAdvisorAutoProxyCreator:在方法开头,首先就调用了AopNamespeceUtils去注册了这个Bean,那么这个Bean是干什么用的呢?还是先看看这个类的结构。这个类继承了AbstractAutoProxyCreator,看到这个名字,熟悉AOP的话应该已经知道它是怎么做的了吧?其次这个类还实现了BeanPostProcessor接口,凡事实现了这个BeanPost接口的类,我们首先关注的就是它的postProcessAfterInitialization方法,这里在其父类也就是刚刚提到的AbstractAutoProxyCreator这里去实现。(这里需要知道Spring容器初始化Bean的过程,关于BeanPostProcessor的使用我会另开一篇讲解。如果不知道只需了解如果一个Bean实现了BeanPostProcessor接口,当所有Bean实例化且依赖注入之后初始化方法之后会执行这个实现Bean的postProcessAfterInitialization方法) + +进入这个函数: + +```java +public static void registerAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); +} + +@Nullable +public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + + return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); +} +``` + +对于解析来的代码流程AOP中已经有所分析,上面的两个函数主要目的是注册了InfrastructureAdvisorAutoProxyCreator类型的bean,那么注册这个类的目的是什么呢?查看这个类的层次,如下图所示: + +![](http://img.topjavaer.cn/img/202310031554128.png) + +从上面的层次结构中可以看到,InfrastructureAdvisorAutoProxyCreator间接实现了SmartInstantiationAwareBeanPostProcessor,而SmartInstantiationAwareBeanPostProcessor又继承自InstantiationAwareBeanPostProcessor,也就是说在Spring中,所有bean实例化时Spring都会保证调用其postProcessAfterInstantiation方法,其实现是在父类AbstractAutoProxyCreator类中实现。 + +以之前的示例为例,当实例化AccountServiceImpl的bean时便会调用此方法,方法如下: + +```java +@Override +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + // 根据给定的bean的class和name构建出key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + // 如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +这里实现的主要目的是对指定bean进行封装,当然首先要确定是否需要封装,检测与封装的工作都委托给了wrapIfNecessary函数进行。 + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果处理过这个bean的话直接返回 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 之后如果Bean匹配不成功,会将Bean的cacheKey放入advisedBeans中 + // value为false,所以这里可以用cacheKey判断此bean是否之前已经代理不成功了 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 这里会将Advise、Pointcut、Advisor类型的类过滤,直接不进行代理,return + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + // 这里即为不成功的情况,将false放入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 这里是主要验证的地方,传入Bean的class与beanName去判断此Bean有哪些Advisor + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果有相应的advisor被找到,则用advisor与此bean做一个动态代理,将这两个的信息 + // 放入代理类中进行代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理的地方 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + // 返回代理对象 + return proxy; + } + // 如果此Bean没有一个Advisor匹配,将返回null也就是DO_NOT_PROXY + // 也就是会走到这一步,将其cacheKey,false存入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + // 不代理直接返回原bean + return bean; +} +``` + +wrapIfNecessary函数功能实现起来很复杂,但是逻辑上理解起来还是相对简单的,在wrapIfNecessary函数中主要的工作如下: + +(1)找出指定bean对应的增强器。 + +(2)根据找出的增强器创建代理。 + +听起来似乎简单的逻辑,Spring中又做了哪些复杂的工作呢?对于创建代理的部分,通过之前的分析相信大家已经很熟悉了,但是对于增强器的获取,Spring又是怎么做的呢? + +## 获取对应class/method的增强器 + +获取指定bean对应的增强器,其中包含两个关键字:增强器与对应。也就是说在 getAdvicesAndAdvisorsForBean函数中,不但要找出增强器,而且还需要判断增强器是否满足要求。 + +```java +@Override +@Nullable +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} + +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + + + +### 寻找候选增强器 + +```java +protected List findCandidateAdvisors() { + Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); + return this.advisorRetrievalHelper.findAdvisorBeans(); +} + +public List findAdvisorBeans() { + // Determine list of advisor bean names, if not cached already. + String[] advisorNames = this.cachedAdvisorBeanNames; + if (advisorNames == null) { + // 获取BeanFactory中所有对应Advisor.class的类名 + // 这里和AspectJ的方式有点不同,AspectJ是获取所有的Object.class,然后通过反射过滤有注解AspectJ的类 + advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Advisor.class, true, false); + this.cachedAdvisorBeanNames = advisorNames; + } + if (advisorNames.length == 0) { + return new ArrayList<>(); + } + + List advisors = new ArrayList<>(); + for (String name : advisorNames) { + if (isEligibleBean(name)) { + if (this.beanFactory.isCurrentlyInCreation(name)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping currently created advisor '" + name + "'"); + } + } + else { + try { + //直接获取advisorNames的实例,封装进advisors数组 + advisors.add(this.beanFactory.getBean(name, Advisor.class)); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException) { + BeanCreationException bce = (BeanCreationException) rootCause; + String bceBeanName = bce.getBeanName(); + if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping advisor '" + name + + "' with dependency on currently created bean: " + ex.getMessage()); + } + continue; + } + } + throw ex; + } + } + } + } + return advisors; +} +``` + +首先是通过BeanFactoryUtils类提供的工具方法获取所有对应Advisor.class的类,获取办法无非是使用ListableBeanFactory中提供的方法: + +```java +String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit); +``` + +在我们讲解自定义标签时曾经注册了一个类型为 BeanFactoryTransactionAttributeSourceAdvisor 的 bean,而在此 bean 中我们又注入了另外两个Bean,那么此时这个 Bean 就会被开始使用了。因为 BeanFactoryTransactionAttributeSourceAdvisor同样也实现了 Advisor接口,那么在获取所有增强器时自然也会将此bean提取出来, 并随着其他增强器一起在后续的步骤中被织入代理。 + +### 候选增强器中寻找到匹配项 + +当找出对应的增强器后,接下来的任务就是看这些增强器是否与对应的class匹配了,当然不只是class,class内部的方法如果匹配也可以通过验证。 + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + // 引介增强已经处理 + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} + +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} +``` + +BeanFactoryTransactionAttributeSourceAdvisor 间接实现了PointcutAdvisor。 因此,在canApply函数中的第二个if判断时就会通过判断,会将BeanFactoryTransactionAttributeSourceAdvisor中的getPointcut()方法返回值作为参数继续调用canApply方法,而 getPoint()方法返回的是TransactionAttributeSourcePointcut类型的实例。对于 transactionAttributeSource这个属性大家还有印象吗?这是在解析自定义标签时注入进去的。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +那么,使用TransactionAttributeSourcePointcut类型的实例作为函数参数继续跟踪canApply。 + +```java +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + // 此时的pc表示TransactionAttributeSourcePointcut + // pc.getMethodMatcher()返回的正是自身(this) + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + //获取对应类的所有接口 + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + //对类进行遍历 + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //对类和方法进行增强器匹配 + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + +通过上面函数大致可以理清大体脉络,首先获取对应类的所有接口并连同类本身一起遍历,遍历过程中又对类中的方法再次遍历,一旦匹配成功便认为这个类适用于当前增强器。 + +到这里我们不禁会有疑问,对于事物的配置不仅仅局限于在函数上配置,我们都知道,在类或接口上的配置可以延续到类中的每个函数,那么,如果针对每个函数迸行检测,在类本身上配罝的事务属性岂不是检测不到了吗?带着这个疑问,我们继续探求matcher方法。 + +做匹配的时候 methodMatcher.matches(method, targetClass)会使用 TransactionAttributeSourcePointcut 类的 matches 方法。 + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (TransactionalProxy.class.isAssignableFrom(targetClass)) { + return false; + } + // 自定义标签解析时注入 + TransactionAttributeSource tas = getTransactionAttributeSource(); + return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); +} +``` + +此时的 tas 表示 AnnotationTransactionAttributeSource 类型,这里会判断**tas.getTransactionAttribute(method, targetClass) != null,**而 AnnotationTransactionAttributeSource 类型的 getTransactionAttribute 方法如下: + +```java +@Override +@Nullable +public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { + if (method.getDeclaringClass() == Object.class) { + return null; + } + + Object cacheKey = getCacheKey(method, targetClass); + Object cached = this.attributeCache.get(cacheKey); + //先从缓存中获取TransactionAttribute + if (cached != null) { + if (cached == NULL_TRANSACTION_ATTRIBUTE) { + return null; + } + else { + return (TransactionAttribute) cached; + } + } + else { + // 如果缓存中没有,工作又委托给了computeTransactionAttribute函数 + TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); + // Put it in the cache. + if (txAttr == null) { + // 设置为空 + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } + else { + String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + if (txAttr instanceof DefaultTransactionAttribute) { + ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); + } + if (logger.isDebugEnabled()) { + logger.debug("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); + } + //加入缓存中 + this.attributeCache.put(cacheKey, txAttr); + } + return txAttr; + } +} +``` + +尝试从缓存加载,如果对应信息没有被缓存的话,工作又委托给了computeTransactionAttribute函数,在computeTransactionAttribute函数中我们终于看到了事务标签的提取过程。 + +### 提取事务标签 + +```java +@Nullable +protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + // method代表接口中的方法,specificMethod代表实现类中的方法 + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + // First try is the method in the target class. + // 查看方法中是否存在事务声明 + TransactionAttribute txAttr = findTransactionAttribute(specificMethod); + if (txAttr != null) { + return txAttr; + } + + // Second try is the transaction attribute on the target class. + // 查看方法所在类中是否存在事务声明 + txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + + // 如果存在接口,则到接口中去寻找 + if (specificMethod != method) { + // Fallback is to look at the original method. + // 查找接口方法 + txAttr = findTransactionAttribute(method); + if (txAttr != null) { + return txAttr; + } + // Last fallback is the class of the original method. + // 到接口中的类中去寻找 + txAttr = findTransactionAttribute(method.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + } + + return null; +} +``` + +对于事务属性的获取规则相信大家都已经很清楚,如果方法中存在事务属性,则使用方法上的属性,否则使用方法所在的类上的属性,如果方法所在类的属性上还是没有搜寻到对应的事务属性,那么在搜寻接口中的方法,再没有的话,最后尝试搜寻接口的类上面的声明。对于函数computeTransactionAttribute中的逻辑与我们所认识的规则并无差別,但是上面函数中并没有真正的去做搜寻事务属性的逻辑,而是搭建了个执行框架,将搜寻事务属性的任务委托给了 findTransactionAttribute 方法去执行。 + +```java +@Override +@Nullable +protected TransactionAttribute findTransactionAttribute(Class clazz) { + return determineTransactionAttribute(clazz); +} + +@Nullable +protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) { + for (TransactionAnnotationParser annotationParser : this.annotationParsers) { + TransactionAttribute attr = annotationParser.parseTransactionAnnotation(ae); + if (attr != null) { + return attr; + } + } + return null; +} +``` + +this.annotationParsers 是在当前类 AnnotationTransactionAttributeSource 初始化的时候初始化的,其中的值被加入了 SpringTransactionAnnotationParser,也就是当进行属性获取的时候其实是使用 SpringTransactionAnnotationParser 类的 parseTransactionAnnotation 方法进行解析的。 + +```java +@Override +@Nullable +public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { + AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( + ae, Transactional.class, false, false); + if (attributes != null) { + return parseTransactionAnnotation(attributes); + } + else { + return null; + } +} +``` + +至此,我们终于看到了想看到的获取注解标记的代码。首先会判断当前的类是否含有 Transactional注解,这是事务属性的基础,当然如果有的话会继续调用parseTransactionAnnotation 方法解析详细的属性。 + +```java +protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + Propagation propagation = attributes.getEnum("propagation"); + // 解析propagation + rbta.setPropagationBehavior(propagation.value()); + Isolation isolation = attributes.getEnum("isolation"); + // 解析isolation + rbta.setIsolationLevel(isolation.value()); + // 解析timeout + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + // 解析readOnly + rbta.setReadOnly(attributes.getBoolean("readOnly")); + // 解析value + rbta.setQualifier(attributes.getString("value")); + ArrayList rollBackRules = new ArrayList<>(); + // 解析rollbackFor + Class[] rbf = attributes.getClassArray("rollbackFor"); + for (Class rbRule : rbf) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析rollbackForClassName + String[] rbfc = attributes.getStringArray("rollbackForClassName"); + for (String rbRule : rbfc) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackFor + Class[] nrbf = attributes.getClassArray("noRollbackFor"); + for (Class rbRule : nrbf) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackForClassName + String[] nrbfc = attributes.getStringArray("noRollbackForClassName"); + for (String rbRule : nrbfc) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + rbta.getRollbackRules().addAll(rollBackRules); + return rbta; +} +``` + +至此,我们终于完成了事务标签的解析。回顾一下,我们现在的任务是找出某个增强器是否适合于对应的类,而是否匹配的关键则在于是否从指定的类或类中的方法中找到对应的事务属性,现在,我们以AccountServiceImpl为例,已经在它的接口AccountServiceImp中找到了事务属性,所以,它是与事务增强器匹配的,也就是它会被事务功能修饰。 + +至此,事务功能的初始化工作便结束了,当判断某个bean适用于事务增强时,也就是适用于增强器BeanFactoryTransactionAttributeSourceAdvisor。 + +BeanFactoryTransactionAttributeSourceAdvisor 作为 Advisor 的实现类,自然要遵从 Advisor 的处理方式,当代理被调用时会调用这个类的增强方法,也就是此bean的Advice,又因为在解析事务定义标签时我们把Transactionlnterceptor类的bean注人到了 BeanFactoryTransactionAttributeSourceAdvisor中,所以,在调用事务增强器增强的代理类时会首先执行Transactionlnterceptor进行增强,同时,也就是在Transactionlnterceptor类中的invoke方法中完成了整个事务的逻辑。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) + diff --git a/docs/source/spring/18-transaction-advice.md b/docs/source/spring/18-transaction-advice.md new file mode 100644 index 0000000..fd08171 --- /dev/null +++ b/docs/source/spring/18-transaction-advice.md @@ -0,0 +1,840 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物增强器,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章我们讲解了事务的Advisor是如何注册进Spring容器的,也讲解了Spring是如何将有配置事务的类配置上事务的,实际上也就是用了AOP那一套,也讲解了Advisor,pointcut验证流程,至此,事务的初始化工作都已经完成了,在之后的调用过程,如果代理类的方法被调用,都会调用BeanFactoryTransactionAttributeSourceAdvisor这个Advisor的增强方法,也就是我们还未提到的那个Advisor里面的advise,还记得吗,在自定义标签的时候我们将TransactionInterceptor这个Advice作为bean注册进IOC容器,并且将其注入进Advisor中,这个Advice在代理类的invoke方法中会被封装到拦截器链中,最终事务的功能都在advise中体现,所以我们先来关注一下TransactionInterceptor这个类吧。[最全面的Java面试网站](https://topjavaer.cn) + +TransactionInterceptor类继承自MethodInterceptor,所以调用该类是从其invoke方法开始的,首先预览下这个方法: + +```java +@Override +@Nullable +public Object invoke(final MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); +} +``` + +重点来了,进入invokeWithinTransaction方法: + +```java +@Nullable +protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. + TransactionAttributeSource tas = getTransactionAttributeSource(); + // 获取对应事务属性 + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + // 获取beanFactory中的transactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 构造方法唯一标识(类.方法,如:service.UserServiceImpl.save) + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + // 声明式事务处理 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // 创建TransactionInfo + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + Object retVal = null; + try { + // 执行原方法 + // 继续调用方法拦截器链,这里一般将会调用目标类的方法,如:AccountServiceImpl.save方法 + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // 异常回滚 + completeTransactionAfterThrowing(txInfo, ex); + // 手动向上抛出异常,则下面的提交事务不会执行 + // 如果子事务出异常,则外层事务代码需catch住子事务代码,不然外层事务也会回滚 + throw ex; + } + finally { + // 消除信息 + cleanupTransactionInfo(txInfo); + } + // 提交事务 + commitTransactionAfterReturning(txInfo); + return retVal; + } + + else { + final ThrowableHolder throwableHolder = new ThrowableHolder(); + try { + // 编程式事务处理 + Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> { + TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); + try { + return invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + throwableHolder.throwable = ex; + return null; + } + } + finally { + cleanupTransactionInfo(txInfo); + } + }); + + // Check result state: It might indicate a Throwable to rethrow. + if (throwableHolder.throwable != null) { + throw throwableHolder.throwable; + } + return result; + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + catch (TransactionSystemException ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + ex2.initApplicationException(throwableHolder.throwable); + } + throw ex2; + } + catch (Throwable ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + } + throw ex2; + } + } +} +``` + +## 创建事务Info对象 + +我们先分析事务创建的过程。 + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + // 如果没有名称指定则使用方法唯一标识,并使用DelegatingTransactionAttribute封装txAttr + if (txAttr != null && txAttr.getName() == null) { + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { + // 获取TransactionStatus + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } + // 根据指定的属性与status准备一个TransactionInfo + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +对于createTransactionlfNecessary函数主要做了这样几件事情。 + +(1)使用 DelegatingTransactionAttribute 封装传入的 TransactionAttribute 实例。 + +对于传入的TransactionAttribute类型的参数txAttr,当前的实际类型是RuleBasedTransactionAttribute,是由获取事务属性时生成,主要用于数据承载,而这里之所以使用DelegatingTransactionAttribute进行封装,当然是提供了更多的功能。 + +(2)获取事务。 + +事务处理当然是以事务为核心,那么获取事务就是最重要的事情。 + +(3)构建事务信息。 + +根据之前几个步骤获取的信息构建Transactionlnfo并返回。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +### 获取事务 + +其中核心是在getTransaction方法中: + +```java +@Override +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // 获取一个transaction + Object transaction = doGetTransaction(); + + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + definition = new DefaultTransactionDefinition(); + } + // 如果在这之前已经存在事务了,就进入存在事务的方法中 + if (isExistingTransaction(transaction)) { + return handleExistingTransaction(definition, transaction, debugEnabled); + } + + // 事务超时设置验证 + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // 走到这里说明此时没有存在事务,如果传播特性是MANDATORY时抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); + } + // 如果此时不存在事务,当传播特性是REQUIRED或NEW或NESTED都会进入if语句块 + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } + } + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { + logger.warn("Custom isolation level specified but no actual transaction initiated; " + + "isolation level will effectively be ignored: " + definition); + } + // 其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} +``` + +先来看看transaction是如何被创建出来的: + +```java +@Override +protected Object doGetTransaction() { + // 这里DataSourceTransactionObject是事务管理器的一个内部类 + // DataSourceTransactionObject就是一个transaction,这里new了一个出来 + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + // 解绑与绑定的作用在此时体现,如果当前线程有绑定的话,将会取出holder + // 第一次conHolder肯定是null + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); + // 此时的holder被标记成一个旧holder + txObject.setConnectionHolder(conHolder, false); + return txObject; +} +``` + +创建transaction过程很简单,接着就会判断当前是否存在事务: + +```java +@Override +protected boolean isExistingTransaction(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); +} + +public boolean hasConnectionHolder() { + return (this.connectionHolder != null); +} +``` + +这里判断是否存在事务的依据主要是获取holder中的transactionActive变量是否为true,如果是第一次进入事务,holder直接为null判断不存在了,如果是第二次进入事务transactionActive变量是为true的(后面会提到是在哪里把它变成true的),由此来判断当前是否已经存在事务了。 + +至此,源码分成了2条处理线: + +**1.当前已存在事务:isExistingTransaction()判断是否存在事务,存在事务handleExistingTransaction()根据不同传播机制不同处理** + +**2.当前不存在事务: 不同传播机制不同处理** + + + +## 当前不存在事务 + +如果不存在事务,传播特性又是REQUIRED或NEW或NESTED,将会先挂起null,这个挂起方法我们后面再讲,然后创建一个DefaultTransactionStatus ,并将其标记为新事务,然后执行doBegin(transaction, definition);这个方法也是一个关键方法 + +### 神秘又关键的status对象 + +**TransactionStatus接口** + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +再来看看实现类DefaultTransactionStatus + +**DefaultTransactionStatus** + +```java +public class DefaultTransactionStatus extends AbstractTransactionStatus { + + //事务对象 + @Nullable + private final Object transaction; + + //事务对象 + private final boolean newTransaction; + + private final boolean newSynchronization; + + private final boolean readOnly; + + private final boolean debug; + + //事务对象 + @Nullable + private final Object suspendedResources; + + public DefaultTransactionStatus( + @Nullable Object transaction, boolean newTransaction, boolean newSynchronization, + boolean readOnly, boolean debug, @Nullable Object suspendedResources) { + + this.transaction = transaction; + this.newTransaction = newTransaction; + this.newSynchronization = newSynchronization; + this.readOnly = readOnly; + this.debug = debug; + this.suspendedResources = suspendedResources; + } + + //略... +} +``` + +我们看看这行代码 DefaultTransactionStatus status = **newTransactionStatus**( definition, **transaction, true**, newSynchronization, debugEnabled, **suspendedResources**); + +```java +// 这里是构造一个status对象的方法 +protected DefaultTransactionStatus newTransactionStatus( + TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + + boolean actualNewSynchronization = newSynchronization && + !TransactionSynchronizationManager.isSynchronizationActive(); + return new DefaultTransactionStatus( + transaction, newTransaction, actualNewSynchronization, + definition.isReadOnly(), debug, suspendedResources); +} +``` + +实际上就是封装了事务属性definition,新创建的**transaction,**并且将事务状态属性设置为新事务,最后一个参数为被挂起的事务。 + +简单了解一下关键参数即可: + +第二个参数transaction:事务对象,在一开头就有创建,其就是事务管理器的一个内部类。 + +第三个参数newTransaction:布尔值,一个标识,用于判断是否是新的事务,用于提交或者回滚方法中,是新的才会提交或者回滚。 + +最后一个参数suspendedResources:被挂起的对象资源,挂起操作会返回旧的holder,将其与一些事务属性一起封装成一个对象,就是这个suspendedResources这个对象了,它会放在status中,在最后的清理工作方法中判断status中是否有这个挂起对象,如果有会恢复它 + +接着我们来看看关键代码 **doBegin(transaction, definition);** + +```java + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + // 判断如果transaction没有holder的话,才去从dataSource中获取一个新连接 + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + //通过dataSource获取连接 + Connection newCon = this.dataSource.getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // 所以,只有transaction中的holder为空时,才会设置为新holder + // 将获取的连接封装进ConnectionHolder,然后封装进transaction的connectionHolder属性 + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } +      //设置新的连接为事务同步中 + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); +      //conn设置事务隔离级别,只读 + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel);//DataSourceTransactionObject设置事务隔离级别 + + // 如果是自动提交切换到手动提交 + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } +      // 如果只读,执行sql设置事务只读 + prepareTransactionalConnection(con, definition); + // 设置connection持有者的事务开启状态 + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + // 设置超时秒数 + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // 将当前获取到的连接绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); + } + }catch (Throwable ex) { + if (txObject.isNewConnectionHolder()) { + DataSourceUtils.releaseConnection(con, this.dataSource); + txObject.setConnectionHolder(null, false); + } + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } + } +``` + +conn设置事务隔离级别 + +```java +@Nullable +public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) + throws SQLException { + + Assert.notNull(con, "No Connection specified"); + + // Set read-only flag. + // 设置数据连接的只读标识 + if (definition != null && definition.isReadOnly()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Setting JDBC Connection [" + con + "] read-only"); + } + con.setReadOnly(true); + } + catch (SQLException | RuntimeException ex) { + Throwable exToCheck = ex; + while (exToCheck != null) { + if (exToCheck.getClass().getSimpleName().contains("Timeout")) { + // Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0 + throw ex; + } + exToCheck = exToCheck.getCause(); + } + // "read-only not supported" SQLException -> ignore, it's just a hint anyway + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + + // Apply specific isolation level, if any. + // 设置数据库连接的隔离级别 + Integer previousIsolationLevel = null; + if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + if (logger.isDebugEnabled()) { + logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + + definition.getIsolationLevel()); + } + int currentIsolation = con.getTransactionIsolation(); + if (currentIsolation != definition.getIsolationLevel()) { + previousIsolationLevel = currentIsolation; + con.setTransactionIsolation(definition.getIsolationLevel()); + } + } + + return previousIsolationLevel; +} +``` + +我们看到都是通过 Connection 去设置 + +#### 线程变量的绑定 + +我们看 doBegin 方法的47行,**将当前获取到的连接绑定到当前线程,**绑定与解绑围绕一个线程变量,此变量在**`TransactionSynchronizationManager`**类中: + +```java +private static final ThreadLocal> resources = new NamedThreadLocal<>("Transactional resources"); +``` + + 这是一个 **static final** 修饰的 线程变量,存储的是一个Map,我们来看看47行的静态方法,**bindResource** + +```java +public static void bindResource(Object key, Object value) throws IllegalStateException { + // 从上面可知,线程变量是一个Map,而这个Key就是dataSource + // 这个value就是holder + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Assert.notNull(value, "Value must not be null"); + // 获取这个线程变量Map + Map map = resources.get(); + // set ThreadLocal Map if none found + if (map == null) { + map = new HashMap<>(); + resources.set(map); + } + // 将新的holder作为value,dataSource作为key放入当前线程Map中 + Object oldValue = map.put(actualKey, value); + // Transparently suppress a ResourceHolder that was marked as void... + if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { + oldValue = null; + } + if (oldValue != null) { + throw new IllegalStateException("Already value [" + oldValue + "] for key [" + + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); + } Thread.currentThread().getName() + "]"); + } + // 略... +} +``` + +### 扩充知识点 + +这里再扩充一点,mybatis中获取的数据库连接,就是根据 **dataSource** 从ThreadLocal中获取的 + +以查询举例,会调用Executor#doQuery方法: + +![](http://img.topjavaer.cn/img/202310031612318.png) + +最终会调用DataSourceUtils#doGetConnection获取,真正的数据库连接,其中TransactionSynchronizationManager中保存的就是方法调用前,spring增强方法中绑定到线程的connection,从而保证整个事务过程中connection的一致性 + +![](http://img.topjavaer.cn/img/202310031612223.png) + +![](http://img.topjavaer.cn/img/202310031612891.png) + + 我们看看TransactionSynchronizationManager.getResource(Object key)这个方法 + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + +**就是从线程变量的Map中根据** **DataSource获取** **ConnectionHolder** + +## 已经存在的事务 + +前面已经提到,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入handleExistingTransaction方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + +对于已经存在事务的处理过程中,我们看到了很多熟悉的操作,但是,也有些不同的地方,函数中对已经存在的事务处理考虑两种情况。 + +(1)PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则在这个方法运行期间被挂起。而Spring中对于此种传播方式的处理与新事务建立最大的不同点在于使用suspend方法将原事务挂起。 将信息挂起的目的当然是为了在当前事务执行完毕后在将原事务还原。 + +(2)PROPAGATION_NESTED表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像PROPAGATION_REQUIRES_NEW。对于嵌入式事务的处理,Spring中主要考虑了两种方式的处理。 + +- Spring中允许嵌入事务的时候,则首选设置保存点的方式作为异常处理的回滚。 +- 对于其他方式,比如JTA无法使用保存点的方式,那么处理方式与PROPAGATION_ REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复 + + + +### 小结 + +到这里我们可以知道,在当前存在事务的情况下,根据传播特性去决定是否为新事务,是否挂起当前事务。 + +**NOT_SUPPORTED :会挂起事务,不运行doBegin方法传空`transaction`,标记为旧事务。封装`status`对象:** + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources) +``` + + + +**REQUIRES_NEW :将会挂起事务且运行doBegin方法,标记为新事务。封装`status`对象:** + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +**NESTED :不会挂起事务且不会运行doBegin方法,标记为旧事务,但会创建`savePoint`。封装`status`对象:** + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); +``` + +**其他事务例如REQUIRED :不会挂起事务,封装原有的transaction不会运行doBegin方法,标记旧事务,封装`status`对象:** + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +### 挂起 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复: + +```java +@Nullable +protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + // 这里是真正做挂起的方法,这里返回的是一个holder + suspendedResources = doSuspend(transaction); + } + // 这里将名称、隔离级别等信息从线程变量中取出并设置对应属性为null到线程变量 + String name = TransactionSynchronizationManager.getCurrentTransactionName(); + TransactionSynchronizationManager.setCurrentTransactionName(null); + boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); + Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); + boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); + TransactionSynchronizationManager.setActualTransactionActive(false); + // 将事务各个属性与挂起的holder一并封装进SuspendedResourcesHolder对象中 + return new SuspendedResourcesHolder( + suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); + } + catch (RuntimeException | Error ex) { + // doSuspend failed - original transaction is still active... + doResumeSynchronization(suspendedSynchronizations); + throw ex; + } + } + else if (transaction != null) { + // Transaction active but no synchronization active. + Object suspendedResources = doSuspend(transaction); + return new SuspendedResourcesHolder(suspendedResources); + } + else { + // Neither transaction nor synchronization active. + return null; + } +} +``` + + + +```java +@Override +protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + // 将transaction中的holder属性设置为空 + txObject.setConnectionHolder(null); + // ConnnectionHolder从线程变量中解绑! + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); +} +``` + +我们来看看 **unbindResource** + +```java +private static Object doUnbindResource(Object actualKey) { + // 取得当前线程的线程变量Map + Map map = resources.get(); + if (map == null) { + return null; + } + // 将key为dataSourece的value移除出Map,然后将旧的Holder返回 + Object value = map.remove(actualKey); + // Remove entire ThreadLocal if empty... + // 如果此时map为空,直接清除线程变量 + if (map.isEmpty()) { + resources.remove(); + } + // Transparently suppress a ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + value = null; + } + if (value != null && logger.isTraceEnabled()) { + logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" + + Thread.currentThread().getName() + "]"); + } + // 将旧Holder返回 + return value; +} +``` + +可以回头看一下解绑操作的介绍。这里挂起主要干了三件事: + +1. **将transaction中的holder属性设置为空** +2. **解绑(会返回线程中的那个旧的holder出来,从而封装到SuspendedResourcesHolder对象中)** +3. **将SuspendedResourcesHolder放入status中,方便后期子事务完成后,恢复外层事务** + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/19-transaction-rollback-commit.md b/docs/source/spring/19-transaction-rollback-commit.md new file mode 100644 index 0000000..fbb7f2d --- /dev/null +++ b/docs/source/spring/19-transaction-rollback-commit.md @@ -0,0 +1,975 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,事物回滚,事物提交,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章讲解了获取事务,并且通过获取的connection设置只读、隔离级别等,这篇文章讲解剩下的事务的回滚和提交。[最全面的Java面试网站](https://topjavaer.cn) + +## 回滚处理 + +之前已经完成了目标方法运行前的事务准备工作,而这些准备工作最大的目的无非是对于程序没有按照我们期待的那样进行,也就是出现特定的错误,那么,当出现错误的时候,Spring是怎么对数据进行恢复的呢? + +```java + protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { + // 当抛出异常时首先判断当前是否存在事务,这是基础依据 + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + + "] after exception: " + ex); + } + // 这里判断是否回滚默认的依据是抛出的异常是否是RuntimeException或者是Error的类型 + if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { + try { + txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + } + else { + // We don't roll back on this exception. + // Will still roll back if TransactionStatus.isRollbackOnly() is true. + // 如果不满足回滚条件即使抛出异常也同样会提交 + try { + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by commit exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by commit exception", ex); + throw ex2; + } + } + } + } +``` + +在对目标方法的执行过程中,一旦出现Throwable就会被引导至此方法处理,但是并不代表所有的Throwable都会被回滚处理,比如我们最常用的Exception,默认是不会被处理的。 默认情况下,即使出现异常,数据也会被正常提交,而这个关键的地方就是在txlnfo.transactionAttribute.rollbackOn(ex)这个函数。 + +### 回滚条件 + +```java +@Override +public boolean rollbackOn(Throwable ex) { + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + +默认情况下Spring中的亊务异常处理机制只对RuntimeException和Error两种情况感兴趣,我们可以利用注解方式来改变,例如: + +```java +@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) +``` + +### 回滚处理 + +当然,一旦符合回滚条件,那么Spring就会将程序引导至回滚处理函数中。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd +> +> + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + // 如果status有savePoint,说明此事务是NESTD,且为子事务,只回滚到savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + //回滚到保存点 + status.rollbackToHeldSavepoint(); + } + // 如果此时的status显示是新的事务才进行回滚 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + //如果此时是子事务,我们想想哪些类型的事务会进入到这里? + //回顾上一篇文章中已存在事务的处理,NOT_SUPPORTED创建的Status是prepareTransactionStatus(definition, null, false...),说明是旧事物,并且事务为null,不会进入 + //REQUIRES_NEW会创建一个新的子事务,Status是newTransactionStatus(definition, transaction, true...)说明是新事务,将会进入到这个分支 + //PROPAGATION_NESTED创建的Status是prepareTransactionStatus(definition, transaction, false...)是旧事物,使用的是外层的事务,不会进入 + //PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY存在事务加入事务即可,标记为旧事务,prepareTransactionStatus(definition, transaction, false..) + //说明当子事务,只有REQUIRES_NEW会进入到这里进行回滚 + doRollback(status); + } + else { + // Participating in larger transaction + // 如果status中有事务,进入下面 + // 根据上面分析,PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY创建的Status是prepareTransactionStatus(definition, transaction, false..) + // 如果此事务时子事务,表示存在事务,并且事务为旧事物,将进入到这里 + if (status.hasTransaction()) { + if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { + if (status.isDebug()) { + logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); + } + // 对status中的transaction作一个回滚了的标记,并不会立即回滚 + doSetRollbackOnly(status); + } + else { + if (status.isDebug()) { + logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); + } + } + } + else { + logger.debug("Should roll back transaction but cannot - no transaction available"); + } + // Unexpected rollback only matters here if we're asked to fail early + if (!isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = false; + } + } + } + catch (RuntimeException | Error ex) { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + throw ex; + } + + triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + // 子事务结束了,之前挂起的事务就要恢复了 + cleanupAfterCompletion(status); + } +} +``` + + + +我i们先来看看第13行,回滚到保存点的代码,根据保存点回滚的实现方式其实是根据底层的数据库连接进行的。回滚到保存点之后,也要释放掉当前的保存点 + +```java +public void rollbackToHeldSavepoint() throws TransactionException { + Object savepoint = getSavepoint(); + if (savepoint == null) { + throw new TransactionUsageException( + "Cannot roll back to savepoint - no savepoint associated with current transaction"); + } + getSavepointManager().rollbackToSavepoint(savepoint); + getSavepointManager().releaseSavepoint(savepoint); + setSavepoint(null); +} +``` + +这里使用的是JDBC的方式进行数据库连接,那么getSavepointManager()函数返回的是JdbcTransactionObjectSupport,也就是说上面函数会调用JdbcTransactionObjectSupport 中的 rollbackToSavepoint 方法。 + +```java +@Override +public void rollbackToSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().rollback((Savepoint) savepoint); + conHolder.resetRollbackOnly(); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } +} +``` + +当之前已经保存的事务信息中的事务为新事物,那么直接回滚。常用于单独事务的处理。对于没有保存点的回滚,Spring同样是使用底层数据库连接提供的API来操作的。由于我们使用的是DataSourceTransactionManager,那么doRollback函数会使用此类中的实现: + +```java +@Override +protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not roll back JDBC transaction", ex); + } +} +``` + +当前事务信息中表明是存在事务的,又不属于以上两种情况,只做回滚标识,等到提交的时候再判断是否有回滚标识,下面回滚的时候再介绍,子事务中状态为**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**回滚的时候将会标记为回滚标识,我们来看看是怎么标记的 + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们看到将status中的Transaction中的 ConnectionHolder的属性**rollbackOnly标记为true,**这里我们先不多考虑,等到下面提交的时候再介绍 + +我们简单的做个小结 + +- **status.hasSavepoint()如果status中有savePoint,只回滚到savePoint!** +- **status.isNewTransaction()如果status是一个新事务,才会真正去回滚!** +- **status.hasTransaction()如果status有事务,将会对staus中的事务标记!** + + + +## 事务提交 + +在事务的执行并没有出现任何的异常,也就意味着事务可以走正常事务提交的流程了。这里回到流程中去,看看`commitTransactionAfterReturning(txInfo)`方法做了什么: + +```java +protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); + } + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} +``` + +在真正的数据提交之前,还需要做个判断。不知道大家还有没有印象,在我们分析事务异常处理规则的时候,当某个事务既没有保存点又不是新事物,Spring对它的处理方式只是设置一个回滚标识。这个回滚标识在这里就会派上用场了,如果子事务状态是 + +**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY,**将会在外层事务中运行,回滚的时候,并不执行回滚,只是标记一下回滚状态,当外层事务提交的时候,会先判断ConnectionHolder中的回滚状态,如果已经标记为回滚,则不会提交,而是外层事务进行回滚 + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + if (status.isCompleted()) { + throw new IllegalTransactionStateException( + "Transaction is already completed - do not call commit or rollback more than once per transaction"); + } + + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 如果在事务链中已经被标记回滚,那么不会尝试提交事务,直接回滚 + if (defStatus.isLocalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Transactional code has requested rollback"); + } + processRollback(defStatus, false); + return; + } + + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 这里会进行回滚,并且抛出一个异常 + processRollback(defStatus, true); + return; + } + + // 如果没有被标记回滚之类的,这里才真正判断是否提交 + processCommit(defStatus); + } +``` + +而当事务执行一切都正常的时候,便可以真正地进入提交流程了。 + +```java +private void processCommit(DefaultTransactionStatus status) throws TransactionException { + try { + boolean beforeCompletionInvoked = false; + + try { + boolean unexpectedRollback = false; + prepareForCommit(status); + triggerBeforeCommit(status); + triggerBeforeCompletion(status); + beforeCompletionInvoked = true; + + // 判断是否有savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Releasing transaction savepoint"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 不提交,仅仅是释放savePoint + status.releaseHeldSavepoint(); + } + // 判断是否是新事务 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction commit"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 这里才真正去提交! + doCommit(status); + } + else if (isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = status.isGlobalRollbackOnly(); + } + + // Throw UnexpectedRollbackException if we have a global rollback-only + // marker but still didn't get a corresponding exception from commit. + if (unexpectedRollback) { + throw new UnexpectedRollbackException( + "Transaction silently rolled back because it has been marked as rollback-only"); + } + } + catch (UnexpectedRollbackException ex) { + // 略... + } + + // Trigger afterCommit callbacks, with an exception thrown there + // propagated to callers but the transaction still considered as committed. + try { + triggerAfterCommit(status); + } + finally { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); + } + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + cleanupAfterCompletion(status); + } +} +``` + + + +- status.hasSavepoint()如果status有savePoint,说明此时的事务是嵌套事务NESTED,这个事务外面还有事务,这里不提交,只是释放保存点。这里也可以看出来NESTED的传播行为了。 +- status.isNewTransaction()如果是新的事务,才会提交!!,这里如果是子事务,只有**PROPAGATION_NESTED**状态才会走到这里提交,也说明了此状态子事务提交和外层事务是隔离的 +- **如果是子事务,PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**这几种状态是旧事物,提交的时候将什么都不做,因为他们是运行在外层事务当中,如果子事务没有回滚,将由外层事务一次性提交 + +如果程序流通过了事务的层层把关,最后顺利地进入了提交流程,那么同样,Spring会将事务提交的操作引导至底层数据库连接的API,进行事务提交。 + +```java +@Override +protected void doCommit(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Committing JDBC transaction on Connection [" + con + "]"); + } + try { + con.commit(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not commit JDBC transaction", ex); + } +} +``` + +**从回滚和提交的逻辑看,只有status是新事务,才会进行提交或回滚,需要读者记好这个状态–>是否是新事务。** + + + +## 清理工作 + +而无论是在异常还是没有异常的流程中,最后的finally块中都会执行一个方法**cleanupAfterCompletion(status)** + +```java +private void cleanupAfterCompletion(DefaultTransactionStatus status) { + // 设置完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.clear(); + } + if (status.isNewTransaction()) { + doCleanupAfterCompletion(status.getTransaction()); + } + if (status.getSuspendedResources() != null) { + if (status.isDebug()) { + logger.debug("Resuming suspended transaction after completion of inner transaction"); + } + Object transaction = (status.hasTransaction() ? status.getTransaction() : null); + // 结束之前事务的挂起状态 + resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} +``` + +如果是新事务需要做些清除资源的工作? + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + // 将数据库连接从当前线程中解除绑定,解绑过程我们在挂起的过程中已经分析过 + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } + + // Reset connection. + // 释放连接,当前事务完成,则需要将连接释放,如果有线程池,则重置数据库连接,放回线程池 + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + // 恢复数据库连接的自动提交属性 + con.setAutoCommit(true); + } + // 重置数据库连接 + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } + // 如果当前事务是独立的新创建的事务则在事务完成时释放数据库连接 + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); +} +``` + +如果在事务执行前有事务挂起,那么当前事务执行结束后需要将挂起事务恢复。 + +如果有挂起的事务的话,`status.getSuspendedResources() != null`,也就是说status中会有suspendedResources这个属性,取得status中的transaction后进入resume方法: + +```java +protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) +throws TransactionException { + + if (resourcesHolder != null) { + Object suspendedResources = resourcesHolder.suspendedResources; + // 如果有被挂起的事务才进入 + if (suspendedResources != null) { + // 真正去resume恢复的地方 + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + // 将上面提到的TransactionSynchronizationManager专门存放线程变量的类中 + // 的属性设置成被挂起事务的属性 + TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); + TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); + doResumeSynchronization(suspendedSynchronizations); + } + } +} +``` + +我们来看看doResume + +```java +@Override +protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); +} +``` + +这里恢复只是把`suspendedResources`重新绑定到线程中。 + + + +## 几种事务传播属性详解 + +我们先来看看七种传播属性 + +Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + + + +### 当前不存在事务的情况下 + +每次创建一个TransactionInfo的时候都会去new一个transaction,然后去线程变量Map中拿holder,当此时线程变量的Map中holder为空时,就视为当前情况下不存在事务,所以此时transaction中holder = null。 + +**1、PROPAGATION_MANDATORY** + +**使用当前事物,如果当前没有事物,则抛出异常** + +在上一篇博文中我们在getTransaction方法中可以看到如下代码,当前线程不存在事务时,如果传播属性为PROPAGATION_MANDATORY,直接抛出异常,因为PROPAGATION_MANDATORY必须要在事务中运行 + +```java +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); +} +``` + +**2、REQUIRED、REQUIRES_NEW、NESTED** + +我们继续看上一篇博文中的getTransaction方法 + +```java +else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } +} +``` + + + +此时会讲null挂起,此时的status变量为: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +此时的transaction中holder依然为null,标记为新事务,接着就会执行doBegin方法了: + +```java +@Override +protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + // 此时会进入这个if语句块,因为此时的holder依然为null + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + // 从dataSource从取得一个新的connection + Connection newCon = obtainDataSource().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // new一个新的holder放入新的连接,设置为新的holder + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + // 略... + + prepareTransactionalConnection(con, definition); + // 将holder设置avtive = true + txObject.getConnectionHolder().setTransactionActive(true); + + // Bind the connection holder to the thread. + // 绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); + } + } +} +``` + + + +所以,一切都是新的,新的事务,新的holder,新的连接,在当前不存在事务的时候一切都是新创建的。 + +这三种传播特性在当前不存在事务的情况下是没有区别的,此事务都为新创建的连接,在回滚和提交的时候都可以正常回滚或是提交,就像正常的事务操作那样。 + +**3、PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER** + +我们看看当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时的代码,getTransaction方法 + +```java +else { + //其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); +} +``` + + + +我们看到Status中第二个参数传的是**null**,表示一个空事务,意思是当前线程中并没有Connection,那如何进行数据库的操作呢?上一篇文章中我们有一个扩充的知识点,Mybaits中使用的数据库连接是从通过**`TransactionSynchronizationManager.getResource(Object key)获取spring增强方法中绑定到线程的connection,`**`如下代码,那当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时,并没有创建新的Connection,当前线程中也没有绑定Connection,那Mybatis是如何获取Connecion的呢?这里留一个疑问,我们后期看Mybatis的源码的时候来解决这个疑问` + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + + + +此时我们知道Status中的Transaction为null,在目标方法执行完毕后,进行回滚或提交的时候,会判断当前事务是否是新事务,代码如下 + +```java +@Override +public boolean isNewTransaction() { + return (hasTransaction() && this.newTransaction); +} +``` + +**此时transacion为null,回滚或提交的时候将什么也不做** + + + +### 当前存在事务情况下 + +上一篇文章中已经讲过,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入**handleExistingTransaction**方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + + + +**1、NERVER** + +**不支持当前事务;如果当前事务存在,抛出异常** + +```java +// 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); +} +``` + + + +我们看到如果当前线程中存在事务,传播属性为**PROPAGATION_NEVER,会直接抛出异常** + +**2、NOT_SUPPORTED** + +**以非事物方式执行,如果当前存在事物,则挂起当前事物** + +我们看上面代码第9行,如果传播属性为PROPAGATION_NOT_SUPPORTED,会先将原来的transaction挂起,此时status为: + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); +``` + +transaction为空,旧事务,挂起的对象存入status中。 + +**此时与外层事务隔离了,在这种传播特性下,是不进行事务的,当提交时,因为是旧事务,所以不会commit,失败时也不会回滚rollback** + +**3、REQUIRES_NEW** + +此时会先挂起,然后去执行doBegin方法,此时会创建一个新连接,新holder,新holder有什么用呢? + +如果是新holder,会在doBegin中做绑定操作,将新holder绑定到当前线程,其次,在提交或是回滚时finally语句块始终会执行清理方法时判断新holder会进行解绑操作。 + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } +} +``` + + + +符合传播特性,所以这里**REQUIRES_NEW**这个传播特性是与原事务相隔的,用的连接都是新new出来的。 + +此时返回的status是这样的: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +其中transaction中holder为新holder,连接都是新的。标记为新事务,在开头的回顾中提到,如果是新事务,提交时才能成功提交。并且在最后一个参数放入挂起的对象,之后将会恢复它。 + +**REQUIRES_NEW小结** + +会于前一个事务隔离,自己新开一个事务,与上一个事务无关,如果报错,上一个事务catch住异常,上一个事务是不会回滚的,这里要注意**(在invokeWithinTransaction方法中的catch代码块中,处理完异常后,还通过** **throw ex;将异常抛给了上层,所以上层要catch住子事务的异常,子事务回滚后,上层事务也会回滚),**而只要自己提交了之后,就算上一个事务后面的逻辑报错,自己是不会回滚的(因为被标记为新事务,所以在提交阶段已经提交了)。 + +**4、NESTED** + +不挂起事务,并且返回的status对象如下: + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + +status.createAndHoldSavepoint(); +``` + +不同于其他的就是此传播特性会创建savePoint,有什么用呢?前面说到,如果是旧事务的话回滚是不会执行的,但先看看它的status,虽然标记为旧事务,但它还有savePoint,如果有savePoint,会回滚到保存点去,提交的时候,会释放保存点,但是不提交!切记,这里就是NESTED与REQUIRES_NEW不同点之一了,NESTED只会在外层事务成功时才进行提交,实际提交点只是去释放保存点,外层事务失败,NESTED也将回滚,但如果是REQUIRES_NEW的话,不管外层事务是否成功,它都会提交不回滚。这就是savePoint的作用。 + +由于不挂起事务,可以看出来,此时transaction中的holder用的还是旧的,连接也是上一个事务的连接,可以看出来,这个传播特性会将原事务和自己当成一个事务来做。 + +**NESTED 小结** + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的holder,同样也是老的connection,没有挂起的事务。关键点在这个传播特性在存在事务情况下会创建savePoint,但不存在事务情况下是不会创建savePoint的。在提交时不真正提交,只是释放了保存点而已,在回滚时会回滚到保存点位置,如果上层事务catch住异常的话,是不会影响上层事务的提交的,外层事务提交时,会统一提交,外层事务回滚的话,会全部回滚 + +**5、REQUIRED 、PROPAGATION_REQUIRED或PROPAGATION_MANDATORY** + +**存在事务加入事务即可,标记为旧事务,空挂起** + +status为: + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +使用旧事务,标记为旧事务,挂起对象为空。 + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的connection,但此时被标记成旧事务,所以,在提交阶段不会真正提交的,在外层事务提交阶段,才会把事务提交。 + +如果此时这里出现了异常,内层事务执行回滚时,旧事务是不会去回滚的,而是进行回滚标记,我们看看文章开头处回滚的处理函数**processRollback中第39行,当前事务信息中表明是存在事务的,但是既没有保存点,又不是新事务,回滚的时候只做回滚标识,等到提交的时候再判断是否有回滚标识,commit的时候,如果有回滚标识,就进行回滚** + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们知道,在内层事务中transaction对象中的holder对象其实就是外层事务transaction里的holder,holder是一个对象,指向同一个地址,在这里设置holder标记,外层事务transaction中的holder也是会被设置到的,在外层事务提交的时候有这样一段代码: + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + // 略... + + // !shouldCommitOnGlobalRollbackOnly()只有JTA与JPA事务管理器才会返回false + // defStatus.isGlobalRollbackOnly()这里判断status是否被标记了 + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 如果内层事务抛异常,外层事务是会走到这个方法中的,而不是去提交 + processRollback(defStatus, true); + return; + } + + // 略... +} +``` + +在外层事务提交的时候是会去验证transaction中的holder里是否被标记rollback了,内层事务回滚,将会标记holder,而holder是线程变量,在此传播特性中holder是同一个对象,外层事务将无法正常提交而进入processRollback方法进行回滚,并抛出异常: + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + // 此时这个值为true + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + status.rollbackToHeldSavepoint(); + } + // 新事务,将进行回滚操作 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + // 回滚! + doRollback(status); + } + + // 略... + + // Raise UnexpectedRollbackException if we had a global rollback-only marker + // 抛出一个异常 + if (unexpectedRollback) { + // 这个就是上文说到的抛出的异常类型 + throw new UnexpectedRollbackException( + "Transaction rolled back because it has been marked as rollback-only"); + } + } + finally { + cleanupAfterCompletion(status); + } +} +``` + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git a/docs/source/spring/2-ioc-overview.md b/docs/source/spring/2-ioc-overview.md new file mode 100644 index 0000000..08811f1 --- /dev/null +++ b/docs/source/spring/2-ioc-overview.md @@ -0,0 +1,994 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +上一篇我们了解了Spring的整体架构,这篇我们开始真正的阅读Spring源码。在分析spring的源码之前,我们先来简单回顾下spring核心功能的简单使用。 + +## 容器的基本用法 + +bean是spring最核心的东西。我们简单看下bean的定义,代码如下: + +```java +public class MyTestBean { + private String name = "dabin"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +代码很简单,bean没有特别之处,spring的的目的就是让我们的bean成为一个纯粹的的POJO,这就是spring追求的,接下来就是在配置文件中定义这个bean,配置文件如下: + +```xml + + + + + + +``` + +在上面的配置中我们可以看到bean的声明方式,在spring中的bean定义有N种属性,但是我们只要像上面这样简单的声明就可以使用了。 +具体测试代码如下: + +``` +import com.dabin.spring.MyTestBean; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + System.out.println(myTestBean.getName()); + } +} +``` + +运行上述测试代码就可以看到输出结果如下: + +``` +dabin +``` + +其实直接使用BeanFactory作为容器对于Spring的使用并不多见,因为企业级应用项目中大多会使用的是ApplicationContext(后面我们会讲两者的区别,这里只是测试) + + + +## 功能分析 + +接下来我们分析2中代码完成的功能; + +- 读取配置文件spring-config.xml。 + +- 根据spring-config.xml中的配置找到对应的类的配置,并实例化。 +- 调用实例化后的实例 + +下图是一个最简单spring功能架构,如果想完成我们预想的功能,至少需要3个类: + +![](http://img.topjavaer.cn/img/202309171627312.png) + +**ConfigReader** :用于读取及验证自己直文件 我们妥用配直文件里面的东西,当然首先 要做的就是读取,然后放直在内存中. + +**ReflectionUtil** :用于根据配置文件中的自己直进行反射实例化,比如在上例中 spring-config.xml 出现的``,我们就可以根据 com.dabin.spring.MyTestBean 进行实例化。 + +**App** :用于完成整个逻辑的串联。 + + + +## 工程搭建 + +spring的源码中用于实现上面功能的是spring-bean这个工程,所以我们接下来看这个工程,当然spring-core是必须的。 + +### beans包的层级结构 + +阅读源码最好的方式是跟着示例操作一遍,我们先看看beans工程的源码结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309171650124.png) + +- src/main/java 用于展现Spring的主要逻辑 +- src/main/resources 用于存放系统的配置文件 +- src/test/java 用于对主要逻辑进行单元测试 +- src/test/resources 用于存放测试用的配置文件 + + + +### 核心类介绍 + +接下来我们先了解下spring-bean最核心的两个类:DefaultListableBeanFactory和XmlBeanDefinitionReader + +#### *DefaultListableBeanFactory* + +XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方其实是在XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。以下是ConfigurableListableBeanFactory的层次结构图以下相关类图: + +![](http://img.topjavaer.cn/img/202309171654763.png) + +上面类图中各个类及接口的作用如下: +- AliasRegistry:定义对alias的简单增删改等操作 +- SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口AliasRegistry进行实现 +- SingletonBeanRegistry:定义对单例的注册及获取 +- BeanFactory:定义获取bean及bean的各种属性 +- DefaultSingletonBeanRegistry:默认对接口SingletonBeanRegistry各函数的实现 +- HierarchicalBeanFactory:继承BeanFactory,也就是在BeanFactory定义的功能的基础上增加了对parentFactory的支持 +- BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作 +- FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能 +- ConfigurableBeanFactory:提供配置Factory的各种方法 +- ListableBeanFactory:根据各种条件获取bean的配置清单 +- AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能 +- AutowireCapableBeanFactory:提供创建bean、自动注入、初始化以及应用bean的后处理器 +- AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现 +- ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等 +- DefaultListableBeanFactory:综合上面所有功能,主要是对Bean注册后的处理 +XmlBeanFactory对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档中读取BeanDefinition,对于注册及获取Bean都是使用从父类DefaultListableBeanFactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性。在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册 + +#### XmlBeanDefinitionReader + +XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先看看各个类的功能 + +- ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource +- BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能 +- EnvironmentCapable:定义获取Environment方法 +- DocumentLoader:定义从资源文件加载到转换为Document的功能 +- AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现 +- BeanDefinitionDocumentReader:定义读取Document并注册BeanDefinition功能 +- BeanDefinitionParserDelegate:定义解析Element的各种方法 + +整个XML配置文件读取的大致流程,在XmlBeanDefinitionReader中主要包含以下几步处理 + +![](http://img.topjavaer.cn/img/202309171638903.png) + +(1)通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourceLoader将资源文件路径转换为对应的Resource文件 +(2)通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件 +(3)通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析 + +## 容器的基础XmlBeanFactory + + 通过上面的内容我们对spring的容器已经有了大致的了解,接下来我们详细探索每个步骤的详细实现,接下来要分析的功能都是基于如下代码: + +```java +BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); +``` + +首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了。有了Resource后就可以对BeanFactory进行初始化操作,那配置文件是如何封装的呢? + +### 配置文件的封装 + + Spring的配置文件读取是通过ClassPathResource进行封装的,Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源,如下源码: + +```java +public interface InputStreamSource { + InputStream getInputStream() throws IOException; +} +public interface Resource extends InputStreamSource { + boolean exists(); + default boolean isReadable() { + return true; + } + default boolean isOpen() { + return false; + } + default boolean isFile() { + return false; + } + URL getURL() throws IOException; + URI getURI() throws IOException; + File getFile() throws IOException; + default ReadableByteChannel readableChannel() throws IOException { + return Channels.newChannel(getInputStream()); + } + long contentLength() throws IOException; + long lastModified() throws IOException; + Resource createRelative(String relativePath) throws IOException; + String getFilename(); + String getDescription(); +} +``` + + InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等, 它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象 。 + +Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法,为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative(),还提供了getDescription()方法用于在错误处理中的打印信息。 + +对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。 + +在日常开发中我们可以直接使用spring提供的类来加载资源文件,比如在希望加载资源文件时可以使用下面的代码: + +``` +Resource resource = new ClassPathResource("spring-config.xml"); +InputStream is = resource.getInputStream(); +``` + +有了 Resource 接口便可以对所有资源文件进行统一处理 至于实现,其实是非常简单的,以 getlnputStream 为例,ClassPathResource 中的实现方式便是通 class 或者 classLoader 提供的底层方法进行调用,而对于 FileSystemResource 其实更简单,直接使用 FileInputStream 对文件进行实例化。 + +**ClassPathResource.java** + +```java +InputStream is; +if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); +} +else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); +} +else { + is = ClassLoader.getSystemResourceAsStream(this.path); +} +``` + +**FileSystemResource.java** + +```java +public InputStream getinputStream () throws IOException { + return new FilelnputStream(this file) ; +} +``` + +当通过Resource相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。 +接下来就进入到XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下: + +**XmlBeanFactory.java** + +```java +public XmlBeanFactory(Resource resource) throws BeansException { + this(resource, null); +} +public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); +} +``` + + + +上面函数中的代码this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),我们按照代码层级进行跟踪,首先跟踪到如下父类代码: + +```java +public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + super(parentBeanFactory); +} +``` + +然后继续跟踪,跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中: + +```java +public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + this(); + setParentBeanFactory(parentBeanFactory); +} +public AbstractAutowireCapableBeanFactory() { + super(); + ignoreDependencyInterface(BeanNameAware.class); + ignoreDependencyInterface(BeanFactoryAware.class); + ignoreDependencyInterface(BeanClassLoaderAware.class); +} +``` + +这里有必要提及 ignoreDependencylnterface方法,ignoreDependencylnterface 的主要功能是 忽略给定接口的向动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢? + +举例来说,当 A 中有属性 B ,那么当 Spring 在获取 A的 Bean 的时候如果其属性 B 还没有 初始化,那么 Spring 会自动初始化 B,这也是 Spring 提供的一个重要特性 。但是,某些情况 下, B不会被初始化,其中的一种情况就是B 实现了 BeanNameAware 接口 。Spring 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是边过其他方式解析 Application 上下文注册依赖,类似于 BeanFactor 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。 + +调用ignoreDependencyInterface方法后,被忽略的接口会存储在BeanFactory的名为ignoredDependencyInterfaces的Set集合中: + +```java +public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory + implements AutowireCapableBeanFactory { + + private final Set> ignoredDependencyInterfaces = new HashSet<>(); + + public void ignoreDependencyInterface(Class ifc) { + this.ignoredDependencyInterfaces.add(ifc); + } +... +} +``` + +ignoredDependencyInterfaces集合在同类中被使用仅在一处——isExcludedFromDependencyCheck方法中: + +```java +protected boolean isExcludedFromDependencyCheck(PropertyDescriptor pd) { + return (AutowireUtils.isExcludedFromDependencyCheck(pd) || this.ignoredDependencyTypes.contains(pd.getPropertyType()) || AutowireUtils.isSetterDefinedInInterface(pd, this.ignoredDependencyInterfaces)); +} +``` + +而ignoredDependencyInterface的真正作用还得看AutowireUtils类的isSetterDfinedInInterface方法。 + +```java +public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set> interfaces) { + //获取bean中某个属性对象在bean类中的setter方法 + Method setter = pd.getWriteMethod(); + if (setter != null) { + // 获取bean的类型 + Class targetClass = setter.getDeclaringClass(); + for (Class ifc : interfaces) { + if (ifc.isAssignableFrom(targetClass) && // bean类型是否接口的实现类 + ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { // 接口是否有入参和bean类型完全相同的setter方法 + return true; + } + } + } + return false; +} +``` + +ignoredDependencyInterface方法并不是让我们在自动装配时直接忽略实现了该接口的依赖。这个方法的真正意思是忽略该接口的实现类中和接口setter方法入参类型相同的依赖。 + +举个例子。首先定义一个要被忽略的接口。 + +```java +public interface IgnoreInterface { + + void setList(List list); + + void setSet(Set set); +} +``` + +然后需要实现该接口,在实现类中注意要有setter方法入参相同类型的域对象,在例子中就是`List`和`Set`。 + +```java +public class IgnoreInterfaceImpl implements IgnoreInterface { + + private List list; + private Set set; + + @Override + public void setList(List list) { + this.list = list; + } + + @Override + public void setSet(Set set) { + this.set = set; + } + + public List getList() { + return list; + } + + public Set getSet() { + return set; + } +} +``` + +定义xml配置文件: + +```xml + + + + + + + + foo + bar + + + + + + + + foo + bar + + + + + + + +``` + +最后调用ignoreDependencyInterface: + +```xml +beanFactory.ignoreDependencyInterface(IgnoreInterface.class); +``` + +运行结果: + +``` +null +null +``` + +而如果不调用ignoreDependencyInterface,则是: + +``` +[foo, bar] +[bar, foo] +``` + +我们最初理解是在自动装配时忽略该接口的实现,实际上是在自动装配时忽略该接口实现类中和setter方法入参相同的类型,也就是忽略该接口实现类中存在依赖外部的bean属性注入。 + +典型应用就是BeanFactoryAware和ApplicationContextAware接口。 + +首先看该两个接口的源码: + +```java +public interface BeanFactoryAware extends Aware { + void setBeanFactory(BeanFactory beanFactory) throws BeansException; +} + +public interface ApplicationContextAware extends Aware { + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; +} +``` + +在Spring源码中在不同的地方忽略了该两个接口: + +```java +beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); +ignoreDependencyInterface(BeanFactoryAware.class); +``` + +使得我们的BeanFactoryAware接口实现类在自动装配时不能被注入BeanFactory对象的依赖: + +```java +public class MyBeanFactoryAware implements BeanFactoryAware { + private BeanFactory beanFactory; // 自动装配时忽略注入 + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } +} +``` + +ApplicationContextAware接口实现类中的ApplicationContext对象的依赖同理: + +```java +public class MyApplicationContextAware implements ApplicationContextAware { + private ApplicationContext applicationContext; // 自动装配时被忽略注入 + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return applicationContext; + } +} +``` + +这样的做法使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依赖在自动装配时被忽略,而统一由框架设置依赖,如ApplicationContextAware接口的设置会在ApplicationContextAwareProcessor类中完成: + +```java +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof Aware) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware) { + ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware) { + ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware) { + ((MessageSourceAware) bean).setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + } +} +``` + +通过这种方式保证了ApplicationContextAware和BeanFactoryAware中的容器保证是生成该bean的容器。 + + + +### bean加载 + +在之前XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法 + +this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,这个方法的时序图如下: + +![](http://img.topjavaer.cn/img/202309171650829.png) + +我们来梳理下上述时序图的处理过程: + +(1)封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装 +(2)获取输入流。从Resource中获取对应的InputStream并构造InputSource +(3)通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions,loadBeanDefinitions函数具体的实现过程: + +```java +public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Loading XML bean definitions from " + encodedResource); + } + + Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); + if (currentResources == null) { + currentResources = new HashSet<>(4); + this.resourcesCurrentlyBeingLoaded.set(currentResources); + } + if (!currentResources.add(encodedResource)) { + throw new BeanDefinitionStoreException( + "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); + } + try { + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + inputStream.close(); + } + } + ... +} +``` + +EncodedResource的作用是对资源文件的编码进行处理的,其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码,在构造好了encodeResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource)),这个方法内部才是真正的数据准备阶段,代码如下: + +```java +protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + try { + // 获取 Document 实例 + Document doc = doLoadDocument(inputSource, resource); + // 根据 Document 实例****注册 Bean信息 + return registerBeanDefinitions(doc, resource); + } + ... +} +``` + +核心部分就是 try 块的两行代码。 + +1. 调用 `doLoadDocument()` 方法,根据 xml 文件获取 Document 实例。 +2. 根据获取的 Document 实例注册 Bean 信息 + +其实在`doLoadDocument()`方法内部还获取了 xml 文件的验证模式。如下: + +```java +protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { + return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, + getValidationModeForResource(resource), isNamespaceAware()); +} +``` + +调用 `getValidationModeForResource()` 获取指定资源(xml)的验证模式。所以 `doLoadBeanDefinitions()`主要就是做了三件事情。 + +1. 调用 `getValidationModeForResource()` 获取 xml 文件的验证模式 + 2. 调用 `loadDocument()` 根据 xml 文件获取相应的 Document 实例。 + 3. 调用 `registerBeanDefinitions()` 注册 Bean 实例。 + +### 获取XML的验证模式 + +#### *DTD和XSD区别* + +DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符合规则。 + +使用DTD验证模式的时候需要在XML文件的头部声明,以下是在Spring中使用DTD声明方式的代码: + +```xml + + +``` + +XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构,可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求,文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。 + +在使用XML Schema文档对XML实例文档进行检验,除了要声明**名称空间**外(**xmlns=http://www.Springframework.org/schema/beans**),还必须指定该名称空间所对应的**XML Schema文档的存储位置**,通过**schemaLocation**属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就该名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans **http://www.Springframework.org/schema/beans/Spring-beans.xsd**“),代码如下: + +```xml + + + + + + +``` + +#### *验证模式的读取* + +在spring中,是通过getValidationModeForResource方法来获取对应资源的验证模式,其源码如下: + +```java +protected int getValidationModeForResource(Resource resource) { + int validationModeToUse = getValidationMode(); + if (validationModeToUse != VALIDATION_AUTO) { + return validationModeToUse; + } + int detectedMode = detectValidationMode(resource); + if (detectedMode != VALIDATION_AUTO) { + return detectedMode; + } + // Hmm, we didn't get a clear indication... Let's assume XSD, + // since apparently no DTD declaration has been found up until + // detection stopped (before finding the document's root tag). + return VALIDATION_XSD; +} +``` + +方法的实现还是很简单的,如果设定了验证模式则使用设定的验证模式(可以通过使用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectValidationMode方法中,而在此方法中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector的validationModeDetector方法,具体代码如下: + +```java +public int detectValidationMode(InputStream inputStream) throws IOException { + // Peek into the file to look for DOCTYPE. + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + try { + boolean isDtdValidated = false; + String content; + while ((content = reader.readLine()) != null) { + content = consumeCommentTokens(content); + if (this.inComment || !StringUtils.hasText(content)) { + continue; + } + if (hasDoctype(content)) { + isDtdValidated = true; + break; + } + if (hasOpeningTag(content)) { + // End of meaningful data... + break; + } + } + return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); + } + catch (CharConversionException ex) { + // Choked on some character encoding... + // Leave the decision up to the caller. + return VALIDATION_AUTO; + } + finally { + reader.close(); + } +} +``` + +从代码中看,主要是通过读取 XML 文件的内容,判断内容中是否包含有 DOCTYPE ,如果是 则为 DTD,否则为 XSD,当然只会读取到 第一个 “<” 处,因为 验证模式一定会在第一个 “<” 之前。如果当中出现了 CharConversionException 异常,则为 XSD模式。 + + + +### 获取Document + +经过了验证模式准备的步骤就可以进行Document加载了,对于文档的读取委托给了DocumentLoader去执行,这里的DocumentLoader是个接口,而真正调用的是DefaultDocumentLoader,解析代码如下: + +```java +public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, + ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { + DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); + if (logger.isDebugEnabled()) { + logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]"); + } + DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); + return builder.parse(inputSource); +} +``` + +分析代码,首选创建DocumentBuildFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputSource来返回Document对象。对于参数entityResolver,传入的是通过getEntityResolver()函数获取的返回值,代码如下: + +```java +protected EntityResolver getEntityResolver() { + if (this.entityResolver == null) { + // Determine default EntityResolver to use. + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader != null) { + this.entityResolver = new ResourceEntityResolver(resourceLoader); + } + else { + this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); + } + } + return this.entityResolver; +} +``` + +这个entityResolver是做什么用的呢,接下来我们详细分析下。 + +#### EntityResolver 的用法 + +对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证,默认的寻找规则,即通过网络(实现上就是声明DTD的URI地址)来下载相应的DTD声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有被找到的原因. + +EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可,在EntityResolver的接口只有一个方法声明: + +```java +public abstract InputSource resolveEntity (String publicId, String systemId) + throws SAXException, IOException; +``` + +它接收两个参数publicId和systemId,并返回一个InputSource对象,以特定配置文件来进行讲解 + +(1)如果在解析验证模式为XSD的配置文件,代码如下: + +```xml + + +.... + +``` + +则会读取到以下两个参数 + +- publicId:null +- systemId:[http://www.Springframework.org/schema/beans/Spring-beans.xsd](http://www.springframework.org/schema/beans/Spring-beans.xsd) + +![](http://img.topjavaer.cn/img/202309171647085.png) + +(2)如果解析验证模式为DTD的配置文件,代码如下: + +```xml + + +.... + +``` + +读取到以下两个参数 +- publicId:-//Spring//DTD BEAN 2.0//EN +- systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd + +![](http://img.topjavaer.cn/img/202309171647627.png) + +一般都会把验证文件放置在自己的工程里,如果把URL转换为自己工程里对应的地址文件呢?以加载DTD文件为例来看看Spring是如何实现的。根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolveEntity实现方法如下: + +```java +@Override +@Nullable +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException { + if (systemId != null) { + if (systemId.endsWith(DTD_SUFFIX)) { + return this.dtdResolver.resolveEntity(publicId, systemId); + } + else if (systemId.endsWith(XSD_SUFFIX)) { + return this.schemaResolver.resolveEntity(publicId, systemId); + } + } + return null; +} +``` + +不同的验证模式使用不同的解析器解析,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载。 BeansDtdResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public ID [" + publicId + + "] and system ID [" + systemId + "]"); + } + if (systemId != null && systemId.endsWith(DTD_EXTENSION)) { + int lastPathSeparator = systemId.lastIndexOf('/'); + int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator); + if (dtdNameStart != -1) { + String dtdFile = DTD_NAME + DTD_EXTENSION; + if (logger.isTraceEnabled()) { + logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath"); + } + try { + Resource resource = new ClassPathResource(dtdFile, getClass()); + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile); + } + return source; + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex); + } + } + } + } + return null; +} +``` + +从上面的代码中我们可以看到加载 DTD 类型的 `BeansDtdResolver.resolveEntity()` 只是对 systemId 进行了简单的校验(从最后一个 / 开始,内容中是否包含 `spring-beans`),然后构造一个 InputSource 并设置 publicId、systemId,然后返回。 PluggableSchemaResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public id [" + publicId + + "] and system id [" + systemId + "]"); + } + + if (systemId != null) { + String resourceLocation = getSchemaMappings().get(systemId); + if (resourceLocation != null) { + Resource resource = new ClassPathResource(resourceLocation, this.classLoader); + try { + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); + } + return source; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); + } + } + } + } + return null; +} +``` + +首先调用 getSchemaMappings() 获取一个映射表(systemId 与其在本地的对照关系),然后根据传入的 systemId 获取该 systemId 在本地的路径 resourceLocation,最后根据 resourceLocation 构造 InputSource 对象。 映射表如下(部分): + +![](http://img.topjavaer.cn/img/202309171648620.png) + +### 解析及注册BeanDefinitions + +当把文件转换成Document后,接下来就是对bean的提取及注册,当程序已经拥有了XML文档文件的Document实例对象时,就会被引入到XmlBeanDefinitionReader.registerBeanDefinitions这个方法: + +```java +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + int countBefore = getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + return getRegistry().getBeanDefinitionCount() - countBefore; +} +``` + +其中的doc参数即为上节读取的document,而BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册,如下代码: + +```java +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + logger.debug("Loading bean definitions"); + Element root = doc.getDocumentElement(); + doRegisterBeanDefinitions(root); +} +``` + +通过这里我们看到终于到了解析逻辑的核心方法doRegisterBeanDefinitions,接着跟踪源码如下: + +```java +protected void doRegisterBeanDefinitions(Element root) { + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = createDelegate(getReaderContext(), root, parent); + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray( + profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); + if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (logger.isInfoEnabled()) { + logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + + "] not matching: " + getReaderContext().getResource()); + } + return; + } + } + } + preProcessXml(root); + parseBeanDefinitions(root, this.delegate); + postProcessXml(root); + this.delegate = parent; +} +``` + +我们看到首先要解析profile属性,然后才开始XML的读取,具体的代码如下: + +```java +protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + if (delegate.isDefaultNamespace(ele)) { + parseDefaultElement(ele, delegate); + } + else { + delegate.parseCustomElement(ele); + } + } + } + } + else { + delegate.parseCustomElement(root); + } +} +``` + +最终解析动作落地在两个方法处:`parseDefaultElement(ele, delegate)` 和 `delegate.parseCustomElement(root)`。我们知道在 Spring 有两种 Bean 声明方式: + +- 配置文件式声明:`` +- 自定义注解方式:`` + +两种方式的读取和解析都存在较大的差异,所以采用不同的解析方法,如果根节点或者子节点采用默认命名空间的话,则调用 `parseDefaultElement()` 进行解析,否则调用 `delegate.parseCustomElement()` 方法进行自定义解析。 + +而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认,否则就认为是自定义。 + +#### profile的用法 + +通过profile标记不同的环境,可以通过设置spring.profiles.active和spring.profiles.default激活指定profile环境。如果设置了active,default便失去了作用。如果两个都没有设置,那么带有profiles的bean都不会生成。 + +配置spring配置文件最下面配置如下beans + +```xml + + + + + + + + + + + + + + +``` + +配置web.xml + +```xml + + + spring.profiles.default + production + + + + + + spring.profiles.active + test + +``` + +这样启动的时候就可以按照切换spring.profiles.active的属性值来进行切换了。 \ No newline at end of file diff --git a/docs/source/spring/3-ioc-tag-parse-1.md b/docs/source/spring/3-ioc-tag-parse-1.md new file mode 100644 index 0000000..3edbb35 --- /dev/null +++ b/docs/source/spring/3-ioc-tag-parse-1.md @@ -0,0 +1,879 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +本文主要研究Spring标签的解析,Spring的标签中有默认标签和自定义标签,两者的解析有着很大的不同,这次重点说默认标签的解析过程。 + +默认标签的解析是在DefaultBeanDefinitionDocumentReader.parseDefaultElement函数中进行的,分别对4种不同的标签(import,alias,bean和beans)做了不同处理。我们先看下此函数的源码: + +```java +private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { + importBeanDefinitionResource(ele); + } + else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { + processAliasRegistration(ele); + } + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } +} +``` + +## Bean标签的解析及注册 + +在4中标签中对bean标签的解析最为复杂也最为重要,所以从此标签开始深入分析,如果能理解这个标签的解析过程,其他标签的解析就迎刃而解了。对于bean标签的解析用的是processBeanDefinition函数,首先看看函数processBeanDefinition(ele,delegate),其代码如下: + +```java +protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + if (bdHolder != null) { + bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); + try { + // Register the final decorated instance. + BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to register bean definition with name '" + + bdHolder.getBeanName() + "'", ele, ex); + } + // Send registration event. + getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); + } +} +``` + +刚开始看这个函数体时一头雾水,没有以前的函数那样的清晰的逻辑,我们细致的理下逻辑,大致流程如下: + +- 首先委托BeanDefinitionDelegate类的parseBeanDefinitionElement方法进行元素的解析,返回BeanDefinitionHolder类型的实例bdHolder,经过这个方法后bdHolder实例已经包含了我们配置文件中的各种属性了,例如class,name,id,alias等。 + +- 当返回的dbHolder不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析 +- 当解析完成后,需要对解析后的bdHolder进行注册,注册过程委托给了BeanDefinitionReaderUtils的registerBeanDefinition方法。 +- 最后发出响应事件,通知相关的监听器已经加载完这个Bean了。 + +### 解析BeanDefinition + +接下来我们就针对具体的方法进行分析,首先我们从元素解析及信息提取开始,也就是BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele),进入 BeanDefinitionDelegate 类的 parseBeanDefinitionElement 方法。我们看下源码: + +```java +public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + // 解析 ID 属性 + String id = ele.getAttribute(ID_ATTRIBUTE); + // 解析 name 属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + + // 分割 name 属性 + List aliases = new ArrayList<>(); + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { + beanName = aliases.remove(0); + if (logger.isDebugEnabled()) { + logger.debug("No XML 'id' specified - using '" + beanName + + "' as bean name and " + aliases + " as aliases"); + } + } + + // 检查 name 的唯一性 + if (containingBean == null) { + checkNameUniqueness(beanName, aliases, ele); + } + + // 解析 属性,构造 AbstractBeanDefinition + AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + // 如果 beanName 不存在,则根据条件构造一个 beanName + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + beanName = BeanDefinitionReaderUtils.generateBeanName( + beanDefinition, this.readerContext.getRegistry(), true); + } + else { + beanName = this.readerContext.generateBeanName(beanDefinition); + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null && + beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && + !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { + aliases.add(beanClassName); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Neither XML 'id' nor 'name' specified - " + + "using generated bean name [" + beanName + "]"); + } + } + catch (Exception ex) { + error(ex.getMessage(), ele); + return null; + } + } + String[] aliasesArray = StringUtils.toStringArray(aliases); + + // 封装 BeanDefinitionHolder + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } + + return null; +} +``` + +上述方法就是对默认标签解析的全过程了,我们分析下当前层完成的工作: + +(1)提取元素中的id和name属性 +(2)进一步解析其他所有属性并统一封装到GenericBeanDefinition类型的实例中 +(3)如果检测到bean没有指定beanName,那么使用默认规则为此bean生成beanName。 +(4)将获取到的信息封装到BeanDefinitionHolder的实例中。 + +代码:`AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);`是用来对标签中的其他属性进行解析,我们详细看下源码: + +```java +public AbstractBeanDefinition parseBeanDefinitionElement( + Element ele, String beanName, @Nullable BeanDefinition containingBean) { + + this.parseState.push(new BeanEntry(beanName)); + + String className = null; + //解析class属性 + if (ele.hasAttribute(CLASS_ATTRIBUTE)) { + className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); + } + String parent = null; + //解析parent属性 + if (ele.hasAttribute(PARENT_ATTRIBUTE)) { + parent = ele.getAttribute(PARENT_ATTRIBUTE); + } + + try { + //创建用于承载属性的AbstractBeanDefinition类型的GenericBeanDefinition实例 + AbstractBeanDefinition bd = createBeanDefinition(className, parent); + //硬编码解析bean的各种属性 + parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + //设置description属性 + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); + //解析元素 + parseMetaElements(ele, bd); + //解析lookup-method属性 + parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + //解析replace-method属性 + parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + //解析构造函数的参数 + parseConstructorArgElements(ele, bd); + //解析properties子元素 + parsePropertyElements(ele, bd); + //解析qualifier子元素 + parseQualifierElements(ele, bd); + bd.setResource(this.readerContext.getResource()); + bd.setSource(extractSource(ele)); + + return bd; + } + catch (ClassNotFoundException ex) { + error("Bean class [" + className + "] not found", ele, ex); + } + catch (NoClassDefFoundError err) { + error("Class that bean class [" + className + "] depends on not found", ele, err); + } + catch (Throwable ex) { + error("Unexpected failure during bean definition parsing", ele, ex); + } + finally { + this.parseState.pop(); + } + + return null; +} +``` + +接下来我们一步步分析解析过程。 + +## bean详细解析过程 + +### 创建用于承载属性的BeanDefinition + +BeanDefinition是一个接口,在spring中此接口有三种实现:RootBeanDefinition、ChildBeanDefinition已经GenericBeanDefinition。而三种实现都继承了AbstractBeanDefinition,其中BeanDefinition是配置文件元素标签在容器中的内部表示形式。元素标签拥有class、scope、lazy-init等属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和``中的属性一一对应。其中RootBeanDefinition是最常用的实现类,他对应一般性的元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务的。 + +在配置文件中可以定义父和字,父用RootBeanDefinition表示,而子用ChildBeanDefinition表示,而没有父的就使用RootBeanDefinition表示。AbstractBeanDefinition对两者共同的类信息进行抽象。 + +Spring通过BeanDefinition将配置文件中的配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。Spring容器的BeanDefinitionRegistry就像是Spring配置信息的内存数据库,主要是以map的形式保存,后续操作直接从BeanDefinitionResistry中读取配置信息。它们之间的关系如下图所示: + +![](http://img.topjavaer.cn/img/202309171708954.png) + +因此,要解析属性首先要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例。而代码createBeanDefinition(className,parent)的作用就是实现此功能。我们详细看下方法体,代码如下: + +```java +protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName) + throws ClassNotFoundException { + + return BeanDefinitionReaderUtils.createBeanDefinition( + parentName, className, this.readerContext.getBeanClassLoader()); +} +public static AbstractBeanDefinition createBeanDefinition( + @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { + + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setParentName(parentName); + if (className != null) { + if (classLoader != null) { + bd.setBeanClass(ClassUtils.forName(className, classLoader)); + } + else { + bd.setBeanClassName(className); + } + } + return bd; +} +``` + + + +### 各种属性的解析 + + 当创建好了承载bean信息的实例后,接下来就是解析各种属性了,首先我们看下parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);方法,代码如下: + +```java +public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, + @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { + //解析singleton属性 + if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { + error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele); + } + //解析scope属性 + else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { + bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); + } + else if (containingBean != null) { + // Take default from containing bean in case of an inner bean definition. + bd.setScope(containingBean.getScope()); + } + //解析abstract属性 + if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { +bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); + } + //解析lazy_init属性 + String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); + if (DEFAULT_VALUE.equals(lazyInit)) { + lazyInit = this.defaults.getLazyInit(); + } + bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); + //解析autowire属性 + String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); + bd.setAutowireMode(getAutowireMode(autowire)); + //解析dependsOn属性 + if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { + String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); + bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); + } + //解析autowireCandidate属性 + String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); + if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) { + String candidatePattern = this.defaults.getAutowireCandidates(); + if (candidatePattern != null) { + String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); + bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); + } + } + else { + bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); + } + //解析primary属性 + if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { + bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); + } + //解析init_method属性 + if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { + String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); + bd.setInitMethodName(initMethodName); + } + else if (this.defaults.getInitMethod() != null) { + bd.setInitMethodName(this.defaults.getInitMethod()); + bd.setEnforceInitMethod(false); + } + //解析destroy_method属性 + if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { + String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); + bd.setDestroyMethodName(destroyMethodName); + } + else if (this.defaults.getDestroyMethod() != null) { + bd.setDestroyMethodName(this.defaults.getDestroyMethod()); + bd.setEnforceDestroyMethod(false); + } + //解析factory_method属性 + if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { + bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); + } + //解析factory_bean属性 + if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { + bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); + } + + return bd; +} +``` + + + +### 解析meta元素 + + 在开始对meta元素解析分析前我们先简单回顾下meta属性的使用,简单的示例代码如下: + +```xml + + + + +``` + +这段代码并不会提现在demo的属性中,而是一个额外的声明,如果需要用到这里面的信息时可以通过BeanDefinition的getAttribute(key)方法获取,对meta属性的解析用的是:parseMetaElements(ele, bd);具体的方法体如下: + +```java +public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) { + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) { + Element metaElement = (Element) node; + String key = metaElement.getAttribute(KEY_ATTRIBUTE); + String value = metaElement.getAttribute(VALUE_ATTRIBUTE); + BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value); + attribute.setSource(extractSource(metaElement)); + attributeAccessor.addMetadataAttribute(attribute); + } + } +} +``` + + + +### 解析replaced-method属性 + + 在分析代码前我们还是先简单的了解下replaced-method的用法,其主要功能是方法替换:即在运行时用新的方法替换旧的方法。与之前的lookup-method不同的是此方法不仅可以替换返回的bean,还可以动态的更改原有方法的运行逻辑,我们看下使用: + +```java +//原有的changeMe方法 +public class TestChangeMethod { + public void changeMe() + { + System.out.println("ChangeMe"); + } +} +//新的实现方法 +public class ReplacerChangeMethod implements MethodReplacer { + public Object reimplement(Object o, Method method, Object[] objects) throws Throwable { + System.out.println("I Replace Method"); + return null; + } +} +//新的配置文件 + + + + + + + + +//测试方法 +public class TestDemo { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("replaced-method.xml"); + + TestChangeMethod test =(TestChangeMethod) context.getBean("changeMe"); + + test.changeMe(); + } +} +``` + +接下来我们看下解析replaced-method的方法代码: + +```java +public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) { + Element replacedMethodEle = (Element) node; + String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE); + String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE); + ReplaceOverride replaceOverride = new ReplaceOverride(name, callback); + // Look for arg-type match elements. + List argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT); + for (Element argTypeEle : argTypeEles) { + String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE); + match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle)); + if (StringUtils.hasText(match)) { + replaceOverride.addTypeIdentifier(match); + } + } + replaceOverride.setSource(extractSource(replacedMethodEle)); + overrides.addOverride(replaceOverride); + } + } +} +``` + +我们可以看到无论是 look-up 还是 replaced-method 是构造了 MethodOverride ,并最终记录在了 AbstractBeanDefinition 中的 methodOverrides 属性中 + +### 解析constructor-arg + +对构造函数的解析式非常常用,也是非常复杂的,我们先从一个简单配置构造函数的例子开始分析,代码如下: + +```java +public void parseConstructorArgElement(Element ele, BeanDefinition bd) { + //提前index属性 + String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE); + //提前type属性 + String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE); + //提取name属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + if (StringUtils.hasLength(indexAttr)) { + try { + int index = Integer.parseInt(indexAttr); + if (index < 0) { + error("'index' cannot be lower than 0", ele); + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry(index)); + //解析ele对应的元素属性 + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) { + error("Ambiguous constructor-arg entries for index " + index, ele); + } + else { + bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder); + } + } + finally { + this.parseState.pop(); + } + } + } + catch (NumberFormatException ex) { + error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele); + } + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry()); + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder); + } + finally { + this.parseState.pop(); + } + } +} +``` + +上述代码的流程可以简单的总结为如下: + +(1)首先提取index、type、name等属性 +(2)根据是否配置了index属性解析流程不同 + +如果配置了index属性,解析流程如下: + +(1)使用parsePropertyValue(ele, bd, null)方法读取constructor-arg的子元素 + +(2)使用ConstructorArgumentValues.ValueHolder封装解析出来的元素 + +(3)将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的indexedArgumentValues,而indexedArgumentValues是一个map类型 + +如果没有配置index属性,将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的genericArgumentValues中 + +```java +public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + String elementName = (propertyName != null) ? + " element for property '" + propertyName + "'" : + " element"; + + // Should only have one child element: ref, value, list, etc. + NodeList nl = ele.getChildNodes(); + Element subElement = null; + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + //略过description和meta属性 + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) && + !nodeNameEquals(node, META_ELEMENT)) { + // Child element is what we're looking for. + if (subElement != null) { + error(elementName + " must not contain more than one sub-element", ele); + } + else { + subElement = (Element) node; + } + } + } + //解析ref属性 + boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE); + //解析value属性 + boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE); + if ((hasRefAttribute && hasValueAttribute) || + ((hasRefAttribute || hasValueAttribute) && subElement != null)) { + error(elementName + + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele); + } + + if (hasRefAttribute) { + String refName = ele.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(elementName + " contains empty 'ref' attribute", ele); + } + //使用RuntimeBeanReference来封装ref对应的bean + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(ele)); + return ref; + } + else if (hasValueAttribute) { + //使用TypedStringValue 来封装value属性 + TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE)); + valueHolder.setSource(extractSource(ele)); + return valueHolder; + } + else if (subElement != null) { + //解析子元素 + return parsePropertySubElement(subElement, bd); + } + else { + // Neither child element nor "ref" or "value" attribute found. + error(elementName + " must specify a ref or value", ele); + return null; + } +} +``` + +上述代码的执行逻辑简单总结为: + +(1)首先略过decription和meta属性 + +(2)提取constructor-arg上的ref和value属性,并验证是否存在 + +(3)存在ref属性时,用RuntimeBeanReference来封装ref + +(4)存在value属性时,用TypedStringValue来封装 + +(5)存在子元素时,对于子元素的处理使用了方法parsePropertySubElement(subElement, bd);,其代码如下: + +```java +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + return parsePropertySubElement(ele, bd, null); +} +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + //判断是否是默认标签处理 + if (!isDefaultNamespace(ele)) { + return parseNestedCustomElement(ele, bd); + } + //对于bean标签的处理 + else if (nodeNameEquals(ele, BEAN_ELEMENT)) { + BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd); + if (nestedBd != null) { + nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd); + } + return nestedBd; + } + else if (nodeNameEquals(ele, REF_ELEMENT)) { + // A generic reference to any name of any bean. + String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); + boolean toParent = false; + if (!StringUtils.hasLength(refName)) { + // A reference to the id of another bean in a parent context. + refName = ele.getAttribute(PARENT_REF_ATTRIBUTE); + toParent = true; + if (!StringUtils.hasLength(refName)) { + error("'bean' or 'parent' is required for element", ele); + return null; + } + } + if (!StringUtils.hasText(refName)) { + error(" element contains empty target attribute", ele); + return null; + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent); + ref.setSource(extractSource(ele)); + return ref; + } + //idref元素处理 + else if (nodeNameEquals(ele, IDREF_ELEMENT)) { + return parseIdRefElement(ele); + } + //value元素处理 + else if (nodeNameEquals(ele, VALUE_ELEMENT)) { + return parseValueElement(ele, defaultValueType); + } + //null元素处理 + else if (nodeNameEquals(ele, NULL_ELEMENT)) { + // It's a distinguished null value. Let's wrap it in a TypedStringValue + // object in order to preserve the source location. + TypedStringValue nullHolder = new TypedStringValue(null); + nullHolder.setSource(extractSource(ele)); + return nullHolder; + } + //array元素处理 + else if (nodeNameEquals(ele, ARRAY_ELEMENT)) { + return parseArrayElement(ele, bd); + } + //list元素处理 + else if (nodeNameEquals(ele, LIST_ELEMENT)) { + return parseListElement(ele, bd); + } + //set元素处理 + else if (nodeNameEquals(ele, SET_ELEMENT)) { + return parseSetElement(ele, bd); + } + //map元素处理 + else if (nodeNameEquals(ele, MAP_ELEMENT)) { + return parseMapElement(ele, bd); + } + //props元素处理 + else if (nodeNameEquals(ele, PROPS_ELEMENT)) { + return parsePropsElement(ele); + } + else { + error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele); + return null; + } +} +``` + + + +### 解析子元素properties + +对于propertie元素的解析是使用的parsePropertyElements(ele, bd);方法,我们看下其源码如下: + +```java +public void parsePropertyElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) { + parsePropertyElement((Element) node, bd); + } + } +} +``` + +里面实际的解析是用的parsePropertyElement((Element) node, bd);方法,继续跟踪代码: + +```java +public void parsePropertyElement(Element ele, BeanDefinition bd) { + String propertyName = ele.getAttribute(NAME_ATTRIBUTE); + if (!StringUtils.hasLength(propertyName)) { + error("Tag 'property' must have a 'name' attribute", ele); + return; + } + this.parseState.push(new PropertyEntry(propertyName)); + try { + //不允许多次对同一属性配置 + if (bd.getPropertyValues().contains(propertyName)) { + error("Multiple 'property' definitions for property '" + propertyName + "'", ele); + return; + } + Object val = parsePropertyValue(ele, bd, propertyName); + PropertyValue pv = new PropertyValue(propertyName, val); + parseMetaElements(ele, pv); + pv.setSource(extractSource(ele)); + bd.getPropertyValues().addPropertyValue(pv); + } + finally { + this.parseState.pop(); + } +} +``` + +我们看到代码的逻辑非常简单,在获取了propertie的属性后使用PropertyValue 进行封装,然后将其添加到BeanDefinition的propertyValueList中 + +### 解析子元素 qualifier + +对于 qualifier 元素的获取,我们接触更多的是注解的形式,在使用 Spring 架中进行自动注入时,Spring 器中匹配的候选 Bean 数目必须有且仅有一个,当找不到一个匹配的 Bean 时, Spring容器将抛出 BeanCreationException 异常, 并指出必须至少拥有一个匹配的 Bean。 + +Spring 允许我们通过Qualifier 指定注入 Bean的名称,这样歧义就消除了,而对于配置方式使用如: + +```xml + + + +``` + +其解析过程与之前大同小异 这里不再重复叙述 + +至此我们便完成了对 XML 文档到 GenericBeanDefinition 的转换, 就是说到这里, XML 中所有的配置都可以在 GenericBeanDefinition的实例类中应找到对应的配置。 + +GenericBeanDefinition 只是子类实现,而大部分的通用属性都保存在了 bstractBeanDefinition 中,那么我们再次通过 AbstractBeanDefinition 的属性来回顾一 下我们都解析了哪些对应的配置。 + +```java +public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor + implements BeanDefinition, Cloneable { + + // 此处省略静态变量以及final变量 + + @Nullable + private volatile Object beanClass; + /** + * bean的作用范围,对应bean属性scope + */ + @Nullable + private String scope = SCOPE_DEFAULT; + /** + * 是否是抽象,对应bean属性abstract + */ + private boolean abstractFlag = false; + /** + * 是否延迟加载,对应bean属性lazy-init + */ + private boolean lazyInit = false; + /** + * 自动注入模式,对应bean属性autowire + */ + private int autowireMode = AUTOWIRE_NO; + /** + * 依赖检查,Spring 3.0后弃用这个属性 + */ + private int dependencyCheck = DEPENDENCY_CHECK_NONE; + /** + * 用来表示一个bean的实例化依靠另一个bean先实例化,对应bean属性depend-on + */ + @Nullable + private String[] dependsOn; + /** + * autowire-candidate属性设置为false,这样容器在查找自动装配对象时, + * 将不考虑该bean,即它不会被考虑作为其他bean自动装配的候选者, + * 但是该bean本身还是可以使用自动装配来注入其他bean的 + */ + private boolean autowireCandidate = true; + /** + * 自动装配时出现多个bean候选者时,将作为首选者,对应bean属性primary + */ + private boolean primary = false; + /** + * 用于记录Qualifier,对应子元素qualifier + */ + private final Map qualifiers = new LinkedHashMap<>(0); + + @Nullable + private Supplier instanceSupplier; + /** + * 允许访问非公开的构造器和方法,程序设置 + */ + private boolean nonPublicAccessAllowed = true; + /** + * 是否以一种宽松的模式解析构造函数,默认为true, + * 如果为false,则在以下情况 + * interface ITest{} + * class ITestImpl implements ITest{}; + * class Main { + * Main(ITest i) {} + * Main(ITestImpl i) {} + * } + * 抛出异常,因为Spring无法准确定位哪个构造函数程序设置 + */ + private boolean lenientConstructorResolution = true; + /** + * 对应bean属性factory-bean,用法: + * + * + */ + @Nullable + private String factoryBeanName; + /** + * 对应bean属性factory-method + */ + @Nullable + private String factoryMethodName; + /** + * 记录构造函数注入属性,对应bean属性constructor-arg + */ + @Nullable + private ConstructorArgumentValues constructorArgumentValues; + /** + * 普通属性集合 + */ + @Nullable + private MutablePropertyValues propertyValues; + /** + * 方法重写的持有者,记录lookup-method、replaced-method元素 + */ + @Nullable + private MethodOverrides methodOverrides; + /** + * 初始化方法,对应bean属性init-method + */ + @Nullable + private String initMethodName; + /** + * 销毁方法,对应bean属性destroy-method + */ + @Nullable + private String destroyMethodName; + /** + * 是否执行init-method,程序设置 + */ + private boolean enforceInitMethod = true; + /** + * 是否执行destroy-method,程序设置 + */ + private boolean enforceDestroyMethod = true; + /** + * 是否是用户定义的而不是应用程序本身定义的,创建AOP时候为true,程序设置 + */ + private boolean synthetic = false; + /** + * 定义这个bean的应用,APPLICATION:用户,INFRASTRUCTURE:完全内部使用,与用户无关, + * SUPPORT:某些复杂配置的一部分 + * 程序设置 + */ + private int role = BeanDefinition.ROLE_APPLICATION; + /** + * bean的描述信息 + */ + @Nullable + private String description; + /** + * 这个bean定义的资源 + */ + @Nullable + private Resource resource; +} +``` \ No newline at end of file diff --git a/docs/source/spring/4-ioc-tag-parse-2.md b/docs/source/spring/4-ioc-tag-parse-2.md new file mode 100644 index 0000000..4d5dcff --- /dev/null +++ b/docs/source/spring/4-ioc-tag-parse-2.md @@ -0,0 +1,517 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +**正文** + +在上一篇我们已经完成了从xml配置文件到BeanDefinition的转换,转换后的实例是GenericBeanDefinition的实例。本文主要来看看标签解析剩余部分及BeanDefinition的注册。 + +## 默认标签中的自定义标签解析 + +在上篇博文中我们已经分析了对于默认标签的解析,我们继续看戏之前的代码,如下图片中有一个方法:delegate.decorateBeanDefinitionIfRequired(ele, bdHolder) + +![](http://img.topjavaer.cn/img/202309180843559.png) + +这个方法的作用是什么呢?首先我们看下这种场景,如下配置文件: + +```xml + + + + + +``` + +这个配置文件中有个自定义的标签,decorateBeanDefinitionIfRequired方法就是用来处理这种情况的,其中的null是用来传递父级BeanDefinition的,我们进入到其方法体: + +```java +public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) { + return decorateBeanDefinitionIfRequired(ele, definitionHolder, null); +} +public BeanDefinitionHolder decorateBeanDefinitionIfRequired( + Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) { + + BeanDefinitionHolder finalDefinition = definitionHolder; + + // Decorate based on custom attributes first. + NamedNodeMap attributes = ele.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + + // Decorate based on custom nested elements. + NodeList children = ele.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + } + return finalDefinition; +} +``` + +我们看到上面的代码有两个遍历操作,一个是用于对所有的属性进行遍历处理,另一个是对所有的子节点进行处理,两个遍历操作都用到了decorateIfRequired(node, finalDefinition, containingBd);方法,我们继续跟踪代码,进入方法体: + +```java +public BeanDefinitionHolder decorateIfRequired( + Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { + // 获取自定义标签的命名空间 + String namespaceUri = getNamespaceURI(node); + // 过滤掉默认命名标签 + if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) { + // 获取相应的处理器 + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler != null) { + // 进行装饰处理 + BeanDefinitionHolder decorated = + handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd)); + if (decorated != null) { + return decorated; + } + } + else if (namespaceUri.startsWith("http://www.springframework.org/")) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]"); + } + } + } + return originalDef; +} + +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} + +public boolean isDefaultNamespace(@Nullable String namespaceUri) { + //BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; + return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri)); +} +``` + +首先获取自定义标签的命名空间,如果不是默认的命名空间则根据该命名空间获取相应的处理器,最后调用处理器的 `decorate()` 进行装饰处理。具体的装饰过程这里不进行讲述,在后面分析自定义标签时会做详细说明。 + + + +## 注册解析的BeanDefinition + +对于配置文件,解析和装饰完成之后,对于得到的beanDefinition已经可以满足后续的使用要求了,还剩下注册,也就是processBeanDefinition函数中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,getReaderContext().getRegistry())代码的解析了。进入方法体: + +```java +public static void registerBeanDefinition( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) + throws BeanDefinitionStoreException { + // Register bean definition under primary name. + //使用beanName做唯一标识注册 + String beanName = definitionHolder.getBeanName(); + registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); + + // Register aliases for bean name, if any. + //注册所有的别名 + String[] aliases = definitionHolder.getAliases(); + if (aliases != null) { + for (String alias : aliases) { + registry.registerAlias(beanName, alias); + } + } +} +``` + +从上面的代码我们看到是用了beanName作为唯一标示进行注册的,然后注册了所有的别名aliase。而beanDefinition最终都是注册到BeanDefinitionRegistry中,接下来我们具体看下注册流程。 + + + +## 通过beanName注册BeanDefinition + +在spring中除了使用beanName作为key将BeanDefinition放入Map中还做了其他一些事情,我们看下方法registerBeanDefinition代码,BeanDefinitionRegistry是一个接口,他有三个实现类,DefaultListableBeanFactory、SimpleBeanDefinitionRegistry、GenericApplicationContext,其中SimpleBeanDefinitionRegistry非常简单,而GenericApplicationContext最终也是使用的DefaultListableBeanFactory中的实现方法,我们看下代码: + +```java +public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + // 校验 beanName 与 beanDefinition + Assert.hasText(beanName, "Bean name must not be empty"); + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + + if (beanDefinition instanceof AbstractBeanDefinition) { + try { + // 校验 BeanDefinition + // 这是注册前的最后一次校验了,主要是对属性 methodOverrides 进行校验 + ((AbstractBeanDefinition) beanDefinition).validate(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Validation of bean definition failed", ex); + } + } + + BeanDefinition oldBeanDefinition; + + // 从缓存中获取指定 beanName 的 BeanDefinition + oldBeanDefinition = this.beanDefinitionMap.get(beanName); + /** + * 如果存在 + */ + if (oldBeanDefinition != null) { + // 如果存在但是不允许覆盖,抛出异常 + if (!isAllowBeanDefinitionOverriding()) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + + "': There is already [" + oldBeanDefinition + "] bound."); + } + // + else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { + // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + if (this.logger.isWarnEnabled()) { + this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + + "' with a framework-generated bean definition: replacing [" + + oldBeanDefinition + "] with [" + beanDefinition + "]"); + } + } + // 覆盖 beanDefinition 与 被覆盖的 beanDefinition 不是同类 + else if (!beanDefinition.equals(oldBeanDefinition)) { + if (this.logger.isInfoEnabled()) { + this.logger.info("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + + // 允许覆盖,直接覆盖原有的 BeanDefinition + this.beanDefinitionMap.put(beanName, beanDefinition); + } + /** + * 不存在 + */ + else { + // 检测创建 Bean 阶段是否已经开启,如果开启了则需要对 beanDefinitionMap 进行并发控制 + if (hasBeanCreationStarted()) { + // beanDefinitionMap 为全局变量,避免并发情况 + synchronized (this.beanDefinitionMap) { + // + this.beanDefinitionMap.put(beanName, beanDefinition); + List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); + updatedDefinitions.addAll(this.beanDefinitionNames); + updatedDefinitions.add(beanName); + this.beanDefinitionNames = updatedDefinitions; + if (this.manualSingletonNames.contains(beanName)) { + Set updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames); + updatedSingletons.remove(beanName); + this.manualSingletonNames = updatedSingletons; + } + } + } + else { + // 不会存在并发情况,直接设置 + this.beanDefinitionMap.put(beanName, beanDefinition); + this.beanDefinitionNames.add(beanName); + this.manualSingletonNames.remove(beanName); + } + this.frozenBeanDefinitionNames = null; + } + + if (oldBeanDefinition != null || containsSingleton(beanName)) { + // 重新设置 beanName 对应的缓存 + resetBeanDefinition(beanName); + } +} +``` + +处理过程如下: + +- 首先 BeanDefinition 进行校验,该校验也是注册过程中的最后一次校验了,主要是对 AbstractBeanDefinition 的 methodOverrides 属性进行校验 +- 根据 beanName 从缓存中获取 BeanDefinition,如果缓存中存在,则根据 allowBeanDefinitionOverriding 标志来判断是否允许覆盖,如果允许则直接覆盖,否则抛出 BeanDefinitionStoreException 异常 +- 若缓存中没有指定 beanName 的 BeanDefinition,则判断当前阶段是否已经开始了 Bean 的创建阶段(),如果是,则需要对 beanDefinitionMap 进行加锁控制并发问题,否则直接设置即可。对于 `hasBeanCreationStarted()` 方法后续做详细介绍,这里不过多阐述。 +- 若缓存中存在该 beanName 或者 单利 bean 集合中存在该 beanName,则调用 `resetBeanDefinition()` 重置 BeanDefinition 缓存。 + +其实整段代码的核心就在于 `this.beanDefinitionMap.put(beanName, beanDefinition);` 。BeanDefinition 的缓存也不是神奇的东西,就是定义 一个 ConcurrentHashMap,key 为 beanName,value 为 BeanDefinition。 + + + +## 通过别名注册BeanDefinition + +通过别名注册BeanDefinition最终是在SimpleBeanDefinitionRegistry中实现的,我们看下代码: + +```java +public void registerAlias(String name, String alias) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(alias, "'alias' must not be empty"); + synchronized (this.aliasMap) { + if (alias.equals(name)) { + this.aliasMap.remove(alias); + } + else { + String registeredName = this.aliasMap.get(alias); + if (registeredName != null) { + if (registeredName.equals(name)) { + // An existing alias - no need to re-register + return; + } + if (!allowAliasOverriding()) { + throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + + name + "': It is already registered for name '" + registeredName + "'."); + } + } + //当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + checkForAliasCircle(name, alias); + this.aliasMap.put(alias, name); + } + } +} +``` + +上述代码的流程总结如下: + +(1)alias与beanName相同情况处理,若alias与beanName并名称相同则不需要处理并删除原有的alias + +(2)alias覆盖处理。若aliasName已经使用并已经指向了另一beanName则需要用户的设置进行处理 + +(3)alias循环检查,当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + + + +## alias标签的解析 + +对应bean标签的解析是最核心的功能,对于alias、import、beans标签的解析都是基于bean标签解析的,接下来我就分析下alias标签的解析。我们回到 parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate)方法,继续看下方法体,如下图所示: + +![](http://img.topjavaer.cn/img/202309180844832.png) + +对bean进行定义时,除了用id来 指定名称外,为了提供多个名称,可以使用alias标签来指定。而所有这些名称都指向同一个bean。在XML配置文件中,可用单独的元素来完成bean别名的定义。我们可以直接使用bean标签中的name属性,如下: + +```xml + + + +``` + +在Spring还有另外一种声明别名的方式: + +```xml + + +``` + +我们具体看下alias标签的解析过程,解析使用的方法processAliasRegistration(ele),方法体如下: + +```java +protected void processAliasRegistration(Element ele) { + //获取beanName + String name = ele.getAttribute(NAME_ATTRIBUTE); + //获取alias + String alias = ele.getAttribute(ALIAS_ATTRIBUTE); + boolean valid = true; + if (!StringUtils.hasText(name)) { + getReaderContext().error("Name must not be empty", ele); + valid = false; + } + if (!StringUtils.hasText(alias)) { + getReaderContext().error("Alias must not be empty", ele); + valid = false; + } + if (valid) { + try { + //注册alias + getReaderContext().getRegistry().registerAlias(name, alias); + } + catch (Exception ex) { + getReaderContext().error("Failed to register alias '" + alias + + "' for bean with name '" + name + "'", ele, ex); + } + getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); + } +} +``` + +通过代码可以发现解析流程与bean中的alias解析大同小异,都是讲beanName与别名alias组成一对注册到registry中。跟踪代码最终使用了SimpleAliasRegistry中的registerAlias(String name, String alias)方法 + + + +## import标签的解析 + +对于Spring配置文件的编写,经历过大型项目的人都知道,里面有太多的配置文件了。基本采用的方式都是分模块,分模块的方式很多,使用import就是其中一种,例如我们可以构造这样的Spring配置文件: + +```xml + + + + + + + + + +``` + +applicationContext.xml文件中使用import方式导入有模块配置文件,以后若有新模块入加,那就可以简单修改这个文件了。这样大大简化了配置后期维护的复杂度,并使配置模块化,易于管理。我们来看看Spring是如何解析import配置文件的呢。解析import标签使用的是importBeanDefinitionResource(ele),进入方法体: + +```java +protected void importBeanDefinitionResource(Element ele) { + // 获取 resource 的属性值 + String location = ele.getAttribute(RESOURCE_ATTRIBUTE); + // 为空,直接退出 + if (!StringUtils.hasText(location)) { + getReaderContext().error("Resource location must not be empty", ele); + return; + } + + // 解析系统属性,格式如 :"${user.dir}" + location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); + + Set actualResources = new LinkedHashSet<>(4); + + // 判断 location 是相对路径还是绝对路径 + boolean absoluteLocation = false; + try { + absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); + } + catch (URISyntaxException ex) { + // cannot convert to an URI, considering the location relative + // unless it is the well-known Spring prefix "classpath*:" + } + + // 绝对路径 + if (absoluteLocation) { + try { + // 直接根据地址加载相应的配置文件 + int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]"); + } + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error( + "Failed to import bean definitions from URL location [" + location + "]", ele, ex); + } + } + else { + // 相对路径则根据相应的地址计算出绝对路径地址 + try { + int importCount; + Resource relativeResource = getReaderContext().getResource().createRelative(location); + if (relativeResource.exists()) { + importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); + actualResources.add(relativeResource); + } + else { + String baseLocation = getReaderContext().getResource().getURL().toString(); + importCount = getReaderContext().getReader().loadBeanDefinitions( + StringUtils.applyRelativePath(baseLocation, location), actualResources); + } + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]"); + } + } + catch (IOException ex) { + getReaderContext().error("Failed to resolve current resource location", ele, ex); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", + ele, ex); + } + } + // 解析成功后,进行监听器激活处理 + Resource[] actResArray = actualResources.toArray(new Resource[0]); + getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); +} +``` + +解析 import 过程较为清晰,整个过程如下: + +1. 获取 source 属性的值,该值表示资源的路径 +2. 解析路径中的系统属性,如”${user.dir}” +3. 判断资源路径 location 是绝对路径还是相对路径 +4. 如果是绝对路径,则调递归调用 Bean 的解析过程,进行另一次的解析 +5. 如果是相对路径,则先计算出绝对路径得到 Resource,然后进行解析 +6. 通知监听器,完成解析 + +**判断路径** + +方法通过以下方法来判断 location 是为相对路径还是绝对路径: + +```java +absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); +``` + +判断绝对路径的规则如下: + +- 以 classpath*: 或者 classpath: 开头为绝对路径 +- 能够通过该 location 构建出 `java.net.URL`为绝对路径 +- 根据 location 构造 `java.net.URI` 判断调用 `isAbsolute()` 判断是否为绝对路径 + +如果 location 为绝对路径则调用 `loadBeanDefinitions()`,该方法在 AbstractBeanDefinitionReader 中定义。 + +```java +public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws BeanDefinitionStoreException { + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader == null) { + throw new BeanDefinitionStoreException( + "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); + } + + if (resourceLoader instanceof ResourcePatternResolver) { + // Resource pattern matching available. + try { + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + int loadCount = loadBeanDefinitions(resources); + if (actualResources != null) { + for (Resource resource : resources) { + actualResources.add(resource); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]"); + } + return loadCount; + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "Could not resolve bean definition resource pattern [" + location + "]", ex); + } + } + else { + // Can only load single resources by absolute URL. + Resource resource = resourceLoader.getResource(location); + int loadCount = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); + } + return loadCount; + } +} +``` + + + +整个逻辑比较简单,首先获取 ResourceLoader,然后根据不同的 ResourceLoader 执行不同的逻辑,主要是可能存在多个 Resource,但是最终都会回归到 `XmlBeanDefinitionReader.loadBeanDefinitions()` ,所以这是一个递归的过程。 + +至此,import 标签解析完毕,整个过程比较清晰明了:获取 source 属性值,得到正确的资源路径,然后调用 `loadBeanDefinitions()` 方法进行递归的 BeanDefinition 加载。 \ No newline at end of file diff --git a/docs/source/spring/5-ioc-tag-custom.md.md b/docs/source/spring/5-ioc-tag-custom.md.md new file mode 100644 index 0000000..a902899 --- /dev/null +++ b/docs/source/spring/5-ioc-tag-custom.md.md @@ -0,0 +1,618 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +## 概述 + +之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示: + +![](http://img.topjavaer.cn/img/202309180850242.png) + +我们看到自定义标签的解析是通过BeanDefinitionParserDelegate.parseCustomElement(ele)进行的,解析来我们进行详细分析。 + + + +## 自定义标签的使用 + +扩展 Spring 自定义标签配置一般需要以下几个步骤: + +1. 创建一个需要扩展的组件 +2. 定义一个 XSD 文件,用于描述组件内容 +3. 创建一个实现 AbstractSingleBeanDefinitionParser 接口的类,用来解析 XSD 文件中的定义和组件定义 +4. 创建一个 Handler,继承 NamespaceHandlerSupport ,用于将组件注册到 Spring 容器 +5. 编写 Spring.handlers 和 Spring.schemas 文件 + +下面就按照上面的步骤来实现一个自定义标签组件。 + +### 创建组件 + +该组件就是一个普通的 JavaBean,没有任何特别之处。这里我创建了两个组件,为什么是两个,后面有用到 + +**User.java** + +```java +package dabin.spring01; + +public class User { + + private String id; + + private String userName; + + private String email;public void setId(String id) { + this.id = id; + }public void setUserName(String userName) { + this.userName = userName; + }public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"id\":\"") + .append(id).append('\"'); + sb.append(",\"userName\":\"") + .append(userName).append('\"'); + sb.append(",\"email\":\"") + .append(email).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +**Phone.java** + +```java +package dabin.spring01; + +public class Phone { + + private String color; + + private int size; + + private String remark; + + + public void setColor(String color) { + this.color = color; + } + + public void setSize(int size) { + this.size = size; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"color\":\"") + .append(color).append('\"'); + sb.append(",\"size\":") + .append(size); + sb.append(",\"remark\":\"") + .append(remark).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +### 定义 XSD 文件 + + + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +在上述XSD文件中描述了一个新的targetNamespace,并在这个空间里定义了一个name为**user**和**phone**的element 。user里面有三个attribute。主要是为了验证Spring配置文件中的自定义格式。再进一步解释,就是,Spring位置文件中使用的user自定义标签中,属性只能是上面的三种,有其他的属性的话,就会报错。 + + + +### Parser 类 + +定义一个 Parser 类,该类继承 **AbstractSingleBeanDefinitionParser** ,并实现 **`getBeanClass()`** 和 **`doParse()`** 两个方法。主要是用于解析 XSD 文件中的定义和组件定义。这里定义了两个Parser类,一个是解析User类,一个用来解析Phone类。 + +**UserBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +**PhoneBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return Phone.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String color = element.getAttribute("color"); + int size=Integer.parseInt(element.getAttribute("size")); + String remark=element.getAttribute("remark"); + if(StringUtils.hasText(color)){ + builder.addPropertyValue("color",color); + } + if(StringUtils.hasText(String.valueOf(size))){ + builder.addPropertyValue("size", size); + } + if(StringUtils.hasText(remark)){ + builder.addPropertyValue("remark", remark); + } + + } +} +``` + + + +### Handler 类 + +定义 Handler 类,继承 NamespaceHandlerSupport ,主要目的是将上面定义的解析器**Parser类**注册到 Spring 容器中。 + +```java +package dabin.spring01; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +我们看看 registerBeanDefinitionParser 方法做了什么 + +```java +private final Map parsers = new HashMap<>(); + +protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); +} +``` + +就是将解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的实例放到全局的Map中,key为user和phone。 + +### Spring.handlers和Spring.schemas + +编写Spring.handlers和Spring.schemas文件,默认位置放在工程的META-INF文件夹下 + +**Spring.handlers** + +``` +http\://www.dabin.com/schema/user=dabin.spring01.MyNamespaceHandler +``` + +**Spring.schemas** + +``` +http\://www.dabin.com/schema/user.xsd=org/user.xsd +``` + +而 Spring 加载自定义的大致流程是遇到自定义标签然后 就去 Spring.handlers 和 Spring.schemas 中去找对应的 handler 和 XSD ,默认位置是 META-INF 下,进而有找到对应的handler以及解析元素的 Parser ,从而完成了整个自定义元素的解析,也就是说 Spring 将向定义标签解析的工作委托给了 用户去实现。 + +### 创建测试配置文件 + +经过上面几个步骤,就可以使用自定义的标签了。在 xml 配置文件中使用如下: + +```xml + + + + + + + + + + +``` + +**xmlns:myTag**表示myTag的命名空间是 **http://www.dabin.com/schema/user ,**在文章开头的判断处 if (delegate.isDefaultNamespace(ele)) 肯定会返回false,将进入到自定义标签的解析 + +### 测试 + +``` +import dabin.spring01.MyTestBean; +import dabin.spring01.Phone; +import dabin.spring01.User; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean"); + User user = (User) bf.getBean("user"); + Phone iphone = (Phone) bf.getBean("iphone"); + + System.out.println(user); + System.out.println(iphone); + } + +} +``` + +输出结果: + +``` +("id":"user","userName":"dabin","email":"dabin@163. com”} +{"color":"black","size":128,"remark":"iphone XR"} +``` + + + +## 自定义标签的解析 + + 了解了自定义标签的使用后,接下来我们分析下自定义标签的解析,自定义标签解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),进入方法体: + +```java +public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + // 获取 标签对应的命名空间 + String namespaceUri = getNamespaceURI(ele); + if (namespaceUri == null) { + return null; + } + + // 根据 命名空间找到相应的 NamespaceHandler + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + + // 调用自定义的 Handler 处理 + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +``` + +相信了解了自定义标签的使用方法后,或多或少会对向定义标签的实现过程有一个自己的想法。其实思路非常的简单,无非是根据对应的bean 获取对应的命名空间 ,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析。 + + + +### 获取标签的命名空间 + + 标签的解析是从命名空间的提起开始的,元论是**区分 Spring中默认标签和自定义标** 还是 **区分自定义标签中不同标签的处理器**都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实并不需要我们亲内去实现,在 org.w3c.dom.Node 中已经提供了方法供我们直接调用: + +``` +String namespaceUri = getNamespaceURI(ele); +@Nullable +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} +``` + +这里我们可以通过DEBUG看出**myTag:user自定义标签对应的** namespaceUri 是 **http://www.dabin.com/schema/user** + + + +### 读取自定义标签处理器 + +根据 namespaceUri 获取 Handler,这个映射关系我们在 Spring.handlers 中已经定义了,所以只需要找到该类,然后初始化返回,最后调用该 Handler 对象的 `parse()` 方法处理,该方法我们也提供了实现。所以上面的核心就在于怎么找到该 Handler 类。调用方法为:`this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)` + +```java +public NamespaceHandler resolve(String namespaceUri) { + // 获取所有已经配置的 Handler 映射 + Map handlerMappings = getHandlerMappings(); + + // 根据 namespaceUri 获取 handler的信息:这里一般都是类路径 + Object handlerOrClassName = handlerMappings.get(namespaceUri); + if (handlerOrClassName == null) { + return null; + } + else if (handlerOrClassName instanceof NamespaceHandler) { + // 如果已经做过解析,直接返回 + return (NamespaceHandler) handlerOrClassName; + } + else { + String className = (String) handlerOrClassName; + try { + + Class handlerClass = ClassUtils.forName(className, this.classLoader); + if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { + throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); + } + + // 初始化类 + NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); + + // 调用 自定义NamespaceHandler 的init() 方法 + namespaceHandler.init(); + + // 记录在缓存 + handlerMappings.put(namespaceUri, namespaceHandler); + return namespaceHandler; + } + catch (ClassNotFoundException ex) { + throw new FatalBeanException("Could not find NamespaceHandler class [" + className + + "] for namespace [" + namespaceUri + "]", ex); + } + catch (LinkageError err) { + throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + + className + "] for namespace [" + namespaceUri + "]", err); + } + } +} +``` + +首先调用 `getHandlerMappings()` 获取所有配置文件中的映射关系 handlerMappings ,就是我们在 Spring.handlers 文件中配置 命名空间与命名空间处理器的映射关系,该关系为 <命名空间,类路径>,然后根据命名空间 namespaceUri 从映射关系中获取相应的信息,如果为空或者已经初始化了就直接返回,否则根据反射对其进行初始化,同时调用其 `init()`方法,最后将该 Handler 对象缓存。我们再次回忆下示例中对于命名空间处理器的内容: + +```java +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +当得到自定义命名空间处理后会马上执行 namespaceHandler.init() 来进行自定义 BeanDefinitionParser的注册,在这里,你可以注册多个标签解析器,如当前示例中 parsers` 。 + +### 标签解析 + +得到了解析器和分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了,对于标签的解析使用的是:NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd))方法,进入到方法体内: + +```java +public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionParser parser = findParserForElement(element, parserContext); + return (parser != null ? parser.parse(element, parserContext) : null); +} +``` + +调用 `findParserForElement()` 方法获取 BeanDefinitionParser 实例,其实就是获取在 `init()` 方法里面注册的实例对象。如下: + +```java +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + //获取元素名称,也就是 beanClass = getBeanClass(element); + if (beanClass != null) { + builder.getRawBeanDefinition().setBeanClass(beanClass); + } + else { + // beanClass 为 null,意味着子类并没有重写 getBeanClass() 方法,则尝试去判断是否重写了 getBeanClassName() + String beanClassName = getBeanClassName(element); + if (beanClassName != null) { + builder.getRawBeanDefinition().setBeanClassName(beanClassName); + } + } + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); + if (containingBd != null) { + // Inner bean definition must receive same scope as containing bean. + builder.setScope(containingBd.getScope()); + } + if (parserContext.isDefaultLazyInit()) { + // Default-lazy-init applies to custom bean definitions as well. + builder.setLazyInit(true); + } + + // 调用子类的 doParse() 进行解析 + doParse(element, parserContext, builder); + return builder.getBeanDefinition(); +} + +public static BeanDefinitionBuilder genericBeanDefinition() { + return new BeanDefinitionBuilder(new GenericBeanDefinition()); +} + +protected Class getBeanClass(Element element) { + return null; +} + +protected void doParse(Element element, BeanDefinitionBuilder builder) { +} +``` + +在该方法中我们主要关注两个方法:**`getBeanClass()` 、`doParse()`**。对于 `getBeanClass()` 方法,AbstractSingleBeanDefinitionParser 类并没有提供具体实现,而是直接返回 null,意味着它希望子类能够重写该方法,当然如果没有重写该方法,这会去调用 `getBeanClassName()` ,判断子类是否已经重写了该方法。对于 `doParse()` 则是直接空实现。所以对于 `parseInternal()` 而言它总是期待它的子类能够实现 `getBeanClass()`、`doParse()`,其中 `doParse()` 尤为重要,如果你不提供实现,怎么来解析自定义标签呢?最后将自定义的解析器:UserDefinitionParser 再次回观。 + +```java +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +我们看看 builder.addPropertyValue ("id",id) ,实际上是将自定义标签中的属性解析,存入 BeanDefinitionBuilder 中的 beanDefinition实例中 + +``` +private final AbstractBeanDefinition beanDefinition; + +public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) { + this.beanDefinition.getPropertyValues().add(name, value); + return this; +} +``` + +最后 将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册 registerBeanDefinition(holder, parserContext.getRegistry());这就和默认标签的注册是一样了。 + +至此,自定义标签的解析过程已经分析完成了。其实整个过程还是较为简单:首先会加载 handlers 文件,将其中内容进行一个解析,形成 这样的一个映射,然后根据获取的 namespaceUri 就可以得到相应的类路径,对其进行初始化等到相应的 Handler 对象,调用 `parse()` 方法,在该方法中根据标签的 localName 得到相应的 BeanDefinitionParser 实例对象,调用 `parse()` ,该方法定义在 AbstractBeanDefinitionParser 抽象类中,核心逻辑封装在其 `parseInternal()` 中,该方法返回一个 AbstractBeanDefinition 实例对象,其主要是在 AbstractSingleBeanDefinitionParser 中实现,对于自定义的 Parser 类,其需要实现 `getBeanClass()` 或者 `getBeanClassName()` 和 `doParse()`。最后将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册。 \ No newline at end of file diff --git a/docs/source/spring/6-bean-load.md b/docs/source/spring/6-bean-load.md new file mode 100644 index 0000000..be0683e --- /dev/null +++ b/docs/source/spring/6-bean-load.md @@ -0,0 +1,665 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean加载,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +## 概述 + +前面我们已经分析了spring对于xml配置文件的解析,将分析的信息组装成 BeanDefinition,并将其保存注册到相应的 BeanDefinitionRegistry 中。至此,Spring IOC 的初始化工作完成。接下来我们将对bean的加载进行探索。 + +## BeanFactory + +当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + } +} +``` + +我们看到这个方法是在接口BeanFactory中定义的,我们看下BeanFactory体系结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309200725956.png) + +从上图我们看到:   + +(1)BeanFactory作为一个主接口不继承任何接口,暂且称为一级接口。 + +(2)有3个子接口继承了它,进行功能上的增强。这3个子接口称为二级接口。 + +(3)ConfigurableBeanFactory可以被称为三级接口,对二级接口HierarchicalBeanFactory进行了再次增强,它还继承了另一个外来的接口SingletonBeanRegistry + +(4)ConfigurableListableBeanFactory是一个更强大的接口,继承了上述的所有接口,无所不包,称为四级接口。(这4级接口是BeanFactory的基本接口体系。 + +(5)AbstractBeanFactory作为一个抽象类,实现了三级接口ConfigurableBeanFactory大部分功能。 + +(6)AbstractAutowireCapableBeanFactory同样是抽象类,继承自AbstractBeanFactory,并额外实现了二级接口AutowireCapableBeanFactory + +(7)DefaultListableBeanFactory继承自AbstractAutowireCapableBeanFactory,实现了最强大的四级接口ConfigurableListableBeanFactory,并实现了一个外来接口BeanDefinitionRegistry,它并非抽象类。 + +(8)最后是最强大的XmlBeanFactory,继承自DefaultListableBeanFactory,重写了一些功能,使自己更强大。 + + + +#### 定义 + +BeanFactory,以Factory结尾,表示它是一个工厂类(接口), **它负责生产和管理bean的一个工厂**。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,其中**XmlBeanFactory就是常用的一个,该实现将以XML方式描述组成应用的对象及对象间的依赖关系**。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。 + +BeanFactory是Spring IOC容器的鼻祖,是IOC容器的基础接口,所有的容器都是从它这里继承实现而来。可见其地位。BeanFactory提供了最基本的IOC容器的功能,即所有的容器至少需要实现的标准。 + +XmlBeanFactory,只是提供了最基本的IOC容器的功能。而且XMLBeanFactory,继承自DefaultListableBeanFactory。DefaultListableBeanFactory实际包含了基本IOC容器所具有的所有重要功能,是一个完整的IOC容器。 + +ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先。 + +BeanFactory体系结构是典型的工厂方法模式,即什么样的工厂生产什么样的产品。BeanFactory是最基本的抽象工厂,而其他的IOC容器只不过是具体的工厂,对应着各自的Bean定义方法。但同时,其他容器也针对具体场景不同,进行了扩充,提供具体的服务。 如下: + +```java +Resource resource = new FileSystemResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ClassPathResource resource = new ClassPathResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"applicationContext.xml"}); +BeanFactory factory = (BeanFactory) context; +``` + +基本就是这些了,接着使用getBean(String beanName)方法就可以取得bean的实例;BeanFactory提供的方法及其简单,仅提供了六种方法供客户调用: + +- boolean containsBean(String beanName) 判断工厂中是否包含给定名称的bean定义,若有则返回true +- Object getBean(String) 返回给定名称注册的bean实例。根据bean的配置情况,如果是singleton模式将返回一个共享实例,否则将返回一个新建的实例,如果没有找到指定bean,该方法可能会抛出异常 +- Object getBean(String, Class) 返回以给定名称注册的bean实例,并转换为给定class类型 +- Class getType(String name) 返回给定名称的bean的Class,如果没有找到指定的bean实例,则排除NoSuchBeanDefinitionException异常 +- boolean isSingleton(String) 判断给定名称的bean定义是否为单例模式 +- String[] getAliases(String name) 返回给定bean名称的所有别名 + +```java +package org.springframework.beans.factory; +import org.springframework.beans.BeansException; +public interface BeanFactory { + String FACTORY_BEAN_PREFIX = "&"; + Object getBean(String name) throws BeansException; + T getBean(String name, Class requiredType) throws BeansException; + T getBean(Class requiredType) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; + boolean containsBean(String name); + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; + boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException; + Class getType(String name) throws NoSuchBeanDefinitionException; + String[] getAliases(String name); +} +``` + + + +## FactoryBean + +一般情况下,Spring通过反射机制利用``的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为`FactoryBean`的形式。 + +以Bean结尾,表示它是一个Bean,不同于普通Bean的是:**它是实现了`FactoryBean`接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取**。 + +```java +package org.springframework.beans.factory; +public interface FactoryBean { + T getObject() throws Exception; + Class getObjectType(); + boolean isSingleton(); +} +``` + +在该接口中还定义了以下3个方法: + +- **T getObject()**:返回由FactoryBean创建的Bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中; +- **boolean isSingleton()**:返回由FactoryBean创建的Bean实例的作用域是singleton还是prototype; +- `Class getObjectType()`:返回FactoryBean创建的Bean类型。 + +当配置文件中``的class属性配置的实现类是FactoryBean时,通过getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。 +例:如果使用传统方式配置下面Car的``时,Car的每个属性分别对应一个``元素标签。 + +```java +public class Car { + private int maxSpeed ; + private String brand ; + private double price ; + //get//set 方法 +} +``` + + + +如果用FactoryBean的方式实现就灵活点,下例通过逗号分割符的方式一次性的为Car的所有属性指定配置值: + +```java +import org.springframework.beans.factory.FactoryBean; +public class CarFactoryBean implements FactoryBean { + private String carInfo ; + public Car getObject() throws Exception { + Car car = new Car(); + String[] infos = carInfo.split(","); + car.setBrand(infos[0]); + car.setMaxSpeed(Integer.valueOf(infos[1])); + car.setPrice(Double.valueOf(infos[2])); + return car; + } + public Class getObjectType(){ + return Car.class ; + } + public boolean isSingleton(){ + return false ; + } + public String getCarInfo(){ + return this.carInfo; + } + + //接受逗号分割符设置属性信息 + public void setCarInfo (String carInfo){ + this.carInfo = carInfo; + } +} +``` + +有了这个CarFactoryBean后,就可以在配置文件中使用下面这种自定义的配置方式配置CarBean了: + +```xml + +``` + +当调用getBean("car")时,Spring通过反射机制发现CarFactoryBean实现了FactoryBean的接口,这时Spring容器就调用接口方法CarFactoryBean#getObject()方法返回。如果希望获取CarFactoryBean的实例,则需要在使用getBean(beanName)方法时在beanName前显示的加上"&"前缀:如getBean("&car"); + +## 获取bean + +接下来我们回到加载bean的阶段,当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public Object getBean(String name) throws BeansException { + return doGetBean(name, null, null, false); +} +``` + +内部调用 `doGetBean()` 方法,这个方法的代码比较长,各位耐心看下: + +```java +@SuppressWarnings("unchecked") +protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + //获取 beanName,这里是一个转换动作,将 name 转换为 beanName + final String beanName = transformedBeanName(name); + Object bean; + /* + *检查缓存中的实例工程是否存在对应的实例 + *为何要优先使用这段代码呢? + *因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖 + *spring创建bean的原则是在不等bean创建完就会将创建bean的objectFactory提前曝光,即将其加入到缓存中,一旦下个bean创建时依赖上个bean则直接使用objectFactory + *直接从缓存中或singletonFactories中获取objectFactory + *就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + */ + // Eagerly check singleton cache for manually registered singletons. + Object sharedInstance = getSingleton(beanName); + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + if (isSingletonCurrentlyInCreation(beanName)) { + logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + + "' that is not fully initialized yet - a consequence of a circular reference"); + } + else { + logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); + } + } + //返回对应的实例,有些时候并不是直接返回实例,而是返回某些方法返回的实例 + //这里涉及到我们上面讲的FactoryBean,如果此Bean是FactoryBean的实现类,如果name前缀为"&",则直接返回此实现类的bean,如果没有前缀"&",则需要调用此实现类的getObject方法,返回getObject里面真是的返回对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + else { + //只有在单例的情况下才会解决循环依赖 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + //尝试从parentBeanFactory中查找bean + BeanFactory parentBeanFactory = getParentBeanFactory(); + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + String nameToLookup = originalBeanName(name); + if (parentBeanFactory instanceof AbstractBeanFactory) { + return ((AbstractBeanFactory) parentBeanFactory).doGetBean( + nameToLookup, requiredType, args, typeCheckOnly); + } + else if (args != null) { + // Delegation to parent with explicit args. + return (T) parentBeanFactory.getBean(nameToLookup, args); + } + else { + // No args -> delegate to standard getBean method. + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + } + //如果不是仅仅做类型检查,则这里需要创建bean,并做记录 + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + try { + //将存储XML配置文件的GenericBeanDefinition转换为RootBeanDefinition,同时如果存在父bean的话则合并父bean的相关属性 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + //如果存在依赖则需要递归实例化依赖的bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + if (isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + registerDependentBean(dep, beanName); + try { + getBean(dep); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "'" + beanName + "' depends on missing bean '" + dep + "'", ex); + } + } + } + + // 单例模式 + // 实例化依赖的bean后对bean本身进行实例化 + if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + // 原型模式 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + // 从指定的 scope 下创建 bean + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + return convertedBean; + } + catch (TypeMismatchException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert bean '" + name + "' to required type '" + + ClassUtils.getQualifiedName(requiredType) + "'", ex); + } + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } + return (T) bean; +} +``` + +代码是相当长,处理逻辑也是相当复杂,下面将其进行拆分讲解。 + +### 获取 beanName + +```java +final String beanName = transformedBeanName(name); +``` + +这里传递的是 name,不一定就是 beanName,可能是 aliasName,也有可能是 FactoryBean(带“&”前缀),所以这里需要调用 `transformedBeanName()` 方法对 name 进行一番转换,主要如下: + +```java +protected String transformedBeanName(String name) { + return canonicalName(BeanFactoryUtils.transformedBeanName(name)); +} + +// 去除 FactoryBean 的修饰符 +public static String transformedBeanName(String name) { + Assert.notNull(name, "'name' must not be null"); + String beanName = name; + while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + } + return beanName; +} + +// 转换 aliasName +public String canonicalName(String name) { + String canonicalName = name; + // Handle aliasing... + String resolvedName; + do { + resolvedName = this.aliasMap.get(canonicalName); + if (resolvedName != null) { + canonicalName = resolvedName; + } + } + while (resolvedName != null); + return canonicalName; +} +``` + +主要处理过程包括两步: + +1. 去除 FactoryBean 的修饰符。如果 name 以 “&” 为前缀,那么会去掉该 “&”,例如,`name = "&studentService"`,则会是 `name = "studentService"`。 +2. 取指定的 alias 所表示的最终 beanName。主要是一个循环获取 beanName 的过程,例如别名 A 指向名称为 B 的 bean 则返回 B,若 别名 A 指向别名 B,别名 B 指向名称为 C 的 bean,则返回 C。 + + + +### 缓存中获取单例bean + +单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例缓存中获取,当然这里也只是尝试加载,首先尝试从缓存中加载,然后再次尝试从singletonFactorry加载因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖,Spring创建bean的原则不等bean创建完成就会创建bean的ObjectFactory提早曝光加入到缓存中,一旦下一个bean创建时需要依赖上个bean,则直接使用ObjectFactory;就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到。接下来我们看下获取单例bean的方法getSingleton(beanName),进入方法体: + +```java +@Override +@Nullable +public Object getSingleton(String beanName) { + //参数true是允许早期依赖 + return getSingleton(beanName, true); +} +@Nullable +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + //检查缓存中是否存在实例,这里就是上面说的单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + Object singletonObject = this.singletonObjects.get(beanName); + //如果缓存为空且单例bean正在创建中,则锁定全局变量,为什么要判断bean在创建中呢?这里就是可以判断是否循环依赖了。 + //A依赖B,B也依赖A,A实例化的时候,发现依赖B,则递归去实例化B,B发现依赖A,则递归实例化A,此时会走到原点A的实例化,第一次A的实例化还没完成,只不过把实例化的对象加入到缓存中,但是状态还是正在创建中,由此回到原点发现A正在创建中,由此可以判断是循环依赖了 + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + //如果此bean正在加载,则不处理 + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + //当某些方法需要提前初始化的时候会直接调用addSingletonFactory把对应的ObjectFactory初始化策略存储在singletonFactory中 + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + //使用预先设定的getObject方法 + singletonObject = singletonFactory.getObject(); + 记录在缓存中,注意earlySingletonObjects和singletonFactories是互斥的 + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +接下来我们根据源码再来梳理下这个方法,这样更易于理解,这个方法先尝试从singletonObjects里面获取实例,如果如果获取不到再从earlySingletonObjects里面获取,如果还获取不到,再尝试从singletonFactories里面获取beanName对应的ObjectFactory,然后再调用这个ObjectFactory的getObject方法创建bean,并放到earlySingletonObjects里面去,并且从singletonFactoryes里面remove调这个ObjectFactory,而对于后续所有的内存操作都只为了循环依赖检测时候使用,即allowEarlyReference为true的时候才会使用。 +这里涉及到很多个存储bean的不同map,简单解释下: + +1.singletonObjects:用于保存BeanName和创建bean实例之间的关系,beanName–>bean Instance + +2.singletonFactories:用于保存BeanName和创建bean的工厂之间的关系,banName–>ObjectFactory + +3.earlySingletonObjects:也是保存BeanName和创建bean实例之间的关系,与singletonObjects的不同之处在于,当一个单例bean被放到这里面后,那么当bean还在创建过程中,就可以通过getBean方法获取到了,其目的是用来检测循环引用。 + +4.registeredSingletons:用来保存当前所有已注册的bean. + + + +### 从bean的实例中获取对象 + +获取到bean以后就要获取实例对象了,这里用到的是getObjectForBeanInstance方法。getObjectForBeanInstance是个频繁使用的方法,无论是从缓存中获得bean还是根据不同的scope策略加载bean.总之,我们得到bean的实例后,要做的第一步就是调用这个方法来检测一下正确性,其实就是检测获得Bean是不是FactoryBean类型的bean,如果是,那么需要调用该bean对应的FactoryBean实例中的getObject()作为返回值。接下来我们看下此方法的源码: + +```java +protected Object getObjectForBeanInstance( + Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) { + //如果指定的name是工厂相关的(以&开头的) + if (BeanFactoryUtils.isFactoryDereference(name)) { + //如果是NullBean则直接返回此bean + if (beanInstance instanceof NullBean) { + return beanInstance; + } + //如果不是FactoryBean类型,则验证不通过抛出异常 + if (!(beanInstance instanceof FactoryBean)) { + throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); + } + } + // Now we have the bean instance, which may be a normal bean or a FactoryBean. + // If it's a FactoryBean, we use it to create a bean instance, unless the + // caller actually wants a reference to the factory. + //如果获取的beanInstance不是FactoryBean类型,则说明是普通的Bean,可直接返回 + //如果获取的beanInstance是FactoryBean类型,但是是以(以&开头的),也直接返回,此时返回的是FactoryBean的实例 + if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { + return beanInstance; + } + Object object = null; + if (mbd == null) { + object = getCachedObjectForFactoryBean(beanName); + } + if (object == null) { + // Return bean instance from factory. + FactoryBean factory = (FactoryBean) beanInstance; + // Caches object obtained from FactoryBean if it is a singleton. + if (mbd == null && containsBeanDefinition(beanName)) { + mbd = getMergedLocalBeanDefinition(beanName); + } + boolean synthetic = (mbd != null && mbd.isSynthetic()); + //到了这里说明获取的beanInstance是FactoryBean类型,但没有以"&"开头,此时就要返回factory内部getObject里面的对象了 + object = getObjectFromFactoryBean(factory, beanName, !synthetic); + } + return object; +} +``` + +接着我们来看看真正的核心功能getObjectFromFactoryBean(factory, beanName, !synthetic)方法中实现的,继续跟进代码: + +```java +protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + // 为单例模式且缓存中存在 + if (factory.isSingleton() && containsSingleton(beanName)) { + + synchronized (getSingletonMutex()) { + // 从缓存中获取指定的 factoryBean + Object object = this.factoryBeanObjectCache.get(beanName); + + if (object == null) { + // 为空,则从 FactoryBean 中获取对象 + object = doGetObjectFromFactoryBean(factory, beanName); + + // 从缓存中获取 + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + // 需要后续处理 + if (shouldPostProcess) { + // 若该 bean 处于创建中,则返回非处理对象,而不是存储它 + if (isSingletonCurrentlyInCreation(beanName)) { + return object; + } + // 前置处理 + beforeSingletonCreation(beanName); + try { + // 对从 FactoryBean 获取的对象进行后处理 + // 生成的对象将暴露给bean引用 + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + } + // 缓存 + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } + } + } + return object; + } + } + else { + // 非单例 + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); + } + } + return object; + } +} +``` + +该方法应该就是创建 bean 实例对象中的核心方法之一了。这里我们关注三个方法:`beforeSingletonCreation()` 、 `afterSingletonCreation()` 、 `postProcessObjectFromFactoryBean()`。可能有小伙伴觉得前面两个方法不是很重要,LZ 可以肯定告诉你,这两方法是非常重要的操作,因为他们记录着 bean 的加载状态,是检测当前 bean 是否处于创建中的关键之处,对解决 bean 循环依赖起着关键作用。before 方法用于标志当前 bean 处于创建中,after 则是移除。其实在这篇博客刚刚开始就已经提到了 `isSingletonCurrentlyInCreation()` 是用于检测当前 bean 是否处于创建之中,如下: + +```java +public boolean isSingletonCurrentlyInCreation(String beanName) { + return this.singletonsCurrentlyInCreation.contains(beanName); +} +``` + +是根据 singletonsCurrentlyInCreation 集合中是否包含了 beanName,集合的元素则一定是在 `beforeSingletonCreation()` 中添加的,如下: + +```java +protected void beforeSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } +} +``` + +`afterSingletonCreation()` 为移除,则一定就是对 singletonsCurrentlyInCreation 集合 remove 了,如下: + +```java +protected void afterSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) { + throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation"); + } +} +``` + +我们再来看看真正的核心方法 doGetObjectFromFactoryBean + +```java +private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName) + throws BeanCreationException { + + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + object = AccessController.doPrivileged((PrivilegedExceptionAction) factory::getObject, acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + object = factory.getObject(); + } + } + catch (FactoryBeanNotInitializedException ex) { + throw new BeanCurrentlyInCreationException(beanName, ex.toString()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); + } + + // Do not accept a null value for a FactoryBean that's not fully + // initialized yet: Many FactoryBeans just return null then. + if (object == null) { + if (isSingletonCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException( + beanName, "FactoryBean which is currently in creation returned null from getObject"); + } + object = new NullBean(); + } + return object; +} +``` + +以前我们曾经介绍过FactoryBean的调用方法,如果bean声明为FactoryBean类型,则当提取bean时候提取的不是FactoryBean,而是FactoryBean中对应的getObject方法返回的bean,而doGetObjectFromFactroyBean真是实现这个功能。 + +而调用完doGetObjectFromFactoryBean方法后,并没有直接返回,getObjectFromFactoryBean方法中还调用了object = postProcessObjectFromFactoryBean(object, beanName);方法,在子类AbstractAutowireCapableBeanFactory,有这个方法的实现: + +```java +@Override +protected Object postProcessObjectFromFactoryBean(Object object, String beanName) { + return applyBeanPostProcessorsAfterInitialization(object, beanName); +} +@Override +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + Object current = beanProcessor.postProcessAfterInitialization(result, beanName); + if (current == null) { + return result; + } + result = current; + } + return result; +} +``` + +对于后处理器的使用,我们目前还没接触,后续会有大量篇幅介绍,这里我们只需要了解在Spring获取bean的规则中有这样一条:尽可能保证所有bean初始化后都会调用注册的BeanPostProcessor的postProcessAfterInitialization方法进行处理,在实际开发过程中大可以针对此特性设计自己的业务处理。 \ No newline at end of file diff --git a/docs/source/spring/7-bean-build.md b/docs/source/spring/7-bean-build.md new file mode 100644 index 0000000..6e37369 --- /dev/null +++ b/docs/source/spring/7-bean-build.md @@ -0,0 +1,718 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Bean创建,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + +**正文** + +在 Spring 中存在着不同的 scope,默认是 singleton ,还有 prototype、request 等等其他的 scope,他们的初始化步骤是怎样的呢?这个答案在这篇博客中给出。 + +## singleton + +Spring 的 scope 默认为 singleton,第一部分分析了从缓存中获取单例模式的 bean,但是如果缓存中不存在呢?则需要从头开始加载 bean,这个过程由 `getSingleton()` 实现。其初始化的代码如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +这里我们看到了 java8的新特性lambda表达式 () -> , getSingleton方法的第二个参数为 `ObjectFactory singletonFactory,() ->`相当于创建了一个ObjectFactory类型的匿名内部类,去实现ObjectFactory接口中的getObject()方法,其中{}中的代码相当于写在匿名内部类中getObject()的代码片段,等着getSingleton方法里面通过ObjectFactory singletonFactory去显示调用,如singletonFactory.getObject()。上述代码可以反推成如下代码: + +```java +sharedInstance = getSingleton(beanName, new ObjectFactory() { + @Override + public Object getObject() { + try { + return createBean(beanName, mbd, args); + } catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + } +}); +``` + +下面我们进入到 **getSingleton**方法中 + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + + // 全局加锁 + synchronized (this.singletonObjects) { + // 从缓存中检查一遍 + // 因为 singleton 模式其实就是复用已经创建的 bean 所以这步骤必须检查 + Object singletonObject = this.singletonObjects.get(beanName); + // 为空,开始加载过程 + if (singletonObject == null) { + // 省略 部分代码 + + // 加载前置处理 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + // 省略代码 + try { + // 初始化 bean + // 这个过程就是我上面讲的调用匿名内部类的方法,其实是调用 createBean() 方法 + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + // 省略 catch 部分 + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + // 加入缓存中 + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + // 直接返回 + return singletonObject; + } +} +``` + +上述代码中其实,使用了回调方法,使得程序可以在单例创建的前后做一些准备及处理操作,而真正获取单例bean的方法其实并不是在此方法中实现的,其实现逻辑是在ObjectFactory类型的实例singletonFactory中实现的(即上图贴上的第一段代码)。而这些准备及处理操作包括如下内容。 + +(1)检查缓存是否已经加载过 + +(2)如果没有加载,则记录beanName的正在加载状态 + +(3)加载单例前记录加载状态。 可能你会觉得beforeSingletonCreation方法是个空实现,里面没有任何逻辑,但其实这个函数中做了一个很重要的操作:记录加载状态,也就是通过this.singletonsCurrentlyInCreation.add(beanName)将当前正要创建的bean记录在缓存中,这样便可以对循环依赖进行检测。 我们上一篇文章已经讲过,可以去看看。 + +(4)通过调用参数传入的ObjectFactory的个体Object方法实例化bean + +(5)加载单例后的处理方法调用。 同步骤3的记录加载状态相似,当bean加载结束后需要移除缓存中对该bean的正在加载状态的记录。 + +(6)将结果记录至缓存并删除加载bean过程中所记录的各种辅助状态。 + +(7)返回处理结果 + +我们看另外一个方法 `addSingleton()`。 + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +一个 put、一个 add、两个 remove。singletonObjects 单例 bean 的缓存,singletonFactories 单例 bean Factory 的缓存,earlySingletonObjects “早期”创建的单例 bean 的缓存,registeredSingletons 已经注册的单例缓存。 + +加载了单例 bean 后,调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。该方法我们在上一篇中已经讲过。 + +## 原型模式 + +```java +else if (mbd.isPrototype()) { + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); +} +``` + +原型模式的初始化过程很简单:直接创建一个新的实例就可以了。过程如下: + +1. 调用 `beforeSingletonCreation()` 记录加载原型模式 bean 之前的加载状态,即前置处理。 +2. 调用 `createBean()` 创建一个 bean 实例对象。 +3. 调用 `afterSingletonCreation()` 进行加载原型模式 bean 后的后置处理。 +4. 调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。 + + + +## 其他作用域 + +```java +String scopeName = mbd.getScope(); +final Scope scope = this.scopes.get(scopeName); +if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); +} +try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); +} +catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); +} +``` + +核心流程和原型模式一样,只不过获取 bean 实例是由 `scope.get()` 实现,如下: + +```java +public Object get(String name, ObjectFactory objectFactory) { + // 获取 scope 缓存 + Map scope = this.threadScope.get(); + Object scopedObject = scope.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + // 加入缓存 + scope.put(name, scopedObject); + } + return scopedObject; +} +``` + +对于上面三个模块,其中最重要的方法,是 `createBean(),也就是核心创建bean的过程,下面我们来具体看看。` + +## 准备创建bean + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +如上所示,createBean是真正创建bean的地方,此方法是定义在AbstractAutowireCapableBeanFactory中,我们看下其源码: + +```java +protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + throws BeanCreationException { + + if (logger.isDebugEnabled()) { + logger.debug("Creating instance of bean '" + beanName + "'"); + } + RootBeanDefinition mbdToUse = mbd; + + // 确保此时的 bean 已经被解析了 + // 如果获取的class 属性不为null,则克隆该 BeanDefinition + // 主要是因为该动态解析的 class 无法保存到到共享的 BeanDefinition + Class resolvedClass = resolveBeanClass(mbd, beanName); + if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { + mbdToUse = new RootBeanDefinition(mbd); + mbdToUse.setBeanClass(resolvedClass); + } + + try { + // 验证和准备覆盖方法 + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } + + try { + // 给 BeanPostProcessors 一个机会用来返回一个代理类而不是真正的类实例 + // AOP 的功能就是基于这个地方 + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + catch (Throwable ex) { + throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, + "BeanPostProcessor before instantiation of bean failed", ex); + } + + try { + // 执行真正创建 bean 的过程 + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + if (logger.isDebugEnabled()) { + logger.debug("Finished creating instance of bean '" + beanName + "'"); + } + return beanInstance; + } + catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); + } +} +``` + +### 实例化的前置处理 + +`resolveBeforeInstantiation()` 的作用是给 BeanPostProcessors 后置处理器返回一个代理对象的机会,其实在调用该方法之前 Spring 一直都没有创建 bean ,那么这里返回一个 bean 的代理类有什么作用呢?作用体现在后面的 `if` 判断: + +```java +if (bean != null) { + return bean; +} +``` + +如果代理对象不为空,则直接返回代理对象,这一步骤有非常重要的作用,Spring 后续实现 AOP 就是基于这个地方判断的。 + +```java +protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + Object bean = null; + if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + Class targetType = determineTargetType(beanName, mbd); + if (targetType != null) { + bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); + if (bean != null) { + bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); + } + } + } + mbd.beforeInstantiationResolved = (bean != null); + } + return bean; +} +``` + +这个方法核心就在于 `applyBeanPostProcessorsBeforeInstantiation()` 和 `applyBeanPostProcessorsAfterInitialization()` 两个方法,before 为实例化前的后处理器应用,after 为实例化后的后处理器应用,由于本文的主题是创建 bean,关于 Bean 的增强处理后续 LZ 会单独出博文来做详细说明。 + +### 创建 bean + +如果没有代理对象,就只能走常规的路线进行 bean 的创建了,该过程有 `doCreateBean()` 实现,如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // BeanWrapper是对Bean的包装,其接口中所定义的功能很简单包括设置获取被包装的对象,获取被包装bean的属性描述器 + BeanWrapper instanceWrapper = null; + // 单例模型,则从未完成的 FactoryBean 缓存中删除 + if (mbd.isSingleton()) {anceWrapper = this.factoryBeanInstanceCache.remove(beanName); + } + + // 使用合适的实例化策略来创建新的实例:工厂方法、构造函数自动注入、简单初始化 + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + // 包装的实例对象 + final Object bean = instanceWrapper.getWrappedInstance(); + // 包装的实例对象的类型 + Class beanType = instanceWrapper.getWrappedClass(); + if (beanType != NullBean.class) { + mbd.resolvedTargetType = beanType; + } + + // 检测是否有后置处理 + // 如果有后置处理,则允许后置处理修改 BeanDefinition + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + // applyMergedBeanDefinitionPostProcessors + // 后置处理修改 BeanDefinition + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + // 解决单例模式的循环依赖 + // 单例模式 & 允许循环依赖&当前单例 bean 是否正在被创建 + boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); + if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + // 提前将创建的 bean 实例加入到ObjectFactory 中 + // 这里是为了后期避免循环依赖 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } + + /* + * 开始初始化 bean 实例对象 + */ + Object exposedObject = bean; + try { + // 对 bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 bean 的属性 + // 则会递归初始依赖 bean + populateBean(beanName, mbd, instanceWrapper); + // 调用初始化方法,比如 init-method + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + catch (Throwable ex) { + if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { + throw (BeanCreationException) ex; + } + else { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); + } + } + + /** + * 循环依赖处理 + */ + if (earlySingletonExposure) { + // 获取 earlySingletonReference + Object earlySingletonReference = getSingleton(beanName, false); + // 只有在存在循环依赖的情况下,earlySingletonReference 才不会为空 + if (earlySingletonReference != null) { + // 如果 exposedObject 没有在初始化方法中被改变,也就是没有被增强 + if (exposedObject == bean) { + exposedObject = earlySingletonReference; + } + // 处理依赖 + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { + String[] dependentBeans = getDependentBeans(beanName); + Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + for (String dependentBean : dependentBeans) { + if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { + actualDependentBeans.add(dependentBean); + } + } + if (!actualDependentBeans.isEmpty()) { + throw new BeanCurrentlyInCreationException(beanName, + "Bean with name '" + beanName + "' has been injected into other beans [" + + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + + "] in its raw version as part of a circular reference, but has eventually been " + + "wrapped. This means that said other beans do not use the final version of the " + + "bean. This is often the result of over-eager type matching - consider using " + + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); + } + } + } + } + try { + // 注册 bean + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); + } + + return exposedObject; +} +``` + +大概流程如下: + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +#### createBeanInstance + +我们首先从createBeanInstance方法开始。方法代码如下: + +```java +protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { + // 解析 bean,将 bean 类名解析为 class 引用 + Class beanClass = resolveBeanClass(mbd, beanName); + + if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); + } + + // 如果存在 Supplier 回调,则使用给定的回调方法初始化策略 + Supplier instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName); + } + + // 如果工厂方法不为空,则使用工厂方法初始化策略,这里推荐看Factory-Method实例化Bean + if (mbd.getFactoryMethodName() != null) { + return instantiateUsingFactoryMethod(beanName, mbd, args); + } + + boolean resolved = false; + boolean autowireNecessary = false; + if (args == null) { + // constructorArgumentLock 构造函数的常用锁 + synchronized (mbd.constructorArgumentLock) { + // 如果已缓存的解析的构造函数或者工厂方法不为空,则可以利用构造函数解析 + // 因为需要根据参数确认到底使用哪个构造函数,该过程比较消耗性能,所有采用缓存机制 + if (mbd.resolvedConstructorOrFactoryMethod != null) { + resolved = true; + autowireNecessary = mbd.constructorArgumentsResolved; + } + } + } + // 已经解析好了,直接注入即可 + if (resolved) { + // 自动注入,调用构造函数自动注入 + if (autowireNecessary) { + return autowireConstructor(beanName, mbd, null, null); + } + else { + // 使用默认构造函数构造 + return instantiateBean(beanName, mbd); + } + } + + // 确定解析的构造函数 + // 主要是检查已经注册的 SmartInstantiationAwareBeanPostProcessor + Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); + if (ctors != null || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || + mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { + // 构造函数自动注入 + return autowireConstructor(beanName, mbd, ctors, args); + } + + //使用默认构造函数注入 + return instantiateBean(beanName, mbd); +} +``` + +实例化 bean 是一个复杂的过程,其主要的逻辑为: + +- 如果存在 Supplier 回调,则调用 `obtainFromSupplier()` 进行初始化 +- 如果存在工厂方法,则使用工厂方法进行初始化 +- 首先判断缓存,如果缓存中存在,即已经解析过了,则直接使用已经解析了的,根据 constructorArgumentsResolved 参数来判断是使用构造函数自动注入还是默认构造函数 +- 如果缓存中没有,则需要先确定到底使用哪个构造函数来完成解析工作,因为一个类有多个构造函数,每个构造函数都有不同的构造参数,所以需要根据参数来锁定构造函数并完成初始化,如果存在参数则使用相应的带有参数的构造函数,否则使用默认构造函数。 + +**instantiateBean** + +不带参数的构造函数的实例化过程使用的方法是instantiateBean(beanName, mbd),我们看下源码: + +```java +protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { + try { + Object beanInstance; + final BeanFactory parent = this; + if (System.getSecurityManager() != null) { + beanInstance = AccessController.doPrivileged((PrivilegedAction) () -> + getInstantiationStrategy().instantiate(mbd, beanName, parent), + getAccessControlContext()); + } + else { + beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); + } + BeanWrapper bw = new BeanWrapperImpl(beanInstance); + initBeanWrapper(bw); + return bw; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); + } +} +``` + +**实例化策略** + +实例化过程中,反复提到了实例化策略,这是做什么的呢?其实,经过前面的分析,我们已经得到了足以实例化的相关信息,完全可以使用最简单的反射方法来构造实例对象,但Spring却没有这么做。 + +接下来我们看下Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner)方法,具体的实现是在SimpleInstantiationStrategy中,具体代码如下: + +```java +public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + // 没有覆盖 + // 直接使用反射实例化即可 + if (!bd.hasMethodOverrides()) { + // 重新检测获取下构造函数 + // 该构造函数是经过前面 N 多复杂过程确认的构造函数 + Constructor constructorToUse; + synchronized (bd.constructorArgumentLock) { + // 获取已经解析的构造函数 + constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; + // 如果为 null,从 class 中解析获取,并设置 + if (constructorToUse == null) { + final Class clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + constructorToUse = AccessController.doPrivileged( + (PrivilegedExceptionAction>) clazz::getDeclaredConstructor); + } + else { + //利用反射获取构造器 + constructorToUse = clazz.getDeclaredConstructor(); + } + bd.resolvedConstructorOrFactoryMethod = constructorToUse; + } + catch (Throwable ex) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + } + } + + // 通过BeanUtils直接使用构造器对象实例化bean + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // 生成CGLIB创建的子类对象 + return instantiateWithMethodInjection(bd, beanName, owner); + } +} +``` + +如果该 bean 没有配置 lookup-method、replaced-method 标签或者 @Lookup 注解,则直接通过反射的方式实例化 bean 即可,方便快捷,但是如果存在需要覆盖的方法或者动态替换的方法则需要使用 CGLIB 进行动态代理,因为可以在创建代理的同时将动态方法织入类中。 + +调用工具类 BeanUtils 的 `instantiateClass()` 方法完成反射工作: + +```java +public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { + Assert.notNull(ctor, "Constructor must not be null"); + try { + ReflectionUtils.makeAccessible(ctor); + return (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? + KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); + } + // 省略一些 catch +} +``` + +**CGLIB** + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + throw new UnsupportedOperationException("Method Injection not supported in SimpleInstantiationStrategy"); +} +``` + +方法默认是没有实现的,具体过程由其子类 CglibSubclassingInstantiationStrategy 实现: + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + return instantiateWithMethodInjection(bd, beanName, owner, null); +} + +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + @Nullable Constructor ctor, @Nullable Object... args) { + + // 通过CGLIB生成一个子类对象 + return new CglibSubclassCreator(bd, owner).instantiate(ctor, args); +} +``` + +创建一个 CglibSubclassCreator 对象,调用其 `instantiate()` 方法生成其子类对象: + +```java +public Object instantiate(@Nullable Constructor ctor, @Nullable Object... args) { + // 通过 Cglib 创建一个代理类 + Class subclass = createEnhancedSubclass(this.beanDefinition); + Object instance; + // 没有构造器,通过 BeanUtils 使用默认构造器创建一个bean实例 + if (ctor == null) { + instance = BeanUtils.instantiateClass(subclass); + } + else { + try { + // 获取代理类对应的构造器对象,并实例化 bean + Constructor enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes()); + instance = enhancedSubclassConstructor.newInstance(args); + } + catch (Exception ex) { + throw new BeanInstantiationException(this.beanDefinition.getBeanClass(), + "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex); + } + } + + // 为了避免memory leaks异常,直接在bean实例上设置回调对象 + Factory factory = (Factory) instance; + factory.setCallbacks(new Callback[] {NoOp.INSTANCE, + new CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor(this.beanDefinition, this.owner), + new CglibSubclassingInstantiationStrategy.ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)}); + return instance; +} +``` + +当然这里还没有具体分析 CGLIB 生成子类的详细过程,具体的过程等后续分析 AOP 的时候再详细地介绍。 + +### 记录创建bean的ObjectFactory + +在刚刚创建完Bean的实例后,也就是刚刚执行完构造器实例化后,doCreateBean方法中有下面一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + //为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂 + //依赖处理:在Spring中会有循环依赖的情况,例如,当A中含有B的属性,而B中又含有A的属性时就会 + //构成一个循环依赖,此时如果A和B都是单例,那么在Spring中的处理方式就是当创建B的时候,涉及 + //自动注入A的步骤时,并不是直接去再次创建A,而是通过放入缓存中的ObjectFactory来创建实例, + //这样就解决了循环依赖的问题。 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +isSingletonCurrentlyInCreation(beanName):该bean是否在创建中。在Spring中,会有个专门的属性默认为DefaultSingletonBeanRegistry的singletonsCurrentlyInCreation来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将beanName移除。那么我们跟随代码一路走下来可以对这个属性的记录并没有多少印象,这个状态是在哪里记录的呢?不同scope的记录位置不一样,我们以singleton为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry类的public Object getSingleton(String beanName,ObjectFactory singletonFactory)函数的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在这两段函数中分别this.singletonsCurrentlyInCreation.add(beanName)与this.singletonsCurrentlyInCreation.remove(beanName)来进行状态的记录与移除。 + +变量earlySingletonExposure是否是单例,是否允许循环依赖,是否对应的bean正在创建的条件的综合。当这3个条件都满足时会执行addSingletonFactory操作,那么加入SingletonFactory的作用是什么?又是在什么时候调用的? + +我们还是以最简单AB循环为例,类A中含有属性B,而类B中又会含有属性A,那么初始化beanA的过程如下: + +![](http://img.topjavaer.cn/img/202309200747808.png) + +上图展示了创建BeanA的流程,在创建A的时候首先会记录类A所对应额beanName,并将beanA的创建工厂加入缓存中,而在对A的属性填充也就是调用pupulateBean方法的时候又会再一次的对B进行递归创建。同样的,因为在B中同样存在A属性,因此在实例化B的populateBean方法中又会再次地初始化B,也就是图形的最后,调用getBean(A).关键是在这里,我们之前分析过,在这个函数中并不是直接去实例化A,而是先去检测缓存中是否有已经创建好的对应的bean,或者是否已经创建的ObjectFactory,而此时对于A的ObjectFactory我们早已经创建,所以便不会再去向后执行,而是直接调用ObjectFactory去创建A.这里最关键的是ObjectFactory的实现。 + +其中getEarlyBeanReference的代码如下: + +```java +protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { + Object exposedObject = bean; + if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { + SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; + exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); + if (exposedObject == null) { + return exposedObject; + } + } + } + } + return exposedObject; +} +``` + +在getEarlyBeanReference函数中除了后处理的调用外没有别的处理工作,根据分析,基本可以理清Spring处理循环依赖的解决办法,在B中创建依赖A时通过ObjectFactory提供的实例化方法来获取原始A,使B中持有的A仅仅是刚刚初始化并没有填充任何属性的A,而这初始化A的步骤还是刚刚创建A时进行的,但是因为A与B中的A所表示的属性地址是一样的所以在A中创建好的属性填充自然可以通过B中的A获取,这样就解决了循环依赖的问题。 \ No newline at end of file diff --git a/docs/source/spring/8-ioc-attribute-fill.md b/docs/source/spring/8-ioc-attribute-fill.md new file mode 100644 index 0000000..3c7ac08 --- /dev/null +++ b/docs/source/spring/8-ioc-attribute-fill.md @@ -0,0 +1,590 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,属性填充,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +`doCreateBean()` 主要用于完成 bean 的创建和初始化工作,我们可以将其分为四个过程: + +> [最全面的Java面试网站](https://topjavaer.cn) + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +第一个过程实例化 bean在前面一篇博客中已经分析完了,这篇博客开始分析 属性填充,也就是 `populateBean()` + +```java +protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { + PropertyValues pvs = mbd.getPropertyValues(); + + if (bw == null) { + if (!pvs.isEmpty()) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); + } + else { + // Skip property population phase for null instance. + return; + } + } + + // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the + // state of the bean before properties are set. This can be used, for example, + // to support styles of field injection. + boolean continueWithPropertyPopulation = true; + + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //返回值为是否继续填充bean + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + //如果后处理器发出停止填充命令则终止后续的执行 + if (!continueWithPropertyPopulation) { + return; + } + + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + MutablePropertyValues newPvs = new MutablePropertyValues(pvs); + + // Add property values based on autowire by name if applicable. + //根据名称自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + //根据类型自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + //后处理器已经初始化 + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); + //需要依赖检查 + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); + if (hasInstAwareBpps) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //对所有需要依赖检查的属性进行后处理 + pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); + if (pvs == null) { + return; + } + } + } + } + if (needsDepCheck) { + //依赖检查,对应depends-on属性,3.0已经弃用此属性 + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + //将属性应用到bean中 + //将所有ProtertyValues中的属性填充至BeanWrapper中。 + applyPropertyValues(beanName, mbd, bw, pvs); +} +``` + +我们来分析下populateBean的流程: + +(1)首先进行属性是否为空的判断 + +(2)通过调用InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)方法来控制程序是否继续进行属性填充 + +(3)根据注入类型(byName/byType)提取依赖的bean,并统一存入PropertyValues中 + +(4)应用InstantiationAwareBeanPostProcessor的postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName)方法,对属性获取完毕填充前的再次处理,典型的应用是RequiredAnnotationBeanPostProcesser类中对属性的验证 + +(5)将所有的PropertyValues中的属性填充至BeanWrapper中 + +上面步骤中有几个地方是我们比较感兴趣的,它们分别是依赖注入(autowireByName/autowireByType)以及属性填充,接下来进一步分析这几个功能的实现细节 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + + + +## 自动注入 + +Spring 会根据注入类型( byName / byType )的不同,调用不同的方法(`autowireByName()` / `autowireByType()`)来注入属性值。 + +### autowireByName() + +```java +protected void autowireByName( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + // 获取 Bean 对象中非简单属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + // 如果容器中包含指定名称的 bean,则将该 bean 注入到 bean中 + if (containsBean(propertyName)) { + // 递归初始化相关 bean + Object bean = getBean(propertyName); + // 为指定名称的属性赋予属性值 + pvs.add(propertyName, bean); + // 属性依赖注入 + registerDependentBean(propertyName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Added autowiring by name from bean name '" + beanName + + "' via property '" + propertyName + "' to bean named '" + propertyName + "'"); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName + + "' by name: no matching bean found"); + } + } + } +} +``` + +该方法逻辑很简单,获取该 bean 的非简单属性,什么叫做非简单属性呢?就是类型为对象类型的属性,但是这里并不是将所有的对象类型都都会找到,比如 8 个原始类型,String 类型 ,Number类型、Date类型、URL类型、URI类型等都会被忽略,如下: + +```java +protected String[] unsatisfiedNonSimpleProperties(AbstractBeanDefinition mbd, BeanWrapper bw) { + Set result = new TreeSet<>(); + PropertyValues pvs = mbd.getPropertyValues(); + PropertyDescriptor[] pds = bw.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (pd.getWriteMethod() != null && !isExcludedFromDependencyCheck(pd) && !pvs.contains(pd.getName()) && + !BeanUtils.isSimpleProperty(pd.getPropertyType())) { + result.add(pd.getName()); + } + } + return StringUtils.toStringArray(result); +} +``` + +这里获取的就是需要依赖注入的属性。 + +autowireByName()函数的功能就是根据传入的参数中的pvs中找出已经加载的bean,并递归实例化,然后加入到pvs中 + + + +### autowireByType + +autowireByType与autowireByName对于我们理解与使用来说复杂程度相似,但是实现功能的复杂度却不一样,我们看下方法代码: + +```java +protected void autowireByType( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + + Set autowiredBeanNames = new LinkedHashSet(4); + //寻找bw中需要依赖注入的属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + try { + PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); + // Don't try autowiring by type for type Object: never makes sense, + // even if it technically is a unsatisfied, non-simple property. + if (!Object.class.equals(pd.getPropertyType())) { + //探测指定属性的set方法 + MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); + // Do not allow eager init for type matching in case of a prioritized post-processor. + boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass()); + DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); + //解析指定beanName的属性所匹配的值,并把解析到的属性名称存储在autowiredBeanNames中, + Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); + if (autowiredArgument != null) { + pvs.add(propertyName, autowiredArgument); + } + for (String autowiredBeanName : autowiredBeanNames) { + //注册依赖 + registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" + + propertyName + "' to bean named '" + autowiredBeanName + "'"); + } + } + autowiredBeanNames.clear(); + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex); + } + } +} +``` + +根据名称第一步与根据属性第一步都是寻找bw中需要依赖注入的属性,然后遍历这些属性并寻找类型匹配的bean,其中最复杂就是寻找类型匹配的bean。spring中提供了对集合的类型注入支持,如使用如下注解方式: + +```java +@Autowired +private List tests; +``` + +这种方式spring会把所有与Test匹配的类型找出来并注入到tests属性中,正是由于这一因素,所以在autowireByType函数,新建了局部遍历autowireBeanNames,用于存储所有依赖的bean,如果只是对非集合类的属性注入来说,此属性并无用处。 + +对于寻找类型匹配的逻辑实现是封装在了resolveDependency函数中,其实现如下: + +```java +public Object resolveDependency(DependencyDescriptor descriptor, String beanName, Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); + if (descriptor.getDependencyType().equals(ObjectFactory.class)) { + //ObjectFactory类注入的特殊处理 + return new DependencyObjectFactory(descriptor, beanName); + } + else if (descriptor.getDependencyType().equals(javaxInjectProviderClass)) { + //javaxInjectProviderClass类注入的特殊处理 + return new DependencyProviderFactory().createDependencyProvider(descriptor, beanName); + } + else { + //通用处理逻辑 + return doResolveDependency(descriptor, descriptor.getDependencyType(), beanName, autowiredBeanNames, typeConverter); + } +} + +protected Object doResolveDependency(DependencyDescriptor descriptor, Class type, String beanName, + Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + /* + * 用于支持Spring中新增的注解@Value + */ + Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); + if (value != null) { + if (value instanceof String) { + String strVal = resolveEmbeddedValue((String) value); + BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); + value = evaluateBeanDefinitionString(strVal, bd); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return (descriptor.getField() != null ? + converter.convertIfNecessary(value, type, descriptor.getField()) : + converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + } + //如果解析器没有成功解析,则需要考虑各种情况 + //属性是数组类型 + if (type.isArray()) { + Class componentType = type.getComponentType(); + //根据属性类型找到beanFactory中所有类型的匹配bean, + //返回值的构成为:key=匹配的beanName,value=beanName对应的实例化后的bean(通过getBean(beanName)返回) + Map matchingBeans = findAutowireCandidates(beanName, componentType, descriptor); + if (matchingBeans.isEmpty()) { + //如果autowire的require属性为true而找到的匹配项却为空则只能抛出异常 + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(componentType, "array of " + componentType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + //通过转换器将bean的值转换为对应的type类型 + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Collection类型 + else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { + Class elementType = descriptor.getCollectionType(); + if (elementType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No element type declared for collection [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, elementType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(elementType, "collection of " + elementType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Map类型 + else if (Map.class.isAssignableFrom(type) && type.isInterface()) { + Class keyType = descriptor.getMapKeyType(); + if (keyType == null || !String.class.isAssignableFrom(keyType)) { + if (descriptor.isRequired()) { + throw new FatalBeanException("Key type [" + keyType + "] of map [" + type.getName() + + "] must be assignable to [java.lang.String]"); + } + return null; + } + Class valueType = descriptor.getMapValueType(); + if (valueType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No value type declared for map [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, valueType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(valueType, "map with value type " + valueType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + return matchingBeans; + } + else { + Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(type, "", descriptor); + } + return null; + } + if (matchingBeans.size() > 1) { + String primaryBeanName = determinePrimaryCandidate(matchingBeans, descriptor); + if (primaryBeanName == null) { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + if (autowiredBeanNames != null) { + autowiredBeanNames.add(primaryBeanName); + } + return matchingBeans.get(primaryBeanName); + } + // We have exactly one match. + Map.Entry entry = matchingBeans.entrySet().iterator().next(); + if (autowiredBeanNames != null) { + autowiredBeanNames.add(entry.getKey()); + } + //已经确定只有一个匹配项 + return entry.getValue(); + } +} +``` + +主要就是通过Type从BeanFactory中找到对应的benaName,然后通过getBean获取实例 + +```java +protected Map findAutowireCandidates( + @Nullable String beanName, Class requiredType, DependencyDescriptor descriptor) { + //在BeanFactory找到所有Type类型的beanName + String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this, requiredType, true, descriptor.isEager()); + Map result = new LinkedHashMap<>(candidateNames.length); + + //遍历所有的beanName,通过getBean获取 + for (String candidate : candidateNames) { + if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) { + // + addCandidateEntry(result, candidate, descriptor, requiredType); + } + } + return result; +} + +private void addCandidateEntry(Map candidates, String candidateName, + DependencyDescriptor descriptor, Class requiredType) { + + Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this); + if (!(beanInstance instanceof NullBean)) { + candidates.put(candidateName, beanInstance); + } +} + +public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) + throws BeansException { + //通过类型找到beanName,然后再找到其实例 + return beanFactory.getBean(beanName); +} +``` + +## applyPropertyValues + +程序运行到这里,已经完成了对所有注入属性的获取,但是获取的属性是以PropertyValues形式存在的,还并没有应用到已经实例化的bean中,这一工作是在applyPropertyValues中。继续跟踪到方法体中: + +```java +protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs == null || pvs.isEmpty()) { + return; + } + + MutablePropertyValues mpvs = null; + List original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; + //如果mpvs中的值已经被转换为对应的类型那么可以直接设置到beanwapper中 + if (mpvs.isConverted()) { + // Shortcut: use the pre-converted values as-is. + try { + bw.setPropertyValues(mpvs); + return; + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } + } + original = mpvs.getPropertyValueList(); + } + else { + //如果pvs并不是使用MutablePropertyValues封装的类型,那么直接使用原始的属性获取方法 + original = Arrays.asList(pvs.getPropertyValues()); + } + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + //获取对应的解析器 + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); + + // Create a deep copy, resolving any references for values. + List deepCopy = new ArrayList(original.size()); + boolean resolveNecessary = false; + //遍历属性,将属性转换为对应类的对应属性的类型 + for (PropertyValue pv : original) { + if (pv.isConverted()) { + deepCopy.add(pv); + } + else { + String propertyName = pv.getName(); + Object originalValue = pv.getValue(); + Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); + Object convertedValue = resolvedValue; + boolean convertible = bw.isWritableProperty(propertyName) && + !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + if (convertible) { + convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); + } + // Possibly store converted value in merged bean definition, + // in order to avoid re-conversion for every created bean instance. + if (resolvedValue == originalValue) { + if (convertible) { + pv.setConvertedValue(convertedValue); + } + deepCopy.add(pv); + } + else if (convertible && originalValue instanceof TypedStringValue && + !((TypedStringValue) originalValue).isDynamic() && + !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { + pv.setConvertedValue(convertedValue); + deepCopy.add(pv); + } + else { + resolveNecessary = true; + deepCopy.add(new PropertyValue(pv, convertedValue)); + } + } + } + if (mpvs != null && !resolveNecessary) { + mpvs.setConverted(); + } + + // Set our (possibly massaged) deep copy. + try { + bw.setPropertyValues(new MutablePropertyValues(deepCopy)); + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } +} +``` + +我们来看看具体的属性赋值过程 + +```java +public class MyTestBean { + private String name ; + + public MyTestBean(String name) { + this.name = name; + } + + public MyTestBean() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + + + + +``` + +如上 **bw.setPropertyValues** 最终都会走到如下方法 + +```java +@Override +public void setValue(final @Nullable Object value) throws Exception { + //获取writeMethod,也就是我们MyTestBean的setName方法 + final Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? + ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : + this.pd.getWriteMethod()); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(writeMethod); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + writeMethod.invoke(getWrappedInstance(), value), acc); + } + catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } + else { + ReflectionUtils.makeAccessible(writeMethod); + //通过反射调用方法进行赋值 + writeMethod.invoke(getWrappedInstance(), value); + } +} +``` + +就是利用反射进行调用对象的set方法赋值 + +至此,`doCreateBean()` 第二个过程:属性填充 已经分析完成了,下篇分析第三个过程:循环依赖的处理,其实循环依赖并不仅仅只是在 `doCreateBean()` 中处理,其实在整个加载 bean 的过程中都有涉及,所以下篇内容并不仅仅只局限于 `doCreateBean()`。 \ No newline at end of file diff --git a/docs/source/spring/9-ioc-circular-dependency.md b/docs/source/spring/9-ioc-circular-dependency.md new file mode 100644 index 0000000..0d5e755 --- /dev/null +++ b/docs/source/spring/9-ioc-circular-dependency.md @@ -0,0 +1,204 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,循环依赖,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- +## **什么是循环依赖** + +循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示: + +![](http://img.topjavaer.cn/img/202309210848022.png) + +注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。 + +> [最全面的Java面试网站](https://topjavaer.cn) + +Spring中循环依赖场景有: + +(1)构造器的循环依赖 + +(2)field属性的循环依赖。 + +对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。 + +Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。 + +### **如何检测循环依赖** + +检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。 + +### 解决循环依赖 + +我们先从加载 bean 最初始的方法 `doGetBean()` 开始。 + +在 `doGetBean()` 中,首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回。 + +```java +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +这个方法主要是从三个缓存中获取,分别是:singletonObjects、earlySingletonObjects、singletonFactories,三者定义如下: + +```java +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +private final Map> singletonFactories = new HashMap<>(16); + +private final Map earlySingletonObjects = new HashMap<>(16); +``` + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +这三级缓存分别指: + +(1)singletonFactories : 单例对象工厂的cache + +(2)earlySingletonObjects :提前暴光的单例对象的Cache + +(3)singletonObjects:单例对象的cache + +他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。这里我们可以通过 `getSingleton()` 看到他们是如何配合的,这分析该方法之前,提下其中的 `isSingletonCurrentlyInCreation()` 和 `allowEarlyReference`。 + +- `isSingletonCurrentlyInCreation()`:判断当前 singleton bean 是否处于创建中。bean 处于创建中也就是说 bean 在初始化但是没有完成初始化,有一个这样的过程其实和 Spring 解决 bean 循环依赖的理念相辅相成,因为 Spring 解决 singleton bean 的核心就在于提前曝光 bean。 +- allowEarlyReference:从字面意思上面理解就是允许提前拿到引用。其实真正的意思是是否允许从 singletonFactories 缓存中通过 `getObject()` 拿到对象,为什么会有这样一个字段呢?原因就在于 singletonFactories 才是 Spring 解决 singleton bean 的诀窍所在,这个我们后续分析。 + +`getSingleton()` 整个过程如下:首先从一级缓存 singletonObjects 获取,如果没有且当前指定的 beanName 正在创建,就再从二级缓存中 earlySingletonObjects 获取,如果还是没有获取到且运行 singletonFactories 通过 `getObject()` 获取,则从三级缓存 singletonFactories 获取,如果获取到则,通过其 `getObject()` 获取对象,并将其加入到二级缓存 earlySingletonObjects 中 从三级缓存 singletonFactories 删除,如下: + +```java +singletonObject = singletonFactory.getObject(); +this.earlySingletonObjects.put(beanName, singletonObject); +this.singletonFactories.remove(beanName); +``` + +这样就从三级缓存升级到二级缓存了。 + +上面是从缓存中获取,但是缓存中的数据从哪里添加进来的呢?一直往下跟会发现在 `doCreateBean()` ( AbstractAutowireCapableBeanFactory ) 中,有这么一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +也就是我们上一篇文章中讲的最后一部分,提前将创建好但还未进行属性赋值的的Bean放入缓存中。 + +如果 `earlySingletonExposure == true` 的话,则调用 `addSingletonFactory()` 将他们添加到缓存中,但是一个 bean 要具备如下条件才会添加至缓存中: + +- 单例 +- 运行提前暴露 bean +- 当前 bean 正在创建中 + +`addSingletonFactory()` 代码如下: + +```java +protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(singletonFactory, "Singleton factory must not be null"); + synchronized (this.singletonObjects) { + if (!this.singletonObjects.containsKey(beanName)) { + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } + } +} +``` + +从这段代码我们可以看出 singletonFactories 这个三级缓存才是解决 Spring Bean 循环依赖的诀窍所在。同时这段代码发生在 `createBeanInstance()` 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了,所以 Spring 在这个时候选择将该对象提前曝光出来让大家认识认识。 + +介绍到这里我们发现三级缓存 singletonFactories 和 二级缓存 earlySingletonObjects 中的值都有出处了,那一级缓存在哪里设置的呢?在类 DefaultSingletonBeanRegistry 中可以发现这个 `addSingleton()` 方法,源码如下: + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +添加至一级缓存,同时从二级、三级缓存中删除。这个方法在我们创建 bean 的链路中有哪个地方引用呢?其实在前面博客已经提到过了,在 `doGetBean()` 处理不同 scope 时,如果是 singleton,则调用 `getSingleton()`,如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + + + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + synchronized (this.singletonObjects) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + //.... + try { + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + //..... + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + return singletonObject; + } +} +``` + +至此,Spring 关于 singleton bean 循环依赖已经分析完毕了。所以我们基本上可以确定 Spring 解决循环依赖的方案了:Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 `getObject()` 获取了,也就是 `getSingleton()`中的代码片段了。 + +最后来描述下就上面那个循环依赖 Spring 解决的过程:首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来,然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来,这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存 singletonFactories ),通过 ObjectFactory 提前曝光,所以可以通过 `ObjectFactory.getObject()` 拿到 A 对象,C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中,回到 B ,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了。 \ No newline at end of file diff --git a/docs/system-design/2-order-timeout-auto-cancel.md b/docs/system-design/2-order-timeout-auto-cancel.md new file mode 100644 index 0000000..d971ea2 --- /dev/null +++ b/docs/system-design/2-order-timeout-auto-cancel.md @@ -0,0 +1,648 @@ +--- +sidebar: heading +--- + +# 订单30分钟未支付自动取消怎么实现? + +推荐大家加入我的[**学习圈**](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247491988&idx=1&sn=a7d50c0994cbfdede312715b393daa8a&scene=21#wechat_redirect),目前已经有100多位小伙伴加入了,下面有50元的**优惠券**,**扫描二维码**领取优惠券加入(**即将恢复原价**)。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含**场景设计、系统设计、分布式、微服务**等),持续更新 + +![](http://img.topjavaer.cn/img/image-20230120085023054.png) + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + +![前面内容](http://img.topjavaer.cn/img/202304212238005.png) + +--分割线-- + +**目录** + +- 了解需求 +- 方案 1:数据库轮询 +- 方案 2:JDK 的延迟队列 +- 方案 3:时间轮算法 +- 方案 4:redis 缓存 +- 方案 5:使用消息队列 + +## 了解需求 + +在开发中,往往会遇到一些关于延时任务的需求。 + +例如 + +- 生成订单 30 分钟未支付,则自动取消 +- 生成订单 60 秒后,给用户发短信 + +对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?一共有如下几点区别 + +定时任务有明确的触发时间,延时任务没有 + +定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 + +定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 + +下面,我们以判断订单是否超时为例,进行方案分析 + +## 方案 1:数据库轮询 + +### 思路 + +该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作 + +### 实现 + +可以用 quartz 来实现的,简单介绍一下 + +maven 项目引入一个依赖如下所示 + +``` + + org.quartz-scheduler + quartz + 2.2.2 + +``` + +调用 Demo 类 MyJob 如下所示 + +``` +package com.rjzheng.delay1; + +import org.quartz.*; +import org.quartz.impl.StdSchedulerFactory; + +public class MyJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("要去数据库扫描啦。。。"); + } + + public static void main(String[] args) throws Exception { + // 创建任务 + JobDetail jobDetail = JobBuilder.newJob(MyJob.class) + .withIdentity("job1", "group1").build(); + // 创建触发器 每3秒钟执行一次 + Trigger trigger = TriggerBuilder + .newTrigger() + .withIdentity("trigger1", "group3") + .withSchedule( + SimpleScheduleBuilder + .simpleSchedule() + .withIntervalInSeconds(3). + repeatForever()) + .build(); + Scheduler scheduler = new StdSchedulerFactory().getScheduler(); + // 将任务及其触发器放入调度器 + scheduler.scheduleJob(jobDetail, trigger); + // 调度器开始调度任务 + scheduler.start(); + } + +} +``` + +运行代码,可发现每隔 3 秒,输出如下 + +``` +要去数据库扫描啦。。。 +``` + +### 优点 + +简单易行,支持集群操作 + +### 缺点 + +- 对服务器内存消耗大 +- 存在延迟,比如你每隔 3 分钟扫描一次,那最坏的延迟时间就是 3 分钟 +- 假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大 + +## 方案 2:JDK 的延迟队列 + +### 思路 + +该方案是利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。 + +DelayedQueue 实现工作流程如下图所示 + +![](http://img.topjavaer.cn/img/订单取消1.png) + +其中 Poll():获取并移除队列的超时元素,没有则返回空 + +take():获取并移除队列的超时元素,如果没有则 wait 当前线程,直到有元素满足超时条件,返回结果。 + +### 实现 + +定义一个类 OrderDelay 实现 Delayed,代码如下 + +``` +package com.rjzheng.delay2; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class OrderDelay implements Delayed { + + private String orderId; + + private long timeout; + + OrderDelay(String orderId, long timeout) { + this.orderId = orderId; + this.timeout = timeout + System.nanoTime(); + } + + public int compareTo(Delayed other) { + if (other == this) { + return 0; + } + OrderDelay t = (OrderDelay) other; + long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS)); + return (d == 0) ? 0 : ((d < 0) ? -1 : 1); + } + + // 返回距离你自定义的超时时间还有多少 + public long getDelay(TimeUnit unit) { + return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS); + } + + void print() { + System.out.println(orderId + "编号的订单要删除啦。。。。"); + } + +} +``` + +运行的测试 Demo 为,我们设定延迟时间为 3 秒 + +``` +package com.rjzheng.delay2; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.TimeUnit; + +public class DelayQueueDemo { + + public static void main(String[] args) { + List list = new ArrayList(); + list.add("00000001"); + list.add("00000002"); + list.add("00000003"); + list.add("00000004"); + list.add("00000005"); + + DelayQueue queue = newDelayQueue < OrderDelay > (); + long start = System.currentTimeMillis(); + for (int i = 0; i < 5; i++) { + //延迟三秒取出 + queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS))); + try { + queue.take().print(); + System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + +} +``` + +输出如下 + +``` +00000001编号的订单要删除啦。。。。 +After 3003 MilliSeconds +00000002编号的订单要删除啦。。。。 +After 6006 MilliSeconds +00000003编号的订单要删除啦。。。。 +After 9006 MilliSeconds +00000004编号的订单要删除啦。。。。 +After 12008 MilliSeconds +00000005编号的订单要删除啦。。。。 +After 15009 MilliSeconds +``` + +可以看到都是延迟 3 秒,订单被删除 + +### 优点 + +效率高,任务触发时间延迟低。 + +### 缺点 + +- 服务器重启后,数据全部消失,怕宕机 +- 集群扩展相当麻烦 +- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 +- 代码复杂度较高 + +## 方案 3:时间轮算法 + +### 思路 + +先上一张时间轮的图(这图到处都是啦) + +![](http://img.topjavaer.cn/img/订单取消2.png) + +时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个 3 个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。 + +如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在 20 秒之后执行怎么办,由于这个环形结构槽数只到 8,如果要 20 秒,指针需要多转 2 圈。位置是在 2 圈之后的 5 上面(20 % 8 + 1) + +### 实现 + +我们用 Netty 的 HashedWheelTimer 来实现 + +给 Pom 加上下面的依赖 + +``` + + io.netty + netty-all + 4.1.24.Final + +``` + +测试代码 HashedWheelTimerTest 如下所示 + +``` +package com.rjzheng.delay3; + +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.TimerTask; + +import java.util.concurrent.TimeUnit; + +public class HashedWheelTimerTest { + + static class MyTimerTask implements TimerTask { + + boolean flag; + + public MyTimerTask(boolean flag) { + this.flag = flag; + } + + public void run(Timeout timeout) throws Exception { + System.out.println("要去数据库删除订单了。。。。"); + this.flag = false; + } + } + + public static void main(String[] argv) { + MyTimerTask timerTask = new MyTimerTask(true); + Timer timer = new HashedWheelTimer(); + timer.newTimeout(timerTask, 5, TimeUnit.SECONDS); + int i = 1; + while (timerTask.flag) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(i + "秒过去了"); + i++; + } + } + +} +``` + +输出如下 + +``` +1秒过去了 +2秒过去了 +3秒过去了 +4秒过去了 +5秒过去了 +要去数据库删除订单了。。。。 +6秒过去了 +``` + +### 优点 + +效率高,任务触发时间延迟时间比 delayQueue 低,代码复杂度比 delayQueue 低。 + +### 缺点 + +- 服务器重启后,数据全部消失,怕宕机 +- 集群扩展相当麻烦 +- 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 + +## 方案 4:redis 缓存 + +### 思路一 + +利用 redis 的 zset,zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值 + +添加元素:ZADD key score member [[score member][score member] …] + +按顺序查询元素:ZRANGE key start stop [WITHSCORES] + +查询元素 score:ZSCORE key member + +移除元素:ZREM key member [member …] + +测试如下 + +``` +添加单个元素 +redis> ZADD page_rank 10 google.com +(integer) 1 + +添加多个元素 +redis> ZADD page_rank 9 baidu.com 8 bing.com +(integer) 2 + +redis> ZRANGE page_rank 0 -1 WITHSCORES +1) "bing.com" +2) "8" +3) "baidu.com" +4) "9" +5) "google.com" +6) "10" + +查询元素的score值 +redis> ZSCORE page_rank bing.com +"8" + +移除单个元素 +redis> ZREM page_rank google.com +(integer) 1 + +redis> ZRANGE page_rank 0 -1 WITHSCORES +1) "bing.com" +2) "8" +3) "baidu.com" +4) "9" +``` + +那么如何实现呢?我们将订单超时时间戳与订单号分别设置为 score 和 member,系统扫描第一个元素判断是否超时,具体如下图所示 + +![](http://img.topjavaer.cn/img/订单取消3.png) + +### 实现一 + +``` +package com.rjzheng.delay4; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.Tuple; + +import java.util.Calendar; +import java.util.Set; + +public class AppTest { + + private static final String ADDR = "127.0.0.1"; + + private static final int PORT = 6379; + + private static JedisPool jedisPool = new JedisPool(ADDR, PORT); + + public static Jedis getJedis() { + return jedisPool.getResource(); + } + + //生产者,生成5个订单放进去 + public void productionDelayMessage() { + for (int i = 0; i < 5; i++) { + //延迟3秒 + Calendar cal1 = Calendar.getInstance(); + cal1.add(Calendar.SECOND, 3); + int second3later = (int) (cal1.getTimeInMillis() / 1000); + AppTest.getJedis().zadd("OrderId", second3later, "OID0000001" + i); + System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID0000001" + i); + } + } + + //消费者,取订单 + + public void consumerDelayMessage() { + Jedis jedis = AppTest.getJedis(); + while (true) { + Set items = jedis.zrangeWithScores("OrderId", 0, 1); + if (items == null || items.isEmpty()) { + System.out.println("当前没有等待的任务"); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + continue; + } + int score = (int) ((Tuple) items.toArray()[0]).getScore(); + Calendar cal = Calendar.getInstance(); + int nowSecond = (int) (cal.getTimeInMillis() / 1000); + if (nowSecond >= score) { + String orderId = ((Tuple) items.toArray()[0]).getElement(); + jedis.zrem("OrderId", orderId); + System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId); + } + } + } + + public static void main(String[] args) { + AppTest appTest = new AppTest(); + appTest.productionDelayMessage(); + appTest.consumerDelayMessage(); + } + +} +``` + +此时对应输出如下 + +![](http://img.topjavaer.cn/img/订单取消4.png) + +可以看到,几乎都是 3 秒之后,消费订单。 + +然而,这一版存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号,我们上测试代码 ThreadTest + +``` +package com.rjzheng.delay4; + +import java.util.concurrent.CountDownLatch; + +public class ThreadTest { + + private static final int threadNum = 10; + private static CountDownLatch cdl = newCountDownLatch(threadNum); + + static class DelayMessage implements Runnable { + public void run() { + try { + cdl.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + AppTest appTest = new AppTest(); + appTest.consumerDelayMessage(); + } + } + + public static void main(String[] args) { + AppTest appTest = new AppTest(); + appTest.productionDelayMessage(); + for (int i = 0; i < threadNum; i++) { + new Thread(new DelayMessage()).start(); + cdl.countDown(); + } + } + +} +``` + +输出如下所示 + +![](http://img.topjavaer.cn/img/订单取消5.png) + +显然,出现了多个线程消费同一个资源的情况。 + +### 解决方案 + +(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。 + +(2)对 ZREM 的返回值进行判断,只有大于 0 的时候,才消费数据,于是将 consumerDelayMessage()方法里的 + +``` +if(nowSecond >= score){ + String orderId = ((Tuple)items.toArray()[0]).getElement(); + jedis.zrem("OrderId", orderId); + System.out.println(System.currentTimeMillis()+"ms:redis消费了一个任务:消费的订单OrderId为"+orderId); +} +``` + +修改为 + +``` +if (nowSecond >= score) { + String orderId = ((Tuple) items.toArray()[0]).getElement(); + Long num = jedis.zrem("OrderId", orderId); + if (num != null && num > 0) { + System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId); + } +} +``` + +在这种修改后,重新运行 ThreadTest 类,发现输出正常了 + +### 思路二 + +该方案使用 redis 的 Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在 key 失效之后,提供一个回调,实际上是 redis 会给客户端发送一个消息。是需要 redis 版本 2.8 以上。 + +### 实现二 + +在 redis.conf 中,加入一条配置 + +notify-keyspace-events Ex + +运行代码如下 + +``` +package com.rjzheng.delay5; + +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPubSub; + +public class RedisTest { + + private static final String ADDR = "127.0.0.1"; + private static final int PORT = 6379; + private static JedisPool jedis = new JedisPool(ADDR, PORT); + private static RedisSub sub = new RedisSub(); + + public static void init() { + new Thread(new Runnable() { + public void run() { + jedis.getResource().subscribe(sub, "__keyevent@0__:expired"); + } + }).start(); + } + + public static void main(String[] args) throws InterruptedException { + init(); + for (int i = 0; i < 10; i++) { + String orderId = "OID000000" + i; + jedis.getResource().setex(orderId, 3, orderId); + System.out.println(System.currentTimeMillis() + "ms:" + orderId + "订单生成"); + } + } + + static class RedisSub extends JedisPubSub { + @Override + public void onMessage(String channel, String message) { + System.out.println(System.currentTimeMillis() + "ms:" + message + "订单取消"); + + } + } +} +``` + +输出如下 + +![](http://img.topjavaer.cn/img/订单取消6.png) + +可以明显看到 3 秒过后,订单取消了 + +ps:redis 的 pub/sub 机制存在一个硬伤,官网内容如下 + +原:Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost. + +翻: Redis 的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。因此,方案二不是太推荐。当然,如果你对可靠性要求不高,可以使用。 + +### 优点 + +(1) 由于使用 Redis 作为消息通道,消息都存储在 Redis 中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性。 + +(2) 做集群扩展相当方便 + +(3) 时间准确度高 + +### 缺点 + +需要额外进行 redis 维护 + +## 方案 5:使用消息队列 + +### 思路 + +我们可以采用 rabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 + +RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter + +lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能,具体的,我改天再写一篇文章,这里再讲下去,篇幅太长。 + +### 优点 + +高效,可以利用 rabbitmq 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。 + +### 缺点 + +本身的易用度要依赖于 rabbitMq 的运维.因为要引用 rabbitMq,所以复杂度和成本变高。 + +--end-- + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.dabin-coder.cn/image/Image.png) + +![](http://img.dabin-coder.cn/image/image-20221030094126118.png) + +**Github地址**:https://github.com/Tyson0314/java-books \ No newline at end of file diff --git a/docs/system-design/README.md b/docs/system-design/README.md new file mode 100644 index 0000000..dac1d6d --- /dev/null +++ b/docs/system-design/README.md @@ -0,0 +1,27 @@ +**系统设计高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到Java面试手册**完整版**。 + +![](http://img.topjavaer.cn/img/image-20230105001012520.png) + +除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230102210715391.png) + +![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) + +另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 + +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git "a/\345\267\245\345\205\267/docker.md" b/docs/tools/docker-overview.md similarity index 76% rename from "\345\267\245\345\205\267/docker.md" rename to docs/tools/docker-overview.md index 0359ec2..6d35f1f 100644 --- "a/\345\267\245\345\205\267/docker.md" +++ b/docs/tools/docker-overview.md @@ -1,56 +1,17 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [简介](#%E7%AE%80%E4%BB%8B) - - [基本概念](#%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5) -- [docker镜像常用命令](#docker%E9%95%9C%E5%83%8F%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4) - - [添加docker仓库位置](#%E6%B7%BB%E5%8A%A0docker%E4%BB%93%E5%BA%93%E4%BD%8D%E7%BD%AE) - - [安装docker服务](#%E5%AE%89%E8%A3%85docker%E6%9C%8D%E5%8A%A1) - - [启动 docker 服务](#%E5%90%AF%E5%8A%A8-docker-%E6%9C%8D%E5%8A%A1) - - [重启docker服务](#%E9%87%8D%E5%90%AFdocker%E6%9C%8D%E5%8A%A1) - - [搜索镜像](#%E6%90%9C%E7%B4%A2%E9%95%9C%E5%83%8F) - - [下载镜像:](#%E4%B8%8B%E8%BD%BD%E9%95%9C%E5%83%8F) - - [列出镜像](#%E5%88%97%E5%87%BA%E9%95%9C%E5%83%8F) - - [删除镜像](#%E5%88%A0%E9%99%A4%E9%95%9C%E5%83%8F) - - [打包镜像](#%E6%89%93%E5%8C%85%E9%95%9C%E5%83%8F) - - [创建镜像](#%E5%88%9B%E5%BB%BA%E9%95%9C%E5%83%8F) - - [推送镜像:](#%E6%8E%A8%E9%80%81%E9%95%9C%E5%83%8F) - - [docker hub](#docker-hub) - - [修改镜像存放位置](#%E4%BF%AE%E6%94%B9%E9%95%9C%E5%83%8F%E5%AD%98%E6%94%BE%E4%BD%8D%E7%BD%AE) -- [docker容器常用命令](#docker%E5%AE%B9%E5%99%A8%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4) - - [新建容器](#%E6%96%B0%E5%BB%BA%E5%AE%B9%E5%99%A8) - - [查看容器](#%E6%9F%A5%E7%9C%8B%E5%AE%B9%E5%99%A8) - - [停止容器](#%E5%81%9C%E6%AD%A2%E5%AE%B9%E5%99%A8) - - [启动容器](#%E5%90%AF%E5%8A%A8%E5%AE%B9%E5%99%A8) - - [重启容器](#%E9%87%8D%E5%90%AF%E5%AE%B9%E5%99%A8) - - [进入容器](#%E8%BF%9B%E5%85%A5%E5%AE%B9%E5%99%A8) - - [删除容器](#%E5%88%A0%E9%99%A4%E5%AE%B9%E5%99%A8) - - [容器日志](#%E5%AE%B9%E5%99%A8%E6%97%A5%E5%BF%97) - - [容器ip](#%E5%AE%B9%E5%99%A8ip) - - [容器启动方式](#%E5%AE%B9%E5%99%A8%E5%90%AF%E5%8A%A8%E6%96%B9%E5%BC%8F) - - [资源占用](#%E8%B5%84%E6%BA%90%E5%8D%A0%E7%94%A8) - - [磁盘使用情况](#%E7%A3%81%E7%9B%98%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5) - - [执行容器内部命令](#%E6%89%A7%E8%A1%8C%E5%AE%B9%E5%99%A8%E5%86%85%E9%83%A8%E5%91%BD%E4%BB%A4) - - [网络](#%E7%BD%91%E7%BB%9C) - - [复制文件](#%E5%A4%8D%E5%88%B6%E6%96%87%E4%BB%B6) -- [docker-compose](#docker-compose) - - [安装](#%E5%AE%89%E8%A3%85) - - [常用命令](#%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4) - - [部署多个服务](#%E9%83%A8%E7%BD%B2%E5%A4%9A%E4%B8%AA%E6%9C%8D%E5%8A%A1) - - [定义配置文件](#%E5%AE%9A%E4%B9%89%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) - - [启动服务](#%E5%90%AF%E5%8A%A8%E6%9C%8D%E5%8A%A1) -- [maven插件构建docker镜像](#maven%E6%8F%92%E4%BB%B6%E6%9E%84%E5%BB%BAdocker%E9%95%9C%E5%83%8F) - - [docker镜像仓库](#docker%E9%95%9C%E5%83%8F%E4%BB%93%E5%BA%93) - - [docker开启远程访问](#docker%E5%BC%80%E5%90%AF%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE) - - [docker支持http上传镜像](#docker%E6%94%AF%E6%8C%81http%E4%B8%8A%E4%BC%A0%E9%95%9C%E5%83%8F) - - [重启docker](#%E9%87%8D%E5%90%AFdocker) - - [开放端口](#%E5%BC%80%E6%94%BE%E7%AB%AF%E5%8F%A3) - - [构建docker镜像](#%E6%9E%84%E5%BB%BAdocker%E9%95%9C%E5%83%8F) -- [其他](#%E5%85%B6%E4%BB%96) - - [给nginx增加端口映射](#%E7%BB%99nginx%E5%A2%9E%E5%8A%A0%E7%AB%AF%E5%8F%A3%E6%98%A0%E5%B0%84) - - +--- +sidebar: heading +title: Docker学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Docker,Docker命令,docker镜像,docker容器 + - - meta + - name: description + content: Docker常见知识点和面试题总结,让天下没有难背的八股文! +--- ## 简介 @@ -58,8 +19,6 @@ Docker是一个开源的应用容器引擎,通过容器可以隔离应用程 ### 基本概念 -[docker](https://zhuanlan.zhihu.com/p/23599229) - docker的基本概念: 1. 镜像(image),类似于虚拟机中的镜像,可以理解为可执行程序。 @@ -71,12 +30,13 @@ docker的基本概念: docker的基本命令: -- docker build:我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,生成的可执行程序就是image。 +1、docker build:我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,生成的可执行程序就是image。 + +2、docker run:运行image,运行起来后就是docker container。 -- docker run:运行image,运行起来后就是docker container。 -- docker pull:到Docker Hub(docker registry)下载别人写好的image。 +3、docker pull:到Docker Hub(docker registry)下载别人写好的image。 -![](../img/docker/docker.jpg) +![](http://img.topjavaer.cn/img/docker.jpg) > 图片来源:知乎小灰 @@ -265,36 +225,32 @@ Docker官方维护了一个DockerHub的公共仓库,里边包含有很多平 ### 修改镜像存放位置 -1. 查看镜像存放位置: - - ``` - docker info | grep "Docker Root Dir" - Docker Root Dir: /var/lib/docker - ``` - -2. 关闭docker服务: - - ``` - systemctl stop docker - ``` +1、查看镜像存放位置: -3. 原镜像目录移动到目标目录: - - ``` - mv /var/lib/docker /home/data/docker - ``` +``` +docker info | grep "Docker Root Dir" + Docker Root Dir: /var/lib/docker +``` -4. 建立软连接: +2、关闭docker服务: - ``` - ln -s /home/data/docker /var/lib/docker - ``` +``` +systemctl stop docker +``` -5. 再次查看镜像存放位置,发现已经修改。 +3、原镜像目录移动到目标目录: +``` +mv /var/lib/docker /home/data/docker +``` +4、建立软连接: +``` +ln -s /home/data/docker /var/lib/docker +``` +5、再次查看镜像存放位置,发现已经修改。 ## docker容器常用命令 @@ -312,13 +268,13 @@ docker run -p 33055:33055 --name nginx \ # -d nginx:1.18.0 -d表示容器以后台方式运行 ``` -- -p:将宿主机和容器端口进行映射,格式为:宿主机端口:容器端口; -- --name:指定容器名称,之后可以通过容器名称来操作容器; -- -e:设置容器的环境变量,这里设置的是时区; -- -v:将宿主机上的文件挂载到容器上,格式为:宿主机文件目录:容器文件目录; -- -d:表示容器以后台方式运行。终端不会输出任何运行信息; -- -i: 以交互模式运行容器,通常与 -t 同时使用; -- -t: 为容器分配一个终端,通常与 -i 同时使用。 +> - -p:将宿主机和容器端口进行映射,格式为:宿主机端口:容器端口; +> - --name:指定容器名称,之后可以通过容器名称来操作容器; +> - -e:设置容器的环境变量,这里设置的是时区; +> - -v:将宿主机上的文件挂载到容器上,格式为:宿主机文件目录:容器文件目录; +> - -d:表示容器以后台方式运行。终端不会输出任何运行信息; +> - -i: 以交互模式运行容器,通常与 -t 同时使用; +> - -t: 为容器分配一个终端,通常与 -i 同时使用。 使用-it,此时如果使用exit退出,则容器的状态处于Exit,而不是后台运行。如果想让容器一直运行,而不是停止,可以使用快捷键 ctrl+p ctrl+q 退出,此时容器的状态为Up。 @@ -328,11 +284,11 @@ docker run -p 33055:33055 --name nginx \ docker run --name mysql4blog -e MYSQL_ROOT_PASSWORD=123456 -p 3307:3307 -d mysql:8.0.20 ``` -- `-name`: 给新创建的容器命名,此处命名为`mysql4blog` -- `-e`: 配置信息,此处配置MySQL的 root 用户的登录密码 -- `-p`: 端口映射,此处映射主机的3307端口到容器的3307端口 -- -d: 成功启动同期后输出容器的完整ID -- 最后一个`mysql:8.0.20`指的是`mysql`镜像 +> - `-name`: 给新创建的容器命名,此处命名为`mysql4blog` +> - `-e`: 配置信息,此处配置MySQL的 root 用户的登录密码 +> - `-p`: 端口映射,此处映射主机的3307端口到容器的3307端口 +> - -d: 成功启动同期后输出容器的完整ID +> - 最后一个`mysql:8.0.20`指的是`mysql`镜像 ### 查看容器 @@ -386,17 +342,17 @@ docker attach container_name/container_id 使用exit退出命令行之后,重新进入容器: -1. 先查询容器id:`docker inspect --format "{{.State.Pid}}" nginx` +1、先查询容器id:`docker inspect --format "{{.State.Pid}}" nginx` -2. 根据查到的容器id进入容器:`nsenter --target 28487 --mount --uts --ipc --net --pid` +2、根据查到的容器id进入容器:`nsenter --target 28487 --mount --uts --ipc --net --pid` - ``` - [root@VM_0_7_centos ~]# docker inspect --format "{{.State.Pid}}" nginx - 28487 - [root@VM_0_7_centos ~]# nsenter --target 28487 --mount --uts --ipc --net --pid - mesg: ttyname failed: No such device - root@b217a35fc808:/# ls -l - ``` +``` +[root@VM_0_7_centos ~]# docker inspect --format "{{.State.Pid}}" nginx +28487 +[root@VM_0_7_centos ~]# nsenter --target 28487 --mount --uts --ipc --net --pid +mesg: ttyname failed: No such device +root@b217a35fc808:/# ls -l +``` ### 删除容器 @@ -645,8 +601,6 @@ docker-compose up -d ## maven插件构建docker镜像 - - ### docker镜像仓库 服务器创建docker镜像仓库docker registry: @@ -838,3 +792,6 @@ docker start nginx +## 参考内容 + +[只要一小时,零基础入门Docker](https://zhuanlan.zhihu.com/p/23599229) diff --git a/docs/tools/docker/1-introduce.md b/docs/tools/docker/1-introduce.md new file mode 100644 index 0000000..09bab8a --- /dev/null +++ b/docs/tools/docker/1-introduce.md @@ -0,0 +1,27 @@ +# 简介 + +Docker是一个开源的应用容器引擎,通过容器可以隔离应用程序的运行时环境(程序运行时依赖的各种库和配置),比虚拟机更轻量(虚拟机在操作系统层面进行隔离)。docker的另一个优点就是build once, run everywhere,只编译一次,就可以在各个平台(windows、linux等)运行。 + +## 基本概念 + +docker的基本概念: + +1. 镜像(image),类似于虚拟机中的镜像,可以理解为可执行程序。 + +2. 容器(container),类似于一个轻量级的沙盒,可以将其看作一个极简的Linux系统环境。Docker引擎利用容器来运行、隔离各个应用。容器是镜像创建的应用实例,可以创建、启动、停止、删除容器,各个容器之间是是相互隔离的,互不影响。 + +3. 镜像仓库(repository),是Docker用来集中存放镜像文件的地方。注意与注册服务器(Registry)的区别:注册服务器是存放仓库的地方,一般会有多个仓库;而仓库是存放镜像的地方,一般每个仓库存放一类镜像,每个镜像利用tag进行区分,比如Ubuntu仓库存放有多个版本(12.04、14.04等)的Ubuntu镜像。 +4. dockerfile,image的编译配置文件,docker就是"编译器"。 + +docker的基本命令: + +1、docker build:我们只需要在dockerfile中指定需要哪些程序、依赖什么样的配置,之后把dockerfile交给“编译器”docker进行“编译”,生成的可执行程序就是image。 + +2、docker run:运行image,运行起来后就是docker container。 + +3、docker pull:到Docker Hub(docker registry)下载别人写好的image。 + +![](http://img.topjavaer.cn/img/docker.jpg) + +> 图片来源:知乎小灰 + diff --git a/docs/tools/docker/2-image-command.md b/docs/tools/docker/2-image-command.md new file mode 100644 index 0000000..00a69e6 --- /dev/null +++ b/docs/tools/docker/2-image-command.md @@ -0,0 +1,209 @@ +# docker镜像常用命令 + +## 添加docker仓库位置 + +安装yum-utils: + +``` +yum install -y yum-utils device-mapper-persistent-data lvm2 +``` + +添加仓库地址: + +``` +yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +``` + +## 安装docker服务 + +``` +yum install docker-ce +``` + +## 启动 docker 服务 + +``` +systemctl start docker +``` + +## 重启docker服务 + +```java +systemctl restart docker.service +``` + +## 搜索镜像 + +``` +docker search java +``` + +## 下载镜像: + +不加版本号,则默认下载最新版本。 + +``` +docker pull nginx:1.18.0 +``` + +## 列出镜像 + +``` +docker images + +REPOSITORY TAG IMAGE ID CREATED SIZE +nginx 1.18.0 c2c45d506085 7 days ago 133MB +nginx 1.17.0 719cd2e3ed04 22 months ago 109MB +java 8 d23bdf5b1b1b 4 years ago 643MB +``` + +## 删除镜像 + +删除镜像前必须先删除以此镜像为基础的容器。 + +``` +docker rmi -f imageID # -f 强制删除镜像 +``` + +删除所有没有引用的镜像(找出IMAGE ID为none的镜像): + +``` +docker rmi `docker images | grep none | awk '{print $3}'` +``` + +强制删除所有镜像: + +``` +docker rmi -f $(docker images) +``` + +## 打包镜像 + +``` +# -t 表示指定镜像仓库名称/镜像名称:镜像标签 .表示使用当前目录下的Dockerfile文件 +docker build -t mall/mall-admin:1.0-SNAPSHOT . +``` + +## 创建镜像 + +镜像的创建也有两种: + +- 利用已有的镜像创建容器之后进行修改,之后commit生成镜像 +- 利用Dockerfile创建镜像 + +(1)利用已有的镜像创建容器之后进行修改 + +利用镜像启动容器: + +``` +[root@xxx ~]# docker run -it java:8 /bin/bash # 启动一个容器 +[root@72f1a8a0e394 /]# # 这里命令行形式变了,表示已经进入了一个新环境 +[root@72f1a8a0e394 /]# vim # 此时的容器中没有vim +bash: vim: command not found +[root@72f1a8a0e394 /]# apt-get update # 更新软件包 +[root@72f1a8a0e394 /]# apt-get install vim # 安装vim +``` + +exit退出该容器,然后查看docker中运行的程序(容器): + +``` +[root@VM_0_7_centos ~]# docker ps -a +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +857aa43eeed4 java:8 "/bin/bash" 19 minutes ago Exited (127) 4 seconds ago gifted_blackburn +``` + +将容器转化为一个镜像,此时Docker引擎中就有了我们新建的镜像,此镜像和原有的java镜像区别在于多了个vim工具。 + +``` +[root@VM_0_7_centos ~]# docker commit -m "java with vim" -a "tyson" 857aa43eeed4 tyson/java:vim +sha256:67c4b3658485690c9128e0b6d4c5dfa63ec100c89b417e3148f3c808254d6b9b +[root@VM_0_7_centos ~]# docker images +REPOSITORY TAG IMAGE ID CREATED SIZE +tyson/java vim 67c4b3658485 7 seconds ago 684MB +``` + +运行新建的镜像: + +``` +[root@VM_0_7_centos ~]# docker run -it tyson/java:vim /bin/bash +root@88fead8e7db5:/# ls +bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var +root@88fead8e7db5:/# touch demo.txt +root@88fead8e7db5:/# vim demo.txt +``` + +(2)利用Dockerfile创建镜像 + +dockerfile用于告诉docker build执行哪些操作。 + +``` +#该镜像以哪个镜像为基础 +FROM java:8 + +#构建者的信息 +MAINTAINER tyson + +#build镜像执行的操作 +RUN apt-get update +RUN apt-get install vim + +#拷贝本地文件到镜像中 +COPY ./* /usr/tyson/ +``` + +利用build命令构建镜像: + +``` +docker build -t="tyson/java:vim" . +``` + +其中-t用来指定新镜像的用户信息、tag等。最后的点表示在当前目录寻找Dockerfile。 + +构建完成之后,通过`docker images`可以查看生成的镜像。 + +## 推送镜像 + +``` +# 登录Docker Hub +docker login +# 给本地镜像打标签为远程仓库名称 +docker tag mall/mall-admin:1.0-SNAPSHOT macrodocker/mall-admin:1.0-SNAPSHOT +# 推送到远程仓库 +docker push macrodocker/mall-admin:1.0-SNAPSHOT +``` + +**什么是docker hub**? + +Docker官方维护了一个DockerHub的公共仓库,里边包含有很多平时用的较多的镜像。除了从上边下载镜像之外,我们也可以将自己自定义的镜像发布(push)到DockerHub上。 + +1. 登录docker hub:`docker login` +2. 将本地镜像推送到docker hub,镜像的username需要与docker hub的username一致:`docker push tyson14/java:vim` + +## 修改镜像存放位置 + +1、查看镜像存放位置: + +``` +docker info | grep "Docker Root Dir" + Docker Root Dir: /var/lib/docker +``` + +2、关闭docker服务: + +``` +systemctl stop docker +``` + +3、原镜像目录移动到目标目录: + +``` +mv /var/lib/docker /home/data/docker +``` + +4、建立软连接: + +``` +ln -s /home/data/docker /var/lib/docker +``` + +5、再次查看镜像存放位置,发现已经修改。 \ No newline at end of file diff --git a/docs/tools/docker/3-container-command.md b/docs/tools/docker/3-container-command.md new file mode 100644 index 0000000..e949669 --- /dev/null +++ b/docs/tools/docker/3-container-command.md @@ -0,0 +1,242 @@ +# docker容器常用命令 + +## 新建容器 + +新建并启动容器(docker容器运行必须有一个前台进程, 如果没有前台进程执行,容器认为空闲,就会自行退出。所以需要先创建一个前台进程): + +``` +docker run -it --name java java:8 /bin/bash + +docker run -p 33055:33055 --name nginx \ +> -v /home/data:/home/data \ +> nginx:1.18.0 \ +> -e TZ="Asia/Shanghai" +# -d nginx:1.18.0 -d表示容器以后台方式运行 +``` + +> - -p:将宿主机和容器端口进行映射,格式为:宿主机端口:容器端口; +> - --name:指定容器名称,之后可以通过容器名称来操作容器; +> - -e:设置容器的环境变量,这里设置的是时区; +> - -v:将宿主机上的文件挂载到容器上,格式为:宿主机文件目录:容器文件目录; +> - -d:表示容器以后台方式运行。终端不会输出任何运行信息; +> - -i: 以交互模式运行容器,通常与 -t 同时使用; +> - -t: 为容器分配一个终端,通常与 -i 同时使用。 + +使用-it,此时如果使用exit退出,则容器的状态处于Exit,而不是后台运行。如果想让容器一直运行,而不是停止,可以使用快捷键 ctrl+p ctrl+q 退出,此时容器的状态为Up。 + +**创建MySQL容器**: + +```java +docker run --name mysql4blog -e MYSQL_ROOT_PASSWORD=123456 -p 3307:3307 -d mysql:8.0.20 +``` + +> - `-name`: 给新创建的容器命名,此处命名为`mysql4blog` +> - `-e`: 配置信息,此处配置MySQL的 root 用户的登录密码 +> - `-p`: 端口映射,此处映射主机的3307端口到容器的3307端口 +> - -d: 成功启动同期后输出容器的完整ID +> - 最后一个`mysql:8.0.20`指的是`mysql`镜像 + +## 查看容器 + +列出运行中的容器,`-a`参数可以列出所有容器: + +``` +docker ps +``` + +## 停止容器 + +使用容器名称或者容器ID: + +``` +docker stop $ContainerName(or $ContainerId) +``` + +强制停止容器: + +``` +docker kill $ContainerName +``` + +## 启动容器 + +``` +docker start $ContainerName +``` + +## 重启容器 + +``` +docker restart nginx +``` + +## 进入容器 + +进入名字为mysql的容器,同时打开命令行终端。使用exit时,容器不会停止。 + +``` +docker exec -it mysql /bin/bash +``` + +docker attach可以将本机的输入直接输到容器中,容器的输出会直接显示在本机的屏幕上。如果使用exit或者ctrl+c退出,会导致容器的停止。 + +``` +docker attach container_name/container_id +``` + +新建并运行容器时,使用`docker run -it tyson/java:vim /bin/bash`,直接进入容器命令行界面。 + +使用exit退出命令行之后,重新进入容器: + +1、先查询容器id:`docker inspect --format "{{.State.Pid}}" nginx` + +2、根据查到的容器id进入容器:`nsenter --target 28487 --mount --uts --ipc --net --pid` + +``` +[root@VM_0_7_centos ~]# docker inspect --format "{{.State.Pid}}" nginx +28487 +[root@VM_0_7_centos ~]# nsenter --target 28487 --mount --uts --ipc --net --pid +mesg: ttyname failed: No such device +root@b217a35fc808:/# ls -l +``` + +## 删除容器 + +根据ContainerName删除容器: + +``` +docker rm nginx +``` + +按名称通配符删除容器,比如删除以名称`mall-`开头的容器: + +``` +docker rm `docker ps -a | grep mall-* | awk '{print $1}'` +``` + +强制删除所有容器: + +``` +docker rm -f $(docker ps -a -q) +``` + +## 容器日志 + +根据ContainerName查看容器产生的日志: + +``` +docker logs nginx +``` + +动态查看容器产生的日志: + +``` +docker logs -f nginx +``` + +实时查看日志最后200行: + +```bash +docker logs -f --tail 200 mysql +``` + +## 容器ip + +``` +docker inspect --format '{{.NetworkSettings.IPAddress}}' nginx +``` + +## 容器启动方式 + +``` +# 将容器启动方式改为always +docker container update --restart=always $ContainerName +``` + +## 资源占用 + +查看指定容器资源占用状况,比如cpu、内存、网络、io状态: + +``` +docker stats nginx + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +b217a35fc808 nginx 0.00% 3.641MiB / 1.795GiB 0.20% 656B / 0B 11.3MB / 8.19kB 2 +``` + +查看所有容器资源占用情况: + +``` +docker stats -a +``` + +## 磁盘使用情况 + +``` +docker system df + +TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 3 1 885.4MB 752.5MB (84%) +Containers 1 1 1.258kB 0B (0%) +Local Volumes 0 0 0B 0B +Build Cache 0 0 0B 0B +``` + +## 执行容器内部命令 + +``` +[root@VM_0_7_centos ~]# docker exec -it nginx /bin/bash +root@b217a35fc808:/# ll +bash: ll: command not found +root@b217a35fc808:/# ls -l +total 80 +drwxr-xr-x 2 root root 4096 Apr 8 00:00 bin +drwxr-xr-x 2 root root 4096 Mar 19 23:44 boot +``` + +指定账号进入容器内部: + +``` +docker exec -it --user root nginx /bin/bash +``` + +## 网络 + +查看网络: + +``` +docker network ls +NETWORK ID NAME DRIVER SCOPE +5f0d326b7082 bridge bridge local +af84aa332f22 host host local +741c1734f3bb none null local +``` + +创建外部网络: + +``` +docker network create -d bridge my-bridge-network +``` + +创建容器时指定网络: + +``` +docker run -p 33056:33056 --name java \ +> --network my-bridge-network \ +> java:8 +``` + +## 复制文件 + +将当前目录tpch文件夹复制到mysql容器相应的位置: + +``` +docker cp tpch mysql56:/var/lib/mysql #mysql56为容器名 +``` + +容器文件拷贝到宿主机: + +```java +docker cp 容器名:要拷贝的文件在容器里面的路径 要拷贝到宿主机的相应路径 +``` + diff --git a/docs/tools/docker/4-docker-compose.md b/docs/tools/docker/4-docker-compose.md new file mode 100644 index 0000000..1538869 --- /dev/null +++ b/docs/tools/docker/4-docker-compose.md @@ -0,0 +1,102 @@ +# docker-compose + +Docker Compose是一个用于定义和运行多个docker容器应用的工具。使用YAML文件配置多个应用服务,通过这个YAML文件一次性部署配置的所有服务。 + +## 安装 + +``` +curl -L https://get.daocloud.io/docker/compose/releases/download/1.24.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose +``` + +修改文件的权限为可执行: + +``` +chmod +x /usr/local/bin/docker-compose +``` + +查看是否安装成功: + +``` +docker-compose --version +``` + +## 常用命令 + +创建、启动容器(默认配置文件docker-compose.yml): + +``` +# -d表示在后台运行 +docker-compose up -d +``` + +指定文件启动: + +``` +docker-compose -f docker-compose.yml up -d +``` + +停止所有相关容器: + +``` +docker-compose stop +``` + +列出所有容器信息: + +``` +docker-compose ps +``` + + + +## 部署多个服务 + +1. 使用docker-compose.yml定义需要部署的服务; +2. 使用docker-compose up命令一次性部署配置的所有服务。 + +## 定义配置文件 + +ports:指定主机和容器的端口映射(HOST:CONTAINER) + +volumes:将主机目录挂载到容器(HOST:CONTAINER) + +link:连接其他容器(同一个网络下)的服务(SERVICE:ALIAS) + +```yaml +services: + elasticsearch: + image: elasticsearch:7.6.2 + container_name: elasticsearch + user: root + environment: + - "cluster.name=elasticsearch" #设置集群名称为elasticsearch + - "discovery.type=single-node" #以单一节点模式启动 + - "ES_JAVA_OPTS=-Xms128m -Xmx128m" #设置使用jvm内存大小 + volumes: + - /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins #插件文件挂载 + - /mydata/elasticsearch/data:/usr/share/elasticsearch/data #数据文件挂载 + ports: + - 9200:9200 + - 9300:9300 + logstash: + image: logstash:7.6.2 + container_name: logstash + environment: + - TZ=Asia/Shanghai + volumes: + - /mydata/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf #挂载logstash的配置文件 + depends_on: + - elasticsearch #kibana在elasticsearch启动之后再启动 + link: + - elasticsearch:es #可以用es这个域名访问elasticsearch服务 +``` + +## 启动服务 + +先将docker-compose.yml上传至Linux服务器,再在当前目录下运行如下命令(默认配置文件docker-compose.yml): + +``` +docker-compose up -d +``` + + diff --git a/docs/tools/docker/5-maven-build.md b/docs/tools/docker/5-maven-build.md new file mode 100644 index 0000000..b734b08 --- /dev/null +++ b/docs/tools/docker/5-maven-build.md @@ -0,0 +1,136 @@ +# maven插件构建docker镜像 + +## docker镜像仓库 + +服务器创建docker镜像仓库docker registry: + +``` +docker run -d -p 5000:5000 --restart=always --name registry2 registry:2 +``` + +## docker开启远程访问 + +docker开启远程访问的端口2375,修改docker.service文件: + +``` +vi /usr/lib/systemd/system/docker.service +``` + +修改内容如下: + +``` +ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock +ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock +``` + +`unix:///var/run/docker.sock`:unix socket,本地客户端将通过这个来连接 Docker Daemon。 +`tcp://0.0.0.0:2375`:tcp socket,表示允许任何远程客户端通过 2375 端口连接 Docker Daemon。 + +## docker支持http上传镜像 + +``` +echo '{ "insecure-registries":["192.168.3.101:5000"] }' > /etc/docker/daemon.json +``` + +## 重启docker + +使配置生效: + +``` +systemctl daemon-reload +``` + +重启docker服务: + +``` +systemctl restart docker +``` + +## 开放端口 + +``` +firewall-cmd --zone=public --add-port=2375/tcp --permanent +firewall-cmd --reload +``` + +## 构建docker镜像 + +在应用的pom.xml文件中添加docker-maven-plugin的依赖: + +```xml + + com.spotify + docker-maven-plugin + 1.1.0 + + + build-image + package + + build + + + + + mall-tiny/${project.artifactId}:${project.version} + http://tysonbin.com:2375 + java:8 + ["java", "-jar","/${project.build.finalName}.jar"] + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + +``` + +相关配置说明: + +- executions.execution.phase:此处配置了在maven打包应用时构建docker镜像; +- imageName:用于指定镜像名称,mall-tiny是仓库名称,`${project.artifactId}`为镜像名称,`${project.version}`为版本号; +- dockerHost:打包后上传到的docker服务器地址; +- baseImage:该应用所依赖的基础镜像,此处为java; +- entryPoint:docker容器启动时执行的命令; +- resources.resource.targetPath:将打包后的资源文件复制到该目录; +- resources.resource.directory:需要复制的文件所在目录,maven打包的应用jar包保存在target目录下面; +- resources.resource.include:需要复制的文件,打包好的应用jar包。 + +修改application.yml中的域名,将localhost改为db。可以把docker中的容器看作独立的虚拟机,mall-tiny-docker访问localhost自然会访问不到mysql,docker容器之间可以通过指定好的服务名称db进行访问,至于db这个名称可以在运行mall-tiny-docker容器的时候指定。 + +```yaml +spring: + datasource: + url: jdbc:mysql://db:3306/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + username: root + password: root +``` + +执行maven的package命令,执行成功之后,镜像仓库会生成mall-tiny-docker镜像。 + +启动mysql服务: + +``` +docker run -p 3306:3306 --name mysql \ +-v /mydata/mysql/log:/var/log/mysql \ +-v /mydata/mysql/data:/var/lib/mysql \ +-v /mydata/mysql/conf:/etc/mysql \ +-e MYSQL_ROOT_PASSWORD=root \ +-d mysql:5.7 +``` + +进入运行mysql的docker容器:`docker exec -it mysql /bin/bash` + +启动mall-tiny-docker服务,通过--link表示在同一个网络内(可以在启动容器时通过--network指定),应用可以用db这个域名访问mysql服务: + +``` +docker run -p 8080:8080 --name mall-tiny-docker \ +--link mysql:db \ # 在同一个网络内不同容器之间可以通过--link指定的服务别名互相访问,containName:alias +-v /etc/localtime:/etc/localtime \ +-v /mydata/app/mall-tiny-docker/logs:/var/logs \ +-d mall-tiny/mall-tiny-docker:0.0.1-SNAPSHOT +``` + diff --git a/docs/tools/docker/6-other.md b/docs/tools/docker/6-other.md new file mode 100644 index 0000000..af19572 --- /dev/null +++ b/docs/tools/docker/6-other.md @@ -0,0 +1,53 @@ +# 其他 + +## 给nginx增加端口映射 + +nginx一开始只映射了80端口,后面载部署项目的时候,需要用到其他端口,不想重新部署容器,所以通过修改配置文件的方式给容器添加其他端口。 + +1、执行命令`docker inspect nginx`,找到容器id + +2、停止容器`docker stop nginx`,不然修改了配置会自动还原 + +3、修改hostconfig.json + +```java +cd /var/lib/docker/containers/135254e3429d1e75aa68569137c753b789416256f2ced52b4c5a85ec3849db87 # container id +vim hostconfig.json +``` + +添加端口: + +```java +"PortBindings": { + "80/tcp": [ + { + "HostIp": "", + "HostPort": "80" + } + ], + "8080/tcp": [ + { + "HostIp": "", + "HostPort": "8080" + } + ] +}, +``` + +4、修改同目录下 config.v2.json + +```java +"ExposedPorts": { + "80/tcp": {}, + "8080/tcp": {}, + "8189/tcp": {} +}, +``` + +5、重启容器 + +```java +systemctl restart docker.service # 重启docker服务 +docker start nginx +``` + diff --git a/docs/tools/docker/README.md b/docs/tools/docker/README.md new file mode 100644 index 0000000..7a4200c --- /dev/null +++ b/docs/tools/docker/README.md @@ -0,0 +1,17 @@ +--- +title: Docker基础 +icon: docker +date: 2022-08-06 +category: docker +star: true +--- + +![](http://img.topjavaer.cn/img/docker-img.png) +## Docker总结 + +- [简介](./1-introduce.md) +- [docker镜像常用命令](./2-image-command.md) +- [docker容器常用命令](./3-container-command.md) +- [docker-compose](./4-docker-compose.md) +- [maven插件构建docker镜像](./5-maven-build.md) +- [其他](./6-other.md) diff --git "a/\345\267\245\345\205\267/progit2.md" b/docs/tools/git-overview.md similarity index 80% rename from "\345\267\245\345\205\267/progit2.md" rename to docs/tools/git-overview.md index a79a027..1e5c54f 100644 --- "a/\345\267\245\345\205\267/progit2.md" +++ b/docs/tools/git-overview.md @@ -1,73 +1,17 @@ - - - - -- [Git 简介](#git-%E7%AE%80%E4%BB%8B) - - [Git工作流程](#git%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B) - - [存储原理](#%E5%AD%98%E5%82%A8%E5%8E%9F%E7%90%86) - - [Git 快照](#git-%E5%BF%AB%E7%85%A7) - - [三种状态](#%E4%B8%89%E7%A7%8D%E7%8A%B6%E6%80%81) - - [配置](#%E9%85%8D%E7%BD%AE) - - [获取帮助](#%E8%8E%B7%E5%8F%96%E5%B8%AE%E5%8A%A9) -- [Git 基础](#git-%E5%9F%BA%E7%A1%80) - - [获取 Git 仓库](#%E8%8E%B7%E5%8F%96-git-%E4%BB%93%E5%BA%93) - - [文件状态](#%E6%96%87%E4%BB%B6%E7%8A%B6%E6%80%81) - - [配置别名](#%E9%85%8D%E7%BD%AE%E5%88%AB%E5%90%8D) - - [工作区](#%E5%B7%A5%E4%BD%9C%E5%8C%BA) - - [暂存区](#%E6%9A%82%E5%AD%98%E5%8C%BA) - - [提交](#%E6%8F%90%E4%BA%A4) - - [修改commit信息](#%E4%BF%AE%E6%94%B9commit%E4%BF%A1%E6%81%AF) - - [查看提交历史](#%E6%9F%A5%E7%9C%8B%E6%8F%90%E4%BA%A4%E5%8E%86%E5%8F%B2) - - [版本回退](#%E7%89%88%E6%9C%AC%E5%9B%9E%E9%80%80) - - [stash](#stash) - - [rm和mv](#rm%E5%92%8Cmv) - - [忽略文件](#%E5%BF%BD%E7%95%A5%E6%96%87%E4%BB%B6) - - [skip-worktree和assume-unchanged](#skip-worktree%E5%92%8Cassume-unchanged) - - [远程仓库](#%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [查看远程仓库](#%E6%9F%A5%E7%9C%8B%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [添加远程仓库](#%E6%B7%BB%E5%8A%A0%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [修改远程仓库](#%E4%BF%AE%E6%94%B9%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [pull 和 fetch](#pull-%E5%92%8C-fetch) - - [本地仓库上传git服务器](#%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93%E4%B8%8A%E4%BC%A0git%E6%9C%8D%E5%8A%A1%E5%99%A8) - - [推送到远程仓库](#%E6%8E%A8%E9%80%81%E5%88%B0%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [查看远程仓库](#%E6%9F%A5%E7%9C%8B%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93-1) - - [远程仓库移除和命名](#%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93%E7%A7%BB%E9%99%A4%E5%92%8C%E5%91%BD%E5%90%8D) - - [标签](#%E6%A0%87%E7%AD%BE) - - [创建标签](#%E5%88%9B%E5%BB%BA%E6%A0%87%E7%AD%BE) - - [附注标签](#%E9%99%84%E6%B3%A8%E6%A0%87%E7%AD%BE) - - [轻量标签](#%E8%BD%BB%E9%87%8F%E6%A0%87%E7%AD%BE) - - [推送标签](#%E6%8E%A8%E9%80%81%E6%A0%87%E7%AD%BE) - - [后期打标签](#%E5%90%8E%E6%9C%9F%E6%89%93%E6%A0%87%E7%AD%BE) - - [共享标签](#%E5%85%B1%E4%BA%AB%E6%A0%87%E7%AD%BE) - - [检出标签](#%E6%A3%80%E5%87%BA%E6%A0%87%E7%AD%BE) - - [git 别名](#git-%E5%88%AB%E5%90%8D) -- [git 分支](#git-%E5%88%86%E6%94%AF) - - [分支创建](#%E5%88%86%E6%94%AF%E5%88%9B%E5%BB%BA) - - [分支切换](#%E5%88%86%E6%94%AF%E5%88%87%E6%8D%A2) - - [分支合并](#%E5%88%86%E6%94%AF%E5%90%88%E5%B9%B6) - - [合并冲突](#%E5%90%88%E5%B9%B6%E5%86%B2%E7%AA%81) - - [rebase](#rebase) - - [删除分支](#%E5%88%A0%E9%99%A4%E5%88%86%E6%94%AF) - - [分支管理](#%E5%88%86%E6%94%AF%E7%AE%A1%E7%90%86) - - [远程分支](#%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF) - - [推送](#%E6%8E%A8%E9%80%81) - - [跟踪分支](#%E8%B7%9F%E8%B8%AA%E5%88%86%E6%94%AF) - - [fetch和pull](#fetch%E5%92%8Cpull) - - [删除远程分支](#%E5%88%A0%E9%99%A4%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF) - - [创建远程分支](#%E5%88%9B%E5%BB%BA%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF) - - [cherry-pick](#cherry-pick) - - [cherry-pick与rebase的区别](#cherry-pick%E4%B8%8Erebase%E7%9A%84%E5%8C%BA%E5%88%AB) - - [补丁](#%E8%A1%A5%E4%B8%81) - - - -> 首先给大家分享一个github仓库,上面放了**200多本经典的计算机书籍**,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -> -> github地址:https://github.com/Tyson0314/java-books -> -> 如果github访问不了,可以访问gitee仓库。 -> -> gitee地址:https://gitee.com/tysondai/java-books +--- +sidebar: heading +title: Git学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Git,git命令,git分支,git标签,git补丁,git推送 + - - meta + - name: description + content: Git常见知识点和面试题总结,让天下没有难背的八股文! +--- # Git 简介 @@ -85,7 +29,7 @@ Git工作流程如下: Git 的工作流程图如下: -![](http://img.dabin-coder.cn/image/git-work-flow.png) +![](http://img.topjavaer.cn/img/git-work-flow.png) > 图片来源:https://blog.csdn.net/ThinkWon/article/details/94346816 @@ -103,11 +47,11 @@ Git 的三种状态:已修改(modified)、已暂存(staged)和已提 基本的 Git 工作流程:在工作目录修改文件;暂存文件,将文件快照放到暂存区域;提交更新到本地库。暂存区保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 -![](http://img.dabin-coder.cn/image/git工作流程.png) +![](http://img.topjavaer.cn/img/git工作流程.png) > 图片来源:`https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190726163829113-2056815874.png` -![](http://img.dabin-coder.cn/image/git-status.png) +![](http://img.topjavaer.cn/img/git-status.png) ## 配置 @@ -156,7 +100,7 @@ git clone https://github.com/... 查看文件状态:`git status` -![git生命周期](http://img.dabin-coder.cn/image/git生命周期.png) +![](http://img.topjavaer.cn/img/git生命周期.png) > 图片来源:`https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190726163854195-886320537.png` @@ -262,7 +206,7 @@ git commit -m "add readme.md" 单独执行`git commit`,不带上-m参数,会进入 vim 编辑器界面: -![](http://img.dabin-coder.cn/image/image-20210830225020753.png) +![](http://img.topjavaer.cn/img/image-20210830225020753.png) 此时应该这么操作: @@ -420,11 +364,11 @@ assume-unchanged: git ls-files -v | grep '^h\ ' ``` -## 远程仓库 +# 远程仓库 远程仓库是指托管在网络中的项目版本库。 -### 查看远程仓库 +## 查看远程仓库 查看远程仓库地址: @@ -434,7 +378,7 @@ origin https://github.com/schacon/ticgit (fetch) origin https://github.com/schacon/ticgit (push) ``` -### 添加远程仓库 +## 添加远程仓库 运行 `git remote add ` 添加远程 Git 仓库,同时指定一个简写名称。 @@ -452,7 +396,7 @@ git remote add pb https://github.com/paulboone/ticgit git remote remove origin ``` -### 给origin设置多个远程仓库 +## 给origin设置多个远程仓库 如果想要给origin设置两个远程仓库地址(git add会报错),可以使用`git remote set-url --add origin url`来设置。 @@ -464,7 +408,7 @@ $ git remote set-url --add origin xxx.git #success ``` -### 修改远程仓库 +## 修改远程仓库 修改远程仓库地址: @@ -472,7 +416,7 @@ $ git remote set-url --add origin xxx.git git remote set-url origin git@github.com:Tyson0314/Blog.git ``` -### pull 和 fetch +## pull 和 fetch 从远程仓库获取数据: @@ -489,7 +433,7 @@ git pull = git fetch + git merge FETCH_HEAD git pull --rebase = git fetch + git rebase FETCH_HEAD ``` -### 本地仓库上传git服务器 +## 本地仓库上传git服务器 ```bash git init # 将目录变成本地仓库 @@ -502,7 +446,7 @@ git push -u origin master # 如果当前分支与多个主机存在追踪关系 ``` -### 推送到远程仓库 +## 推送到远程仓库 推送使用命令:`git push [remote-name] [branch-name]` @@ -510,13 +454,13 @@ git push -u origin master # 如果当前分支与多个主机存在追踪关系 git push origin master ``` -### 查看远程仓库 +## 查看远程仓库 ```bash git remote show origin ``` -### 远程仓库移除和命名 +## 远程仓库移除和命名 移除远程仓库: @@ -530,7 +474,7 @@ git remote rm paul git remote rename old-name new-name ``` -## 标签 +# 标签 给历史的某个提交打标签,如标记发布节点(v1.0等)。 @@ -547,13 +491,13 @@ tag标签可以帮助我们回退到某个版本的代码,我们通过tag的 - 回退到某个版本:git reset --hard commitId - 获取远程分支:git fetch origin tag V2.0 -### 创建标签 +## 创建标签 Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。然而,附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证。 通常建议创建附注标签。 创建的标签都只存储在本地,不会自动推送到远程。 -#### 附注标签 +## 附注标签 添加附注标签: @@ -565,7 +509,7 @@ git tag -a v1.4 -m 'my version 1.4' 使用 `git show v1.4` 命令可以看到标签信息和对应的提交信息。 -#### 轻量标签 +## 轻量标签 添加轻量标签: @@ -575,7 +519,7 @@ git tag v1.4-tyson 此时运行 `git show v1.4-tyson`不会看到额外的标签信息,只显示提交信息。 -### 推送标签 +## 推送标签 推送某个标签到远程,使用命令: @@ -595,7 +539,7 @@ git push origin --tags git push origin :refs/tags/ ``` -### 后期打标签 +## 后期打标签 比如给下面的这个提交( `modified readme.md` )打标签:` git tag -a v1.2 c1285b` @@ -608,7 +552,7 @@ ba8e8a5fb932014b4aaf9ccd3163affb7699d475 renamed d2ffb8c33978295aed189f5854857bc4e7b55358 add readme.md ``` -### 共享标签 +## 共享标签 git push 命令并不会传送标签到远程仓库服务器上。在创建完标签后你必须显式地推送标签到共享服务器上: @@ -622,7 +566,7 @@ git push origin v1.5 git push origin --tags ``` -### 检出标签 +## 检出标签 如果你想要工作目录与仓库中特定的标签版本完全一样,可以使用 `git checkout -b [branchname] [tagname]` 在特定的标签上创建一个新分支: @@ -631,20 +575,6 @@ $ git checkout -b version2 v2.0.0 Switched to a new branch 'version2' ``` -## git 别名 - -取消暂存别名: - -```bash -git config --global alias.unstage 'reset HEAD --' -``` - -最后一次提交: - -```bash -git config --global alias.last 'log -1 HEAD' -``` - # git 分支 Git 鼓励在工作流程中频繁地使用分支与合并。 @@ -748,16 +678,16 @@ merge操作会生成一个新的节点,之前的提交分开显示。 pick c38e7ae rebase content s 595ede1 rebase -# Rebase 8824682..595ede1 onto 8824682 (2 commands) -# -# Commands: -# p, pick = use commit -# r, reword = use commit, but edit the commit message -# e, edit = use commit, but stop for amending -# s, squash = use commit, but meld into previous commit -# f, fixup = like "squash", but discard this commit's log message -# x, exec = run command (the rest of the line) using shell -# d, drop = remove commit +-- Rebase 8824682..595ede1 onto 8824682 (2 commands) + +-- Commands: +-- p, pick = use commit +-- r, reword = use commit, but edit the commit message +-- e, edit = use commit, but stop for amending +-- s, squash = use commit, but meld into previous commit +-- f, fixup = like "squash", but discard this commit's log message +-- x, exec = run command (the rest of the line) using shell +-- d, drop = remove commit ``` `s 595ede1 rebase`会将595ede1合到前一个commit,按下`:wq`之后会弹出对话框,合并commit message。 diff --git a/docs/tools/git.md b/docs/tools/git.md new file mode 100644 index 0000000..c1c07c1 --- /dev/null +++ b/docs/tools/git.md @@ -0,0 +1,101 @@ +## **什么是Git?** + +Git是一个版本控制系统,用于跟踪计算机文件的变化。Git是一个跟踪计算机文件变化的版本控制系统,用于帮助协调一个项目中几个人的工作,同时跟踪一段时间的进展。换句话说,我们可以说它是一个促进软件开发中源代码管理的工具。 + +## **Git和SVN的区别** + +Git是分布式版本控制系统,SVN是集中式版本控制系统 + +## **什么是 Git 仓库?** + + Git 仓库指的是一个用于存放源代码的地方。Git 仓库是指存放所有 Git 文件的地方。这些文件既可以存储在本地仓库,也可以存储在远程仓库。 + +## **有哪些Git命令及其功能?** + +- Git config - 配置用户名和电子邮件地址 +- Git add - 添加一个或多个文件到暂存区域 +- Git diff - 查看对文件的修改情况 +- Git init - 初始化一个空的 Git 仓库 +- Git commit - 将更改提交到头部,但不提交到远程仓库 + +## **使用Git有什么好处?** + +- 更快的发布周期 +- 易于团队协作 +- 广泛的接受度 +- 保持源代码的完整性 +- 拉动请求 + +## **如何解决Git中的冲突?** + +- 识别造成冲突的文件。 +- 对这些文件进行所需的修改 +- 使用 git add 命令添加文件。 +- 最后一步是在git commit命令的帮助下提交文件的修改。 + +## **如何发现一个分支是否已经被合并了?** + +有两个命令可以确定: + +- git branch --merged -- 返回已被合并到当前分支的分支列表。 +- git branch --no-merged --返回尚未合并的分支的列表。 + +## **git remote和git clone什么区别?** + + 'git remote add'在你的git配置中创建了一个条目,指定了一个特定URL的名称,而'git clone'通过复制位于该URL的现有仓库来创建一个新的git仓库。 + +## **reset和Revert的区别是什么?** + +Git reset是一个强大的命令,它可以让你的工作更有效率。 + +- Git reset 是一个强大的命令,用于撤销对 Git 仓库状态的局部修改。Git 重置的操作对象是 "Git 的三棵树",即:提交历史(HEAD)、暂存索引和工作目录。 +- Git的Revert命令创建了一个新的提交,撤销了前一个提交的修改。这个命令为项目添加了一个新的历史。它并不修改现有的历史。 + +## **Git 和 GitHub 的区别是什么?** + + Git 是一个版本控制系统。Git 是一个版本控制系统,用于管理源代码历史。而GitHub则是一个基于云的托管服务,用于管理Git仓库。GitHub的目的是帮助更好地管理开源项目。 + +## git reset的功能是什么? + +Git reset "的功能是将你的索引以及工作目录重置为你最后一次提交的状态。 + +## git fetch&git pull详解 + +git fetch的意思是将远程主机的最新内容拉到本地,用户再检查无误后再决定是否合并到工作本地分支中。 + +git pull 是将远程主机中的最新内容拉取下来后直接合并,即:git pull = git fetch+git merge,这样可能会产生冲突,需要手动解决。 + +## Git stash存储的目的是什么? + +Git stash 获取工作文件和索引的当前状态并放入堆栈以供下一步使用,并返回一个干净的工作文件。因此,如果在对象中间并需要跳转到其他任务,同时不想丢失当前的编辑,可以使用 Git stash。 + +## 说说GIT合并的方法以及区别? + +Git代码合并有两种:git merge 和 git rebase + +git merge:这种合并方式是将两个分支的历史合并到一起,现在的分支不会被更改,它会比对双方不同的文件缓存下来,生成一个commit,去push。 + +git rebase:这种合并方法通常被称为“衍合”。他是提交修改历史,比对双方的commit,然后找出不同的去缓存,然后去push,修改commit历史。 + +## Git提交代码的步骤 + +```git +git clone (这个是你新建本地git仓库,如已有可忽略此步) +git pull 取回远程主机某个分支的更新,再与本地的指定分支合并。 +git status 查看当前状态 +git add + 文件 +git add -u + 路径:将修改过的被跟踪代码提交缓存 +git add -A + 路径: 将修改过的未被跟踪的代码提交至缓存 +git add -u com/breakyizhan/src 将 com/breakyizhan/src 目录下被跟踪的已修改过的代码提交到缓存中 +git commit -m "修复XXbug" 推送修改到本地git库中 +git push 把当前提交到git本地仓库的代码推送到远程主机的某个远程分之上 +``` + +## 什么是“git cherry-pick”? + +git cherry-pick 通常用于把特定提交从存储仓库的一个分支引入到其他分支中。常见的用途是从维护的分支到开发分支进行向前或回滚提交。这与其他操作(例如merge、rebase)形成鲜明对比,后者通常是把许多提交应用到其他分支中。 + +## 说一下Gitflow 工作流程吗? + +Gitflow 工作流程使用两个并行的、长期运行的分支来记录项目的历史记录,分别是 master 和 develop 分支。Master,随时准备发布线上版本的分支,其所有内容都是经过全面测试的。Hotfix,维护或修复分支是用于给快速给生产版本修复打补丁的。修复分支很像发布分支和功能分支,除非它们是基于 master 而不是 develop 分支。Develop,是合并所有功能分支,并执行所有测试的分支。只有当所有内容都经过彻底检查和修复后,才能合并到 master 分支。Feature,每个功能都应留在自己的分支中开发,可以推送到 develop 分支作为功能分支的父分支。 + diff --git a/docs/tools/git/1-introduce.md b/docs/tools/git/1-introduce.md new file mode 100644 index 0000000..7b226d3 --- /dev/null +++ b/docs/tools/git/1-introduce.md @@ -0,0 +1,71 @@ +# Git 简介 + +Git 是一个开源的分布式版本控制系统,可以有效、快速的进行项目版本管理。Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 + +## Git工作流程 + +Git工作流程如下: + +- 从远程仓库中克隆资源作为本地仓库; +- 在本地仓库中进行代码修改; +- 在提交本地仓库前先将代码提交到暂存区; +- 提交修改,提交到本地仓库。本地仓库中保存修改的所有历史版本; +- 在需要和团队成员共享代码时,可以将修改的代码push到远程仓库。 + +Git 的工作流程图如下: + +![](http://img.topjavaer.cn/img/git-work-flow.png) + +> 图片来源:https://blog.csdn.net/ThinkWon/article/details/94346816 + +## 存储原理 + +Git 在保存项目状态时,它主要对全部文件制作一个快照并保存这个快照的索引,如果文件没有被修改,Git 不会重新存储这个文件,而是只保留一个链接指向之前存储的文件。 + +## Git 快照 + +快照就是将旧文件所占的空间保留下来,并且保存一个引用,而新文件中会继续使用与旧文件内容相同部分的磁盘空间,不同部分则写入新的磁盘空间。 + +## 三种状态 + +Git 的三种状态:已修改(modified)、已暂存(staged)和已提交(committed)。已修改表示修改了文件,但还没保存到数据库。已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。已提交表示数据已经安全的保存到本地数据库。 + +基本的 Git 工作流程:在工作目录修改文件;暂存文件,将文件快照放到暂存区域;提交更新到本地库。暂存区保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 + +![](http://img.topjavaer.cn/img/git工作流程.png) + +> 图片来源:`https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190726163829113-2056815874.png` + +![](http://img.topjavaer.cn/img/git-status.png) + +## 配置 + +设置用户名和邮箱地址: + +```bash +git config --global user.name "dabin" +git config --global user.email xxx@xxx.com +``` + +如果使用了 --global 选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情,Git 都会使用那些信息。 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来配置。 + +查看配置信息: + +```bash +git config --list +``` + +查看某一项配置: + +```bash +git config user.name +``` + +## 获取帮助 + +获取 config 命令的手册: + +```bash +git help config +``` + diff --git a/docs/tools/git/2-basic.md b/docs/tools/git/2-basic.md new file mode 100644 index 0000000..1110ac1 --- /dev/null +++ b/docs/tools/git/2-basic.md @@ -0,0 +1,280 @@ +# Git 基础 + +## 获取 Git 仓库 + +在现有目录中初始化仓库:进入项目目录并输入`git init` + +克隆现有的仓库: + +```bash +git clone https://github.com/... +``` + +## 文件状态 + +查看文件状态:`git status` + +![](http://img.topjavaer.cn/img/git生命周期.png) + +> 图片来源:`https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190726163854195-886320537.png` + +**状态说明:** + +新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记,如下图,`MM Rakefile`出现两个M,其中出现在靠左边的 M 表示该文件被修改了并放入了暂存区,出现在右边的 M 表示该文件被修改了但是还没放入暂存区。 + +```bash +$ git status -s + M README # 右边的 M 表示该文件被修改了但是还没放入暂存区 +MM Rakefile # 左边的 M 表示该文件被修改了并放入了暂存区;右边的 M 表示该文件被修改了但是还没放入暂存区 +A lib/git.rb # A表示新添加到暂存区中的文件 +?? LICENSE.txt # ??表示新添加的未跟踪文件 +``` + +## 配置别名 + +有些人可能经常敲错命令,通过配置别名可以简化命令: + +通过命令 `git config --global alias.st status ` 将 命令`git status` 简化为 `git st`: + +```bash +$ git config --global alias.st status + +$ git st +On branch master +Your branch is up to date with 'origin/master'. + +nothing to commit, working tree clean +``` + + +## 工作区 + +查看工作区修改:`git diff` + +```bash +$ git diff +diff --git a/md/leetcode刷题笔记.md b/md/leetcode刷题笔记.md +deleted file mode 100644 +index 63a7c90..0000000 +--- a/md/leetcode刷题笔记.md ++++ /dev/null +``` + +撤销工作区修改: + +```bash +git checkout -- file_name +``` + +此命令会撤销工作区的修改,不可恢复,不会撤销暂存区修改。 + +撤销修改还可以使用 restore 命令(git2.23版本引入)。 + +```java +git restore --worktree demo.txt //撤销文件工作区的修改 +git restore --staged demo.txt //撤销暂存区的修改,将文件状态恢复到未add之前 +git restore -s HEAD~1 demo.txt //将当前工作区切换到上个 commit 版本,-s相当于--source +git restore -s hadn12 demo.txt //将当前工作区切换到指定 commit id 的版本 +``` + + +## 暂存区 + +通过`git add filename` 将工作区的文件放到暂存区。 + +```bash +git add README.md +``` + +查看暂存区修改: + +```bash +$ git diff --staged +diff --git a/README.md b/README.md +index ecd6c7a..653f001 100644 +--- a/README.md ++++ b/README.md +``` + +可以看到暂存区中有 README.md 文件,说明README.md文件被放到了暂存区。 + +撤销暂存区修改使用unstage: + +```bash +git reset HEAD file_name +``` + +将文件修改移出暂存区,放到工作区。 + +**git reset 加上 --hard 选项会导致工作目录中所有修改丢失。** + +## 提交 + +任何未提交的修改丢失后很可能不可恢复。提交命令: + +```bash +git commit -m "add readme.md" +``` + +`git commit -a -m "xxx"` 相当于`git add`和`git commit -m "xxx"`,将 tracked 的文件直接提交。untracked 的文件无法使用此命令直接提交,需先执行 git add 命令,再执行 git commit。 + +单独执行`git commit`,不带上-m参数,会进入 vim 编辑器界面: + +![](http://img.topjavaer.cn/img/image-20210830225020753.png) + +此时应该这么操作: + +1. 按下字母键`i`或`a`或`o`,进入到可编辑状态 +2. 输入commit信息之后,按下`Esc`键就可退出编辑状态,回到一般模式 +3. 输入:wq (保存退出)或 :wq!(强行退出,不保存) + +### 修改commit信息 + +如果提交后发现漏掉某些文件或者提交信息写错,使用`git commit --amend`重新提交: + +```bash +git commit -m 'initial commit' +git add forgotten_file +git commit --amend +``` + + +### 查看提交历史 + +`git log`列出所有提交的更新。 + +`git log -p -2`,-p 用来显示每次提交的内容差异,-2表示显示最近两次提交。 + +`git log --stat`每次提交下面都会列出所有被修改的文件、有多少文件被修改和哪些行被修改等。 + +`git log --pretty=oneline`将每个提交放在一行显示。 + +`git log --pretty=format:"%h %s" --graph` format 表示格式化输出,%h 提交对象的简短哈希串,%s 是提交说明,--graph 可以更形象的展示分支、合并历史。 + +```bash +$ git log --pretty=format:"%h %s" --graph +* 75f8b36 update +* cd72e4f 删除查询性能优化 +* 6bddc95 MySQL总结整理 +* f8ace0e java常见关键字总结 +* 0c4efeb 删除android +* 4844de5 mysql执行计划 +* 635c140 redis分布式锁 +* 7b65bc3 update +* e563eec update +* 67e1cf7 update readme +* 218f353 调整目录结构 +* 9428314 整理Java基础内容 +``` + +`git log --since=2.weeks` 按照时间作限制。 + +## 版本回退 + +版本回退使用`git reset`命令。 + +```bash +git reset --hard commit_id +git reset --hard HEAD^ # 回退所有内容到上一个版本 +git reset --hard HEAD^^ # 回退所有内容到上上一个版本 +git reset --hard HEAD~100 # 回退到之前第100个版本 +git reset HEAD readme.txt # 把暂存区的修改撤销掉(unstage), 重新放到工作区 +``` + +## stash + +将未提交的修改保存起来。用于后续恢复当前工作目录。 + +```bash +git stash +git stash pop stash@{id} //恢复后删除 +git stash apply stash@{id} //恢复后不删除,需手动删除 +git stage drop +git stash list //查看stash 列表 +git stash show -p stash@{0} //查看stash具体内容,-p查看diff,stash@{0}可以省略 +``` + +## rm和mv + +`git rm readme.md`:文件未被修改过,从暂存区移除文件,然后提交,相当于 `rm readme.md`和`git add .`。如果只是简单地从工作目录中手工删除文件,运行 git status 时就会在 “Changes not staged for commit”。 + +`git rm --cached README.md`:让文件保留在工作区,但是不想让 Git 继续跟踪。可以使用 --cached 选项来实现。文件被修改过,还没有放进暂存区,则必须要用强制删除选项 -f ,以防止误删还没有添加到暂存区的数据,这样的数据不能被 Git 恢复。 + +git rm 支持正则表达式: + +```bash +git rm log/\*.log +``` + +对文件改名: + +```bash +git mv README.md README +``` + +相当于运行一下三条命令: + +```bash +mv README.md README +git rm README.md +git add README +``` + +## 忽略文件 + +.gitignore 只能忽略未跟踪状态的文件。 + +如果远程仓库已经有了logs文件夹,使用以下命令可以删除文件的跟踪状态。 + +```bash +git rm --cached logs/xx.log +``` + +此时本地工作区修改还在。然后更新 .gitignore 文件,最后使用下面的命令删除远程仓库对应的文件。 + +```bash +git add . & git commit -m "xx" & git push +``` + +### skip-worktree和assume-unchanged + +skip-worktree: + +- skip-worktree 可以实现修改本地文件不会被提交,但又可以拉取最新更改的需求。适用于一些不经常变动,但是必须本地化设置的文件。 + + ```bash + git update-index --skip-worktree [file] + ``` + +- 取消skip-worktree: + + ```bash + git update-index --no-skip-worktree [file] + ``` + +- 查看 skip-worktree 列表: + + ```bash + git ls-files -v | grep '^S\ ' + ``` + +assume-unchanged: + +- 该命令只是假设文件没有变动,使用reset时,会将文件修改回去。当远程仓库相应的文件被修改时,pull更新之后,--assume-unchanged 会被清除。 + + ```bash + git update-index --assume-unchanged [file] + ``` + +- 取消忽略: + + ```bash + git update-index --no-assume-unchanged file/path + ``` + +- 查看忽略了哪些文件: + + ```bash + git ls-files -v | grep '^h\ ' + ``` + diff --git a/docs/tools/git/3-remote-repo.md b/docs/tools/git/3-remote-repo.md new file mode 100644 index 0000000..2be1371 --- /dev/null +++ b/docs/tools/git/3-remote-repo.md @@ -0,0 +1,110 @@ +# 远程仓库 + +远程仓库是指托管在网络中的项目版本库。 + +## 查看远程仓库 + +查看远程仓库地址: + +```bash +$ git remote -v +origin https://github.com/schacon/ticgit (fetch) +origin https://github.com/schacon/ticgit (push) +``` + +## 添加远程仓库 + +运行 `git remote add ` 添加远程 Git 仓库,同时指定一个简写名称。 + +```bash +git remote add pb https://github.com/paulboone/ticgit +``` + +如上命令,可以在命令行中使用字符串 pb 来代替整个 URL。如`git fetch pb`。 + +如果使用 clone 命令克隆了一个仓库,命令会自动将其添加为远程仓库并默认以 origin 为默认简写名称。 + +取消关联Git仓库: + +```bash +git remote remove origin +``` + +## 给origin设置多个远程仓库 + +如果想要给origin设置两个远程仓库地址(git add会报错),可以使用`git remote set-url --add origin url`来设置。 + +```bash +$ git remote add origin xxx.git +fatal: remote origin already exists. + +$ git remote set-url --add origin xxx.git +#success +``` + +## 修改远程仓库 + +修改远程仓库地址: + +```bash +git remote set-url origin git@github.com:Tyson0314/Blog.git +``` + +## pull 和 fetch + +从远程仓库获取数据: + +```bash +git fetch [remote-name] +``` + +git fetch 命令将数据拉取到本地仓库,但它并不会自动合并到本地分支,必须手动将其合并本地分支。 + +git pull 通常会从远程仓库拉取数据并自动尝试合并到当前所在的分支。 + +```bash +git pull = git fetch + git merge FETCH_HEAD +git pull --rebase = git fetch + git rebase FETCH_HEAD +``` + +## 本地仓库上传git服务器 + +```bash +git init # 将目录变成本地仓库 +git add . +git commit -m 'xxx' # 提交到本地仓库 +git remote add origin https://github.com/Tyson0314/profile # 关联远程仓库 +git branch --set-upstream-to=origin/master master # 本地分支关联远程分支 +git pull origin master --allow-unrelated-histories # 允许合并不相关的历史 +git push -u origin master # 如果当前分支与多个主机存在追踪关系,则-u会指定一个默认主机,这样后面就可以不加任何参数使用git push。 +``` + + +## 推送到远程仓库 + +推送使用命令:`git push [remote-name] [branch-name]` + +```bash +git push origin master +``` + +## 查看远程仓库 + +```bash +git remote show origin +``` + +## 远程仓库移除和命名 + +移除远程仓库: + +```bash +git remote rm paul +``` + +重命名远程仓库: + +```bash +git remote rename old-name new-name +``` + diff --git a/docs/tools/git/4-label.md b/docs/tools/git/4-label.md new file mode 100644 index 0000000..604a817 --- /dev/null +++ b/docs/tools/git/4-label.md @@ -0,0 +1,101 @@ +# 标签 + +给历史的某个提交打标签,如标记发布节点(v1.0等)。 + +tag标签可以帮助我们回退到某个版本的代码,我们通过tag的名称即可回退,而不需要根据某个提冗长的commit ID来回退: + +- 查看本地tag:git tag +- 新建tag:git tag -a v2.0 -m 'msg' +- 推送指定tag至远程:git push origin v2.0 +- 推送本地所有tag至远程:git push origin --tags +- 删除本地tag:git tag -d v2.0 +- 删除远程tag:git push origin --delete tag 2.0 +- 本地查看不同tag的代码:get checkout v1.0 +- 查看标签详情(包含commitId):git show v1.0 +- 回退到某个版本:git reset --hard commitId +- 获取远程分支:git fetch origin tag V2.0 + +## 创建标签 + +Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。然而,附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证。 通常建议创建附注标签。 + +创建的标签都只存储在本地,不会自动推送到远程。 + +## 附注标签 + +添加附注标签: + +```bash +git tag -a v1.4 -m 'my version 1.4' +``` + +-m 选项指定标签的信息。 + +使用 `git show v1.4` 命令可以看到标签信息和对应的提交信息。 + +## 轻量标签 + +添加轻量标签: + +```bash +git tag v1.4-tyson +``` + + 此时运行 `git show v1.4-tyson`不会看到额外的标签信息,只显示提交信息。 + +## 推送标签 + +推送某个标签到远程,使用命令: + +```bash +git push origin +``` + +一次性推送全部尚未推送到远程的本地标签: + +````bash +git push origin --tags +```` + +删除远程标签(先删除本地标签) : + +```bash +git push origin :refs/tags/ +``` + +## 后期打标签 + +比如给下面的这个提交( `modified readme.md` )打标签:` git tag -a v1.2 c1285b` + +```bash +$ git log --pretty=oneline +22fb43d9f59b983feb64ee69bd0658f37ea45db6 (HEAD -> master, tag: v1.4-tyson, tag: v1.4) add file note.md +aab2fda0b604dc295fc2bd5bfef14f3b8e3c5a98 add one line +c1285bcff4ef6b2aefdaf94eb9282fd4257621c6 modified readme.md +ba8e8a5fb932014b4aaf9ccd3163affb7699d475 renamed +d2ffb8c33978295aed189f5854857bc4e7b55358 add readme.md +``` + +## 共享标签 + +git push 命令并不会传送标签到远程仓库服务器上。在创建完标签后你必须显式地推送标签到共享服务器上: + +```bash +git push origin v1.5 +``` + +把所有不在远程仓库服务器上的标签全部传送到那里: + +```bash +git push origin --tags +``` + +## 检出标签 + +如果你想要工作目录与仓库中特定的标签版本完全一样,可以使用 `git checkout -b [branchname] [tagname]` 在特定的标签上创建一个新分支: + +```bash +$ git checkout -b version2 v2.0.0 +Switched to a new branch 'version2' +``` + diff --git a/docs/tools/git/5-branch.md b/docs/tools/git/5-branch.md new file mode 100644 index 0000000..5e41e9f --- /dev/null +++ b/docs/tools/git/5-branch.md @@ -0,0 +1,305 @@ +# git 分支 + +Git 鼓励在工作流程中频繁地使用分支与合并。 + +Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。git 提交对象会包含一个指向暂存内容快照的指针。 + +## 分支创建 + +创建 tyson 分支: + +```bash +git branch testing +``` + +查看远程分支: + +```bash +git branch -r +``` + + +## 分支切换 + +使用 `git checkout branch-name` 切换分支: + +```bash +git checkout testing +``` + +查看各个分支当前所指的对象:`git log --oneline --decorate` + +```bash +$ git log --oneline --decorate +22fb43d (HEAD -> master, tag: v1.4-tyson, tag: v1.4, tyson) add file note.md +aab2fda add one line +c1285bc (tag: v1.2) modified readme.md +ba8e8a5 renamed +d2ffb8c add readme.md +``` + +master 和 tyson 分支都指向校验和为 22fb43d 的提交对象。 + +`git checkout -b iss53` = `git branch iss53` + `git checkout iss53` + +## 分支合并 + +合并 iss53 分支到 master 分支: + +```bash +git checkout mastergit merge iss53 +``` + +squash merge:合并多个 commit 为一个,合并完需要重新提交,会修改原 commit 的提交信息,包括 author。 + +### 合并冲突 + +当合并产生冲突时不会自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于 unmerged 状态的文件。 + +```bash +<<<<<<< HEAD:index.html + +======= + +>>>>>>> iss53:index.html +``` + +在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。然后输入 `git commit -m "merge branch iss53"`完成合并提交。 + +## rebase + +现在我们有这样的两个分支,test和master,提交如下: + +```bash + D---E test + / + A---B---C---F--- master +``` + +在master执行git merge test,会生成额外的提交节点G: + +```bash + D--------E + / \ + A---B---C---F----G--- test, master +``` + +在master执行git rebase test,本地提交以补丁形式打在分支的最后面: + +```bash +A---B---D---E---C‘---F‘--- test, master +``` + +merge操作会生成一个新的节点,之前的提交分开显示。 + 而rebase操作不会生成新的节点,是将两个分支融合成一个线性的提交。 + +合并commit:`git rebase -i` + +```bash +pick c38e7ae rebase content +s 595ede1 rebase + +-- Rebase 8824682..595ede1 onto 8824682 (2 commands) + +-- Commands: +-- p, pick = use commit +-- r, reword = use commit, but edit the commit message +-- e, edit = use commit, but stop for amending +-- s, squash = use commit, but meld into previous commit +-- f, fixup = like "squash", but discard this commit's log message +-- x, exec = run command (the rest of the line) using shell +-- d, drop = remove commit +``` + +`s 595ede1 rebase`会将595ede1合到前一个commit,按下`:wq`之后会弹出对话框,合并commit message。 + +## 删除分支 + +删除本地分支: + +```bash +git branch -d tyson +``` + +删除远程分支: + +```bash +git push origin --delete master +``` + + +## 分支管理 + +得到当前所有分支的一个列表: + +``` +$ git branch +* master + tyson +``` + +\*代表当前 HEAD 指针所指向的分支。 + +查看每一个分支的最后一次提交: + +``` +$ git branch -v* master 22fb43d add file note.md tyson 22fb43d add file note.md +``` + +查看哪些分支已经合并到当前分支: + +``` +$git branch --merged iss53 *master +``` + +查看所有包含未合并工作的分支: + +``` +$git branch --no-merged +testing +``` + +如果分支包含未合并的工作,使用 `git branch -d testing` 删除时会出错,可以使用 `git branch -D testing`强制删除。 + +## 远程分支 + +### 推送 + +将本地的 master 分支推送到远程仓库 origin/master 分支: + +```bash +git push origin master +``` + +将本地的 tyson 分支推送到远程仓库的 tyson-branch 分支 : + +```bash +git push origin tyson:tyson-branch +``` + +假如当前本地分支是 tyson,抓取远程仓库数据后,需要进行合并: + +``` +git fetch origin +git merge origin/tyson +``` + +将本地的所有分支都推送到远程主机: + +```bash +git push -all origin +``` + +强制推送(**最好不用**): + +```bash +git push --force origin +``` + + +### 跟踪分支 + +``` +$ git checkout --track origin/tyson +Branch tyson set up to track remote branch tyson from origin. +Switched to a new branch 'tyson' +``` + +本地分支与远程分支设置为不同名字: + +``` +$ git checkout -b tyson-branch origin/tyson +Branch tyson-branch set up to track remote branch tyson from origin. +Switched to a new branch 'tyson-branch' +``` + +设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,使用 -u 或 --set-upstream-to 选项: + +```bash +git branch -u origin master +``` + +查看设置的所有跟踪分支: + +``` +$ git branch -vv +iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets +master 1ae2a45 [origin/master] deploying index fix +* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it +testing 5ea463a trying something new +``` + +这些数据是本地缓存的服务器数据,如果需要最新的数据,可以先运行:`git fetch --all` 然后再运行:`git branch -vv` + +### fetch和pull + +git fetch 会将远程仓库的更新拉取到本地远程仓库的副本,不会自动合并到本地仓库。 + +git fetch 步骤: + +```java +git fetch origin master:tmp //在本地新建一个tmp分支,并将远程origin仓库的master分支代码下载到本地tmp分支 +git diff tmp //来比较本地代码与刚刚从远程下载下来的代码的区别 +git merge tmp//合并tmp分支到本地的master分支 +git branch -d tmp//如果不想保留temp分支 可以用这步删除 +``` + +`git pull` = `git fetch` + `git merge` + +### 删除远程分支 + +Git 服务器会保留数据一段时间,误删的远程分支很容易恢复。 + +```bash +git push origin --delete tyson +``` + +## 创建远程分支 + +基于本地分支创建远程分支: + +```bash +git push origin backup_foreign:backup_foreign +``` + +本地新分支和远程新分支关联: + +```bash +git push --set-upstream origin backup_foreign +``` + +## cherry-pick + +可以用于将在其他分支上的 commit 修改,移植到当前的分支。 + +```bash +git cherry-pick +``` + +当执行完 cherry-pick 之后,将会自动生成一个新的 commit 进行提交,会有一个新的 commit ID,commit 信息与 cherry-pick 的 commit 信息一致。遇到冲突则解决冲突,然后 `git add 产生冲突的文件`,然后使用 `git cherry-pick --continue` 继续。这个过程中可以使用 `git cherry-pick --abort`,恢复分支到 cherry-pick 之前的状态。 + +`git cherry-pick -x ` 增加 -x 参数,表示保留原提交的作者信息进行提交。 + +在 Git 1.7.2 版本开始,新增了支持批量 cherry-pick ,就是可以一次将一个连续的时间序列内的 commit ,设定一个开始和结束的 commit ,进行 cherry-pick 操作。 + +```bash +git cherry-pick +``` + +上述命令将从start-commit-id开始到end-commit-id之间的所有commit-id提交记录都合并过来,需要注意的是,start-commit-id必须比end-commit-id提前提交。 + +### cherry-pick与rebase的区别 + +cherry-pick 操作的是某一个或某几个 commit,rebase 操作的是整个分支。 + +> 参考链接:https://juejin.im/post/5925a2d9a22b9d0058b0fd9b + +## 补丁 + +`git apply xx.patch` 需要自己重新 commit。xx.patch 必须从`git diff`中获得,才能使用 `git apply`。 + +`git am yy.patch` 会保留commit信息,yy.patch是从`git format–patch`获得的。 + + diff --git a/docs/tools/git/README.md b/docs/tools/git/README.md new file mode 100644 index 0000000..cedf61a --- /dev/null +++ b/docs/tools/git/README.md @@ -0,0 +1,17 @@ +--- +title: Git基础 +icon: git +date: 2022-08-06 +category: git +star: true +--- + +![](http://img.topjavaer.cn/img/git-status.png) + +## Git总结 + +- [Git简介](./1-introduce.md) +- [Git基础](./2-basic.md) +- [远程仓库](./3-remote-repo.md) +- [标签](./4-label.md) +- [git分支](./5-branch.md) diff --git a/docs/tools/linux-overview.md b/docs/tools/linux-overview.md new file mode 100644 index 0000000..047d58c --- /dev/null +++ b/docs/tools/linux-overview.md @@ -0,0 +1,662 @@ +--- +sidebar: heading +title: Linux学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: linux,linux基础操作,linux命令 + - - meta + - name: description + content: Linux常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 基本操作 + +## Linux关机,重启 + +``` +#关机 +shutdown -h now + +#重启 +shutdown -r now +``` + +## 查看系统,CPU信息 + +``` +#查看系统内核信息 +uname -a + +#查看系统内核版本 +cat /proc/version + +#查看当前用户环境变量 +env + +cat /proc/cpuinfo + +#查看有几个逻辑cpu, 包括cpu型号 +cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c + +#查看有几颗cpu,每颗分别是几核 +cat /proc/cpuinfo | grep physical | uniq -c + +#查看当前CPU运行在32bit还是64bit模式下, 如果是运行在32bit下也不代表CPU不支持64bit +getconf LONG_BIT + +#结果大于0, 说明支持64bit计算. lm指long mode, 支持lm则是64bit +cat /proc/cpuinfo | grep flags | grep ' lm ' | wc -l +``` + +## 建立软连接 + +``` +ln -s /usr/local/jdk1.8/ jdk +``` + +## rpm相关 + +``` +#查看是否通过rpm安装了该软件 +rpm -qa | grep 软件名 +``` + +## sshkey + +``` +#创建sshkey +ssh-keygen -t rsa -C your_email@example.com + +#id_rsa.pub 的内容拷贝到要控制的服务器的 home/username/.ssh/authorized_keys 中,如果没有则新建(.ssh权限为700, authorized_keys权限为600) +``` + +## 命令重命名 + +``` +#在各个用户的.bash_profile中添加重命名配置 +alias ll='ls -alF' +``` + +## 同步服务器时间 + +``` +sudo ntpdate -u ntp.api.bz +``` + +## 后台运行命令 + +``` +#后台运行,并且有nohup.out输出 +nohup xxx & + +#后台运行, 不输出任何日志 +nohup xxx > /dev/null & + +#后台运行, 并将错误信息做标准输出到日志中 +nohup xxx >out.log 2>&1 & +``` + +## 强制活动用户退出 + +``` +#命令来完成强制活动用户退出.其中TTY表示终端名称 +pkill -kill -t [TTY] +``` + +## 查看命令路径 + +``` +which <命令> +``` + +## 查看进程所有打开最大fd数 + +``` +ulimit -n +``` + +## 配置dns + +``` +vim /etc/resolv.conf +``` + +## nslookup,查看域名路由表 + +``` +nslookup google.com +``` + +## last, 最近登录信息列表 + +``` +#最近登录的5个账号 +last -n 5 +``` + +## 设置固定ip + +``` +ifconfig em1 192.168.5.177 netmask 255.255.255.0 +``` + +## 查看进程内加载的环境变量 + +``` +#也可以去 cd /proc 目录下, 查看进程内存中加载的东西 +ps eww -p XXXXX(进程号) +``` + +## 查看进程树找到服务器进程 + +``` +ps auwxf +``` + +## 查看进程启动路径 + +``` +cd /proc/xxx(进程号) +ls -all +#cwd对应的是启动路径 +``` + +## 添加用户, 配置sudo权限 + +``` +#新增用户 +useradd 用户名 +passwd 用户名 + +#增加sudo权限 +vim /etc/sudoers +#修改文件里面的 +#root ALL=(ALL) ALL +#用户名 ALL=(ALL) ALL +``` + +## 强制关闭进程名包含xxx的所有进程 + +``` +ps aux|grep xxx | grep -v grep | awk '{print $2}' | xargs kill -9 +``` + +# 磁盘,文件,目录相关操作 + +## vim操作 + +``` +#normal模式下 g表示全局, x表示查找的内容, y表示替换后的内容 +:%s/x/y/g + +#normal模式下 +0 #光标移到行首(数字0) +$ #光标移至行尾 +shift + g #跳到文件最后 +gg #跳到文件头 + +#显示行号 +:set nu + +#去除行号 +:set nonu + +#检索 +/xxx(检索内容) #从头检索, 按n查找下一个 +?xxx(检索内容) #从尾部检索 +``` + +## 打开只读文件,修改后需要保存时(不用切换用户即可保存的方式) + +``` +#在normal模式下 +:w !sudo tee % +``` + +## 查看磁盘, 文件目录基本信息 + +``` +#查看磁盘挂载情况 +mount + +#查看磁盘分区信息 +df + +#查看目录及子目录大小 +du -H -h + +#查看当前目录下各个文件, 文件夹占了多少空间, 不会递归 +du -sh * +``` + +## wc命令 + +``` +#查看文件里有多少行 +wc -l filename + +#看文件里有多少个word +wc -w filename + +#文件里最长的那一行是多少个字 +wc -L filename + +#统计字节数 +wc -c +``` + +## 常用压缩, 解压缩命令 + +### 压缩命令 + +``` +tar czvf xxx.tar 压缩目录 + +zip -r xxx.zip 压缩目录 +``` + +### 解压缩命令 + +``` +tar zxvf xxx.tar + +#解压到指定文件夹 +tar zxvf xxx.tar -C /xxx/yyy/ + +unzip xxx.zip +``` + +## 变更文件所属用户, 用户组 + +``` +chown eagleye.eagleye xxx.log +``` + +## cp, scp, mkdir + +``` +#复制 +cp xxx.log + +#复制并强制覆盖同名文件 +cp -f xxx.log + +#复制文件夹 +cp -r xxx(源文件夹) yyy(目标文件夹) + +#远程复制 +scp -P ssh端口 username@10.10.10.101:/home/username/xxx /home/xxx + +#级联创建目录 +mkdir -p /xxx/yyy/zzz + +#批量创建文件夹, 会在test,main下都创建java, resources文件夹 +mkdir -p src/{test,main}/{java,resources} +``` + +## 比较两个文件 + +``` +diff -u 1.txt 2.txt +``` + +## 日志输出的字节数,可以用作性能测试 + +``` +#如果做性能测试, 可以每执行一次, 往日志里面输出 “.” , 这样日志中的字节数就是实际的性能测试运行的次数, 还可以看见实时速率. +tail -f xxx.log | pv -bt +``` + +## 查看, 去除特殊字符 + +``` +#查看特殊字符 +cat -v xxx.sh + +#去除特殊字符 +sed -i 's/^M//g’ env.sh 去除文件的特殊字符, 比如^M: 需要这样输入: ctrl+v+enter +``` + +## 处理因系统原因引起的文件中特殊字符的问题 + +``` +#可以转换为该系统下的文件格式 +cat file.sh > file.sh_bak + +#先将file.sh中文件内容复制下来然后运行, 然后粘贴内容, 最后ctrl + d 保存退出 +cat > file1.sh + +#在vim中通过如下设置文件编码和文件格式 +:set fileencodings=utf-8 ,然后 w (存盘)一下即可转化为 utf8 格式, +:set fileformat=unix + +#在mac下使用dos2unix进行文件格式化 +find . -name "*.sh" | xargs dos2unix +``` + +## tee, 重定向的同时输出到屏幕 + +``` +awk ‘{print $0}’ xxx.log | tee test.log +``` + +# 检索相关 + +## grep + +``` +#反向匹配, 查找不包含xxx的内容 +grep -v xxx + +#排除所有空行 +grep -v '^/pre> + +#返回结果 2,则说明第二行是空行 +grep -n “^$” 111.txt + +#查询以abc开头的行 +grep -n “^abc” 111.txt + +#同时列出该词语出现在文章的第几行 +grep 'xxx' -n xxx.log + +#计算一下该字串出现的次数 +grep 'xxx' -c xxx.log + +#比对的时候,不计较大小写的不同 +grep 'xxx' -i xxx.log +``` + +## awk + +``` +#以':' 为分隔符,如果第五域有user则输出该行 +awk -F ':' '{if ($5 ~ /user/) print $0}' /etc/passwd + +#统计单个文件中某个字符(串)(中文无效)出现的次数 +awk -v RS='character' 'END {print --NR}' xxx.txt +``` + +## find检索命令 + +``` +#在目录下找后缀是.mysql的文件 +find /home/eagleye -name '*.mysql' -print + +#会从 /usr 目录开始往下找,找最近3天之内存取过的文件。 +find /usr -atime 3 –print + +#会从 /usr 目录开始往下找,找最近5天之内修改过的文件。 +find /usr -ctime 5 –print + +#会从 /doc 目录开始往下找,找jacky 的、文件名开头是 j的文件。 +find /doc -user jacky -name 'j*' –print + +#会从 /doc 目录开始往下找,找寻文件名是 ja 开头或者 ma开头的文件。 +find /doc \( -name 'ja*' -o- -name 'ma*' \) –print + +#会从 /doc 目录开始往下找,找到凡是文件名结尾为 bak的文件,把它删除掉。-exec 选项是执行的意思,rm 是删除命令,{ } 表示文件名,“\;”是规定的命令结尾。 +find /doc -name '*bak' -exec rm {} \; +``` + +# 网络相关 + +## 查看什么进程使用了该端口 + +``` +lsof -i:port +``` + +## 获取本机ip地址 + +``` +/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:" +``` + +## iptables + +``` +#查看iptables状态 +service iptables status + +#要封停一个ip +iptables -I INPUT -s ***.***.***.*** -j DROP + +#要解封一个IP,使用下面这条命令: +iptables -D INPUT -s ***.***.***.*** -j DROP + +备注: 参数-I是表示Insert(添加),-D表示Delete(删除)。后面跟的是规则,INPUT表示入站,***.***.***.***表示要封停的IP,DROP表示放弃连接。 + +#开启9090端口的访问 +/sbin/iptables -I INPUT -p tcp --dport 9090 -j ACCEPT + +#防火墙开启、关闭、重启 +/etc/init.d/iptables status +/etc/init.d/iptables start +/etc/init.d/iptables stop +/etc/init.d/iptables restart +``` + +## nc命令, tcp调试利器 + +``` +#给某一个endpoint发送TCP请求,就将data的内容发送到对端 +nc 192.168.0.11 8000 < data.txt + +#nc可以当做服务器,监听某个端口号,把某一次请求的内容存储到received_data里 +nc -l 8000 > received_data + +#上边只监听一次,如果多次可以加上-k参数 +nc -lk 8000 +``` + +## tcpdump + +``` +#dump出本机12301端口的tcp包 +tcpdump -i em1 tcp port 12301 -s 1500 -w abc.pcap +``` + +## 跟踪网络路由路径 + +``` +#traceroute默认使用udp方式, 如果是-I则改成icmp方式 +traceroute -I www.163.com + +#从ttl第3跳跟踪 +traceroute -M 3 www.163.com + +#加上端口跟踪 +traceroute -p 8080 192.168.10.11 +``` + +## ss + +``` +#显示本地打开的所有端口 +ss -l + +#显示每个进程具体打开的socket +ss -pl + +#显示所有tcp socket +ss -t -a + +#显示所有的UDP Socekt +ss -u -a + +#显示所有已建立的SMTP连接 +ss -o state established '( dport = :smtp or sport = :smtp )' + +#显示所有已建立的HTTP连接 +ss -o state established '( dport = :http or sport = :http )' + +找出所有连接X服务器的进程 +ss -x src /tmp/.X11-unix/* + +列出当前socket统计信息 +ss -s + +解释:netstat是遍历/proc下面每个PID目录,ss直接读/proc/net下面的统计信息。所以ss执行的时候消耗资源以及消耗的时间都比netstat少很多 +``` + +## netstat + +``` +#输出每个ip的连接数,以及总的各个状态的连接数 +netstat -n | awk '/^tcp/ {n=split($(NF-1),array,":");if(n<=2)++S[array[(1)]];else++S[array[(4)]];++s[$NF];++N} END {for(a in S){printf("%-20s %s\n", a, S[a]);++I}printf("%-20s %s\n","TOTAL_IP",I);for(a in s) printf("%-20s %s\n",a, s[a]);printf("%-20s %s\n","TOTAL_LINK",N);}' + +#统计所有连接状态, +#CLOSED:无连接是活动的或正在进行 +#LISTEN:服务器在等待进入呼叫 +#SYN_RECV:一个连接请求已经到达,等待确认 +#SYN_SENT:应用已经开始,打开一个连接 +#ESTABLISHED:正常数据传输状态 +#FIN_WAIT1:应用说它已经完成 +#FIN_WAIT2:另一边已同意释放 +#ITMED_WAIT:等待所有分组死掉 +#CLOSING:两边同时尝试关闭 +#TIME_WAIT:主动关闭连接一端还没有等到另一端反馈期间的状态 +#LAST_ACK:等待所有分组死掉 +netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' + +#查找较多time_wait连接 +netstat -n|grep TIME_WAIT|awk '{print $5}'|sort|uniq -c|sort -rn|head -n20 +``` + +# 监控linux性能命令 + +## top + +``` +按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序, 然后回车。而大写的 R 键可以将当前的排序倒转 +``` + +| 列名 | 含义 | +| :------ | :----------------------------------------------------------- | +| PID | 进程id | +| PPID | 父进程id | +| RUSER | Real user name | +| UID | 进程所有者的用户id | +| USER | 进程所有者的用户名 | +| GROUP | 进程所有者的组名 | +| TTY | 启动进程的终端名。不是从终端启动的进程则显示为 ? | +| PR | 优先级 | +| NI | nice值。负值表示高优先级,正值表示低优先级 | +| P | 最后使用的CPU,仅在多CPU环境下有意义 | +| %CPU | 上次更新到现在的CPU时间占用百分比 | +| TIME | 进程使用的CPU时间总计,单位秒 | +| TIME+ | 进程使用的CPU时间总计,单位1/100秒 | +| %MEM | 进程使用的物理内存百分比 | +| VIRT | 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES | +| SWAP | 进程使用的虚拟内存中,被换出的大小,单位kb。 | +| RES | 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA | +| CODE | 可执行代码占用的物理内存大小,单位kb | +| DATA | 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb | +| SHR | 共享内存大小,单位kb | +| nFLT | 页面错误次数 | +| nDRT | 最后一次写入到现在,被修改过的页面数。 | +| S | 进程状态。D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程 | +| COMMAND | 命令名/命令行 | +| WCHAN | 若该进程在睡眠,则显示睡眠中的系统函数名 | +| Flags | 任务标志,参考 sched.h | + +## dmesg,查看系统日志 + +``` +dmesg +``` + +## iostat,磁盘IO情况监控 + +``` +iostat -xz 1 + +#r/s, w/s, rkB/s, wkB/s:分别表示每秒读写次数和每秒读写数据量(千字节)。读写量过大,可能会引起性能问题。 +#await:IO操作的平均等待时间,单位是毫秒。这是应用程序在和磁盘交互时,需要消耗的时间,包括IO等待和实际操作的耗时。如果这个数值过大,可能是硬件设备遇到了瓶颈或者出现故障。 +#avgqu-sz:向设备发出的请求平均数量。如果这个数值大于1,可能是硬件设备已经饱和(部分前端硬件设备支持并行写入)。 +#%util:设备利用率。这个数值表示设备的繁忙程度,经验值是如果超过60,可能会影响IO性能(可以参照IO操作平均等待时间)。如果到达100%,说明硬件设备已经饱和。 +#如果显示的是逻辑设备的数据,那么设备利用率不代表后端实际的硬件设备已经饱和。值得注意的是,即使IO性能不理想,也不一定意味这应用程序性能会不好,可以利用诸如预读取、写缓存等策略提升应用性能。 +``` + +## free,内存使用情况 + +``` +free -m + +eg: + + total used free shared buffers cached +Mem: 1002 769 232 0 62 421 +-/+ buffers/cache: 286 715 +Swap: 1153 0 1153 + +第一部分Mem行: +total 内存总数: 1002M +used 已经使用的内存数: 769M +free 空闲的内存数: 232M +shared 当前已经废弃不用,总是0 +buffers Buffer 缓存内存数: 62M +cached Page 缓存内存数:421M + +关系:total(1002M) = used(769M) + free(232M) + +第二部分(-/+ buffers/cache): +(-buffers/cache) used内存数:286M (指的第一部分Mem行中的used – buffers – cached) +(+buffers/cache) free内存数: 715M (指的第一部分Mem行中的free + buffers + cached) + +可见-buffers/cache反映的是被程序实实在在吃掉的内存,而+buffers/cache反映的是可以挪用的内存总数. + +第三部分是指交换分区 +``` + +## sar,查看网络吞吐状态 + +``` +#sar命令在这里可以查看网络设备的吞吐率。在排查性能问题时,可以通过网络设备的吞吐量,判断网络设备是否已经饱和 +sar -n DEV 1 + +# +#sar命令在这里用于查看TCP连接状态,其中包括: +#active/s:每秒本地发起的TCP连接数,既通过connect调用创建的TCP连接; +#passive/s:每秒远程发起的TCP连接数,即通过accept调用创建的TCP连接; +#retrans/s:每秒TCP重传数量; +#TCP连接数可以用来判断性能问题是否由于建立了过多的连接,进一步可以判断是主动发起的连接,还是被动接受的连接。TCP重传可能是因为网络环境恶劣,或者服务器压力过大导致丢包 +sar -n TCP,ETCP 1 +``` + +## vmstat, 给定时间监控CPU使用率, 内存使用, 虚拟内存交互, IO读写 + +``` +#2表示每2秒采集一次状态信息, 1表示只采集一次(忽略既是一直采集) +vmstat 2 1 + +eg: +r b swpd free buff cache si so bi bo in cs us sy id wa +1 0 0 3499840 315836 3819660 0 0 0 1 2 0 0 0 100 0 +0 0 0 3499584 315836 3819660 0 0 0 0 88 158 0 0 100 0 +0 0 0 3499708 315836 3819660 0 0 0 2 86 162 0 0 100 0 +0 0 0 3499708 315836 3819660 0 0 0 10 81 151 0 0 100 0 +1 0 0 3499732 315836 3819660 0 0 0 2 83 154 0 0 100 0 +``` + +- r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。 +- b 表示阻塞的进程,这个不多说,进程阻塞,大家懂的。 +- swpd 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。 +- free 空闲的物理内存的大小,我的机器内存总共8G,剩余3415M。 +- buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M +- cache cache直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。) +- si 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。 +- so 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上。 +- bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒 +- bo 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。 +- in 每秒CPU的中断次数,包括时间中断 +- cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。 +- us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。 +- sy 系统CPU时间,如果太高,表示系统调用时间长,例如是IO操作频繁。 +- id 空闲 CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。 +- wt 等待IO CPU时间。 diff --git a/docs/tools/linux/1-basic.md b/docs/tools/linux/1-basic.md new file mode 100644 index 0000000..3703c5d --- /dev/null +++ b/docs/tools/linux/1-basic.md @@ -0,0 +1,172 @@ +# 基本操作 + +## Linux关机,重启 + +``` +#关机 +shutdown -h now + +#重启 +shutdown -r now +``` + +## 查看系统,CPU信息 + +``` +#查看系统内核信息 +uname -a + +#查看系统内核版本 +cat /proc/version + +#查看当前用户环境变量 +env + +cat /proc/cpuinfo + +#查看有几个逻辑cpu, 包括cpu型号 +cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c + +#查看有几颗cpu,每颗分别是几核 +cat /proc/cpuinfo | grep physical | uniq -c + +#查看当前CPU运行在32bit还是64bit模式下, 如果是运行在32bit下也不代表CPU不支持64bit +getconf LONG_BIT + +#结果大于0, 说明支持64bit计算. lm指long mode, 支持lm则是64bit +cat /proc/cpuinfo | grep flags | grep ' lm ' | wc -l +``` + +## 建立软连接 + +``` +ln -s /usr/local/jdk1.8/ jdk +``` + +## rpm相关 + +``` +#查看是否通过rpm安装了该软件 +rpm -qa | grep 软件名 +``` + +## sshkey + +``` +#创建sshkey +ssh-keygen -t rsa -C your_email@example.com + +#id_rsa.pub 的内容拷贝到要控制的服务器的 home/username/.ssh/authorized_keys 中,如果没有则新建(.ssh权限为700, authorized_keys权限为600) +``` + +## 命令重命名 + +``` +#在各个用户的.bash_profile中添加重命名配置 +alias ll='ls -alF' +``` + +## 同步服务器时间 + +``` +sudo ntpdate -u ntp.api.bz +``` + +## 后台运行命令 + +``` +#后台运行,并且有nohup.out输出 +nohup xxx & + +#后台运行, 不输出任何日志 +nohup xxx > /dev/null & + +#后台运行, 并将错误信息做标准输出到日志中 +nohup xxx >out.log 2>&1 & +``` + +## 强制活动用户退出 + +``` +#命令来完成强制活动用户退出.其中TTY表示终端名称 +pkill -kill -t [TTY] +``` + +## 查看命令路径 + +``` +which <命令> +``` + +## 查看进程所有打开最大fd数 + +``` +ulimit -n +``` + +## 配置dns + +``` +vim /etc/resolv.conf +``` + +## nslookup,查看域名路由表 + +``` +nslookup google.com +``` + +## last, 最近登录信息列表 + +``` +#最近登录的5个账号 +last -n 5 +``` + +## 设置固定ip + +``` +ifconfig em1 192.168.5.177 netmask 255.255.255.0 +``` + +## 查看进程内加载的环境变量 + +``` +#也可以去 cd /proc 目录下, 查看进程内存中加载的东西 +ps eww -p XXXXX(进程号) +``` + +## 查看进程树找到服务器进程 + +``` +ps auwxf +``` + +## 查看进程启动路径 + +``` +cd /proc/xxx(进程号) +ls -all +#cwd对应的是启动路径 +``` + +## 添加用户, 配置sudo权限 + +``` +#新增用户 +useradd 用户名 +passwd 用户名 + +#增加sudo权限 +vim /etc/sudoers +#修改文件里面的 +#root ALL=(ALL) ALL +#用户名 ALL=(ALL) ALL +``` + +## 强制关闭进程名包含xxx的所有进程 + +``` +ps aux|grep xxx | grep -v grep | awk '{print $2}' | xargs kill -9 +``` + diff --git a/docs/tools/linux/2-disk-file.md b/docs/tools/linux/2-disk-file.md new file mode 100644 index 0000000..ea23537 --- /dev/null +++ b/docs/tools/linux/2-disk-file.md @@ -0,0 +1,159 @@ +# 磁盘,文件,目录相关操作 + +## vim操作 + +``` +#normal模式下 g表示全局, x表示查找的内容, y表示替换后的内容 +:%s/x/y/g + +#normal模式下 +0 #光标移到行首(数字0) +$ #光标移至行尾 +shift + g #跳到文件最后 +gg #跳到文件头 + +#显示行号 +:set nu + +#去除行号 +:set nonu + +#检索 +/xxx(检索内容) #从头检索, 按n查找下一个 +?xxx(检索内容) #从尾部检索 +``` + +## 打开只读文件,修改后需要保存时(不用切换用户即可保存的方式) + +``` +#在normal模式下 +:w !sudo tee % +``` + +## 查看磁盘, 文件目录基本信息 + +``` +#查看磁盘挂载情况 +mount + +#查看磁盘分区信息 +df + +#查看目录及子目录大小 +du -H -h + +#查看当前目录下各个文件, 文件夹占了多少空间, 不会递归 +du -sh * +``` + +## wc命令 + +``` +#查看文件里有多少行 +wc -l filename + +#看文件里有多少个word +wc -w filename + +#文件里最长的那一行是多少个字 +wc -L filename + +#统计字节数 +wc -c +``` + +## 常用压缩, 解压缩命令 + +### 压缩命令 + +``` +tar czvf xxx.tar 压缩目录 + +zip -r xxx.zip 压缩目录 +``` + +### 解压缩命令 + +``` +tar zxvf xxx.tar + +#解压到指定文件夹 +tar zxvf xxx.tar -C /xxx/yyy/ + +unzip xxx.zip +``` + +## 变更文件所属用户, 用户组 + +``` +chown eagleye.eagleye xxx.log +``` + +## cp, scp, mkdir + +``` +#复制 +cp xxx.log + +#复制并强制覆盖同名文件 +cp -f xxx.log + +#复制文件夹 +cp -r xxx(源文件夹) yyy(目标文件夹) + +#远程复制 +scp -P ssh端口 username@10.10.10.101:/home/username/xxx /home/xxx + +#级联创建目录 +mkdir -p /xxx/yyy/zzz + +#批量创建文件夹, 会在test,main下都创建java, resources文件夹 +mkdir -p src/{test,main}/{java,resources} +``` + +## 比较两个文件 + +``` +diff -u 1.txt 2.txt +``` + +## 日志输出的字节数,可以用作性能测试 + +``` +#如果做性能测试, 可以每执行一次, 往日志里面输出 “.” , 这样日志中的字节数就是实际的性能测试运行的次数, 还可以看见实时速率. +tail -f xxx.log | pv -bt +``` + +## 查看, 去除特殊字符 + +``` +#查看特殊字符 +cat -v xxx.sh + +#去除特殊字符 +sed -i 's/^M//g’ env.sh 去除文件的特殊字符, 比如^M: 需要这样输入: ctrl+v+enter +``` + +## 处理因系统原因引起的文件中特殊字符的问题 + +``` +#可以转换为该系统下的文件格式 +cat file.sh > file.sh_bak + +#先将file.sh中文件内容复制下来然后运行, 然后粘贴内容, 最后ctrl + d 保存退出 +cat > file1.sh + +#在vim中通过如下设置文件编码和文件格式 +:set fileencodings=utf-8 ,然后 w (存盘)一下即可转化为 utf8 格式, +:set fileformat=unix + +#在mac下使用dos2unix进行文件格式化 +find . -name "*.sh" | xargs dos2unix +``` + +## tee, 重定向的同时输出到屏幕 + +``` +awk ‘{print $0}’ xxx.log | tee test.log +``` + diff --git a/docs/tools/linux/3-search.md b/docs/tools/linux/3-search.md new file mode 100644 index 0000000..e8d4109 --- /dev/null +++ b/docs/tools/linux/3-search.md @@ -0,0 +1,59 @@ +# 检索相关 + +## grep + +``` +#反向匹配, 查找不包含xxx的内容 +grep -v xxx + +#排除所有空行 +grep -v '^/pre> + +#返回结果 2,则说明第二行是空行 +grep -n “^$” 111.txt + +#查询以abc开头的行 +grep -n “^abc” 111.txt + +#同时列出该词语出现在文章的第几行 +grep 'xxx' -n xxx.log + +#计算一下该字串出现的次数 +grep 'xxx' -c xxx.log + +#比对的时候,不计较大小写的不同 +grep 'xxx' -i xxx.log +``` + +## awk + +``` +#以':' 为分隔符,如果第五域有user则输出该行 +awk -F ':' '{if ($5 ~ /user/) print $0}' /etc/passwd + +#统计单个文件中某个字符(串)(中文无效)出现的次数 +awk -v RS='character' 'END {print --NR}' xxx.txt +``` + +## find检索命令 + +``` +#在目录下找后缀是.mysql的文件 +find /home/eagleye -name '*.mysql' -print + +#会从 /usr 目录开始往下找,找最近3天之内存取过的文件。 +find /usr -atime 3 –print + +#会从 /usr 目录开始往下找,找最近5天之内修改过的文件。 +find /usr -ctime 5 –print + +#会从 /doc 目录开始往下找,找jacky 的、文件名开头是 j的文件。 +find /doc -user jacky -name 'j*' –print + +#会从 /doc 目录开始往下找,找寻文件名是 ja 开头或者 ma开头的文件。 +find /doc \( -name 'ja*' -o- -name 'ma*' \) –print + +#会从 /doc 目录开始往下找,找到凡是文件名结尾为 bak的文件,把它删除掉。-exec 选项是执行的意思,rm 是删除命令,{ } 表示文件名,“\;”是规定的命令结尾。 +find /doc -name '*bak' -exec rm {} \; +``` + diff --git a/docs/tools/linux/4-net.md b/docs/tools/linux/4-net.md new file mode 100644 index 0000000..2c7fc27 --- /dev/null +++ b/docs/tools/linux/4-net.md @@ -0,0 +1,125 @@ +# 网络相关 + +## 查看什么进程使用了该端口 + +``` +lsof -i:port +``` + +## 获取本机ip地址 + +``` +/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:" +``` + +## iptables + +``` +#查看iptables状态 +service iptables status + +#要封停一个ip +iptables -I INPUT -s ***.***.***.*** -j DROP + +#要解封一个IP,使用下面这条命令: +iptables -D INPUT -s ***.***.***.*** -j DROP + +备注: 参数-I是表示Insert(添加),-D表示Delete(删除)。后面跟的是规则,INPUT表示入站,***.***.***.***表示要封停的IP,DROP表示放弃连接。 + +#开启9090端口的访问 +/sbin/iptables -I INPUT -p tcp --dport 9090 -j ACCEPT + +#防火墙开启、关闭、重启 +/etc/init.d/iptables status +/etc/init.d/iptables start +/etc/init.d/iptables stop +/etc/init.d/iptables restart +``` + +## nc命令, tcp调试利器 + +``` +#给某一个endpoint发送TCP请求,就将data的内容发送到对端 +nc 192.168.0.11 8000 < data.txt + +#nc可以当做服务器,监听某个端口号,把某一次请求的内容存储到received_data里 +nc -l 8000 > received_data + +#上边只监听一次,如果多次可以加上-k参数 +nc -lk 8000 +``` + +## tcpdump + +``` +#dump出本机12301端口的tcp包 +tcpdump -i em1 tcp port 12301 -s 1500 -w abc.pcap +``` + +## 跟踪网络路由路径 + +``` +#traceroute默认使用udp方式, 如果是-I则改成icmp方式 +traceroute -I www.163.com + +#从ttl第3跳跟踪 +traceroute -M 3 www.163.com + +#加上端口跟踪 +traceroute -p 8080 192.168.10.11 +``` + +## ss + +``` +#显示本地打开的所有端口 +ss -l + +#显示每个进程具体打开的socket +ss -pl + +#显示所有tcp socket +ss -t -a + +#显示所有的UDP Socekt +ss -u -a + +#显示所有已建立的SMTP连接 +ss -o state established '( dport = :smtp or sport = :smtp )' + +#显示所有已建立的HTTP连接 +ss -o state established '( dport = :http or sport = :http )' + +找出所有连接X服务器的进程 +ss -x src /tmp/.X11-unix/* + +列出当前socket统计信息 +ss -s + +解释:netstat是遍历/proc下面每个PID目录,ss直接读/proc/net下面的统计信息。所以ss执行的时候消耗资源以及消耗的时间都比netstat少很多 +``` + +## netstat + +``` +#输出每个ip的连接数,以及总的各个状态的连接数 +netstat -n | awk '/^tcp/ {n=split($(NF-1),array,":");if(n<=2)++S[array[(1)]];else++S[array[(4)]];++s[$NF];++N} END {for(a in S){printf("%-20s %s\n", a, S[a]);++I}printf("%-20s %s\n","TOTAL_IP",I);for(a in s) printf("%-20s %s\n",a, s[a]);printf("%-20s %s\n","TOTAL_LINK",N);}' + +#统计所有连接状态, +#CLOSED:无连接是活动的或正在进行 +#LISTEN:服务器在等待进入呼叫 +#SYN_RECV:一个连接请求已经到达,等待确认 +#SYN_SENT:应用已经开始,打开一个连接 +#ESTABLISHED:正常数据传输状态 +#FIN_WAIT1:应用说它已经完成 +#FIN_WAIT2:另一边已同意释放 +#ITMED_WAIT:等待所有分组死掉 +#CLOSING:两边同时尝试关闭 +#TIME_WAIT:主动关闭连接一端还没有等到另一端反馈期间的状态 +#LAST_ACK:等待所有分组死掉 +netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' + +#查找较多time_wait连接 +netstat -n|grep TIME_WAIT|awk '{print $5}'|sort|uniq -c|sort -rn|head -n20 +``` + diff --git a/docs/tools/linux/5-monitor.md b/docs/tools/linux/5-monitor.md new file mode 100644 index 0000000..59b17e9 --- /dev/null +++ b/docs/tools/linux/5-monitor.md @@ -0,0 +1,131 @@ +# 监控linux性能命令 + +## top + +``` +按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序, 然后回车。而大写的 R 键可以将当前的排序倒转 +``` + +| 列名 | 含义 | +| :------ | :----------------------------------------------------------- | +| PID | 进程id | +| PPID | 父进程id | +| RUSER | Real user name | +| UID | 进程所有者的用户id | +| USER | 进程所有者的用户名 | +| GROUP | 进程所有者的组名 | +| TTY | 启动进程的终端名。不是从终端启动的进程则显示为 ? | +| PR | 优先级 | +| NI | nice值。负值表示高优先级,正值表示低优先级 | +| P | 最后使用的CPU,仅在多CPU环境下有意义 | +| %CPU | 上次更新到现在的CPU时间占用百分比 | +| TIME | 进程使用的CPU时间总计,单位秒 | +| TIME+ | 进程使用的CPU时间总计,单位1/100秒 | +| %MEM | 进程使用的物理内存百分比 | +| VIRT | 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES | +| SWAP | 进程使用的虚拟内存中,被换出的大小,单位kb。 | +| RES | 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA | +| CODE | 可执行代码占用的物理内存大小,单位kb | +| DATA | 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb | +| SHR | 共享内存大小,单位kb | +| nFLT | 页面错误次数 | +| nDRT | 最后一次写入到现在,被修改过的页面数。 | +| S | 进程状态。D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程 | +| COMMAND | 命令名/命令行 | +| WCHAN | 若该进程在睡眠,则显示睡眠中的系统函数名 | +| Flags | 任务标志,参考 sched.h | + +## dmesg,查看系统日志 + +``` +dmesg +``` + +## iostat,磁盘IO情况监控 + +``` +iostat -xz 1 + +#r/s, w/s, rkB/s, wkB/s:分别表示每秒读写次数和每秒读写数据量(千字节)。读写量过大,可能会引起性能问题。 +#await:IO操作的平均等待时间,单位是毫秒。这是应用程序在和磁盘交互时,需要消耗的时间,包括IO等待和实际操作的耗时。如果这个数值过大,可能是硬件设备遇到了瓶颈或者出现故障。 +#avgqu-sz:向设备发出的请求平均数量。如果这个数值大于1,可能是硬件设备已经饱和(部分前端硬件设备支持并行写入)。 +#%util:设备利用率。这个数值表示设备的繁忙程度,经验值是如果超过60,可能会影响IO性能(可以参照IO操作平均等待时间)。如果到达100%,说明硬件设备已经饱和。 +#如果显示的是逻辑设备的数据,那么设备利用率不代表后端实际的硬件设备已经饱和。值得注意的是,即使IO性能不理想,也不一定意味这应用程序性能会不好,可以利用诸如预读取、写缓存等策略提升应用性能。 +``` + +## free,内存使用情况 + +``` +free -m + +eg: + + total used free shared buffers cached +Mem: 1002 769 232 0 62 421 +-/+ buffers/cache: 286 715 +Swap: 1153 0 1153 + +第一部分Mem行: +total 内存总数: 1002M +used 已经使用的内存数: 769M +free 空闲的内存数: 232M +shared 当前已经废弃不用,总是0 +buffers Buffer 缓存内存数: 62M +cached Page 缓存内存数:421M + +关系:total(1002M) = used(769M) + free(232M) + +第二部分(-/+ buffers/cache): +(-buffers/cache) used内存数:286M (指的第一部分Mem行中的used – buffers – cached) +(+buffers/cache) free内存数: 715M (指的第一部分Mem行中的free + buffers + cached) + +可见-buffers/cache反映的是被程序实实在在吃掉的内存,而+buffers/cache反映的是可以挪用的内存总数. + +第三部分是指交换分区 +``` + +## sar,查看网络吞吐状态 + +``` +#sar命令在这里可以查看网络设备的吞吐率。在排查性能问题时,可以通过网络设备的吞吐量,判断网络设备是否已经饱和 +sar -n DEV 1 + +# +#sar命令在这里用于查看TCP连接状态,其中包括: +#active/s:每秒本地发起的TCP连接数,既通过connect调用创建的TCP连接; +#passive/s:每秒远程发起的TCP连接数,即通过accept调用创建的TCP连接; +#retrans/s:每秒TCP重传数量; +#TCP连接数可以用来判断性能问题是否由于建立了过多的连接,进一步可以判断是主动发起的连接,还是被动接受的连接。TCP重传可能是因为网络环境恶劣,或者服务器压力过大导致丢包 +sar -n TCP,ETCP 1 +``` + +## vmstat, 给定时间监控CPU使用率, 内存使用, 虚拟内存交互, IO读写 + +``` +#2表示每2秒采集一次状态信息, 1表示只采集一次(忽略既是一直采集) +vmstat 2 1 + +eg: +r b swpd free buff cache si so bi bo in cs us sy id wa +1 0 0 3499840 315836 3819660 0 0 0 1 2 0 0 0 100 0 +0 0 0 3499584 315836 3819660 0 0 0 0 88 158 0 0 100 0 +0 0 0 3499708 315836 3819660 0 0 0 2 86 162 0 0 100 0 +0 0 0 3499708 315836 3819660 0 0 0 10 81 151 0 0 100 0 +1 0 0 3499732 315836 3819660 0 0 0 2 83 154 0 0 100 0 +``` + +- r 表示运行队列(就是说多少个进程真的分配到CPU),我测试的服务器目前CPU比较空闲,没什么程序在跑,当这个值超过了CPU数目,就会出现CPU瓶颈了。这个也和top的负载有关系,一般负载超过了3就比较高,超过了5就高,超过了10就不正常了,服务器的状态很危险。top的负载类似每秒的运行队列。如果运行队列过大,表示你的CPU很繁忙,一般会造成CPU使用率很高。 +- b 表示阻塞的进程,这个不多说,进程阻塞,大家懂的。 +- swpd 虚拟内存已使用的大小,如果大于0,表示你的机器物理内存不足了,如果不是程序内存泄露的原因,那么你该升级内存了或者把耗内存的任务迁移到其他机器。 +- free 空闲的物理内存的大小,我的机器内存总共8G,剩余3415M。 +- buff Linux/Unix系统是用来存储,目录里面有什么内容,权限等的缓存,我本机大概占用300多M +- cache cache直接用来记忆我们打开的文件,给文件做缓冲,我本机大概占用300多M(这里是Linux/Unix的聪明之处,把空闲的物理内存的一部分拿来做文件和目录的缓存,是为了提高 程序执行的性能,当程序使用内存时,buffer/cached会很快地被使用。) +- si 每秒从磁盘读入虚拟内存的大小,如果这个值大于0,表示物理内存不够用或者内存泄露了,要查找耗内存进程解决掉。我的机器内存充裕,一切正常。 +- so 每秒虚拟内存写入磁盘的大小,如果这个值大于0,同上。 +- bi 块设备每秒接收的块数量,这里的块设备是指系统上所有的磁盘和其他块设备,默认块大小是1024byte,我本机上没什么IO操作,所以一直是0,但是我曾在处理拷贝大量数据(2-3T)的机器上看过可以达到140000/s,磁盘写入速度差不多140M每秒 +- bo 块设备每秒发送的块数量,例如我们读取文件,bo就要大于0。bi和bo一般都要接近0,不然就是IO过于频繁,需要调整。 +- in 每秒CPU的中断次数,包括时间中断 +- cs 每秒上下文切换次数,例如我们调用系统函数,就要进行上下文切换,线程的切换,也要进程上下文切换,这个值要越小越好,太大了,要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中,我们一般做性能测试时会进行几千并发甚至几万并发的测试,选择web服务器的进程可以由进程或者线程的峰值一直下调,压测,直到cs到一个比较小的值,这个进程和线程数就是比较合适的值了。系统调用也是,每次调用系统函数,我们的代码就会进入内核空间,导致上下文切换,这个是很耗资源,也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换,导致CPU干正经事的时间少了,CPU没有充分利用,是不可取的。 +- us 用户CPU时间,我曾经在一个做加密解密很频繁的服务器上,可以看到us接近100,r运行队列达到80(机器在做压力测试,性能表现不佳)。 +- sy 系统CPU时间,如果太高,表示系统调用时间长,例如是IO操作频繁。 +- id 空闲 CPU时间,一般来说,id + us + sy = 100,一般我认为id是空闲CPU使用率,us是用户CPU使用率,sy是系统CPU使用率。 diff --git a/docs/tools/linux/README.md b/docs/tools/linux/README.md new file mode 100644 index 0000000..c1f622b --- /dev/null +++ b/docs/tools/linux/README.md @@ -0,0 +1,12 @@ +虽然平时大部分工作都是和Java相关的开发, 但是每天都会接触Linux系统, 尤其是使用了Mac之后, 每天都是工作在黑色背景的命令行环境中. 自己记忆力不好, 很多有用的Linux命令不能很好的记忆, 现在逐渐总结一下, 以便后续查看。 + +> 来源:siye1982.github.io/2016/02/25/linux-list + + +## Linux基础命令 + +- [基本操作](./1-basic.md) +- [磁盘,文件,目录相关操作](./2-disk-file.md) +- [检索相关](./3-search.md) +- [网络相关](./4-net.md) +- [监控linux性能命令](./5-monitor.md) diff --git "a/\345\267\245\345\205\267/Maven\345\256\236\346\210\230.md" b/docs/tools/maven-overview.md similarity index 83% rename from "\345\267\245\345\205\267/Maven\345\256\236\346\210\230.md" rename to docs/tools/maven-overview.md index a8255db..f9506cd 100644 --- "a/\345\267\245\345\205\267/Maven\345\256\236\346\210\230.md" +++ b/docs/tools/maven-overview.md @@ -1,61 +1,33 @@ - - - -- [简介](#%E7%AE%80%E4%BB%8B) - - [配置](#%E9%85%8D%E7%BD%AE) -- [入门](#%E5%85%A5%E9%97%A8) - - [编写测试代码](#%E7%BC%96%E5%86%99%E6%B5%8B%E8%AF%95%E4%BB%A3%E7%A0%81) - - [添加 junit 依赖](#%E6%B7%BB%E5%8A%A0-junit-%E4%BE%9D%E8%B5%96) - - [编译](#%E7%BC%96%E8%AF%91) - - [测试代码](#%E6%B5%8B%E8%AF%95%E4%BB%A3%E7%A0%81) - - [执行测试](#%E6%89%A7%E8%A1%8C%E6%B5%8B%E8%AF%95) - - [`mvn clean test`](#mvn-clean-test) - - [打包和安装](#%E6%89%93%E5%8C%85%E5%92%8C%E5%AE%89%E8%A3%85) -- [依赖](#%E4%BE%9D%E8%B5%96) - - [依赖范围 scope](#%E4%BE%9D%E8%B5%96%E8%8C%83%E5%9B%B4-scope) - - [传递性依赖](#%E4%BC%A0%E9%80%92%E6%80%A7%E4%BE%9D%E8%B5%96) - - [排除依赖](#%E6%8E%92%E9%99%A4%E4%BE%9D%E8%B5%96) - - [优化依赖](#%E4%BC%98%E5%8C%96%E4%BE%9D%E8%B5%96) -- [仓库](#%E4%BB%93%E5%BA%93) - - [本地仓库](#%E6%9C%AC%E5%9C%B0%E4%BB%93%E5%BA%93) - - [远程仓库](#%E8%BF%9C%E7%A8%8B%E4%BB%93%E5%BA%93) - - [认证和部署](#%E8%AE%A4%E8%AF%81%E5%92%8C%E9%83%A8%E7%BD%B2) - - [镜像](#%E9%95%9C%E5%83%8F) -- [生命周期](#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) - - [三套生命周期](#%E4%B8%89%E5%A5%97%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) - - [命令行与生命周期](#%E5%91%BD%E4%BB%A4%E8%A1%8C%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) -- [插件](#%E6%8F%92%E4%BB%B6) - - [内置绑定](#%E5%86%85%E7%BD%AE%E7%BB%91%E5%AE%9A) - - [自定义绑定](#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%91%E5%AE%9A) - - [命令行插件配置](#%E5%91%BD%E4%BB%A4%E8%A1%8C%E6%8F%92%E4%BB%B6%E9%85%8D%E7%BD%AE) - - [插件全局配置](#%E6%8F%92%E4%BB%B6%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE) -- [聚合](#%E8%81%9A%E5%90%88) -- [继承](#%E7%BB%A7%E6%89%BF) - - [依赖管理](#%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86) - - [import 导入依赖管理](#import-%E5%AF%BC%E5%85%A5%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86) - - [插件管理](#%E6%8F%92%E4%BB%B6%E7%AE%A1%E7%90%86) -- [测试](#%E6%B5%8B%E8%AF%95) - - [跳过测试](#%E8%B7%B3%E8%BF%87%E6%B5%8B%E8%AF%95) - - - -## 简介 +--- +sidebar: heading +title: Maven学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: Maven,maven依赖,maven仓库,maven插件,maven聚合,maven生命周期 + - - meta + - name: description + content: Maven常见知识点和面试题总结,让天下没有难背的八股文! +--- + +# 简介 Maven 是强大的构建工具,能够帮我们自动化构建过程--清理、编译、测试、打包和部署。比如测试,我们无需告诉 maven 如何去测试,只需遵循 maven 的约定编写好测试用例,当我们运行构建的时候,这些测试就会自动运行。 Maven 不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮助我们自动下载构件。 -### 配置 +## 配置 配置用户范围 settings.xml。M2_HOME/conf/settings.xml 是全局范围的,而~/.m2/settings.xml 是用户范围的。配置成用户范围便于 Maven 升级。若直接修改 conf 目录下的 settings.xml,每次 Maven 升级时,都需要直接 settings.xml 文件。 +# 入门 +## 编写测试代码 -## 入门 - -### 编写测试代码 - -#### 添加 junit 依赖 +## 添加 junit 依赖 ```xml @@ -70,7 +42,7 @@ Maven 不仅是构建工具,还是一个依赖管理工具和项目信息管 maven 会自动访问中央仓库,下载 junit 依赖。scope 为 test 表明依赖只对测试有效,即测试代码中的 import JUnit 代码没有问题,而主代码中使用 import JUnit 代码,则会产生编译错误。 -#### 编译 +## 编译 主类 HelloWorld @@ -89,7 +61,7 @@ public class HelloWorld { clean 清理输出目录/target,compile 编译项目主代码。 -#### 测试代码 +## 测试代码 ```java public class HelloWorldTest { @@ -101,9 +73,9 @@ public class HelloWorldTest { } ``` -#### 执行测试 +## 执行测试 -#### `mvn clean test` +`mvn clean test` 测试结果: @@ -115,7 +87,7 @@ Running com.tyson.test.HelloWorldTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec ``` -#### 打包和安装 +## 打包和安装 打包:`mvn clean package `将项目代码打包成 jar 包,位于/target 目录。 @@ -153,9 +125,7 @@ Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec 执行顺序:compile->test->package->install - - -## 依赖 +# 依赖 ```xml @@ -172,7 +142,7 @@ Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec - exclusions:用来排除传递性依赖 -### 依赖范围 scope +## 依赖范围 scope maven 在编译、测试和运行项目时会使用不同的 classpath(编译classpath、测试 classpath、运行 classpath)。依赖范围就是用来控制依赖和这三种 classpath 的关系。maven 中有以下几种依赖范围: @@ -184,21 +154,21 @@ maven 在编译、测试和运行项目时会使用不同的 classpath(编译c - import:导入依赖范围 [import 导入依赖管理](#import-导入依赖管理) -### 传递性依赖 +## 传递性依赖 假如项目 account 有一个 compile 范围的 spring-core 依赖,而 spring-core 有一个 compile 范围的 common-logging 依赖,那么 common-logging 就会成为 account 的 compile 范围依赖,common-logging 是 account 的一个传递性依赖。maven 会直接解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到项目中。 spring-core 是 account 的第一直接依赖,common-logging 是 spring-core 的第二直接依赖,common-logging 是 account 的传递性依赖。第一直接依赖的范围和第二直接依赖的范围共同决定了传递性依赖的范围。下表左边是第一直接依赖的范围,上面一行是第二直接依赖的范围,中间部分是传递依赖的范围 -![依赖范围和传递性依赖](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190705222236595-1506009657.png) +![](http://img.topjavaer.cn/img/传递性依赖.png) -### 排除依赖 +## 排除依赖 传递性依赖可能会带来一些问题,像引入一些类库的 SNAPSHOT 版本,会影响到当前项目的稳定性。此时可以通过 exclusions 元素声明排除传递性依赖,exclusions 元素可以包含一个或多个 exclusion 元素,因此可以排除多个传递性依赖。声明 exclusion 时只需要 groupId 和 artifactId,而不需要 version 元素。 -![排除依赖](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706165424929-1192438533.png) +![](http://img.topjavaer.cn/img/排查依赖.png) -### 优化依赖 +## 优化依赖 查看当前项目的依赖:`mvn dependency:list` @@ -206,29 +176,27 @@ spring-core 是 account 的第一直接依赖,common-logging 是 spring-core 分析依赖:`mvn dependency:analyze` - - -## 仓库 +# 仓库 仓库分为两类:本地仓库和远程仓库。当 maven 根据坐标去寻找构件时,首先会去本地仓库查找,如果本地仓库不存在这个构件,maven 就会去远程仓库查找,下载到本地仓库。若远程仓库也没有这个构件,则会报错。 私服是一种特殊的远程仓库,是在局域网内架设的私有的仓库服务器,用其代理所有外部的远程仓库,内部的项目还能部署到私服上供其他项目使用。 -### 本地仓库 +## 本地仓库 要想自定义本地仓库地址,可以修改 C:\Users\Tyson\\.m2 下的 settings.xml 文件(默认不存在,需要到 maven 安装目录复制),不建议直接修改 maven 安装目录下的 settings.xml 文件。 通过`mvn clean install`可以将本地项目安装到本地库,以供其他项目使用。 -### 远程仓库 +## 远程仓库 当默认的中央仓库无法满足项目需要,可以通过 repositories 元素在 POM 中配置远程仓库。maven 中央仓库 id 为 central,若其他仓库 id 命名为 central,则会覆盖中央仓库的配置。 -![使用Jboss maven仓库](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706072227846-1642299607.png) +![](http://img.topjavaer.cn/img/使用Jboss maven仓库.png) maven中的仓库分为两种,snapshot 快照仓库和 release 发布仓库。元素 releases 的 enabled 为 true 表示开启 Jboss 仓库 release 版本下载支持,maven 会从 Jboss 仓库下载 release 版本的构件。 -#### 认证和部署 +## 认证和部署 有些远程仓库需要认证才能访问,可以在 settings.xml 中配置认证信息(更为安全)。 @@ -271,7 +239,7 @@ server元素的id要和pom.xml里需要认证的repository元素的id对应一 配置完 distributionManagement 之后,在命令行运行`mvn clean deploy`,maven 就会将项目构建输出的构件部署到对应的远程仓库,如果项目当前版本是快照版本,则部署到快照版本仓库地址,否则部署到发布版本仓库地址。 -### 镜像 +## 镜像 mirrorsOf 配置为 central,表示其为中央仓库的镜像,任何对中央仓库的请求都会转发到这个镜像。 @@ -294,13 +262,11 @@ mirrorsOf 配置为 central,表示其为中央仓库的镜像,任何对中 `*, !repo1`:匹配除了repo1以外的所有远程仓库。 - - -## 生命周期 +# 生命周期 项目构建过程包括:清理项目- 编译-测试-打包-部署 -### 三套生命周期 +## 三套生命周期 clean 生命周期:pre-clean、clean 和 post-clean; @@ -308,7 +274,7 @@ default 生命周期; site 生命周期:pre-site、site、post-site 和 site-deploy -### 命令行与生命周期 +## 命令行与生命周期 `mvn clean`:调用 clean 生命周期的 clean 阶段,实际上执行的是 clean 的 pre-clean 和 clean 阶段; @@ -318,19 +284,17 @@ site 生命周期:pre-site、site、post-site 和 site-deploy `mvn clean deploy site-deploy`:调用 clean 生命周期的 clean 阶段、default 生命周期的 deploy 阶段,以及 site 生命周期的 site-deploy 阶段。 - - -## 插件 +# 插件 maven 的生命周期和插件相互绑定,用以完成具体的构建任务。 -### 内置绑定 +## 内置绑定 -![内置绑定1](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706171830733-661580052.png) +![](http://img.topjavaer.cn/img/maven内置绑定1.png) -![内置绑定2](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706171854778-608245986.png) +![](http://img.topjavaer.cn/img/maven内置绑定2.png) -### 自定义绑定 +## 自定义绑定 内置绑定无法完成一些任务,如创建项目的源码 jar 包,此时需要用户自行配置。maven-source-plugin 可以完成这个任务,它的 jar-no-fork 目标能够将项目的主代码打包成 jar 文件,可以将其绑定到 default 生命周期的 verify 阶段,在执行完测试和安装构件之前创建源码 jar 包。 @@ -355,13 +319,13 @@ maven 的生命周期和插件相互绑定,用以完成具体的构建任务 ``` -### 命令行插件配置 +## 命令行插件配置 maven-surefire-plugin 提供了一个 maven.test.skip 参数,当其值为 true 时,就会跳过执行测试。 `mvn install -Dmaven.test.skip=true` -D 是 Java 自带的。 -### 插件全局配置 +## 插件全局配置 有些参数值从项目创建到发布都不会改变,可以在 pom 中一次性配置,避免重新在命令行输入。如配置 maven-compiler-plugin ,生成与 JVM1.5 兼容的字节码文件。 @@ -381,13 +345,11 @@ maven-surefire-plugin 提供了一个 maven.test.skip 参数,当其值为 true ``` - - -## 聚合 +# 聚合 项目有多个模块时,使用一个聚合体将这些模块聚合起来,通过聚合体就可以一次构建全部模块。 -![maven聚合](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706142605104-1246458390.png) +![](http://img.topjavaer.cn/img/maven聚合.png) accout-aggregator 的版本号要跟各个模块版本号相同,packaging 的值必须为 pom。module 标签的值是模块根目录名字(为了方便,模块根目录名字常与 artifactId 同名)。这样聚合模块和其他模块的目录结构是父子关系。如果使用平行目录结构,聚合模块的 pom 文件需要做相应的修改。 @@ -398,9 +360,7 @@ accout-aggregator 的版本号要跟各个模块版本号相同,packaging 的 ``` - - -## 继承 +# 继承 使用聚合体的 pom 文件作为公共 pom 文件,配置子模块的共同依赖,消除配置的重复。其中packaging 的值必须是 pom。 @@ -442,7 +402,7 @@ accout-aggregator 的版本号要跟各个模块版本号相同,packaging 的 ``` -### 依赖管理 +## 依赖管理 使用dependencyManagement可以统一管理项目的版本号,确保应用的各个项目的依赖和版本一致,不用每个模块项目都弄一个版本号,不利于管理,当需要变更版本号的时候只需要在父类容器里更新,不需要任何一个子项目的修改;如果某个子项目需要另外一个特殊的版本号时,只需要在自己的模块dependencies中声明一个版本号即可。子类就会使用子类声明的版本号,不继承于父类版本号。 @@ -531,9 +491,9 @@ springframework 依赖的 version 继承自父模块,可以省略,可避免 使用 import 依赖范围可以导入依赖管理配置,将目标 pom 的 dependencyManagement 配置导入合并到当前 pom 的 dependencyManagement 元素中。 -![使用import依赖范围导入依赖管理配置](https://img2018.cnblogs.com/blog/1252910/201907/1252910-20190706155054098-1669414992.png) +![](http://img.topjavaer.cn/img/使用import依赖范围导入依赖管理配置.png) -### 插件管理 +## 插件管理 maven 提供了 pluginManagement 元素帮助管理插件。当项目中的多个模块有相同的插件配置时,应当将配置移到父 pom 的 pluginManagement 元素中,方便统一项目中的插件版本。 @@ -570,11 +530,3 @@ account-parent 的 pluginManagement 配置: ``` - -## 测试 - -maven 通过插件 maven-surefire-plugin 来执行 JUnit 或者 TestNG 的测试用例。默认情况下,maven-surefire-plugin 的 test 目标会自动执行测试源码路径下(src/test/java/)所有符合命名模式(*/Test\*.java、\*/*Test.java、\*/\*TestCase.java)的测试类。 - -### 跳过测试 - -`mvn package -DskipTests` \ No newline at end of file diff --git a/docs/tools/maven/1-introduce.md b/docs/tools/maven/1-introduce.md new file mode 100644 index 0000000..5ac3852 --- /dev/null +++ b/docs/tools/maven/1-introduce.md @@ -0,0 +1,12 @@ +# 简介 + +Maven 是强大的构建工具,能够帮我们自动化构建过程--清理、编译、测试、打包和部署。比如测试,我们无需告诉 maven 如何去测试,只需遵循 maven 的约定编写好测试用例,当我们运行构建的时候,这些测试就会自动运行。 + +Maven 不仅是构建工具,还是一个依赖管理工具和项目信息管理工具。它提供了中央仓库,能帮助我们自动下载构件。 + +## 配置 + +配置用户范围 settings.xml。M2_HOME/conf/settings.xml 是全局范围的,而~/.m2/settings.xml 是用户范围的。配置成用户范围便于 Maven 升级。若直接修改 conf 目录下的 settings.xml,每次 Maven 升级时,都需要直接 settings.xml 文件。 + + + diff --git a/docs/tools/maven/2-basic.md b/docs/tools/maven/2-basic.md new file mode 100644 index 0000000..2b49fa1 --- /dev/null +++ b/docs/tools/maven/2-basic.md @@ -0,0 +1,104 @@ +# 入门 + +## 编写测试代码 + +## 添加 junit 依赖 + +```xml + + + junit + junit + 4.7 + test + + +``` + +maven 会自动访问中央仓库,下载 junit 依赖。scope 为 test 表明依赖只对测试有效,即测试代码中的 import JUnit 代码没有问题,而主代码中使用 import JUnit 代码,则会产生编译错误。 + +## 编译 + +主类 HelloWorld + +```java +public class HelloWorld { + public String sayHello() { + return "Hello world"; + } + public static void main(String[] args) { + System.out.println(new HelloWorld().sayHello()); + } +} +``` + +编译代码:`mvn clean compile` + +clean 清理输出目录/target,compile 编译项目主代码。 + +## 测试代码 + +```java +public class HelloWorldTest { + @Test + public void testHelloWord() { + HelloWorld hw = new HelloWorld(); + hw.sayHello(); + } +} +``` + +## 执行测试 + +`mvn clean test` + +测试结果: + +```java +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running com.tyson.test.HelloWorldTest +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.117 sec +``` + +## 打包和安装 + +打包:`mvn clean package `将项目代码打包成 jar 包,位于/target 目录。 + +安装:`mvn clean install`,将项目输出的 jar 包安装到 maven 本地库,这样其他 maven 项目就可以直接引用这个 jar 包。 + +默认打包生成的 jar 不能直接运行,为了生成可运行的 jar 包,需要借助 maven-shade-plugin。 + +```xml + + HelloWorld + + + org.apache.maven.plugins + maven-shade-plugin + 1.2.1 + + + package + + shade + + + + + com.tyson.maven.HelloWorld + + + + + + + + +``` + +执行顺序:compile->test->package->install + + + diff --git a/docs/tools/maven/3-dependency.md b/docs/tools/maven/3-dependency.md new file mode 100644 index 0000000..fe41a04 --- /dev/null +++ b/docs/tools/maven/3-dependency.md @@ -0,0 +1,53 @@ +# 依赖 + +```xml + + junit + junit + 4.7 + test + +``` + +- scope:依赖的范围 + +- type:依赖的类型,jar 或者 war + +- exclusions:用来排除传递性依赖 + +## 依赖范围 scope + +maven 在编译、测试和运行项目时会使用不同的 classpath(编译classpath、测试 classpath、运行 classpath)。依赖范围就是用来控制依赖和这三种 classpath 的关系。maven 中有以下几种依赖范围: + +- compile:默认值,使用该依赖范围的 maven 依赖,在编译、测试和运行时都需要使用该依赖 +- test:只对测试 classpath 有效,在编译主代码和运行项目时无法使用此类依赖。如 JUnit 只在编译测试代码和运行测试的时候才需要此类依赖 +- provided:已提供依赖范围。对于编译和测试 classpath 有效,但在运行时无效。如 servlet-api,编译和测试时需要该依赖,但在运行项目时,由于容器已经提供此依赖,故不需要 maven重复引入 +- runtime:运行时依赖范围。对于测试和运行 classpath 有效,但在编译主代码时无效。如 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC接口,只有在测试和运行时才需要实现 JDBC 接口的具体实现 +- system:系统依赖范围 +- import:导入依赖范围 [import 导入依赖管理](#import-导入依赖管理) + + +## 传递性依赖 + +假如项目 account 有一个 compile 范围的 spring-core 依赖,而 spring-core 有一个 compile 范围的 common-logging 依赖,那么 common-logging 就会成为 account 的 compile 范围依赖,common-logging 是 account 的一个传递性依赖。maven 会直接解析各个直接依赖的 POM,将那些必要的间接依赖,以传递性依赖的形式引入到项目中。 + +spring-core 是 account 的第一直接依赖,common-logging 是 spring-core 的第二直接依赖,common-logging 是 account 的传递性依赖。第一直接依赖的范围和第二直接依赖的范围共同决定了传递性依赖的范围。下表左边是第一直接依赖的范围,上面一行是第二直接依赖的范围,中间部分是传递依赖的范围 + +![](http://img.topjavaer.cn/img/传递性依赖.png) + +## 排除依赖 + +传递性依赖可能会带来一些问题,像引入一些类库的 SNAPSHOT 版本,会影响到当前项目的稳定性。此时可以通过 exclusions 元素声明排除传递性依赖,exclusions 元素可以包含一个或多个 exclusion 元素,因此可以排除多个传递性依赖。声明 exclusion 时只需要 groupId 和 artifactId,而不需要 version 元素。 + +![](http://img.topjavaer.cn/img/排查依赖.png) + +## 优化依赖 + +查看当前项目的依赖:`mvn dependency:list` + +查看依赖树:`mvn dependency:tree` + +分析依赖:`mvn dependency:analyze` + + + diff --git a/docs/tools/maven/4-repo.md b/docs/tools/maven/4-repo.md new file mode 100644 index 0000000..846bf71 --- /dev/null +++ b/docs/tools/maven/4-repo.md @@ -0,0 +1,88 @@ +# 仓库 + +仓库分为两类:本地仓库和远程仓库。当 maven 根据坐标去寻找构件时,首先会去本地仓库查找,如果本地仓库不存在这个构件,maven 就会去远程仓库查找,下载到本地仓库。若远程仓库也没有这个构件,则会报错。 + +私服是一种特殊的远程仓库,是在局域网内架设的私有的仓库服务器,用其代理所有外部的远程仓库,内部的项目还能部署到私服上供其他项目使用。 + +## 本地仓库 + +要想自定义本地仓库地址,可以修改 C:\Users\Tyson\\.m2 下的 settings.xml 文件(默认不存在,需要到 maven 安装目录复制),不建议直接修改 maven 安装目录下的 settings.xml 文件。 + +通过`mvn clean install`可以将本地项目安装到本地库,以供其他项目使用。 + +## 远程仓库 + +当默认的中央仓库无法满足项目需要,可以通过 repositories 元素在 POM 中配置远程仓库。maven 中央仓库 id 为 central,若其他仓库 id 命名为 central,则会覆盖中央仓库的配置。 + +![](http://img.topjavaer.cn/img/使用Jboss maven仓库.png) + +maven中的仓库分为两种,snapshot 快照仓库和 release 发布仓库。元素 releases 的 enabled 为 true 表示开启 Jboss 仓库 release 版本下载支持,maven 会从 Jboss 仓库下载 release 版本的构件。 + +## 认证和部署 + +有些远程仓库需要认证才能访问,可以在 settings.xml 中配置认证信息(更为安全)。 + +```xml + + + + releases + admin + admin123 + + + snapshots + admin + admin123 + + +``` + +server元素的id要和pom.xml里需要认证的repository元素的id对应一致。 + +```xml + + + + releases + releases + + http://localhost:8081/nexus/content/repositories/releases + + + + snapshots + + http://localhost:8081/nexus/content/repositories/snapshots/ + + + +``` + +配置完 distributionManagement 之后,在命令行运行`mvn clean deploy`,maven 就会将项目构建输出的构件部署到对应的远程仓库,如果项目当前版本是快照版本,则部署到快照版本仓库地址,否则部署到发布版本仓库地址。 + +## 镜像 + +mirrorsOf 配置为 central,表示其为中央仓库的镜像,任何对中央仓库的请求都会转发到这个镜像。 + +```xml + + + alimaven + aliyun maven + http://maven.aliyun.com/nexus/content/groups/public/ + central + + +``` + +`*`:匹配所有远程仓库。 + +`external:*`:匹配所有不在本机上的所有远程仓库。 + +`repo1, repo2`:匹配仓库repo1 和 repo2。 + +`*, !repo1`:匹配除了repo1以外的所有远程仓库。 + + + diff --git a/docs/tools/maven/5-lifecycle.md b/docs/tools/maven/5-lifecycle.md new file mode 100644 index 0000000..a0408c3 --- /dev/null +++ b/docs/tools/maven/5-lifecycle.md @@ -0,0 +1,24 @@ +# 生命周期 + +项目构建过程包括:清理项目- 编译-测试-打包-部署 + +## 三套生命周期 + +clean 生命周期:pre-clean、clean 和 post-clean; + +default 生命周期; + +site 生命周期:pre-site、site、post-site 和 site-deploy + +## 命令行与生命周期 + +`mvn clean`:调用 clean 生命周期的 clean 阶段,实际上执行的是 clean 的 pre-clean 和 clean 阶段; + +`mvn test`:调用 default 生命周期的 test 阶段; + +`mvn clean install`:调用 clean 生命周期的 clean 阶段和 default 生命周期的 install 阶段; + +`mvn clean deploy site-deploy`:调用 clean 生命周期的 clean 阶段、default 生命周期的 deploy 阶段,以及 site 生命周期的 site-deploy 阶段。 + + + diff --git a/docs/tools/maven/6-plugin.md b/docs/tools/maven/6-plugin.md new file mode 100644 index 0000000..e67247b --- /dev/null +++ b/docs/tools/maven/6-plugin.md @@ -0,0 +1,63 @@ +# 插件 + +maven 的生命周期和插件相互绑定,用以完成具体的构建任务。 + +## 内置绑定 + +![](http://img.topjavaer.cn/img/maven内置绑定1.png) + +![](http://img.topjavaer.cn/img/maven内置绑定2.png) + +## 自定义绑定 + +内置绑定无法完成一些任务,如创建项目的源码 jar 包,此时需要用户自行配置。maven-source-plugin 可以完成这个任务,它的 jar-no-fork 目标能够将项目的主代码打包成 jar 文件,可以将其绑定到 default 生命周期的 verify 阶段,在执行完测试和安装构件之前创建源码 jar 包。 + +```xml + + + + org.apache.maven.plugins + maven-source-plugin + 2.1.1 + + + attach-sources + verify + + jar-no-fork + + + + + + +``` + +## 命令行插件配置 + +maven-surefire-plugin 提供了一个 maven.test.skip 参数,当其值为 true 时,就会跳过执行测试。 + +`mvn install -Dmaven.test.skip=true` -D 是 Java 自带的。 + +## 插件全局配置 + +有些参数值从项目创建到发布都不会改变,可以在 pom 中一次性配置,避免重新在命令行输入。如配置 maven-compiler-plugin ,生成与 JVM1.5 兼容的字节码文件。 + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.1 + + 1.5 + 1.5 + + + + +``` + + + diff --git a/docs/tools/maven/7-aggregator.md b/docs/tools/maven/7-aggregator.md new file mode 100644 index 0000000..b61c0b3 --- /dev/null +++ b/docs/tools/maven/7-aggregator.md @@ -0,0 +1,17 @@ +# 聚合 + +项目有多个模块时,使用一个聚合体将这些模块聚合起来,通过聚合体就可以一次构建全部模块。 + +![](http://img.topjavaer.cn/img/maven聚合.png) + +accout-aggregator 的版本号要跟各个模块版本号相同,packaging 的值必须为 pom。module 标签的值是模块根目录名字(为了方便,模块根目录名字常与 artifactId 同名)。这样聚合模块和其他模块的目录结构是父子关系。如果使用平行目录结构,聚合模块的 pom 文件需要做相应的修改。 + +```xml + + ../account-register + ../account-persist + +``` + + + diff --git a/docs/tools/maven/8-inherit.md b/docs/tools/maven/8-inherit.md new file mode 100644 index 0000000..a03b327 --- /dev/null +++ b/docs/tools/maven/8-inherit.md @@ -0,0 +1,169 @@ +# 继承 + +使用聚合体的 pom 文件作为公共 pom 文件,配置子模块的共同依赖,消除配置的重复。其中packaging 的值必须是 pom。 + +```xml + + + 4.0.0 + + com.tyson + account-parent + 1.0.0-SNAPSHOT + pom + account-parent + +``` + +子模块 account-email 继承父模块 account-parent,子模块的 groupId 和 version 可以省略,这样子模块就会隐式继承父模块的这两个元素。 + +```xml + + + 4.0.0 + + + com.tyson + account-parent + 1.0.0-SNAPSHOT + ../pom.xml + + + com.tyson + account-email + 1.0.0-SNAPSHOT + account-email + +``` + +## 依赖管理 + +使用dependencyManagement可以统一管理项目的版本号,确保应用的各个项目的依赖和版本一致,不用每个模块项目都弄一个版本号,不利于管理,当需要变更版本号的时候只需要在父类容器里更新,不需要任何一个子项目的修改;如果某个子项目需要另外一个特殊的版本号时,只需要在自己的模块dependencies中声明一个版本号即可。子类就会使用子类声明的版本号,不继承于父类版本号。 + +```xml + + + 4.0.0 + + com.tyson + account-parent + 1.0.0-SNAPSHOT + pom + account-parent + + + 2.5.6 + + + + + + org.springframework + spring-core + ${springframework.version} + + + org.springframework + spring-beans + ${springframeword.version} + + + org.springframework + spring-context + ${springframeword.version} + + + org.springframework + spring-context-support + ${springframeword.version} + + + + +``` + +子模块 account-email 会继承父模块 account-parent 的 dependencyManagement 配置。 + +```xml + + + 4.0.0 + + + com.tyson + account-parent + 1.0.0-SNAPSHOT + ../pom.xml + + + com.tyson + account-email + 1.0.0-SNAPSHOT + account-email + + + + org.springframework + spring-core + + + +``` + +springframework 依赖的 version 继承自父模块,可以省略,可避免各个子模块使用依赖版本不一致的情况。 + +与dependencies区别: + +1)Dependencies相对于dependencyManagement,所有生命在dependencies里的依赖都会自动引入,并默认被所有的子项目继承。 +2)dependencyManagement里只是声明依赖,并不自动实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。 + +### import 导入依赖管理 + +使用 import 依赖范围可以导入依赖管理配置,将目标 pom 的 dependencyManagement 配置导入合并到当前 pom 的 dependencyManagement 元素中。 + +![](http://img.topjavaer.cn/img/使用import依赖范围导入依赖管理配置.png) + +## 插件管理 + +maven 提供了 pluginManagement 元素帮助管理插件。当项目中的多个模块有相同的插件配置时,应当将配置移到父 pom 的 pluginManagement 元素中,方便统一项目中的插件版本。 + +account-parent 的 pluginManagement 配置: + +```xml + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.5 + 1.5 + + + + + +``` + +子模块 account-email 继承 account-parent 的 pluginManagement 配置。如果子模块不需要父模块中的 pluginManagement 配置,将其忽略就可以。如果子模块需要不同于父模块 pluginManagement 配置的插件,可以自行覆盖父模块的配置。 + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + + + +``` + diff --git a/docs/tools/maven/README.md b/docs/tools/maven/README.md new file mode 100644 index 0000000..5cb7065 --- /dev/null +++ b/docs/tools/maven/README.md @@ -0,0 +1,20 @@ +--- +title: Maven基础 +icon: build +date: 2022-08-08 +category: maven +star: true +--- + +![](http://img.topjavaer.cn/img/maven-img.png) + +## Maven总结 + +- [简介](./1-introduce.md) +- [入门](./2-basic.md) +- [依赖](./3-dependency.md) +- [仓库](./4-repo.md) +- [生命周期](./5-lifecycle.md) +- [插件](./6-plugin.md) +- [聚合](./7-aggregator.md) +- [继承](./8-inherit.md) diff --git a/docs/tools/nginx.md b/docs/tools/nginx.md new file mode 100644 index 0000000..f1e72ac --- /dev/null +++ b/docs/tools/nginx.md @@ -0,0 +1,771 @@ +--- +sidebar: heading +title: Nginx学习笔记 +category: 笔记 +tag: + - 工具 +head: + - - meta + - name: keywords + content: nginx + - - meta + - name: description + content: Nginx常见知识点和面试题总结,让天下没有难背的八股文! +--- + +**文章目录:** + +- 什么是Nginx? +- Nginx 有哪些优点? +- Nginx应用场景? +- Nginx怎么处理请求的? +- Nginx 是如何实现高并发的? +- 什么是正向代理? +- 什么是反向代理? +- 反向代理服务器的优点是什么? +- Nginx目录结构有哪些? +- Nginx配置文件nginx.conf有哪些属性模块? +- cookie和session区别? +- 为什么 Nginx 不使用多线程? +- nginx和apache的区别 +- 什么是动态资源、静态资源分离? +- 为什么要做动、静分离? +- 什么叫 CDN 服务? +- Nginx怎么做的动静分离? +- Nginx负载均衡的算法怎么实现的?策略有哪些? +- 如何用Nginx解决前端跨域问题? +- Nginx虚拟主机怎么配置? +- location的作用是什么? +- 限流怎么做的? +- 漏桶流算法和令牌桶算法知道? +- Nginx配置高可用性怎么配置? +- Nginx怎么判断别IP不可访问? +- 在nginx中,如何使用未定义的服务器名称来阻止处理请求? +- 怎么限制浏览器访问? +- Rewrite全局变量是什么? +- Nginx 如何实现后端服务的健康检查? +- Nginx 如何开启压缩? +- ngx_http_upstream_module的作用是什么? +- 什么是C10K问题? +- Nginx是否支持将请求压缩到上游? +- 如何在Nginx中获得当前的时间? +- 用Nginx服务器解释-s的目的是什么? +- 如何在Nginx服务器上添加模块? +- 生产中如何设置worker进程的数量呢? +- nginx状态码 + +## **什么是Nginx?** + +Nginx是一个 轻量级/高性能的反向代理Web服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。 + +## **Nginx 有哪些优点?** + +- 跨平台、配置简单。 +- 非阻塞、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发。 +- 内存消耗小:开启 10 个 Nginx 才占 150M 内存。 +- 成本低廉,且开源。 +- 稳定性高,宕机的概率非常小。 +- 内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上 + +## **Nginx应用场景?** + +- http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。 +- 虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。 +- 反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。 +- nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。 + +## **Nginx怎么处理请求的?** + +``` +server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.html; # 默认的首页文件,多个用空格分开 +} # 第一个location区块结果 +``` + +- 首先,Nginx 在启动时,会解析配置文件,得到需要监听的端口与 IP 地址,然后在 Nginx 的 Master 进程里面先初始化好这个监控的Socket(创建 S ocket,设置 addr、reuse 等选项,绑定到指定的 ip 地址端口,再 listen 监听)。 +- 然后,再 fork(一个现有进程可以调用 fork 函数创建一个新进程。由 fork 创建的新进程被称为子进程 )出多个子进程出来。 +- 之后,子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与nginx进行三次握手,与 nginx 建立好一个连接后。此时,某一个子进程会 accept 成功,得到这个建立好的连接的 Socket ,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体。 +- 接着,设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。 +- 最后,Nginx 或客户端来主动关掉连接,到此,一个连接就寿终正寝了。 + +## **Nginx 是如何实现高并发的?** + +如果一个 server 采用一个进程(或者线程)负责一个request的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。 + +而 Nginx 的异步非阻塞工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。 + +Nginx是如何利用的呢,简单来说:同样的 4 个进程,如果采用一个进程负责一个 request 的方式,那么,同时进来 4 个 request 之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第 5 个request进来了。就无法及时反应了,因为 4 个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个 request ,就新开个进程来处理。 + +**回想下,BIO 是不是存在酱紫的问题?** + +Nginx 不这样,每进来一个 request ,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request ,并等待请求返回。那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有 request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。 + +这就是为什么说,Nginx 基于事件模型。 + +由于 web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server 机器上的时间片不多。这是几个进程就解决高并发的秘密所在。即: + +webserver 刚好属于网络 IO 密集型应用,不算是计算密集型。 + +异步,非阻塞,使用 epoll ,和大量细节处的优化。也正是 Nginx 之所以然的技术基石。 + +## **什么是正向代理?** + +一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。 + +客户端才能使用正向代理。正向代理总结就一句话:代理端代理的是客户端。例如说:我们使用的OpenVPN 等等。 + +## **什么是反向代理?** + +反向代理(Reverse Proxy)方式,是指以代理服务器来接受 Internet上的连接请求,然后将请求,发给内部网络上的服务器并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 + +> 反向代理总结就一句话:代理端代理的是服务端。 + +## **反向代理服务器的优点是什么?** + +反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。 + +## **Nginx目录结构有哪些?** + +``` +tree /usr/local/nginx +/usr/local/nginx +├── client_body_temp +├── conf # Nginx所有配置文件的目录 +│ ├── fastcgi.conf # fastcgi相关参数的配置文件 +│ ├── fastcgi.conf.default # fastcgi.conf的原始备份文件 +│ ├── fastcgi_params # fastcgi的参数文件 +│ ├── fastcgi_params.default +│ ├── koi-utf +│ ├── koi-win +│ ├── mime.types # 媒体类型 +│ ├── mime.types.default +│ ├── nginx.conf # Nginx主配置文件 +│ ├── nginx.conf.default +│ ├── scgi_params # scgi相关参数文件 +│ ├── scgi_params.default +│ ├── uwsgi_params # uwsgi相关参数文件 +│ ├── uwsgi_params.default +│ └── win-utf +├── fastcgi_temp # fastcgi临时数据目录 +├── html # Nginx默认站点目录 +│ ├── 50x.html # 错误页面优雅替代显示文件,例如当出现502错误时会调用此页面 +│ └── index.html # 默认的首页文件 +├── logs # Nginx日志目录 +│ ├── access.log # 访问日志文件 +│ ├── error.log # 错误日志文件 +│ └── nginx.pid # pid文件,Nginx进程启动后,会把所有进程的ID号写到此文件 +├── proxy_temp # 临时目录 +├── sbin # Nginx命令目录 +│ └── nginx # Nginx的启动命令 +├── scgi_temp # 临时目录 +└── uwsgi_temp # 临时目录 +``` + +## **Nginx配置文件nginx.conf有哪些属性模块?** + +``` +worker_processes 1; # worker进程的数量 +events { # 事件区块开始 + worker_connections 1024; # 每个worker进程支持的最大连接数 +} # 事件区块结束 +http { # HTTP区块开始 + include mime.types; # Nginx支持的媒体类型库文件 + default_type application/octet-stream; # 默认的媒体类型 + sendfile on; # 开启高效传输模式 + keepalive_timeout 65; # 连接超时 + server { # 第一个Server区块开始,表示一个独立的虚拟主机站点 + listen 80; # 提供服务的端口,默认80 + server_name localhost; # 提供服务的域名主机名 + location / { # 第一个location区块开始 + root html; # 站点的根目录,相当于Nginx的安装目录 + index index.html index.htm; # 默认的首页文件,多个用空格分开 + } # 第一个location区块结果 + error_page 500502503504 /50x.html; # 出现对应的http状态码时,使用50x.html回应客户 + location = /50x.html { # location区块开始,访问50x.html + root html; # 指定对应的站点目录为html + } + } + ...... +``` + +## **cookie和session区别?** + +#### 共同: + +存放用户信息。存放的形式:key-value格式 变量和变量内容键值对。 + +#### 区别: + +cookie + +- 存放在客户端浏览器 +- 每个域名对应一个cookie,不能跨跃域名访问其他cookie +- 用户可以查看或修改cookie +- http响应报文里面给你浏览器设置 +- 钥匙(用于打开浏览器上锁头) + +session: + +- 存放在服务器(文件,数据库,redis) +- 存放敏感信息 +- 锁头 + +## **为什么 Nginx 不使用多线程?** + +**Apache:** 创建多个进程或线程,而每个进程或线程都会为其分配 cpu 和内存(线程要比进程小的多,所以 worker 支持比 perfork 高的并发),并发过大会榨干服务器资源。 + +**Nginx:** 采用单线程来异步非阻塞处理请求(管理员可以配置 Nginx 主进程的工作进程的数量)(epoll),不会为每个请求分配 cpu 和内存资源,节省了大量资源,同时也减少了大量的 CPU 的上下文切换。所以才使得 Nginx 支持更高的并发。 + +## **nginx和apache的区别** + +轻量级,同样起web服务,比apache占用更少的内存和资源。 + +抗并发,nginx处理请求是异步非阻塞的,而apache则是阻塞性的,在高并发下nginx能保持低资源,低消耗高性能。 + +高度模块化的设计,编写模块相对简单。 + +最核心的区别在于apache是同步多进程模型,一个连接对应一个进程,nginx是异步的,多个连接可以对应一个进程。 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsN5mNHZoULkBibhIUKh8co4rkVPCE8zwOvrhVFarKsHXgvZ8TxUciblOeA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +## **什么是动态资源、静态资源分离?** + +动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 + +动态资源、静态资源分离简单的概括是:动态文件与静态文件的分离。 + +## **为什么要做动、静分离?** + +在我们的软件开发中,有些请求是需要后台处理的(如:.jsp,.do 等等),有些请求是不需要经过后台处理的(如:css、html、jpg、js 等等文件),这些不需要经过后台处理的文件称为静态文件,否则动态文件。 + +因此我们后台处理忽略静态文件。这会有人又说那我后台忽略静态文件不就完了吗?当然这是可以的,但是这样后台的请求次数就明显增多了。在我们对资源的响应速度有要求的时候,我们应该使用这种动静分离的策略去解决动、静分离将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问 + +这里我们将静态资源放到 Nginx 中,动态资源转发到 Tomcat 服务器中去。 + +当然,因为现在七牛、阿里云等 CDN 服务已经很成熟,主流的做法,是把静态资源缓存到 CDN 服务中,从而提升访问速度。 + +相比本地的 Nginx 来说,CDN 服务器由于在国内有更多的节点,可以实现用户的就近访问。并且,CDN 服务可以提供更大的带宽,不像我们自己的应用服务,提供的带宽是有限的。 + +## **什么叫 CDN 服务?** + +CDN ,即内容分发网络。 + +其目的是,通过在现有的 Internet中 增加一层新的网络架构,将网站的内容发布到最接近用户的网络边缘,使用户可就近取得所需的内容,提高用户访问网站的速度。 + +一般来说,因为现在 CDN 服务比较大众,所以基本所有公司都会使用 CDN 服务。 + +## **Nginx怎么做的动静分离?** + +只需要指定路径对应的目录。location/可以使用正则表达式匹配。并指定对应的硬盘中的目录。如下:(操作都是在Linux上) + +``` +location /image/ { + root /usr/local/static/; + autoindex on; +} +``` + +步骤: + +``` +# 创建目录 +mkdir /usr/local/static/image + +# 进入目录 +cd /usr/local/static/image + +# 上传照片 +photo.jpg + +# 重启nginx +sudo nginx -s reload +``` + +打开浏览器 输入 `server_name/image/1.jpg` 就可以访问该静态图片了 + +## **Nginx负载均衡的算法怎么实现的?策略有哪些?** + +为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。 + +Nginx负载均衡实现的策略有以下五种: + +#### 1 .轮询(默认) + +每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。 + +``` +upstream backserver { + server 192.168.0.12; + server 192.168.0.13; +} +``` + +#### 2. 权重 weight + +weight的值越大,分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。 + +``` +# 权重越高,在被访问的概率越大,如上例,分别是20%,80%。 +upstream backserver { + server 192.168.0.12 weight=2; + server 192.168.0.13 weight=8; +} +``` + +#### 3. ip_hash( IP绑定) + +每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题 + +``` +upstream backserver { + ip_hash; + server 192.168.0.12:88; + server 192.168.0.13:80; +} +``` + +#### 4. fair(第三方插件) + +必须安装upstream_fair模块。 + +对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。 + +``` +# 哪个服务器的响应速度快,就将请求分配到那个服务器上。 +upstream backserver { + server server1; + server server2; + fair; +} +``` + +#### 5.url_hash(第三方插件) + +必须安装Nginx的hash软件包 + +按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。 + +``` +upstream backserver { + server squid1:3128; + server squid2:3128; + hash $request_uri; + hash_method crc32; +} +``` + +## **如何用Nginx解决前端跨域问题?** + +使用Nginx转发请求。把跨域的接口写成调本域的接口,然后将这些接口转发到真正的请求地址。 + +## **Nginx虚拟主机怎么配置?** + +1、基于域名的虚拟主机,通过域名来区分虚拟主机——应用:外部网站 + +2、基于端口的虚拟主机,通过端口来区分虚拟主机——应用:公司内部网站,外部网站的管理后台 + +3、基于ip的虚拟主机。 + +#### 基于虚拟主机配置域名 + +需要建立`/data/www /data/bbs`目录,windows本地hosts添加虚拟机ip地址对应的域名解析;对应域名网站目录下新增index.html文件; + +``` +# 当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/www目录下文件 +server { + listen 80; + server_name www.lijie.com; + location / { + root data/www; + index index.html index.htm; + } +} + +# 当客户端访问www.lijie.com,监听端口号为80,直接跳转到data/bbs目录下文件 + server { + listen 80; + server_name bbs.lijie.com; + location / { + root data/bbs; + index index.html index.htm; + } +} +``` + +#### 基于端口的虚拟主机 + +使用端口来区分,浏览器使用域名或ip地址:端口号 访问 + +``` +# 当客户端访问www.lijie.com,监听端口号为8080,直接跳转到data/www目录下文件 + server { + listen 8080; + server_name 8080.lijie.com; + location / { + root data/www; + index index.html index.htm; + } +} + +# 当客户端访问www.lijie.com,监听端口号为80直接跳转到真实ip服务器地址 127.0.0.1:8080 +server { + listen 80; + server_name www.lijie.com; + location / { + proxy_pass http://127.0.0.1:8080; + index index.html index.htm; + } +} +``` + +## **location的作用是什么?** + +location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。 + +location的语法能说出来吗? + +> 注意:~ 代表自己输入的英文字母 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsNkHmReOhOMLoKpzwbpDweTTIFqGtibCL0leyMic4sWQ2NlOkiavkugsRrQ/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +#### Location正则案例 + +``` +# 优先级1,精确匹配,根路径 +location =/ { + return 400; +} + +# 优先级2,以某个字符串开头,以av开头的,优先匹配这里,区分大小写 +location ^~ /av { + root /data/av/; +} + +# 优先级3,区分大小写的正则匹配,匹配/media*****路径 +location ~ /media { + alias /data/static/; +} + +# 优先级4 ,不区分大小写的正则匹配,所有的****.jpg|gif|png 都走这里 +location ~* .*\.(jpg|gif|png|js|css)$ { + root /data/av/; +} + +# 优先7,通用匹配 +location / { + return 403; +} +``` + +## **限流怎么做的?** + +Nginx限流就是限制用户请求速度,防止服务器受不了 + +限流有3种 + +- 正常限制访问频率(正常流量) +- 突发限制访问频率(突发流量) +- 限制并发连接数 + +Nginx的限流都是基于漏桶流算法 + +> 实现三种限流算法 + +#### 1、正常限制访问频率(正常流量): + +限制一个用户发送的请求,我Nginx多久接收一个请求。 + +Nginx中使用`ngx_http_limit_req_module`模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在nginx.conf配置文件中可以使用`limit_req_zone`命令及`limit_req`命令限制单个IP的请求处理频率。 + +``` +# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 +limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + +# 绑定限流维度 +server{ + + location/seckill.html{ + limit_req zone=zone; + proxy_pass http://lj_seckill; + } + +} +``` + +1r/s代表1秒一个请求,1r/m一分钟接收一个请求, 如果Nginx这时还有别人的请求没有处理完,Nginx就会拒绝处理该用户请求。 + +#### 2、突发限制访问频率(突发流量): + +限制一个用户发送的请求,我Nginx多久接收一个。 + +上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢? + +Nginx提供burst参数结合nodelay参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。我们可以将之前的例子添加burst参数以及nodelay参数: + +``` +# 定义限流维度,一个用户一分钟一个请求进来,多余的全部漏掉 +limit_req_zone $binary_remote_addr zone=one:10m rate=1r/m; + +# 绑定限流维度 +server{ + + location/seckill.html{ + limit_req zone=zone burst=5 nodelay; + proxy_pass http://lj_seckill; + } + +} +``` + +为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表Nginx对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我Nginx就漏掉不接受你的请求 + +#### 3、 限制并发连接数 + +Nginx中的`ngx_http_limit_conn_module`模块提供了限制并发连接数的功能,可以使用`limit_conn_zone`指令以及`limit_conn`执行进行配置。接下来我们可以通过一个简单的例子来看下: + +``` +http { + limit_conn_zone $binary_remote_addr zone=myip:10m; + limit_conn_zone $server_name zone=myServerName:10m; +} + +server { + location / { + limit_conn myip 10; + limit_conn myServerName 100; + rewrite / http://www.lijie.net permanent; + } +} +``` + +上面配置了单个IP同时并发连接数最多只能10个连接,并且设置了整个虚拟服务器同时最大并发数最多只能100个链接。当然,只有当请求的header被服务器处理后,虚拟服务器的连接数才会计数。刚才有提到过Nginx是基于漏桶算法原理实现的,实际上限流一般都是基于漏桶算法和令牌桶算法实现的。 + +## **漏桶流算法和令牌桶算法知道?** + +#### 漏桶算法 + +漏桶算法思路很简单,我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsN9icLF2qLwHhy68fiaHfm5VflRngF5M283iblm5OFvvIZEjZRCoxXeCHzA/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +#### 令牌桶算法 + +令牌桶算法的原理也比较简单,我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。 + +系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsNzGLocl6NRcIWmC2IKxZUlUGVssdqFysG1JDGfbwkKCejswylicjavsw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +## **Nginx配置高可用性怎么配置?** + +当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用 + +Nginx配置代码: + +``` +server { + listen 80; + server_name www.lijie.com; + location / { + ### 指定上游服务器负载均衡服务器 + proxy_pass http://backServer; + ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间 + proxy_connect_timeout 1s; + ###nginx发送给上游服务器(真实访问的服务器)超时时间 + proxy_send_timeout 1s; + ### nginx接受上游服务器(真实访问的服务器)超时时间 + proxy_read_timeout 1s; + index index.html index.htm; + } + } +``` + +## **Nginx怎么判断别IP不可访问?** + +``` + # 如果访问的ip地址为192.168.9.115,则返回403 + if ($remote_addr = 192.168.9.115) { + return 403; + } +``` + +## **在nginx中,如何使用未定义的服务器名称来阻止处理请求?** + +只需将请求删除的服务器就可以定义为: + +服务器名被保留一个空字符串,他在没有主机头字段的情况下匹配请求,而一个特殊的nginx的非标准代码被返回,从而终止连接。 + +## **怎么限制浏览器访问?** + +``` +## 不允许谷歌浏览器访问 如果是谷歌浏览器返回500 +if ($http_user_agent ~ Chrome) { + return 500; +} +``` + +## **Rewrite全局变量是什么?** + +``` +$remote_addr //获取客户端ip +$binary_remote_addr //客户端ip(二进制) +$remote_port //客户端port,如:50472 +$remote_user //已经经过Auth Basic Module验证的用户名 +$host //请求主机头字段,否则为服务器名称,如:blog.sakmon.com +$request //用户请求信息,如:GET ?a=1&b=2 HTTP/1.1 +$request_filename //当前请求的文件的路径名,由root或alias和URI request组合而成,如:/2013/81.html +$status //请求的响应状态码,如:200 +$body_bytes_sent // 响应时送出的body字节数数量。即使连接中断,这个数据也是精确的,如:40 +$content_length // 等于请求行的“Content_Length”的值 +$content_type // 等于请求行的“Content_Type”的值 +$http_referer // 引用地址 +$http_user_agent // 客户端agent信息,如:Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36 +$args //与$query_string相同 等于当中URL的参数(GET),如a=1&b=2 +$document_uri //与$uri相同 这个变量指当前的请求URI,不包括任何参数(见$args) 如:/2013/81.html +$document_root //针对当前请求的根路径设置值 +$hostname //如:centos53.localdomain +$http_cookie //客户端cookie信息 +$cookie_COOKIE //cookie COOKIE变量的值 +$is_args //如果有$args参数,这个变量等于”?”,否则等于”",空值,如? +$limit_rate //这个变量可以限制连接速率,0表示不限速 +$query_string // 与$args相同 等于当中URL的参数(GET),如a=1&b=2 +$request_body // 记录POST过来的数据信息 +$request_body_file //客户端请求主体信息的临时文件名 +$request_method //客户端请求的动作,通常为GET或POST,如:GET +$request_uri //包含请求参数的原始URI,不包含主机名,如:/2013/81.html?a=1&b=2 +$scheme //HTTP方法(如http,https),如:http +$uri //这个变量指当前的请求URI,不包括任何参数(见$args) 如:/2013/81.html +$request_completion //如果请求结束,设置为OK. 当请求未结束或如果该请求不是请求链串的最后一个时,为空(Empty),如:OK +$server_protocol //请求使用的协议,通常是HTTP/1.0或HTTP/1.1,如:HTTP/1.1 +$server_addr //服务器IP地址,在完成一次系统调用后可以确定这个值 +$server_name //服务器名称,如:blog.sakmon.com +$server_port //请求到达服务器的端口号,如:80 +``` + +## **Nginx 如何实现后端服务的健康检查?** + +方式一,利用 nginx 自带模块 `ngx_http_proxy_module` 和 `ngx_http_upstream_module` 对后端节点做健康检查。 + +方式二(推荐),利用 `nginx_upstream_check_module` 模块对后端节点做健康检查。 + +## **Nginx 如何开启压缩?** + +开启nginx gzip压缩后,网页、css、js等静态资源的大小会大大的减少,从而可以节约大量的带宽,提高传输效率,给用户快的体验。虽然会消耗cpu资源,但是为了给用户更好的体验是值得的。 + +开启的配置如下: + +将以上配置放到nginx.conf的`http{ … }`节点中。 + +``` +http { + # 开启gzip + gzip on; + + # 启用gzip压缩的最小文件;小于设置值的文件将不会被压缩 + gzip_min_length 1k; + + # gzip 压缩级别 1-10 + gzip_comp_level 2; + + # 进行压缩的文件类型。 + + gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; + + # 是否在http header中添加Vary: Accept-Encoding,建议开启 + gzip_vary on; +} +``` + +保存并重启nginx,刷新页面(为了避免缓存,请强制刷新)就能看到效果了。以谷歌浏览器为例,通过F12看请求的响应头部: + +我们可以先来对比下,如果我们没有开启zip压缩之前,我们的对应的文件大小,如下所示: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsN5S9ILfsysyKEhibZ1va359rnCdvdp0zVgUUzD4HjXicAYrl6b40Cv26Q/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +现在我们开启了gzip进行压缩后的文件的大小,可以看到如下所示: + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsNxc5qCaV6gu6XHb6QSiadDT28WoNVGMbnu6jSfzSqQiavLSFgoJpmSkCw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +并且我们查看响应头会看到gzip这样的压缩,如下所示 + +![图片](https://mmbiz.qpic.cn/mmbiz_png/z40lCFUAHpmhyoLIHKKIPFUpRayJmOsNL4eVEiadMicsAmXrFs6TWXqzB4MibQozWXTk9WiacMhMzHDN7F1C8g0Y6A/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1) + +gzip压缩前后效果对比:jquery原大小90kb,压缩后只有30kb。 + +gzip虽然好用,但是以下类型的资源不建议启用。 + +#### 1、图片类型 + +原因:图片如jpg、png本身就会有压缩,所以就算开启gzip后,压缩前和压缩后大小没有多大区别,所以开启了反而会白白的浪费资源。(Tips:可以试试将一张jpg图片压缩为zip,观察大小并没有多大的变化。虽然zip和gzip算法不一样,但是可以看出压缩图片的价值并不大) + +#### 2、大文件 + +原因:会消耗大量的cpu资源,且不一定有明显的效果。 + +## **ngx_http_upstream_module的作用是什么?** + +ngx_http_upstream_module用于定义可通过fastcgi传递、proxy传递、uwsgi传递、memcached传递和scgi传递指令来引用的服务器组。 + +## **什么是C10K问题?** + +C10K问题是指无法同时处理大量客户端(10,000)的网络套接字。 + +## **Nginx是否支持将请求压缩到上游?** + +您可以使用Nginx模块gunzip将请求压缩到上游。gunzip模块是一个过滤器,它可以对不支持“gzip”编码方法的客户机或服务器使用“内容编码:gzip”来解压缩响应。 + +## **如何在Nginx中获得当前的时间?** + +要获得Nginx的当前时间,必须使用SSI模块、和date_local的变量。 + +``` +Proxy_set_header THE-TIME $date_gmt; +``` + +## **用Nginx服务器解释-s的目的是什么?** + +用于运行Nginx -s参数的可执行文件。 + +## **如何在Nginx服务器上添加模块?** + +在编译过程中,必须选择Nginx模块,因为Nginx不支持模块的运行时间选择。 + +## **生产中如何设置worker进程的数量呢?** + +在有多个cpu的情况下,可以设置多个worker,worker进程的数量可以设置到和cpu的核心数一样多,如果在单个cpu上起多个worker进程,那么操作系统会在多个worker之间进行调度,这种情况会降低系统性能,如果只有一个cpu,那么只启动一个worker进程就可以了。 + +## **nginx状态码** + +499: + +服务端处理时间过长,客户端主动关闭了连接。 + +502: + +(1).FastCGI进程是否已经启动 + +(2).FastCGI worker进程数是否不够 + +(3).FastCGI执行时间过长 + +- fastcgi_connect_timeout 300; +- fastcgi_send_timeout 300; +- fastcgi_read_timeout 300; + +(4).FastCGI Buffer不够,nginx和apache一样,有前端缓冲限制,可以调整缓冲参数 + +- fastcgi_buffer_size 32k; +- fastcgi_buffers 8 32k; + +(5). Proxy Buffer不够,如果你用了Proxying,调整 + +- proxy_buffer_size 16k; +- proxy_buffers 4 16k; + +(6).php脚本执行时间过长 + +- 将php-fpm.conf的0s的0s改成一个时间 diff --git a/docs/tools/typora-overview.md b/docs/tools/typora-overview.md new file mode 100644 index 0000000..5e1dcaf --- /dev/null +++ b/docs/tools/typora-overview.md @@ -0,0 +1,184 @@ +--- +sidebar: heading +--- + +11月23日,Typora 正式发布 1.0 版本,正式版开始收费了,定价14.99美元。不过,Beta版本还是可以继续免费使用的。 + +作为 Typora 的重度用户,今天给大家介绍一下这款 Markdown 神器。 + +## 简介 + +Typora 是一款**支持实时预览的 Markdown 文本编辑器**。 + +## 特点 + +1. **所见即所得**。输入`Markdown`标记后,会即时渲染成相应格式。大部分的`Markdown`编辑器都是一半是编辑窗口,一半是预览窗口,而Typora合二为一,更为简洁。 +2. **支持 LaTeX 语法**。 +3. **支持图床功能**。 +4. **定制化主题**。 + + + +## Markdown + +Markdown是一种**轻量级标记语言**,排版语法简洁,让使用者更多地关注内容本身而非排版。 + +**基础语法**: + +![](http://img.topjavaer.cn/img/image-20211205134819994.png) + +**代码高亮**:输入 ``` 后并输入语言名,换行,开始写代码,Typora 会自动实现代码高亮的效果(如下图)。 + +![](http://img.topjavaer.cn/img/image-20211205133939439.png) + +## 图床 + +Typora 里的图片是链接到本地图片的,如果将文档同步到其他平台,图片链接会失效。可以使用图床来保证文档在分享后图片仍能正常显示。 + +我使用的是PicGo图床工具,具体配置方法如下: + +1、下载 PicGo:`https://github.com/Molunerfinn/PicGo/releases` + +2、选择图床,设置相关参数。PicGo 支持多个图床平台,如七牛、阿里云OSS等。 + +![](http://img.topjavaer.cn/img/图床1.png) + +3、设置 PicGo server。 + +![](http://img.topjavaer.cn/img/图床2.png) + +4、打开 Typora 中的「文件-偏好设置-图像」选项,配置上传服务为 PicGo 和 PicGo 的路径。 + +![](http://img.topjavaer.cn/img/图床3.png) + +配置完成之后,当你在 Typora 中插入本地图片时,PicGo 会自动将图片上传图床并使用 Markdown 语法替换图片地址。 + +## LaTeX + +Typora 支持 LaTeX 语法,可以往文档插入数学公式。 + +数学公式有两种形式: inline 和 display。 + +- **inline(行间公式)**:在正文插入数学公式,用`$...$` 将公式括起来 +- **display(块间公式)** :独立排列的公式,用 `$$...$$`将公式括起来,默认显示在行中间 + +**常用语法**: + +![](http://img.topjavaer.cn/img/latex语法.png) + +下面举几个例子: + +**分段函数**: +``` +$$ +f(n)= + \begin{cases} + n/2, & \text{if $n$ is even}\\ + 3n+1,& \text{if $n$ is odd} + \end{cases} +$$ +``` +![](http://img.topjavaer.cn/img/image-20211204235551407.png) + +**矩阵**: +``` +$$ +X=\left| + \begin{matrix} + x_{11} & x_{12} & \cdots & x_{1d}\\ + x_{21} & x_{22} & \cdots & x_{2d}\\ + \vdots & \vdots & \ddots & \vdots \\ + x_{11} & x_{12} & \cdots & x_{1d}\\ + \end{matrix} +\right| +$$ +``` +![](http://img.topjavaer.cn/img/image-20211204235601934.png) + +**偏导数和微分:** +``` +$$ +\frac{\partial z}{\partial x_1} + \frac{\partial z}{\partial x_2} \\ +\frac{\mathrm{d}z}{\mathrm{d}x_1}+\frac{\mathrm{d}z}{\mathrm{d}x_2} +$$ +``` + +![](http://img.topjavaer.cn/img/image-20211204235614665.png) + +## 目录 + +markdown文档生成目录,我使用过的两种方法: + +1、在文章开始使用`[TOC]` 将自动在文章生成目录。 + +- 某些平台(如掘金)不支持 + +2、使用插件 doctoc 生成目录(页内超链接)。 + +- 需要执行命令`doctoc xxx.md`生成目录。如果修改了标题,需要再次执行命令更新目录 + +使用 doctoc 生成目录的步骤: + +1. 安装 doctoc,`npm install doctoc` +2. 在文档当前目录执行`doctoc xxx.md`命令,即可生成标题 + +## 定制化主题 + +在 Typora 中 CSS 被称为「主题」,但其本质仍是 CSS 文件。可以在 `文件 - 偏好设置 - 主题 - 打开主题文件夹` 看到这些 CSS 文件。 + +可以自定义修改 CSS 文件,生成新的主题。 + + + +## Mermaid + +`Mermaid`是一个用于画流程图、状态图、时序图、甘特图的库,使用 JavaScript 进行本地渲染,广泛集成于许多 Markdown 编辑器中。Typora也支持`Mermaid`语法。 + +下面举几个例子。 + +**流程图**: + +```mermaid +graph TD; +A-->B; +A-->C; +B-->D; +C-->D; +``` + +![](http://img.topjavaer.cn/img/image-20211204235313626.png) + +**时序图**: + +```mermaid +sequenceDiagram + Alice->>+John: Hello John, how are you? + Alice->>+John: John, can you hear me? + John-->>-Alice: Hi Alice, I can hear you! + John-->>-Alice: I feel great! +``` + +![](http://img.topjavaer.cn/img/image-20211204235348115.png) + +## 导入导出 + +Typora 支持导入和导出的文件格式:`html`、`pdf`、`docx`、`epub`和`latex`等。其中导出`docx`、`epub`和`latex`需要安装 `Pandoc` 插件。 + +## 其他功能 + +**打字机模式**:使得你所编辑的那一行永远处于屏幕正中。 + +**专注模式**:使你正在编辑的那一行保留颜色,而其他行的字体呈灰色。 + +![](http://img.topjavaer.cn/img/image-20211204235513676.png) + + + + + +码字不易,如果觉得对你有帮助,可以**点个赞**鼓励一下! + +我是 程序员大彬,专注Java后端硬核知识分享,欢迎大家关注~ + + + diff --git a/docs/web/tomcat.md b/docs/web/tomcat.md new file mode 100644 index 0000000..c9cc793 --- /dev/null +++ b/docs/web/tomcat.md @@ -0,0 +1,850 @@ +--- +sidebar: heading +title: Tomcat基础知识总结 +category: Tomcat +tag: + - Tomcat基础 +head: + - - meta + - name: keywords + content: Tomcat,server,serivce,executor,connector,engine,context,host,tomcat启动,tomcat组件,tomcat执行流程 + - - meta + - name: description + content: 高质量的Java基础常见知识点和面试题总结,让天下没有难背的八股文! +--- + +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 架构 + +首先,看一下整个架构图 + +![](http://img.topjavaer.cn/img/202305041101996.png) + + 接下来简单解释一下。 + +**Server**:服务器。Tomcat 就是一个 Server 服务器。 + +**Service**:在服务器中可以有多个 Service,只不过在我们常用的这套 Catalina 容器的Tomcat 中只包含一个 Service,在 Service 中包含连接器和容器。一个完整的 Service 才能完成对请求的接收和处理。 + +**连接器**:Coyote 是连接器具体的实现。用于与新来的请求建立连接并解析数据。因为 Tomcat 支持的 IO 模型有 NIO、NIO2、APR,而支持的应用层协议有 HTTP1.1、HTTP2、AJP。所以针对不同的 IO 模型和应用层协议请求,在一个 Service 中可以有多个连接器来适用不同的协议的IO请求。 + +  EndPoint :Coyote 通信端点,即通信监听的接口,是具体 Socket 接收和发送处理器,是用来实现 TCP/IP 传输协议的。 + +    Acceptor:用于接收请求的 socket。 + +    Executor:线程池,在接收到请求的 socket 后会从线程池中分配一条来执行后面的操作。 + +  Processor :Coyote 协议处理接口,是用来实现 HTTP 应用层协议的,接收 EndPoint 、容器传来的 Socket 字节流,解析成 request 或 response 对象。 + +  ProtocolHandler:Coyote 协议接口,通过 EndPoint 和 Processor,实现针对具体协议的处理能力。 + +  Adapter:容器只负责处理数据,对于请求协议不同的数据,容器会无法处理,所以在 ProtocolHandler 处理生成的 request 对象后,还需要将其转成 Tomcat 定义好的统一格式的 ServletRequest 对象,Adapter 就是用来进行这样的操作的。 + +**容器**: Tomcat 的核心组件, 用于处理请求并返回数据。Catalina 是其具体的实现。 + +  Engine:表示整个 Catalina 的 Servlet 引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。但是一个 Engine 可以包含多个 Host。 + +  Host:表示一个主机地址,或者说一个站点,一个 Host 下有可以配置多个 Context。 + +  Context:表示一个 web 应用,一个 Web 应用可以包含多个 Wrapper + +  Wrapper:表示一个 Servlet,是容器中的最底层组件。 + +各组件的比例关系 + +![](http://img.topjavaer.cn/img/202305041102664.png) + +## 各组件的实现与执行 + +### 组件实现 + +前面提到的各个组件名都是接口或者抽象方法,在实际处理请求时执行的都是其子类或者实现类。 + +Server、Service、Engine、Host、Context都是接口, 下图中罗列了这些接口的默认 实现类。 + +![](http://img.topjavaer.cn/img/202305041102161.png) + +Adapter 的实现是 CoyoteAdapter + +对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint接口, 但是有一个抽象类AbstractEndpoint ,其下有三个实现类: NioEndpoint、Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote 时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR ,tomcat8.5版本中,默认采用的是 NioEndpoint。 + +ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor , 实现针对具体协议的处理功能。Tomcat按照协议和IO提供了6个实现类。 + +AJP协议: + +1) AjpNioProtocol :采用NIO的IO模型。 + +2) AjpNio2Protocol:采用NIO2的IO模型。 + +3) AjpAprProtocol :采用APR的IO模型,需要依赖于APR库。 + +HTTP协议: + +1) Http11NioProtocol :采用NIO的IO模型,默认使用的协议(如果服务器没有安装APR)。 + +2) Http11Nio2Protocol:采用NIO2的IO模型。 + +3) Http11AprProtocol :采用APR的IO模型,需要依赖于APR库。 + +![](http://img.topjavaer.cn/img/202305041102287.png) + +这些组件均存在初始化、启动、停止等周期方法,所以 Tomcat 设计了一个 LifeCycle 接口,用于定义这些组件生命周期中需要执行的共同方法,这些组件实现类都实现了这个接口。 + +![](http://img.topjavaer.cn/img/202305041103085.png) + +### 启动流程 + +![](http://img.topjavaer.cn/img/202305041103964.png) + +1) 启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh) , 在 + +startup.bat 脚本中, 调用了catalina.bat。 + +2) 在catalina.bat 脚本文件中,调用了BootStrap 中的main方法。 + +3)在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及 初始化类加载器。 + +4)在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方法。 + +5)在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用于解析 XML。 + +6) 然后在调用后续组件的初始化操作 。。。 + +加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求 。 + +简而言之就是进行各组件逐级执行 init() 和 start() 方法。 + +### 执行流程 + +当一个请求进入 Tomcat 时,执行情况如下( 因为 Tomcat 只有一个 Service,所以下面就将 Service 和 Engine 写在同一个框中): + +![](http://img.topjavaer.cn/img/202305041103891.png) + +定位主要通过 Mapper 组件来实现,其本质就是一个 K、V键值对,在解析时首先会将请求网址进行解析,将其中的 Host 部分在 Mapper 类中的 hosts属性(MappedHost数组,保存所有的 Host 信息)中进行查找,找到后再解析 Context 部分,在该 MapperHost 中又有 contextList 属性(保存所有的 context 信息),然后再向下找,最终得到对应的 Servlet,执行。 + +![](http://img.topjavaer.cn/img/202305041103701.png) + +除此之外,为了增强各组件之间的拓展性,Tomcat 中定义了 Pipeline 和 Valve 两个接口,Pipeline 用于构建责任链, 后者代表责任链上的每个处理器。Pipeline 中维护了一个基础的 Valve,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程。当然,我们也可以调用addValve()方法, 为Pipeline 添加其他的Valve,后添加的Valve 位于基础的Valve之前,并按照添加顺序执行。Pipiline通过获得首个Valve来启动整合链条的执行 。 + +![](http://img.topjavaer.cn/img/202305041104675.png) + +所以最终的执行如下: + +![](http://img.topjavaer.cn/img/202305041104148.png) + +步骤如下: + +1)Connector组件Endpoint中的Acceptor监听客户端套接字连接并接收Socket。 + + 2)将连接交给线程池Executor处理,开始执行请求响应任务。 + +3)Processor组件读取消息报文,解析请求行、请求体、请求头,封装成Request对象。 + +4)Mapper组件根据请求行的URL值和请求头的Host值匹配由哪个Host容器、Context容器、Wrapper容器处理请求。 + +5)CoyoteAdaptor组件负责将Connector组件和Engine容器关联起来,把生成的Request对象和响应对象Response传递到Engine容器中,调用 Pipeline。 + + 6)Engine容器的管道开始处理,管道中包含若干个Valve、每个Valve负责部分处理逻辑。执行完Valve后会执行基础的 Valve--StandardEngineValve,负责调用Host容器的Pipeline。 + +7)Host容器的管道开始处理,流程类似,最后执行 Context容器的Pipeline。 + +8)Context容器的管道开始处理,流程类似,最后执行 Wrapper容器的Pipeline。 + +9)Wrapper容器的管道开始处理,流程类似,最后执行 Wrapper容器对应的Servlet对象的处理方法。 + +## 配置文件 + + 首先看一下 tomcat 的目录结构 + +![](http://img.topjavaer.cn/img/202305041104182.png) + +核心配置文件在 conf 目录下 + +![](http://img.topjavaer.cn/img/202305041104312.png) + +### Server.xml(重点) + + 其中最重要的就是 server.xml,主要配置了 tomcat 容器的所有配置。下面来看一下其中有哪些配置。 + +#### **Server** + +是 server.xml 的根元素,用于创建一个 Server 实例,默认的实现是 + +```javascript + +... + +``` + + + +port:Tomcat监听的关闭服务器的端口 + +shutdown:关闭服务器的指令字符串。 + +**Server 内嵌的子元素为 Listener、GlobalNamingResources、Service。** + +配置的5个Listener 的含义: + +```javascript + + + + + + + + + + + + + + +``` + + + +GlobalNamingResources 中定义了全局命名服务 + +#### Service + +用于创建 Service 实例,**内嵌的元素为:Listener、Executor、Connector、Engine**,其中 : Listener 用于为Service添加生命周期监听器, Executor 用于配置Service 共享线程池,Connector 用于配置Service 包含的链接器, Engine 用于配置Service中链接器对应的Servlet 容器引擎。默认 Service 就叫 Catalina。 + +#### Executor + +默认情况,Service 并未配置共享线程池,各个连接器使用的都是各自的线程池(默认size为10)。如果我们想添加一个线程池,可以在 Service 标签中添加如下配置 + +```javascript + +``` + + + + 相关属性说明: + +![](http://img.topjavaer.cn/img/202305041105579.png) + +#### Connector + +用于创建连接器实例,默认情况下,server.xml 配置了两个连接器,一个支持 HTTP 协议,一个支持 AJP 协议。 + +```javascript + + + +``` + + + +1) port: 端口号,Connector 用于创建服务端Socket 并进行监听, 以等待客户端请求链接。如果该属性设置为0,Tomcat将会随机选择一个可用的端口号给当前Connector 使用。 + +2) protocol : 当前Connector 支持的访问协议。 默认为 HTTP/1.1 , 并采用自动切换机制选择一个基于 JAVA NIO 的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)。如果不希望采用上述自动切换的机制, 而是明确指定协议, 可以使用以下值。 + +Http协议: + +  org.apache.coyote.http11.Http11NioProtocol , 非阻塞式 Java NIO 链接器 + +  org.apache.coyote.http11.Http11Nio2Protocol , 非阻塞式 JAVA NIO2 链接器 + +  org.apache.coyote.http11.Http11AprProtocol , APR 链接器 + +AJP协议: + +  org.apache.coyote.ajp.AjpNioProtocol , 非阻塞式 Java NIO 链接器 + +  org.apache.coyote.ajp.AjpNio2Protocol ,非阻塞式 JAVA NIO2 链接器 + +  org.apache.coyote.ajp.AjpAprProtocol , APR 链接器 + +3) connectionTimeOut : Connector 接收链接后的等待超时时间, 单位为 毫秒。 -1 表示不超时。 + +4) redirectPort:当前Connector 不支持SSL请求, 接收到了一个请求, 并且也符合securityconstraint 约束, 需要SSL传输,Catalina自动将请求重定向到指定的端口。 + +5) executor : 指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池。 + +6) URIEncoding : 用于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-8859-1。 + +#### Engine + +Engine 作为Servlet 引擎的顶级元素,内部可以嵌入: Cluster、Listener、Realm、Valve和 Host。 + +```javascript + +... + +``` + + + +1) name: 用于指定Engine 的名称, 默认为Catalina 。该名称会影响一部分Tomcat的存储路径(如临时文件)。 + +2) defaultHost : 默认使用的虚拟主机名称, 当客户端请求指向的主机无效时, 将交由默认的虚拟主机处理, 默认为localhost。 **在 ip 地址解析时首先根据defaultHost 设置的 Host从 Host 列表中找对用的 Host 跳转,如果没有再从 Host 列表中查找对应的,如果列表中没有,那么就会访问不到。** + +除此之外,在默认的配置文件中还包含 Realn 标签,如下: + +```javascript + + + + + + + + + +``` + + + +`` 标签是用来配置用户权限的。 + +首先说一下 tomcat 的权限管理。因为在 tomcat 中可以配置多个 web 项目,而 tomcat 为这些项目的管理创建了管理页面,也就是默认 webapps 下 host-manager 与 manager 文件夹的项目页面,为了保证安全性,访问这两个项目需要设置权限,但是如果对每个新用户都单独的设置权限比较繁琐麻烦,所以在 tomcat 中定义了几种不同的权限,我们可以自己配置 "角色"(可以看作是特定权限的集合) 和 "用户"(设置登录名、密码,与角色相关联),然后就可以通过自定义的 "用户" 去访问管理页面。"角色" 和 "用户" 的配置默认可以在 tomcat-users.xml 中配置。当 tomcat 启动后,就会通过 conf 目录下的 server.xml 中的 Realm 标签来检查权限。 + +`` 支持多种 Realm 管理方式: + +1 JDBCRealm 用户授权信息存储于某个关系型数据库中,通过JDBC驱动获取信息验证 + +2 DataSourceRealm 用户授权信息存储于关于型数据中,通过JNDI配置JDBC数据源的方式获取信息验证 + +3 JNDIRealm 用户授权信息存储在基于LDAP的目录服务的服务器中,通过JNDI驱动获取并验证 + +**4 UserDatabaseRealm 默认的配置方式,信息存储于XML文档中 conf/tomcat-users.xml** + +5 MemoryRealm 用户信息存储于内存的集合中,对象集合的数据来源于xml文档 conf/tomcat-users.xml + +6 JAASRealm 通过JAAS框架访问授权信息 + +上面代码块中可以看出Realm就是使用默认的 UserDatabaseRealm 方式配置。而它的 resourceName 就对应之前 `` 中配置的 conf 目录下的 tomcat-users.xml 文件。 + +**如果在Engine下配置Realm, 那么此配置将在当前Engine下的所有Host中共享。 同样,如果在Host中配置Realm , 则在当前Host下的所有Context中共享。底层会覆盖掉上层对同一个资源的配置。** + +#### **Host** + +用于配置一个虚拟主机, 它支持以下嵌入元素:Alias、Cluster、Listener、Valve、Realm、Context。一个 Engine 标签下可以配置多个 Host。 + +```javascript + +... + +``` + + + +属性说明: + +1) name: 当前Host通用的网络名称, 必须与DNS服务器上的注册信息一致。 Engine中包含的Host必须存在一个名称与Engine的defaultHost设置一致。 + +2) appBase: 当前Host的应用基础目录, 当前Host上部署的Web应用均在该目录下(可以是绝对目录,相对路径)。默认为webapps。 + +3) unpackWARs: 设置为true, Host在启动时会将appBase目录下war包解压为目录。设置为 false, Host将直接从war文件启动。 + +4) autoDeploy: 控制tomcat是否在运行时定期检测并自动部署新增或变更的web应用。 + +#### Context + + 用于配置一个 Web 应用。 + +```javascript + +.... + +``` + + + +属性描述: + +1) docBase:Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于 Host appBase的相对路径。 + +2) path:Web应用的Context 路径。如果我们Host名为localhost, 则该web应用访问的根路径为:http://localhost:8080/myApp。它支持的内嵌元素为:CookieProcessor, Loader, Manager,Realm,Resources,WatchedResource,JarScanner,Valve。 + +### tomcat-user.xml(权限管理) + +上面的 realm 标签说到这个文件是配合 realm 标签来设置用户权限的,所以就来看一下具体是如何设置的。 + +首先看一下默认配置 + +```javascript + + + + + + + +``` + + + +`` 标签内有两个子标签,`` 和 ``,role 是用来设置 "角色",而 user 是用来设置登陆 "用户" 的。管理页面是 webapps 下的 host-manager 与 manager 目录,分别来管理所有主机以及所有的 web项目。如果我们只将注释的部分打开,还是不能访问管理页面,因为 tomcat 设置了特定的权限名,首先是 manager: + +manager-gui 允许访问html接口(即URL路径为/manager/html/*) + +manager-script 允许访问纯文本接口(即URL路径为/manager/text/*) + +manager-jmx 允许访问JMX代理接口(即URL路径为/manager/jmxproxy/*) + +manager-status 允许访问Tomcat只读状态页面(即URL路径为/manager/status/*) + +对于 host-manager: + +admin-gui 允许访问html接口(即URL路径为/host-manager/html/*) + +admin-script 允许访问纯文本接口(即URL路径为/host-manager/text/*) + +admin-jmx 允许访问JMX代理接口(即URL路径为/host-manager/jmxproxy/*) + +admin-status 允许访问Tomcat只读状态页面(即URL路径为/host-manager/status/*) + +如果我们想让某个角色直接能访问这两个项目页面,可以将 roles 配置成下面的设置,然后就可以访问 manager 和 host-manager 页面了。 + +```javascript + +``` + + + +### Web.xml(不常用) + +web.xml 目前已经很少再用了,所以这部分内容简单了解下即可。web.xml 文件分为 tomcat 安装目录的 conf 下的以及各个项目的 WEB-INF 目录下的。conf 下的是全局配置,所有 web 项目都会受到影响,而 WEB-INF 下的只会作用于当前项目,但是如果与 conf 下的 web.xml 配置冲突,那么就会覆盖掉 conf的。 + +#### ServletContext 初始化全局参数 + +K、V键值对。可以在应用程序中使用 javax.servlet.ServletContext.getInitParameter()方法获取参数值。 + +```javascript + +  contextConfigLocation  +  classpath:applicationContext-*.xml +  Spring Config File Location < +   +``` + +#### 会话设置 + +用于配置Web应用会话,包括 超时时间、Cookie配置以及会话追踪模式。它将覆盖server.xml 和 context.xml 中的配置。 + +```javascript + +  30 +   +    JESSIONID +    www.itcast.cn +    / +    Session Cookie +    true +    false +    3600 +   +  COOKIE + +``` + +1) session-timeout : 会话超时时间,单位:分钟 + +2) cookie-config: 用于配置会话追踪Cookie + +  name:Cookie的名称 + +  domain:Cookie的域名 + +  path:Cookie的路径 + +  comment:注释 + +  http-only:cookie只能通过HTTP方式进行访问,JS无法读取或修改,此项可以增加网站访问的安全性。 + +  secure:此cookie只能通过HTTPS连接传递到服务器,而HTTP 连接则不会传递该信息。注意是从浏览器传递到服务器,服务器端的Cookie对象不受此项影响。 + +  max-age:以秒为单位表示cookie的生存期,默认为-1表示是会话Cookie,浏览器关闭时就会消失。 + +3) tracking-mode :用于配置会话追踪模式,Servlet3.0版本中支持的追踪模式:COOKIE、URL、SSL + +  A. COOKIE : 通过HTTP Cookie 追踪会话是最常用的会话追踪机制, 而且Servlet规范也要求所有的Servlet规范都需要支持Cookie追踪。 + +  B. URL : URL重写是最基本的会话追踪机制。当客户端不支持Cookie时,可以采用URL重写的方式。当采用URL追踪模式时,请求路径需要包含会话标识信息,Servlet容器会根据路径中的会话标识设置请求的会话信息。如: http://www.myserver.com/user/index.html;jessionid=1234567890。 + +  C. SSL : 对于SSL请求, 通过SSL会话标识确定请求会话标识。 + +#### Servlet 配置 + +Servlet 的配置主要是两部分, servlet 和 servlet-mapping : + +```javascript + + myServlet + cn.itcast.web.MyServlet + + fileName + init.conf + + 1 + true + + + myServlet + *.do + /myservet/* + +``` + + + +1)servlet-name : 指定servlet的名称, 该属性在web.xml中唯一。 + +2)servlet-class : 用于指定servlet类名 + +3)init-param: 用于指定servlet的初始化参数, 在应用中可以通过HttpServlet.getInitParameter 获取。 + +4) load-on-startup: 用于控制在Web应用启动时,Servlet的加载顺序。 值小于0,web应用启动时,不加载该servlet, 第一次访问时加载。 + +5) enabled: true , false 。 若为false ,表示Servlet不处理任何请求。 + +6) url-pattern: 用于指定URL表达式,一个 servlet-mapping可以同时配置多个 url-pattern。 + +Servlet 中文件上传配置: + +```javascript + + uploadServlet + cn.itcast.web.UploadServlet + + C://path + 10485760 + 10485760 + 0 + + +``` + + + +1) location:存放生成的文件地址。 + +2) max-file-size:允许上传的文件最大值。 默认值为-1, 表示没有限制。 + +3) max-request-size:针对该 multi/form-data 请求的最大数量,默认值为-1, 表示无限制。 + +4) file-size-threshold:当数量量大于该值时, 内容会被写入文件。 + +#### Listener 配置 + +Listener用于监听servlet中的事件,例如context、request、session对象的创建、修改、删除,并触发响应事件。Listener是观察者模式的实现,在servlet中主要用于对context、request、session对象的生命周期进行监控。在servlet2.5规范中共定义了8中Listener。在启动时,ServletContextListener的执行顺序与web.xml 中的配置顺序一致, 停止时执行顺序相反。 + +```javascript + + org.springframework.web.context.ContextLoaderListener + +``` + + + +#### Filter 配置 + +fifilter 用于配置web应用过滤器, 用来过滤资源请求及响应。 经常用于认证、日志、加密、数据转换等操作, 配置如下: + +```javascript + + myFilter + cn.itcast.web.MyFilter + true + + language + CN + + + + myFilter + /* + +``` + + + +1) filter-name: 用于指定过滤器名称,在web.xml中,过滤器名称必须唯一。 + +2) filter-class : 过滤器的全限定类名, 该类必须实现Filter接口。 + +3) async-supported: 该过滤器是否支持异步 + +4) init-param :用于配置Filter的初始化参数, 可以配置多个, 可以通过 FilterConfig.getInitParameter获取 + +5) url-pattern: 指定该过滤器需要拦截的URL。 + +#### 欢迎页面配置 + +```javascript + + index.html + index.htm + index.jsp + +``` + + + +尝试请求的顺序,从上到下。 + +#### 错误页面配置 + +error-page 用于配置Web应用访问异常时定向到的页面,支持HTTP响应码和异常类两种形式。 + +```javascript + + 404 + /404.html + + + 500 + /500.html + + + java.lang.Exception + /error.jsp + +``` + + + +## 安全与优化 + +### 安全 + +#### 配置安全 + +1) 删除webapps目录下的所有文件,禁用tomcat管理界面; + +2) 注释或删除tomcat-users.xml文件内的所有用户权限; + +3) 更改关闭tomcat指令或禁用;tomcat的server.xml中定义了可以直接关闭 Tomcat 实例的管理端口(默认8005)。可以通过 telnet连接上该端口之后,输入 SHUTDOWN (此为默认关闭指令)即可关闭 Tomcat 实例(注意,此时虽然实例关闭了,但是进程还是存在的)。由于默认关闭Tomcat 的端口和指令都很简单。默认端口为8005,指令为SHUTDOWN 。 + +方案一:更改端口号 + +```javascript + +``` + + + +方案二:禁用8005 端口,设为-1。 + +```javascript + +``` + + + +4) 定义错误页面,如果不定义在发生异常后会显示代码类名以及位置,会泄漏目录结构。在webapps/ROOT目录下定义错误页面 404.html,500.html;然后在tomcat/conf/web.xml中进行配置 , 配置错误页面: + +```javascript + + 404 + /404.html + + + 500 + /500.html + +``` + + + +#### 应用安全 + +应用安全是指在某些隐私页面应该是登陆用户或者管理员用户才能访问的,而对于这些页面在权限不够时应该被拦截,可以使用拦截器或者一些安全框架,比如 SpringSecurity、Shiro 等。 + +#### 传输安全 + +传统的网络应用协议 HTTP 并不安全,此时可以使用 HTTPS 来代替,它在 HTTP 的基础上加入 SSL/TLS 来进行数据加密,保护交换数据不被泄漏、窃取。 + +HTTPS和HTTP的区别主要为以下四点: + +1) HTTPS协议需要到证书颁发机构CA申请SSL证书, 然后与域名进行绑定,HTTP不用申请证书; + +2) HTTP是超文本传输协议,属于应用层信息传输,HTTPS 则是具有SSL加密传安全性传输协议,对数据的传输进行加密,相当于HTTP的升级版; + +3) HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是8080,后者是8443。 + +4) HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。 + +HTTPS协议优势: + +1) 提高网站排名,有利于SEO。谷歌已经公开声明两个网站在搜索结果方面相同,如果一个网站启用了SSL,它可能会获得略高于没有SSL网站的等级,而且百度也表明对安装了SSL的网站表示友好。因此,网站上的内容中启用SSL都有明显的SEO优势。 + +2) 隐私信息加密,防止流量劫持。特别是涉及到隐私信息的网站,互联网大型的数据泄露的事件频发发生,网站进行信息加密势在必行。北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090 + +3) 浏览器受信任。 自从各大主流浏览器大力支持HTTPS协议之后,访问HTTP的网站都会提示“不安全”的警告信息。 + +### 性能优化 + +#### 性能测试 + +ApacheBench(ab)是一款ApacheServer基准的测试工具,用户测试Apache Server的服务能力(每秒处理请求数),它不仅可以用户Apache的测试,还可以用于测试Tomcat、Nginx、lighthttp、IIS等服务器。 + +安装:yum install httpd-tools + +执行:b -n 1000 -c 100 -p data.json -T application/json http://localhost:9000/course/search.do?page=1&pageSize=10 + +参数说明: + +![](http://img.topjavaer.cn/img/202305041105367.png) + +如果此请求需要携带 Post 数据,那么需要自定义一个文件来保存这个数据,一般使用 json 格式来保存传输 + +执行结果部分: + +![](http://img.topjavaer.cn/img/202305041105702.png) + +参数说明: + +![](http://img.topjavaer.cn/img/202305041106869.png) + +重点需要关注的参数: + +![](http://img.topjavaer.cn/img/202305041106517.png) + +#### JVM 优化 + +因为 Tomcat 是一台 Java 服务器,所以它的优化就可以归结到 JVM 的优化上,而 Tomcat 在JVM 上的优化可以分为垃圾回收器的选择以及一些参数配置。关于垃圾回收器和相关参数配置这里就不过多阐述了,这里只介绍下如何在 Tomcat 启动时携带我们想要的配置。 + +windows 下: 修改bin/catalina.bat 文件,在第一行添加 : set JAVA_OPTS=-server -Dfile.encoding=UTF-8 具体配置 + +linux 下:修改 bin/catalina.sh 文件,在第一行添加: JAVA_OPTS=" -server 具体配置" + +#### Tomcat 配置优化 + +连接器的配置是决定 Tomcat 性能的关键,在一般情况下使用默认的就可以了,但是在程序比较吃力时,就需要手动配置它来提高效率,完整的配置如下: + +```javascript + +``` + + + +相关参数: + +maxThreads:表示Tomcat可创建的最大的线程数; + +minSpareThreads:最小空闲线程数,Tomcat初始化时创建的线程数,该值应该少于maxThreads,缺省值为4; + +acceptCount:指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认为10个; + +maxConnections:服务器在任何给定时间接受和处理的最大连接数。 + +connectionTimeout:网络连接超时时间,单位为毫秒,如果设置为“0”则表示永不超时,不建议这样设置; + +compression:默认为 off,开启是连接器在试图节省服务器的带宽使用 HTTP/1.1 GZIP 压缩。关闭会自动在压缩和传输之间进行权衡。 + +compressionMinSize:在 compression 开启时,可以通过这个来配置进行压缩的最小数据量。默认为 "2048"。 + +disableUploadTimeout:上传文件时是否使用超时机制,默认开启,由 ConnectionTimeout 决定,如果为 false,那么只会在设置的 connectionUploadTimeout 设置的时间后才会断开。 + +redirectPort:如果此连接器支持非 SSL 请求,并且收到匹配需要 SSL 传输的请求,Catalina 将自动将请求重定向到此处指定的端口号。 + +其他参数可参考博客 [ tomcat(4)连接器](https://blog.csdn.net/sz85850597/article/details/79954711) 。 + +如果只是想简单配置,可以只配置 maxConnections、maxThreads、acceptCount。 + +## Tomcat 附加功能 WebSocket + +我们在浏览网页时,一般使用的是HTTP 协议或者 HTTPS 协议,这种方式是一种 "请求---响应" 模式,也就是只支持从客户端发送请求,服务器收到后进行处理,然后返回一个响应,但是不能主动发送数据给客户端,这样某些场景下的实现就比较困难,甚至无法实现,比如聊天室实时聊天,可能有人会说直接将在 servlet 中处理向要发送消息的客户端发送不就行了,但是因为是 "请求-响应" 模式,当其他客户端与服务器一段时间没有通信,连接就会断开,服务器也就无法转发消息了。而 WebSocket 则是基于 HTTP 的一种长连接协议,并且是双向通道,可以实现服务器主动向客户端发送消息。 + +### WebSocket 请求过程 + +![](http://img.topjavaer.cn/img/202305041106746.png) + +![](http://img.topjavaer.cn/img/202305041106123.png) + +WebSocket 请求和普通的HTTP请求有几点不同: + +\1. GET请求的地址不是类似 http://,而是以 ws:// 开头的地址; + +\2. 请求头 Connection: Upgrade 和 请求头 Upgrade: websocket 表示这个连接将要被转换为WebSocket 连接; + +\3. Sec-WebSocket-Key 是用于标识这个连接, 是一个BASE64编码的密文, 要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答; + +\4. Sec-WebSocket-Version 指定了WebSocket的协议版本; + +\5. HTTP101 状态码表明服务端已经识别并切换为WebSocket协议 , Sec-WebSocket-Accept是服务端与客户端一致的秘钥计算出来的信息。 + +Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356), 而在7.0.5版本之前(7.0.2之后)则采用自定义API, 即WebSocketServlet实现。Java WebSocket应用由一系列的WebSocketEndpoint组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口, 就像Servlet之与http请求一样。我们可以通过两种方式定义Endpoint: + +1). 第一种是编程式, 即继承类 javax.websocket.Endpoint并实现其方法。 + +2). 第二种是注解式, 即定义一个POJO, 并添加 @ServerEndpoint相关注解。Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下: + +![](http://img.topjavaer.cn/img/202305041106264.png) + +通过为Session添加MessageHandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过 @OnMessage 注解指定接收消息的方法。发送消息则由RemoteEndpoint 完成, 其实例由Session维护, 根据使用情况, 我们可以通过Session.getBasicRemote获取同步消息发送的实例 , 然后调用其sendXxx()方法就可以发送消息, 可以通过Session.getAsyncRemote 获取异步消息发送实例。 + + + +> 参考链接:https://cloud.tencent.com/developer/article/1957959 \ No newline at end of file diff --git a/docs/zookeeper/zk-usage.md b/docs/zookeeper/zk-usage.md new file mode 100644 index 0000000..d36cf35 --- /dev/null +++ b/docs/zookeeper/zk-usage.md @@ -0,0 +1,40 @@ +## Zookeeper有哪些使用场景? + +zookeeper 的使用场景有: + +- 分布式协调 +- 分布式锁 +- 元数据/配置信息管理 +- HA 高可用性 + +### 分布式协调 + +这个其实是 zookeeper 很经典的一个用法,简单来说,比如 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上**对某个节点的值注册个监听器**,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知。 + +![](http://img.topjavaer.cn/img/zookeeper-distributed-coordination.png) + +### 分布式锁 + +比如对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也**尝试去创建**那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。 + +![](http://img.topjavaer.cn/img/zookeeper-distributed-lock-demo.png) + +### 元数据/配置信息管理 + +zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理。 + +![](http://img.topjavaer.cn/img/zookeeper-meta-data-manage.png) + +### HA 高可用性 + +这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个**重要进程一般会做主备**两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。 + +![](http://img.topjavaer.cn/img/zookeeper-active-standby.png) + + + +**参考资料** + +https://zhuanlan.zhihu.com/p/59669985 + +https://doocs.github.io/ \ No newline at end of file diff --git a/docs/zookeeper/zk.md b/docs/zookeeper/zk.md new file mode 100644 index 0000000..e86fb41 --- /dev/null +++ b/docs/zookeeper/zk.md @@ -0,0 +1,143 @@ +--- +sidebar: heading +title: ZooKeeper常见面试题总结 +category: 框架 +tag: + - ZooKeeper +head: + - - meta + - name: keywords + content: ZooKeeper面试题,ZooKeeper部署模式,ZooKeeper宕机,ZooKeeper功能,ZooKeeper主从节点,ZooKeeper和Dubbo + - - meta + - name: description + content: 高质量的ZooKeeper常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## ZooKeeper 是什么? + +ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。 + +ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 + +## Zookeeper 都有哪些功能? + +1. **集群管理**:监控节点存活状态、运行请求等; +2. **主节点选举**:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 Zookeeper 可以协助完成这个过程; +3. **分布式锁**:Zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。Zookeeper 可以对分布式锁进行控制。 +4. **命名服务**:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。 + +## 说说Zookeeper 的文件系统 + +Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。 + +Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M。 + +## Zookeeper 怎么保证主从节点的状态同步? + +Zookeeper 的核心是原子广播机制,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。 + +1、**恢复模式** + +当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。 + +2、**广播模式** + +一旦 leader 已经和多数的 follower 进行了状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 ZooKeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。ZooKeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。 + +## zookeeper 是如何保证事务的顺序一致性的? + +zookeeper 采用了全局递增的事务 Id 来标识,所有的 proposal都在被提出的时候加上了 zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch 用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。 + +## 分布式集群中为什么会有 Master主节点? + +在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行 leader 选举。 + +## zk 节点宕机如何处理? + +Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。 + +如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。 + +ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。 + +所以3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5)。2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票<=1) + +## zookeeper 负载均衡和 nginx 负载均衡区别 + +zk 的负载均衡是可以调控,nginx 只是能调权重,其他需要可控的都需要自己写插件;但是 nginx 的吞吐量比 zk 大很多,应该说按业务选择用哪种方式。 + +## Zookeeper 有哪几种几种部署模式? + +Zookeeper 有三种部署模式: + +1. 单机部署:一台集群上运行; +2. 集群部署:多台集群运行; +3. 伪集群部署:一台集群启动多个 Zookeeper 实例运行。 + +## 集群最少要几台机器,集群规则是怎样的?集群中有 3 台服务器,其中一个节点宕机,这个时候 Zookeeper 还可以使用吗? + +集群规则为 2N+1 台,N>0,即 3 台。可以继续使用,单数服务器只要没超过一半的服务器宕机就可以继续使用。 + +## 集群支持动态添加机器吗? + +其实就是水平扩容了,Zookeeper 在这方面不太好。两种方式: + +全部重启:关闭所有 Zookeeper 服务,修改配置之后启动。不影响之前客户端的会话。 + +逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。 + +3.5 版本开始支持动态扩容。 + +## Zookeeper 对节点的 watch 监听通知是永久的吗?为什么不是永久的? + +不是。官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。 + +为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。 + +一般是客户端执行 getData(“/节点 A”,true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。 + +在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。 + +## ZAB 和 Paxos 算法的联系与区别? + +相同点: + +(1)两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行 + +(2)Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交 + +不同点: + +ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。 + +## ZAB 的两种基本模式? + +**崩溃恢复**:在正常情况下运行非常良好,一旦 Leader 出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。为了程序的正确运行,整个恢复过程后需要选举出一个新的 Leader,因此需要一个高效可靠的选举方法快速选举出一个 Leader。 + +**消息广播**:类似一个两阶段提交过程,针对客户端的事务请求, Leader 服务器会为其生成对应的事务 Proposal,并将其发送给集群中的其余所有机器,再分别收集各自的选票,最后进行事务提交。 + +## 哪些情况会导致 ZAB 进入恢复模式并选取新的 Leader? + +启动过程或 Leader 出现网络中断、崩溃退出与重启等异常情况时。 + +当选举出新的 Leader 后,同时集群中已有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 就会退出恢复模式。 + +## 说一下 Zookeeper 的通知机制? + +client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。 + +## Zookeeper 和 Dubbo 的关系? + +Zookeeper的作用: + +zookeeper用来注册服务和进行负载均衡,哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是ip地址和服务名称的对应关系。当然也可以通过硬编码的方式把这种对应关系在调用方业务代码中实现,但是如果提供服务的机器挂掉调用者无法知晓,如果不更改代码会继续请求挂掉的机器提供服务。zookeeper通过心跳机制可以检测挂掉的机器并将挂掉机器的ip和服务对应关系从列表中删除。至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向zookeeper注册服务,服务的提供者多了能服务的客户就多了。 + +dubbo: + +是管理中间层的工具,在业务层到数据仓库间有非常多服务的接入和服务提供者需要调度,dubbo提供一个框架解决这个问题。注意这里的dubbo只是一个框架,至于你架子上放什么是完全取决于你的,就像一个汽车骨架,你需要配你的轮子引擎。这个框架中要完成调度必须要有一个分布式的注册中心,储存所有服务的元数据,你可以用zk,也可以用别的,只是大家都用zk。 + +zookeeper和dubbo的关系: + +Dubbo 的将注册中心进行抽象,它可以外接不同的存储媒介给注册中心提供服务,有 ZooKeeper,Memcached,Redis 等。 + +引入了 ZooKeeper 作为存储媒介,也就把 ZooKeeper 的特性引进来。首先是负载均衡,单注册中心的承载能力是有限的,在流量达到一定程度的时 候就需要分流,负载均衡就是为了分流而存在的,一个 ZooKeeper 群配合相应的 Web 应用就可以很容易达到负载均衡;资源同步,单单有负载均衡还不 够,节点之间的数据和资源需要同步,ZooKeeper 集群就天然具备有这样的功能;命名服务,将树状结构用于维护全局的服务地址列表,服务提供者在启动 的时候,向 ZooKeeper 上的指定节点 /dubbo/${serviceName}/providers 目录下写入自己的 URL 地址,这个操作就完成了服务的发布。 其他特性还有 Mast 选举,分布式锁等。 diff --git a/docs/zsxq/article/select-max-rows.md b/docs/zsxq/article/select-max-rows.md new file mode 100644 index 0000000..9ffabc2 --- /dev/null +++ b/docs/zsxq/article/select-max-rows.md @@ -0,0 +1,225 @@ +## 问题 + +一条这样的 SQL 语句能查询出多少条记录? + +```SQL +select * from user +``` + +表中有 100 条记录的时候能全部查询出来返回给客户端吗? + +如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢? + +虽然在实际业务操作中我们不会这么干,尤其对于数据量大的表不会这样干,但这是个值得想一想的问题。 + +## 寻找答案 + +前提:以下所涉及资料全部基于 MySQL 8 + +### max_allowed_packet + +在查询资料的过程中发现了这个参数 `max_allowed_packet` + +![](http://img.topjavaer.cn/img/202307231140420.png) + +上图参考了 MySQL 的官方文档,根据文档我们知道: + +- MySQL 客户端 `max_allowed_packet` 值的默认大小为 16M(不同的客户端可能有不同的默认值,但最大不能超过 1G) +- MySQL 服务端 `max_allowed_packet` 值的默认大小为 64M +- `max_allowed_packet` 值最大可以设置为 1G(1024 的倍数) + +然而 根据上图的文档中所述 + +> The maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function + +- one packet +- generated/intermediate string +- any parameter sent by the mysql_smt_send_long_data() C API function + +这三个东东具体都是什么呢? `packet` 到底是结果集大小,还是网络包大小还是什么? 于是 google 了一下,搜索排名第一的是这个: + +![](http://img.topjavaer.cn/img/202307231140408.png) + +根据 “Packet Too Large” 的说明, 通信包 (communication packet) 是 + +- 一个被发送到 MySQL 服务器的单个 SQL 语句 +- 或者是一个被发送到客户端的**单行记录** +- 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。 + +1、3 点好理解,这也同时解释了,如果你发送的一条 SQL 语句特别大可能会执行不成功的原因,尤其是`insert` `update` 这种,单个 SQL 语句不是没有上限的,不过这种情况一般不是因为 SQL 语句写的太长,主要是由于某个字段的值过大,比如有 BLOB 字段。 + +那么第 2 点呢,单行记录,默认值是 64M,会不会太大了啊,一行记录有可能这么大的吗? 有必要设置这么大吗? 单行最大存储空间限制又是多少呢? + +### 单行最大存储空间 + +MySQL 单行最大宽度是 65535 个字节,也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。 + +![](http://img.topjavaer.cn/img/202307231141021.png) + +通过上图可以看到 超过 65535 不行,不过请注意其中的错误提示:“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ,如果字段是变长类型的如 BLOB 和 TEXT 就不包括了,那么我们试一下用和上图一样的字段长度,只把最后一个字段的类型改成 BLOB 和 TEXT + +```SQL +mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000), + c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000), + f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1; +Query OK, 0 rows affected (0.02 sec) +``` + +可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意,字符集是 `latin1` 可以成功,如果换成 `utf8mb4` 或者 `utf8mb3` 就不行了,会报错,仍然是 :“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢? + +**因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了!** + +我们先看一下这个熟悉的 VARCHAR(255) , 你有没有想过为什么用 255,不用 256? + +> 在 4.0 版本以下,varchar(255) 指的是 255 个字节,使用 1 个字节存储长度即可。当大于等于 256 时,要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。 +> +> 但是在 5.0 版本以上,varchar(255) 指的是 255 个字符,每个字符可能占用多个字节,例如使用 UTF8 编码时每个汉字占用 3 字节,使用 GBK 编码时每个汉字占 2 字节。 + +例子中我们用的是 MySQL8 ,由于字符集是 utf8mb3 ,存储一个字要用三个字节, 长度为 255 的话(列宽),总长度要 765 字节 ,再加上用 2 个字节存储长度,那么这个列的总长度就是 767 字节。所以用 latin1 可以成功,是因为一个字符对应一个字节,而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节,VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节,比原来大了 3、4 倍,肯定放不下了。 + +**另外,还有一个要求**,列的宽度不要超过 MySQL 页大小 (默认 16K)的一半,要比一半小一点儿。 例如,对于默认的 16KB `InnoDB` 页面大小,最大行大小略小于 8KB。 + +下面这个例子就是超过了一半,所以报错,当然解决办法也在提示中给出了。 + +```SQL +mysql> CREATE TABLE t4 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(255),c33 CHAR(255) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. +In current row format, BLOB prefix of 0 bytes is stored inline. +``` + +**那么为什么是 8K,不是 7K,也不是 9K 呢?** 这么设计的原因可能是:MySQL 想让一个数据页中能存放更多的数据行,至少也得要存放两行数据(16K)。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。 + +**你可能还会奇怪,不超过 8K ?你前面的例子明明都快 64K 也能存下,那 8K 到 64K 中间这部分怎么解释?** + +答:如果包含可变长度列的行超过 `InnoDB` 最大行大小, `InnoDB` 会选择可变长度列进行页外存储,直到该行适合 `InnoDB` ,这也就是为什么前面有超过 8K 的也能成功,那是因为用的是`VARCHAR`这种可变长度类型。 + +![](http://img.topjavaer.cn/img/202307231141870.png) + +当你往这个数据页中写入一行数据时,即使它很大将达到了数据页的极限,但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。 + +**我们通过 Compact 格式,简单了解一下什么是 `页外存储` 和 `行溢出`** + +MySQL8 InnoDB 引擎目前有 4 种 行记录格式: + +- REDUNDANT +- COMPACT +- DYNAMIC(默认 default 是这个) +- COMPRESSED + +`行记录格式` 决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。 + +![](http://img.topjavaer.cn/img/202307231141440.png) + +Compact 格式的实现思路是:当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据放到其他数据页中去。 + +![](http://img.topjavaer.cn/img/202307231141558.png) + +在 MySQL 设定中,当 varchar 列长度达到 768byte 后,会将该列的前 768byte 当作当作 prefix 存放在行中,多出来的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是 `行溢出`机制 + +> **假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte,这时你可以成功 insert,但是一个数据页又存储不了你插入的数据。这时肯定会行溢出!** + +MySQL 这样做,有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况,避免了 IO 飙升的窘境。 + +### 单行最大列数限制 + +**mysql 单表最大列数也是有限制的,是 4096 ,但 InnoDB 是 1017** + +![](http://img.topjavaer.cn/img/202307231141116.png) + +### 实验 + +前文中我们疑惑 `max_allowed_packet` 在 MySQL8 的默认值是 64M,又说这是限制单行数据的,单行数据有这么大吗? 在前文我们介绍了行溢出, 由于有了 `行溢出` ,单行数据确实有可能比较大。 + +那么还剩下一个问题,`max_allowed_packet` 限制的确定是单行数据吗,难道不是查询结果集的大小吗 ? 下面我们做个实验,验证一下。 + +建表 + +```SQL +CREATE TABLE t1 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(192) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +``` + +经过测试虽然提示的是 `Row size too large (> 8126)` 但如果全部长度加起来是 8126 建表不成功,最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ?可能是还需要存储一些其他的东西占了一些字节吧,比如隐藏字段什么的。 + +用存储过程造一些测试数据,把表中的所有列填满 + +```SQL +create + definer = root@`%` procedure generate_test_data() +BEGIN + DECLARE i INT DEFAULT 0; + DECLARE col_value TEXT DEFAULT REPEAT('a', 255); + WHILE i < 5 DO + INSERT INTO t1 VALUES + ( + col_value, col_value, col_value, + col_value, REPEAT('b', 192) + ); + SET i = i + 1; + END WHILE; +END; +``` + +将 `max_allowed_packet` 设置的小一些,先用 `show VARIABLES like '%max_allowed_packet%';` 看一下当前的大小,我的是 `67108864` 这个单位是字节,等于 64M,然后用 `set global max_allowed_packet =1024` 将它设置成允许的最小值 1024 byte。 设置好后,关闭当前查询窗口再新建一个,然后再查看: + +![](http://img.topjavaer.cn/img/202307231141361.png) + +这时我用 `select * from t1;` 查询表数据时就会报错: + +![](http://img.topjavaer.cn/img/202307231142956.png) + +因为我们一条记录的大小就是 8K 多了,所以肯定超过 1024byte。可见文档的说明是对的, `max_allowed_packet` 确实是可以约束单行记录大小的。 + +## 答案 + +文章写到这里,我有点儿写不下去了,一是因为懒,另外一个原因是关于这个问题:“一条 SQL 最多能查询出来多少条记录?” 肯定没有标准答案 + +目前我们可以知道的是: + +- 你的单行记录大小不能超过 `max_allowed_packet` +- 一个表最多可以创建 1017 列 (InnoDB) +- 建表时定义列的固定长度不能超过 页的一半(8k,16k...) +- 建表时定义列的总长度不能超过 65535 个字节 + +如果这些条件我们都满足了,然后发出了一个没有 where 条件的全表查询 `select *` 那么..... + +首先,你我都知道,这种情况不会发生在生产环境的,如果真发生了,一定是你写错了,忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有,也不能开发,因为这不合理。 + +我考虑的也就是个理论情况,从理论上讲能查询出多少数据不是一个确定的值,除了前文提到的一些条件外,它肯定与以下几项有直接的关系 + +- 数据库的可用内存 +- 数据库内部的缓存机制,比如缓存区的大小 +- 数据库的查询超时机制 +- 应用的可用物理内存 +- ...... + +说到这儿,我确实可以再做个实验验证一下,但因为懒就不做了,大家有兴趣可以自己设定一些条件做个实验试一下,比如在特定内存和特定参数的情况下,到底能查询出多少数据,就能看得出来了。 + +虽然我没能给出文章开头问题的答案,但通过寻找答案也弄清楚了 MySQL 的一些限制条件,并加以了验证,也算是有所收获了。 + + + +**参考**链接:https://juejin.cn/post/7255478273652834360 diff --git a/docs/zsxq/article/sideline-guide.md b/docs/zsxq/article/sideline-guide.md new file mode 100644 index 0000000..a054b0c --- /dev/null +++ b/docs/zsxq/article/sideline-guide.md @@ -0,0 +1,127 @@ +**忠告**:**不要全职接单!不要全职接单!不要全职接单!** + +# 程序员副业指南——接单平台 + +下文是接单平台,内容来自知乎,转载过来的原因有2个: + +1. 方便大家了解这些平台各自的优势,可以结合自己的情况,注册一两个实践一下。**注意哦:请态度随缘,不要期望太高。** 如果你去年被优化,目前还没有找到工作,建议踏踏实实去找工作,不要在这上面浪费时间。 +2. 第二个原因也是想劝退大家入坑:**这么多众包平台,接单平台。去看下注册率和成单率,很差的。而且好的项目基本都被头部的外包公司垄断了,凭啥一个刚入行的小菜鸟能接到单,换位思考一下,科学吗!?** + +## 一、垂直众包平台 + +这类平台是从 15 年到18年开始出现的,专注于 IT 众包领域,职位内容大多集中于 UI 设计、产品设计、程序开发、产品运营等需求,其中又以程序开发和 UI 设计的需求最多,可以提供比较稳定和比较多的兼职需求来供我们选择。这些渠道主要有: + +### 1、YesPMP平台: + +[www.yespmp.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.yespmp.com%2F) + +**首推这个平台的原因只有一个:免费!注册免费,投标免费,而且资源不少。** + +但是每个平台都是有“套路的”,每天只能免费投递3个项目竞标,你如果想竞标更多的项目需要开会员。 + +(教你一招:第二天再投3个项目竞标不就行了,每天都可以免费投递三个) + +### 2、开源众包 : + +[zb.oschina.net/projects/li…](https://link.juejin.cn?target=https%3A%2F%2Fzb.oschina.net%2Fprojects%2Flist.html) + +开源中国旗下众包平台,目前项目以项目整包为主,对接企业接包方多些,个人也可以注册。目前有免费模式和付费模式。平台搞到最后都是为了赚钱,白嫖怪不太可能接到好项目。 + +### 3、人人开发 - 应用市场开发服务平台: + +[www.rrkf.com/](https://link.juejin.cn?target=http%3A%2F%2Fwww.rrkf.com%2F) + +人人开发的注册流程比较简单一点,但是建议大家也要认真填写简历。 + +### 4、英选 : + +[www.yingxuan.io/](https://link.juejin.cn?target=https%3A%2F%2Fwww.yingxuan.io%2F) + +英选有自己的接包团队进行自营业务,也支持外部入驻。 + +### 5、我爱方案网: + +[www.52solution.com/](https://link.juejin.cn?target=http%3A%2F%2Fwww.52solution.com%2F) + +名字比较土,但是对于硬件工程师和嵌入式工程师建议注册下。 + +### 6、码市: + +[codemart.com/](https://link.juejin.cn?target=https%3A%2F%2Fcodemart.com%2F) + +### 7、解放号: + +[www.jfh.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.jfh.com%2F) + +## 二、线上技术论坛 + +### 1、GitHub + +开发者最最最重要的网站:[github.com](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com) + +这个不用多说了吧,代码托管网站,上面有很多资源,想要什么轮子,上去搜就好了。 + +### 2. Stack Overflow + +解决 bug 的社区:[stackoverflow.com/](https://link.juejin.cn?target=https%3A%2F%2Fstackoverflow.com%2F) + +开发过程中遇到什么 bug,上去搜一下,只要搜索的方式对,百分之 99 的问题都能搜到答案。 + +在这里能够与很多有经验的开发者交流,如果你是有经验的开发者,还可以来这儿帮助别人解决问题,提升个人影响力。 + +### 3. 程序员客栈: + +[www.proginn.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.proginn.com%2F) + +程序员客栈是领先的程序员自由工作平台,如果你是有经验有资质的开发者,都可以来上面注册成为开发者,业余的时候做点项目,赚点零花钱。 + +当然,如果你想成为一名自由工作者,程序员客栈也是可以满足的。只要你有技术,不怕赚不到钱。很多程序员日常在这里逛一下,接一点项目做。很多公司也在这发布项目需求。 + +### 4. 掘金 + +帮助开发者成长的技术社区:[juejin.cn/](https://juejin.cn/) + +这个就不用我多说了吧:现在国内优质的开发者交流学习社区,可以去看大佬们写的文章,也可以自己分享学习心的,与更多开发者交流。认识更多的小伙伴儿,提升个人影响力。 + +### 5. v2ex + +[www.v2ex.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.v2ex.com%2F) + +V2EX 是一个关于分享和探索的地方,上面有很多各大公司的员工,程序员。你想要的应有尽有。 + +### 6.电鸭社区 + +[eleduck.com/](https://link.juejin.cn?target=https%3A%2F%2Feleduck.com%2F) + +最近有朋友想找远程办公的岗位,电鸭社区值得好好看一看,可以说是国内远程办公做的相当好的社区了。 + +### 7. Medium + +[medium.com/](https://link.juejin.cn?target=https%3A%2F%2Fmedium.com%2F) + +国外优质文章网站,Medium 的整体结构非常简单,容易让用户沉下心来专注于阅读。上面有很多高质量的技术文章,有很多厉害的人在上面发布内容。 + +### 8. Hacker News + +[news.ycombinator.com/news](https://link.juejin.cn?target=https%3A%2F%2Fnews.ycombinator.com%2Fnews) + +国外优质文章网站,上面有很多高质量的技术文章,有很多厉害的人在上面分享内容。 + +### 9. GeeksforGeeks + +[www.geeksforgeeks.org/](https://link.juejin.cn?target=https%3A%2F%2Fwww.geeksforgeeks.org%2F) + +GeeksforGeeks is a computer science portal for geeks。 + +### 10.飞援 + +[www.freetalen.com/](https://link.juejin.cn?target=https%3A%2F%2Fwww.freetalen.com%2F) + +是一个为程序员、产品经理、设计提供外包兼职和企业雇佣的兼职平台,致力于提供品质可控、体验卓越的专业技术人才灵活雇佣服务。 + +# 忠告 + +**在保证主业工作稳定之后,再搞副业,再去接单。** + +再次友情提醒:还是踏踏实实上班吧! + diff --git a/docs/zsxq/article/site-hack.md b/docs/zsxq/article/site-hack.md new file mode 100644 index 0000000..da92374 --- /dev/null +++ b/docs/zsxq/article/site-hack.md @@ -0,0 +1,84 @@ +你好,我是大彬。 + +昨天收到腾讯云的短信,一开始还以为是营销短信,仔细一看,好家伙,存在**恶意文件**等**安全风险**,看来是被入侵了。。 + +![](http://img.topjavaer.cn/img/202312290909237.png) + +吓得我赶紧上小破站看看,果然,502了。。 + +![](http://img.topjavaer.cn/img/202312290849848.png) + +登录腾讯云控制台,查看监控数据,发现服务器CPU资源持续**100**%。。 + +![](http://img.topjavaer.cn/img/202312290849903.png) + +作为优质八股文选手,这个难不倒我,上来就是**top**命令伺候。 + +![](http://img.topjavaer.cn/img/202312290751593.png) + +可以看到,一个叫 kswapd0 的进程跑的死死的。 + +那么这个 kswapd0 是啥?pid是 27343,通过命令 `ll /proc/27343`,可以看这个进程执行了 一个 `/root/.configrc5/a/kswapd0`的脚本 ,应该就是病毒程序。 + +![](http://img.topjavaer.cn/img/202312290819646.png) + +紧接着查看下这个进程是否有对外连接端口,`netstat -anltp|grep kswapd0`,显示IP是 `179.43.139.84`,是一个瑞士的IP地址。。 + +![](http://img.topjavaer.cn/img/202312290803351.png) + +分析`/var/log/secure` 日志,查找详细的入侵痕迹 `grep 'Accepted' /var/log/secure` + +![](http://img.topjavaer.cn/img/202312291022633.png) + +可以看到,有一个来自加拿大的IP地址 34.215.138.2 成功登录了,通过 publickey 密钥登录(可能是服务器上的恶意程序向.authorized_keys 写入了公钥),看来是被入侵无疑了。 + +然后习惯性看了下定时任务,`crontab -e`,好家伙,定时任务里面也有惊喜,入侵者还留下了几个定时任务。。 + +![](http://img.topjavaer.cn/img/202312290823467.png) + +## 处理措施 + +1、在腾讯云**安全组**限制了 SSH 的登录IP, 之前的安全组 SSH 是放行所有IP。 + +2、将 SSH ROOT 密码修改。 + +![](http://img.topjavaer.cn/img/202312290917431.png) + +3、将`/root/.ssh/authorized_keys`备份,然后删除。 + +4、直接杀掉27343进程,`kill -9 27343` + +5、**清理定时任务**: `crontab -e`(这个命令会清理所有的定时任务,慎用) + +6、删除/root/ 目录下的.configrc5文件夹:`rm -rf /root/.configrc5/` + + + +一番操作之后,CPU就降下来了,重启服务后小破站便恢复正常访问了。 + +![](http://img.topjavaer.cn/img/202312290840463.png) + + + +## 本次服务器被入侵的一些启示 + +1、用好云厂家的**安全组**。对一些关键端口,放行规则尽量最小 + +2、**封闭不使用的端口**,做到用一个开一个(通过防火墙和安全组策略) + +3、服务器相关的一些**密码**尽量增加**复杂性** + +4、检查开机启动 和 crontab 相关的内容 + +5、检查**异常**进程 + + + +以上就是这次服务器被入侵的排查处理过程和一些启示,希望大家以后没有机会用到~ + + + + + + + diff --git a/docs/zsxq/article/sql-optimize.md b/docs/zsxq/article/sql-optimize.md new file mode 100644 index 0000000..952c970 --- /dev/null +++ b/docs/zsxq/article/sql-optimize.md @@ -0,0 +1,196 @@ +在应用开发的早期,数据量少,开发人员开发功能时更重视功能上的实现,随着生产数据的增长,很多SQL语句开始暴露出性能问题,对生产的影响也越来越大,有时可能这些有问题的SQL就是整个系统性能的瓶颈。 + +## SQL优化一般步骤 + +#### 1、通过慢查日志等定位那些执行效率较低的SQL语句 + +#### 2、explain 分析SQL的执行计划 + +需要重点关注type、rows、filtered、extra。 + +type由上至下,效率越来越高 + +- ALL 全表扫描 +- index 索引全扫描 +- range 索引范围扫描,常用语<,<=,>=,between,in等操作 +- ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中 +- eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询 +- const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询 +- null MySQL不访问任何表或索引,直接返回结果 虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为"select * from t where a = 1 and b in (1, 2) order by c";如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2 + +Extra + +- Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。 +- Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化 +- Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。 +- Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。 + +#### 3、show profile 分析 + +了解SQL执行的线程的状态及消耗的时间。默认是关闭的,开启语句“set profiling = 1;” + +``` +SHOW PROFILES ; +SHOW PROFILE FOR QUERY #{id}; +``` + +#### 4、trace + +trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。 + +``` +set optimizer_trace="enabled=on"; +set optimizer_trace_max_mem_size=1000000; +select * from information_schema.optimizer_trace; +``` + +#### 5、确定问题并采用相应的措施 + +- 优化索引 +- 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤 +- 改用其他实现方式:ES、数仓等 +- 数据碎片处理 + +## 场景分析 + +#### 案例1、最左匹配 + +索引 + +``` +KEY `idx_shopid_orderno` (`shop_id`,`order_no`) +``` + +SQL语句 + +``` +select * from _t where orderno='' +``` + +查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(`shop_id`,`order_no`)调换前后顺序。 + +#### 案例2、隐式转换 + +索引 + +``` +KEY `idx_mobile` (`mobile`) +``` + +SQL语句 + +``` +select * from _user where mobile=12345678901 +``` + +隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。 + +#### 案例3、大分页 + +索引 + +``` +KEY `idx_a_b_c` (`a`, `b`, `c`) +``` + +SQL语句 + +``` +select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10; +``` + +对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式, 一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行。 + +另一种是采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下 + +``` +select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id; +``` + +#### 案例4、in + order by + +索引 + +``` +KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and order_status in (1, 2, 3) order by created_at desc limit 10 +``` + +in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。 + +因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。 + +处理方式,可以(`order_status`, `created_at`)互换前后顺序,并且调整SQL为延迟关联。 + +#### 案例5、范围查询阻断,后续字段不能走索引 + +索引 + +``` +KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10 +``` + +范围查询还有“IN、between” + +#### 案例6、不等于、不包含不能用到索引的快速搜索。(可以用到ICP) + +``` +select * from _order where shop_id=1 and order_status not in (1,2) +select * from _order where shop_id=1 and order_status != 1 +``` + +在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等 + +#### 案例7、优化器选择不使用索引的情况 + +如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。 + +``` +select * from _order where order_status = 1 +``` + +查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。 + +#### 案例8、复杂查询 + +``` +select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01'; +select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10; +``` + +如果是统计某些数据,可能改用数仓进行解决; + +如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决。 + +#### 案例9、asc和desc混用 + +``` +select * from _t where a=1 order by b desc, c asc +``` + +desc 和asc混用时会导致索引失效 + +#### 案例10、大数据 + +对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存。 + +那么需要注意,频繁的清理数据,会照成数据碎片,需要联系DBA进行数据碎片处理。 + +参考资料: + +- 深入浅出MySQL:数据库开发、优化与管理维护(唐汉明 / 翟振兴 / 关宝军 / 王洪权) +- MySQL技术内幕——InnoDB存储引擎(姜承尧) +- https://dev.mysql.com/doc/refman/5.7/en/explain-output.html +- https://dev.mysql.com/doc/refman/5.7/en/cost-model.html +- https://www.yuque.com/docs/share/3463148b-05e9-40ce-a551-ce93a53a2c66 \ No newline at end of file diff --git "a/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" new file mode 100644 index 0000000..27752d0 --- /dev/null +++ "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" @@ -0,0 +1,95 @@ +# 每年到年底总结工作述职的时候感觉自己好像啥都没干 + +# 背景 + +年底了,上周老板说要求大家写个述职报告(年底工作总结),然后大家开个会讲一下自己过去这一年时间都做了什么,并且互相之间都看看述职报告,对彼此的工作做个评价 + +周末我大概花了整整一天时间在那憋述职报告,算上标点符号总共就憋出来几十个字儿-_-|| + +述职报告实在是太难写了,明明这一年辛辛苦苦肝了很多事情,工作量也不比别人少,但是写的时候总感觉好像也没干多少重要的事情,写出来总觉得不是那么回事儿 + +和同事朋友交流了一下,发现 ta 们也跟我一样,全都在那现场直憋呢 + +# 为什么述职报告写的这么难受 + +那么问题来了,为什么辛辛苦苦干了一年,过程中都感觉自己挺充实的,但写述职报告的时候就这么难受,感觉写不出来多少东西呢? + +我思考了一下,总结原因大概是以下这几个: + +## 不系统 + +首先是做的事情不系统,用大白话说就是这一年干事情东一榔头西一棒的,很难把它们结合在一起来说,用互联网黑话来说就是很难形成组合拳 + +我以程序员的工作内容来举例子,比如说你的同事今年干的事情都是聚焦在性能优化这块,那么 ta 在写述职报告的时候就很好写了 + +首先是性能优化基于什么背景,性能优化做了什么调研,拆分了哪些阶段去推进的,在这个过程中做了什么技术相关的事情,做完之后对整个项目产生了哪些正面的影响 + +这是一个完整的做事流程,写出来就言之有物 + +相反的,如果你做的事情比较琐碎,做的都是到处补锅或者打杂的事情的话,那么你的述职报告就真的很不好写,因为不完整,不是从头到尾的流程,那你写出来的东西就很散,做的事情无法聚焦起来来体现出更大的价值 + +打个比方就是,你工作了一年是积累了一堆芝麻,而有的人是一直在培育一个西瓜,差不多就是这个意思 + +## 平时没记录 + +还有一个重要原因就是平时没有做好记录 + +首先我是非常反对有的公司要求员工写日报来汇报工作的,我认为这是一种非常低级的管理方式,体现了公司管理者的无能 + +不过我认为我们确实应该定期对自己的工作做一个记录,不需要非常频繁,只要在某个阶段记录好当前阶段做的几个大事情就行了 + +比如说在一个重要的项目上线之后,我们应该自己给自己写一个记录总结,写清楚这个事情的背景、阶段、重点事项、收获以及不足等等 + +注意这个记录总结是给我们自己看的,不是给老板看的,我们除了给老板打工谋生以外,我们也是自己生活的主人,所以很有必要为自己写总结 + +这些阶段性的记录总结,就为年底的述职准备好了素材,到时候需要述职的时候只需要把这些素材汇总起来润色一下即可 + +## 没有主动和 leader 沟通 + +最后一个额外的原因是我觉得平时没有和 leader 做好沟通,据我的观察,很多人在职场里面主动找 leader 沟通的时间太少了,其实这样子不太好 + +一个团队里面,leader 很难对任何事情的细枝末节都了解清楚,ta 一般只会抓重点的一两件事情 + +所以如果你某段时间感觉做事特别不得劲,或是做的事情你感觉特别散不够系统,想做点大块儿的事情,那你完全可以主动和 leader 沟通,表达你的诉求 + +退一万步讲,如果你述职报告写的很难受,这个事情其实也可以找 leader 沟通,也许 leader 会给你点启发,再不济也许可以安排你明年的工作,让你明年写述职报告的时候可以轻松一些,有东西可写 + +# 如何写述职报告 + +刚刚聊的是一些宏观的问题,具体说的是感觉述职报告没什么东西可写的原因,接下来我说一下微观的东西,也就是如何来写述职报告 + +## 做好分类 + +首先是做好分类,我还是以程序员的工作来举例,比如说你这一年大大小小做过无数事情,加过无数班,它们分别归属于什么类目呢?什么事情是业务类的,什么事情是技术优化类的,什么事情是业务方产品方发起的,什么事情是技术团队自己发起的,什么事情是外部合作的,等等 + +首先你得分好了类,你的报告才能结构清晰,就像一个人首先得筋骨强健,才能站的稳,这是身体的基础 + +做好了分类,你做的事情才能一点一点的往每个类目里面加,你也不会越写越乱 + +## 善用 star 模型 + +做好了分类,接下来具体就是每个事情该怎么写了,一般我们是使用 star 模型来写 + +star 模型是个被说烂了的写作套路,但是确实好用 + +啥是 star 模型,我再啰嗦几句,star 就是四个英文单词的缩写: + +1. situation:场景 +2. task:任务 +3. action:行动 +4. result:结果 + +看起来挺难的,其实大白话来讲它很简单 + +比如说你在述职报告上这样写:我今年做了 xx 优化 + +这句话就是很多人会常犯的错误,让人感觉话说了一半 + +用 star 模型来优化一下就是: + +1. situation:我是基于什么样的背景来做 xx 优化的,比如说我在接手项目时,发现了 xx 问题,受限于 xx 条件,我打算做 xx 优化,这部分主要是讲清楚你做事情的原因和出发点,不要让别人觉得很突兀 +2. task:我把 xx 优化拆分为几个任务,这部分是你要做的事情具体有哪些任务,比如说调研优化方案、PC 端优化、移动端优化等等 +3. action:我把每个任务里做了什么,这部分就该说细节了,不管是什么方案、技术工具、优化细节等等,都可以在这里面说 +4. result:最终达到了什么样的效果,这部分是总结做的事情达到的效果,主要是对结果做一个说明,这里需要强调的是有点事情可以定量来说,比如说某某指标提升了 xx%,但是有的事情不太好定量,那就只能定性,比如说用户反馈评价等等,之前我见过某些业务方给技术团队送锦旗的 + +这篇笔记要写的就这么多,最后祝大家年底都能拿个好绩效,开开心心拿满年终奖过年😸 \ No newline at end of file diff --git a/docs/zsxq/inner-material.md b/docs/zsxq/inner-material.md new file mode 100644 index 0000000..613a61e --- /dev/null +++ b/docs/zsxq/inner-material.md @@ -0,0 +1,25 @@ +**JVM重要知识点&高频面试题**是我的**[知识星球](https://topjavaer.cn/zsxq/introduce.html)内部专属资料**,已经整理到Java面试手册完整版。除了Java面试手册完整版之外,星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +![](http://img.topjavaer.cn/img/image-20221229145413500.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20221229145550185.png) + +![](http://img.topjavaer.cn/img/image-20221229145649490.png) + +另外星球还提供**简历指导、修改服务**,大彬已经帮**90**+个小伙伴修改了简历,相对还是比较有经验的。 + +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会优先解答球友的问题。 + +![](http://img.topjavaer.cn/img/达到什么水平找实习.png) + +![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) + +[知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md new file mode 100644 index 0000000..a72a60d --- /dev/null +++ b/docs/zsxq/introduce.md @@ -0,0 +1,214 @@ +--- +sidebar: heading +--- + +不知不觉,我已经工作了四年了。回想起当初**自学转码**的经历,独自一个人“战斗”,**走了很多弯路**。 + +半年前,我决定创建【**大彬的学习圈**】这个知识星球,目的就是帮助那些**自学编程、想要拿到更好offer、想拥有一个良好的学习环境、报团取暖**的小伙伴,在你们遇到问题的时候,可以在学习圈进行讨论交流,或者**向大彬提问**,帮助你更快得到解答。 + +## 星球介绍 + +简单介绍下什么是知识星球。 + +知识星球是个社区,在这里可以和星主进行**一对一交流**,也可以和其他球友共同探讨技术、面试、学习等问题。相比微信群,星球内部每天更新的内容更容易沉淀,管理更高效。 + +每个知识星球都有一个名字,比如【**大彬的学习圈**】,这是大彬建立的知识星球,是一个**专注学习**的圈子。我也会**借助自己的经验**,帮助星球里的小伙伴,解答一些学习、求职过程的疑惑,比如offer选择、如何自学编程等问题。 + +目前知识星球已经支持APP、网页端和小程序,可随时随地使用。 + +APP端页面如下(建议大家**使用APP**,因为APP布局更加美观,且功能支持的更多): + +![](http://img.topjavaer.cn/img/image-20230112081221285.png) + +网页端页面如下: + +![](http://img.topjavaer.cn/img/星球网页版界面.png) + +![](http://img.topjavaer.cn/img/202305180010872.png) + +目前大彬的学习圈已经积累了很多优质内容了,像**Java面试手册完整版、面试真题手册(300多家公司面试真题)、高频场景设计题目、LeetCode刷题笔记**等。 + +![](http://img.topjavaer.cn/img/202305180013526.png) + +![](http://img.topjavaer.cn/img/image-20230102151744058.png) + +![](http://img.topjavaer.cn/img/202404091744332.png) + +此外学习圈还积累了很多的**优质学习资源**,包括**计算机基础、Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源**等等,可以说非常全面了。 + +![](http://img.topjavaer.cn/img/20230116133239.png) + +在这里可以找到大部分你想要的**学习资源**,而且这里有一群和你一样志同道合的小伙伴,可以一起提升编程能力、交流学习心得、分享经验、拿更好的Offer! + +## 球友感言 + +学习圈目前已经运营一年多了,也收到了很多球友的**好评**。 + +![](http://img.topjavaer.cn/img/202310200757694.png) + +![](http://img.topjavaer.cn/img/202310200757131.png) + +也有挺多星球的小伙伴拿到了**大厂offer**! + +![](http://img.topjavaer.cn/img/202404091745315.png) + +![](http://img.topjavaer.cn/img/202310200756703.png) + +![](http://img.topjavaer.cn/img/202310200756575.png) + +![](http://img.topjavaer.cn/img/202404091745397.png) + +## 谁适合加入的学习圈 + +1. **非科班转码**或者计算机小白,没有一个完善的学习路线规划; +2. 即将参与校招、实习面试,需要一个引路人; +3. 刚工作不久,能力一般,想要**提升**自己; +4. 对未来**没有规划**,随波逐流,想要作出改变; +5. 需要一个**好的学习氛围**,一群可以互相交流、互相监督的小伙伴; +6. ... + +如果你满足以上任意一条,那么欢迎你**加入**我的学习圈,大家一起奋斗,努力达到自己的目标。 + +如果你是那种**三天打鱼两天晒网**的人,那么不建议你加入,只会浪费钱,不要妄想加入之后能有什么收获。 + +如果你加入了,希望你也能跟像球友们一样**每天坚持打卡学习,为未来奋斗**~ + +![](http://img.topjavaer.cn/img/202412271108286.png) + +## 学习圈能提供什么? + +学习圈能给你带来的帮助有: + +### 1、最新的面试手册(星球专属)+面试真题手册 + +**精心整理的面试手册最新版**。目前已经更新迭代了**19**个版本,持续在更新中,**面试手册完整版**是星球球友专享,**不会对外**提供下载。 + +![](http://img.topjavaer.cn/img/image-20230102132236357.png) + +![](http://img.topjavaer.cn/img/202305180033154.png) + +![](http://img.topjavaer.cn/img/202404091745512.png) + +![](http://img.topjavaer.cn/img/202404091745321.png) + +面试手册最新版本增加补充了**微服务、分布式、系统设计、场景题目**等高频面试题,同样也是星球球友**专享**的。 + +这份面试手册已经**帮助好多位读者拿到offer**了,其中也有拿了字节、平安等大厂offer的。 + +![](http://img.topjavaer.cn/img/星球面试手册1.png) + +也有不少读者把面试手册**打印**出来了,也能看出质量之高! + +![](http://img.topjavaer.cn/img/星球面试手册打印.png) + +甚至还被某些人拿到网上去卖钱。。 + +![](http://img.topjavaer.cn/img/image-20221229144542099.png) + +### 2、提问答疑 + +**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、技术问题、面试问题、岗位选择等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230319155647933.png) + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![](http://img.topjavaer.cn/img/20230331083521.png) + +学习圈里有**圈友提问**的**汇总贴**,很多现在困扰你的问题,学习圈里都有相应的提问案例,可以做个参考。 + +![](http://img.topjavaer.cn/img/image-20230111232910870.png) + +![](http://img.topjavaer.cn/img/image-20230111233038376.png) + +![](http://img.topjavaer.cn/img/20230331083818.png) + +我会尽自己**最大的努力**去帮助圈子里的小伙伴**解答**问题,比如如何更好的去回答一些面试题、岗位选择问题、学习规划等。 + +![](http://img.topjavaer.cn/img/描述能体现自己编程能力的代码.png) + +![](http://img.topjavaer.cn/img/202305180036559.png) + +![](http://img.topjavaer.cn/img/回答问题.png) + +![](http://img.topjavaer.cn/img/回答问题1.png) + +### 3、简历指导 + +**简历指导、修改服务**,大彬已经帮**180**+个小伙伴修改了简历,还是比较有经验的。 + +![](http://img.topjavaer.cn/img/image-20230111224933180.png) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +![](http://img.topjavaer.cn/img/image-20230111224052702.png) + +![](http://img.topjavaer.cn/img/image-20230111224829883.png) + +![](http://img.topjavaer.cn/img/image-20230111224753836.png) + +### 4、经验分享 + +独家经验分享。 + +![](http://img.topjavaer.cn/img/20230331084406.png) + +![](http://img.topjavaer.cn/img/20230331084453.png) + +![](http://img.topjavaer.cn/img/20230331084324.png) + +### 5、优质编程资源 + +**分享优质编程资源**,包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +学习圈有个**置顶的学习资源汇总贴**,包含了近**1000G**的学习资料,是大彬进入互联网行业几年来的积累,从**计算机基础到高阶架构资料**,基本每个阶段都有配套的资料,而且这些资料都是大彬**精心筛选**过的,都是比较优质的资源,可以帮你省去不少搜索的时间! + +![](http://img.topjavaer.cn/img/20230116133239.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20230111225725073.png) + +![](http://img.topjavaer.cn/img/image-20221229145649490.png) + +### 6、超棒的学习氛围 + +学习圈日常会**组织打卡、读书分享**等活动,**学习氛围**相当好! + +不知道你有没有这样经历,当你在图书馆或者自习室的时候,你会发现自己的学习动力比在家里、宿舍的强太多,**学习效率**也是大幅提升。在学习圈中,每天都有人在打卡、分享,**看到别人都在努力学习**,**每天都在进步**,相信你很难不去学习,你会被这种氛围影响,不再拖延、浪费时间,跟着大伙一起学习提升。 + +![](http://img.topjavaer.cn/img/image-20230319154612255.png) + +![](http://img.topjavaer.cn/img/image-20230319154832832.png) + +![](http://img.topjavaer.cn/img/202305180042396.png) + +很多常见的问题,很可能你的学长学姐已经遇到过了,多看看他们踩过的坑,能让你**少走一些弯路**。 + +![](http://img.topjavaer.cn/img/image-20230112000837321.png) + +![](http://img.topjavaer.cn/img/202305180040872.png) + +如果你自学能力比较差,大彬可以给你一些学习上的**指导**,监督你**学习打卡**,让你学得更加顺畅,不至于半途而废。 + +![](http://img.topjavaer.cn/img/image-20221229102631750.png) + +### 7、大厂内推机会 + +学习圈不定时会分享各个大厂(阿里、腾讯、字节、网易、京东、快手等)的**内推机会**,帮助你更快走完招聘流程、拿offer! + +![](http://img.topjavaer.cn/img/image-20221231224032046.png) + +### 8、学习圈专属福利 + +学习圈不定期会有**抽奖、送书活动**,送书活动的书籍都是精心挑选的**经典好书**,价格甚至超过星球门票! + +## 怎么进入星球? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款** + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/mianshishouce.md b/docs/zsxq/mianshishouce.md new file mode 100644 index 0000000..24d1368 --- /dev/null +++ b/docs/zsxq/mianshishouce.md @@ -0,0 +1,85 @@ +## 最新的面试手册(星球专属) + +**《面试手册》** 是大彬的[知识星球](https://topjavaer.cn/zsxq/introduce.html)的内部资料,这份资料是大彬花了半年时间**精心整理**的。目前已经更新迭代了**19**个版本,持续在更新中(**面试手册**是星球球友专享,**不会对外**提供下载)。 + +![](http://img.topjavaer.cn/img/image-20230102132236357.png) + +## 内容概览 + +![](http://img.topjavaer.cn/img/202305180033154.png) + +![](http://img.topjavaer.cn/img/image-20230102151744058.png) + +这份面试手册已经**帮助好多位读者拿到offer**了,其中也有拿了字节、平安等大厂offer的。 + +![](http://img.topjavaer.cn/img/星球面试手册1.png) + +也有不少读者把面试手册**打印**出来了,也能看出质量之高! + +![](http://img.topjavaer.cn/img/星球面试手册打印.png) + +### 面试准备篇 + +面试准备篇主要是分享如何如何准备面试,内容包括**简历编写、项目经验、算法准备、常见非技术面试题**等。 + +![](http://img.topjavaer.cn/img/202305292304666.png) + +### 技术面试题篇 + +技术面试题包括**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot**等八股文,还包括**分布式、微服务、设计模式、架构、系统设计、高并发**等**进阶**内容。 + +![](http://img.topjavaer.cn/img/202305292308036.png) + +### 面经篇 + +主要会分享一些**高质量的面经**,包括**校招、社招、实习**的都有。也有一些**offer选择、入职经历、转码经验**等分享。 + +![](http://img.topjavaer.cn/img/202305292313633.png) + +## 简历指导 + +另外星球提供了**简历指导、修改服务**,大彬已经帮**180**+个小伙伴修改了简历,还是比较有经验的。 + +![](http://img.topjavaer.cn/img/image-20230111224933180.png) + +![](http://img.topjavaer.cn/img/简历修改1.png) + +## 提问答疑 + +提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、技术问题、面试问题、岗位选择等等。大彬会**优先解答**球友的问题。 + +![](http://img.topjavaer.cn/img/image-20230319155647933.png) + +![](http://img.topjavaer.cn/img/image-20230318103729439.png) + +![](http://img.topjavaer.cn/img/20230331083521.png) + +学习圈里有**圈友提问**的**汇总贴**,很多现在困扰你的问题,学习圈里都有相应的提问案例,可以做个参考。 + +![](http://img.topjavaer.cn/img/image-20230111232910870.png) + +![](http://img.topjavaer.cn/img/image-20230111233038376.png) + +## 优质编程资源 + +**分享优质编程资源**,包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 + +学习圈有个**置顶的学习资源汇总贴**,包含了近**1000G**的学习资料,是大彬进入互联网行业几年来的积累,从**计算机基础到高阶架构资料**,基本每个阶段都有配套的资料,而且这些资料都是大彬**精心筛选**过的,都是比较优质的资源,可以帮你省去不少搜索的时间! + +![](http://img.topjavaer.cn/img/20230116133239.png) + +![](http://img.topjavaer.cn/img/image-20221229145455706.png) + +![](http://img.topjavaer.cn/img/image-20230111225725073.png) + +![](http://img.topjavaer.cn/img/image-20221229145649490.png) + +## 怎么进入星球? + +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 + +PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/2-years-tech-no-upgrade.md b/docs/zsxq/question/2-years-tech-no-upgrade.md new file mode 100644 index 0000000..b4eccf3 --- /dev/null +++ b/docs/zsxq/question/2-years-tech-no-upgrade.md @@ -0,0 +1,61 @@ +--- +sidebar: heading +title: 工作两年多,技术水平没有很大提升,该怎么办? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,程序员,技术提升 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作两年多,技术水平没有很大提升,该怎么办? + +最近在大彬的[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了问题:**工作两年多,技术水平没有很大提升,该怎么办?** + +**原问题如下**: + +大彬大佬能不能给点学习建议,我是**非计算机专业**,**培训**的java后端,现在开发工作两年多了,越是工作其实就越会发现自己的**知识面很窄**,没办法提升技术水平,所以想要自己学习提升下,但是**没啥方向**,不知道该先学什么,所以想请大佬指点下,谢谢! + +--- + +**大彬的回答**: + +最简单的一个方法就是找一个比你现在公司**技术方面强一些**的公司,到他们招聘网站看看**岗位职责描述**(1-3年工作经验的Java开发),对比下自己缺少哪些技能,**查漏补缺**。以跳槽到更好的公司为目标进行学习,这样既有动力也有学习方向。 + +> 附上阿里菜鸟1-3年的JD: +> +> 1. 扎实的编程基础,精通java开发语言,熟悉jvm,web开发、缓存,分布式架构、消息中间件等核心技术; +> 2. 掌握多线程编码及性能调优,有丰富的高并发、高性能系统、幂等设计和开发经验; +> 3. 精通Java EE相关的主流开源框架,能了解到它的原理和机制,如SpringBoot、Spring、Mybatis等; +> 4. 熟悉Oracle、MySql等数据库技术,对sql优化有一定的经验; +> 5. 思路清晰,良好的沟通能力与技术学习能力; +> 6. 有大型网站构建经验优先考虑; + +如果你在一个小公司或外包公司的话,一般一到两年时间就把用到的技术栈基本都摸透了,因为业务量不大,很难接触到像**高并发、分布式、灾备、异地多活、分片**等,每天都是重复的增删改查,**很难有技术沉淀**。 + +工作久了之后,你就会发现,到职业中后期,**公司的技术上限也是你的技术上限**,单靠自己盲目去学,缺少实践机会,技术上也很难精进。只有去更大的平台,你能接触到的业务场景、技术就会更多,技术能力也就能随着慢慢变强了。 + +--- + +最后,推荐大家加入我的[知识星球](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有200多位小伙伴加入了,星球已经更新了多篇**高质量文章、优质资源、经验分享**,利用好的话价值是**远超**门票的。 + + + +星球提供以下这些**服务**: + +1. 星球内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享、面试资料**,让你少走一些弯路 +2. 四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 +3. **一对一答疑**,我会尽自己最大努力为你答疑解惑 +4. **免费的简历修改、面试指导服务**,绝对赚回门票 +5. **中大厂内推**,助你更快走完流程、拿到offer +6. 各个阶段的**优质学习资源**(新手小白到架构师),包括一些大彬自己花钱买的课程,都分享到星球了,超值 +7. 打卡学习、读书分享活动,**大学自习室的氛围**,一起蜕变成长 + +**加入方式**:**扫描二维码**领取优惠券即可加入~ + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/3-years-confusion.md b/docs/zsxq/question/3-years-confusion.md new file mode 100644 index 0000000..1888eda --- /dev/null +++ b/docs/zsxq/question/3-years-confusion.md @@ -0,0 +1,93 @@ +--- +sidebar: heading +title: 工作三年半,有点迷茫 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业发展,迷茫 + - - meta + - name: description + content: 星球问题摘录 +--- + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于职业规划的和自学方面的问题,挺有有代表性的,跟大家分享一下。 + +**圈友提问**: + +本人是19年毕业至今工作了**3年半**左右,一直在同一个岗位上,近段时间因为公司项目组资金不足被调岗了,新岗位和自己的技术栈**不太匹配**,觉得如果一直变动会影响自己的**职业发展**。 + +这么些年来一直有点**混吃等死**,想在现在开始寻求改变,希望通过学习,让自己有所变化。不过对于学习方式以及学习路线很是**迷茫**,希望大彬老师可以帮忙解惑,非常感谢。 + +**目前掌握的技术栈**: + +有一定的Java基础,对JVM原理,体系结构,垃圾回收机制等有部分了解 + +熟悉Scala&Spark开发, 了解部署spark server项目及脚本 + +掌握spring, springCloud微服务架构开发, + +了解Elastic search,Mule gateway, Feign等的使用 + +熟悉Jenkins, Kubernates, PCF, G3等devops部署及调试 + +熟悉AppDynamic, patrol, LWM等程序监控平台的使用及配置 + +了解NexusIQ,Checkmarx,sonar等代码质量监控平台的使用及调优 + +熟悉sqlServer及Mongodb数据库 + +--- + +**大彬的回答**: + +我一直都是认为,在现在这个就业环境下,“专才”的竞争力是要大于“全才”的,**专注一个方向**,对你的职业发展更为有利。 + +从你的技术栈来看,相对还是偏“杂”一些,Java、大数据、devops等都有涉及。而且现在还有调岗的可能,我建议可以准备跳槽,**跳出舒适圈**。 + +至于怎么去学习,我建议你到招聘网站看看Java开发3年经验(Java高级开发)都是什么要求,**面向面试学习**,这样学习效果比较好。 + +比如阿里巴巴和OPPO 3 年左右工作经验的JD,我整理了一下,大概有这些点: + +1、JAVA**基础扎实**,理解io、多线程、集合等基础框架,了解JVM原理;(基础必须要掌握好) + +2、熟悉分布式系统的设计和应用,熟悉**高并发、分布式**、缓存、消息等机制;能对分布式常用技术进行合理应用,解决问题;(高并发、分布式) + +3、对用过的开源框架,能了解到它的**原理**和机制(框架源码) + +4、**性能调优**,解决疑难问题的能力;(平时要注意积累这种能力) + +基本就是这几个要求,对着JD看看自己哪一块薄弱,平时抽空针对性进行查漏补缺,像高并发分布式这种,可以结合工作项目业务场景去思考。平时有遇到性能调优方面的问题(不一定是自己遇到的,也可以是其他人处理的问题,你可以**主动参与**进去,了解怎么去处理),也要记得**复盘总结**,这些都是宝贵的经验,面试能派上用场。 + + + +关于**项目经验**,如果这块有疑问,可以参考下面两篇文章: + +你在项目里遇到的最大困难是什么?https://t.zsxq.com/09KAo4zZU + +项目经验怎么回答?https://t.zsxq.com/09KejfE2I + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有100多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入(**即将恢复原价**)。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + +![](http://img.topjavaer.cn/img/202412271108286.png) + diff --git "a/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" new file mode 100644 index 0000000..ab02966 --- /dev/null +++ "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" @@ -0,0 +1,31 @@ +最近[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于性格测试的疑问: + +**球友提问**: + +大彬大佬,请教一个概念和具体应用上的问题, VO, BO, PO, DO, DTO 这些概念和具体应用是怎样的? + +--- + +**大彬的回答**: + +你好,这个问题要结合实际业务来讲更好理解。 + +比如现在有一个用户登录的业务。 有一张表user存用户数据,这个表里面有 id ,name ,password。首先说说PO,PO比较好理解,就是数据库中的记录,一个PO的数据结构对应着表的结构,表中的一条记录就是一个PO对象。 + +再来看看什么是DO。比如现在需要实现登录功能,那就只需要检查用户输入的 name 和 password 和数据库是否一致就可以了。 这种情况下,就用一个 User 对象来表示这个领域模型就可以了。我一般用 Domain Object ,也就是 DO 表示,对应下来就是 UserDO ( 统一用 Do 也可以) + + + +再假设现在有一个展示用户信息的业务,假想给一个客服(不期望把用户密码给他看到的那种情况) + +一般来说有两种做法: + +第一种,你可以在序列化为 json 字符串的时候,隐藏 password 的序列化来实现; + +第二种,你可以新建一个 VO ( View Object )对象,就叫 UserVO ,然后这里面就只有 id 和 name 两个属性即可; + + + +再假设,现在的场景不是直接展示到页面上,而是被一个业务系统调用,这个系统需要依赖登录的服务。 然后需要提供 sdk 给这个业务系统集成,那么这个时候就可以声明一个 DTO 对象,里面也只有 id 和 name 属性即可。 + +当然实际业务肯定不可能说只有 id ,name ,password 这么简单,这里只是举个例子方便理解。 \ No newline at end of file diff --git a/docs/zsxq/question/familiarize-new-project-qucikly.md b/docs/zsxq/question/familiarize-new-project-qucikly.md new file mode 100644 index 0000000..83e9c61 --- /dev/null +++ b/docs/zsxq/question/familiarize-new-project-qucikly.md @@ -0,0 +1,54 @@ +# 如何快速熟悉一个新项目 + +很多人刚进入一家新公司后,最头疼的就是如何快速了解公司的业务和项目架构。 + +一方面是文档很少或者没有文档, 只能自己硬着头皮摸索;另一方面大家都很忙,很少有人会帮你梳理业务逻辑。如果你碰到一个特别热心的老员工,随时在你身边答疑解惑,那你的运气实在是太好了, 现实是大家都很忙,没人给你讲解。 + +领导只给你几天时间熟悉,接着就要深入项目做开发了,怎么办呢? + +接下来分享我总结的一些经验。 + +## 从页面到数据库 + +对某个具体项目的了解,一定要建立在对整体了解的基础上。首先可以给项目画出一条线,并标明每一个节点的信息,就像这样:页面访问路径--前端项目--后台服务--数据库地址(也可以通过流程图的形式)。 + +这个整理的过程,主要是让自己梳理清楚前端项目分别调用了哪些后台服务,通过后台服务和数据库的名称,我们能大致了解到这条业务线提供了什么功能,从前端项目和页面路径,我们能了解到我们需要给用户展示什么。 + +这个阶段不需要花费太多时间,重点就是仅仅是了解这条业务线的整体内容。 + +## 整理数据库表 + +上面都是整理项目的大体框架,还没有涉及到具体的项目细节。 + +一般业务项目无非就是对数据库的增删改查操作而已,或者从使用者的角度看,一个项目就是输入一些参数得到一些返回结果。 + +接下来要做的就是整理**数据库表**了。 + +这里首先要选择一个核心项目去看,众多项目中一定有一个是核心项目,先从核心项目开始看起。 + +如果数据库的表比较少,直接一个个看就行了。但如果数据库表特别多,那就需要先筛选出哪些是核心的表了。 + +如何判断哪些是核心表呢?最快的方式就是找老员工问一下,一般核心表不会很多。有些表可以先忽略不看,比如copy结(备份),rel结尾的(中间关联表),statistics结尾的(数据统计表),log结尾(日志表),config结尾(配置表),等等。 + +到此,你就对整体的数据库结构有所了解了。根据表名也能对表的大致内容有所了解,接下来就是针对具体的表,看里面具体的字段和前人给出的备注,**这个过程就没有技巧了,要耐心,要慢慢熬**。 + +## 深入代码层 + +当你对数据库表有了大体的了解后,你基本上对这个系统能提供什么服务了解到差不多了。接下来就是深入代码层理解业务逻辑了。 + +一般业务相关的项目代码分三个部分: + +1. 通过前端交互(Controller层)对数据库进行增删改查操作 +2. 通过定时任务对数据库进行增删改查操作 +3. 调用或通知其他服务做一些事情 + +这三种类型的代码研究清楚后,对于一个业务型的项目来说,已经基本足够了。 + +另外,在研究具体业务代码的同时,要不断地跳出来回顾整条业务线的框架,修正之前由于不了解具体业务而出现的理解偏差。 + +## 从小需求开始,尝试编码 + +通过做需求,带着具体问题去看,踩的坑多了,解的bug多了,自然而然就会更熟悉这个项目了。 + + + diff --git a/docs/zsxq/question/frontend-or-backend.md b/docs/zsxq/question/frontend-or-backend.md new file mode 100644 index 0000000..35e6f0c --- /dev/null +++ b/docs/zsxq/question/frontend-or-backend.md @@ -0,0 +1,34 @@ +昨天在大彬的[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)中,有小伙伴提了一个关于方向选择的问题,可能挺多人有这样的困惑,跟大家分享一下。 + +**圈友提问**: + +先介绍一下个人背景: + +学历: **中流 985 硕**,偏门工科方向。 + +基础: 大一学过 C 语言后一直都有零碎地写一写代码,但是系统性地计算机学习不是很多。接触时间长一点点的应该是 C++,但学的不够系统。 从大一 C 语言课入手,后续写过 C++/Python,Python拿来给实验室写过小工具(类似上位机)。 + +**转行**的想法也很简单,因为自己是一个偏冷门一点的工科方向,跟 EE 沾点边,但是根本喝不上芯片大热门的一口汤。去年师兄找工作 offer 基本上动态清零了,大一点的企业投完了就是石沉大海,小一点的公司才会给面试机会,否则只能去各种小研究所。师姐们更惨,一个拿了国奖的师姐投了一圈一个 offer 没有。 + +现在给自己定的目标是二线中小厂。我的问题是:**转码的话,选择前端还是后端更合适**? + + +----- + +**大彬的回答**: + +我说一下我对前端和后端的理解: + +前端开发是创建Web页面或APP等前端界面呈现给用户的过程。除了传统的 Web 前端开发之外,目前 Android 开发、iOS 开发以及第三方开发(各大平台的小程序等)都逐渐并入到了前端开发团队。而且随着 Nodejs 的应用,目前前端开发后端化也是一个比较明显的趋势(大前端)。 +对于非科班同学,前端的**入门难度**比后端低一些,对计算机基础(数据结构&算法)的要求没有那么高。能够通过系统的学习,在**较短的时间**内掌握基本技能。当然,说前端比后端入门难度低,并不是说前端的知识比后端少,相反,前端的领域知识可能比后端还多,**技术更新**也更快。 +我的建议就是:如果你对审美和交互本身就感兴趣,想做出那种让别人看到的东西,比较追求用户体验的话,那么推荐你选择前端。 +另外,从**就业**的角度来说,前端开发相比后端开发,应该会更好找工作一些。 + +后端开发,比较关注业务和技术融合一起的技术整合能力,比较要从业务处理生命周期来设计接口,选择合适的中间件,合理运用各种技术来应对性能,并发,数据各方面的问题。相对前端更多是**业务的技术理解和解决方案能力**。 +后端开发对于程序员**计算机基础**有一定的要求,包括操作系统、算法设计、数据结构、数据库等,这些基础性的内容决定了后端程序员的开发能力和上升空间。所以,如果想在技术领域走得更远,可以重点考虑一下后端开发岗位。后端更加接近**业务**,对业务的理解更为深刻,相比前端来说,**晋升空间**更大一些。 + +---- + +最后,给大家送福利啦,限时发放10张[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)60元的优惠券,先到先得!目前[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)已经有**300**多位成员了,想加入的小伙伴不要错过这一波优惠活动,**扫描下方二维码**领取优惠券即可加入。 + +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/how-to-learn.md b/docs/zsxq/question/how-to-learn.md new file mode 100644 index 0000000..32c34de --- /dev/null +++ b/docs/zsxq/question/how-to-learn.md @@ -0,0 +1,15 @@ +**读者提问** + +彬哥好,请问下平时学技术应该通过什么去学呢?看书还是看视频?网上那些培训课怎么样,值不值得买呢? + +**大彬回答** + +你好!我觉得对大部分人来说,看视频学习的方式比看书学习的效果更好些,比较易于理解和学习。我建议是以视频为主、书籍为辅的方式进行学习。在看视频过程中有不懂的,可以看下相关的书籍。 + +对于视频资源,要选择一个比较完整的视频,可以到B站找几个播放量比较高的视频,对比下评论区和视频章节,选择一个比较适合自己的进行学习。 + +(Java方向的学习路线可以参考星球的这个帖子:https://t.zsxq.com/0ehuyAx9m) + +第三个问题,网上那些培训课怎么样,值不值得买呢? + +基础课程的话,到B站找资源就行,免费资源太多了,没必要花钱买课。如果想进一步深入学习的话,可以看看极客时间的一些课程,相对其他培训机构的课程质量会高一些。另外在学习圈置顶帖(https://articles.zsxq.com/id_5x8zu2718ih5.html)有整理一些付费课程,可以看看是否满足你的需求,当然如果经济能力允许的话,还是建议支持下正版~ \ No newline at end of file diff --git a/docs/zsxq/question/how-to-prepare-job-hopping.md b/docs/zsxq/question/how-to-prepare-job-hopping.md new file mode 100644 index 0000000..59aee75 --- /dev/null +++ b/docs/zsxq/question/how-to-prepare-job-hopping.md @@ -0,0 +1,88 @@ +--- +sidebar: heading +title: 工作一年想要跳槽,不知道应该怎么准备? +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,跳槽怎么准备,程序员跳槽 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 工作一年想要跳槽,不知道应该怎么准备? + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴问我怎么准备跳槽、面试,在这里跟大家分享一下。 + +**圈友提问**: + +先说一下自己**目前的情况**: + +大四,秋招进了一家公司(珠海的一家公司),过年后去公司实习。 + +自己的技能情况:我通过看视频的方式学到了springboot ,中间件有redis,elasticsearch,zookeeper,double……,springcloud只看了一点资料,基本等于没学。 + +找到工作之后就处于摆烂状态,所以知识点我感觉忘记的差不多了,就连基本的spring注解都有些忘了。之前学习的中间件因为没怎么使用加没有复习也跟没学差不多,但是印象还是有。 + +我想在明年的秋招跳槽,但是我不知道应该怎么准备,求彬哥指点一二。 + +最后祝彬哥新年快乐,万事如意! + +--- + +**大彬的回答**: + +师弟新年好~ + +你现在大四,明年跳槽的话,只能参加**社招**了,社招会更加注重你的**实战能力**,所以在项目方面需要花一些功夫。 如果你可以联系上你现在的导师或者Leader的话,可以先问下项目组主要的技术栈是哪些,自己提前自学一下。进入公司实习或者正式工作的时候,多花时间去梳理你负责的项目的**项目架构、业务逻辑、设计上有哪些亮点**等,这些都是面试官会问的,要提前准备好。 + +很多时候面试官会结合你的项目去考察你对知识点的掌握情况,所以还是要把项目“**吃透**”。 + +另外,算法题和八股文也要准备好,如果想进中大厂的话,建议力扣刷个300道左右(高频题),按标签去刷,做好总结。八股文可以看看**[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)内部的面试手册**(在置顶帖),这本面试手册是我花了挺长时间整理的,应该比较全面了。 + + + +总结一下,就是**项目**+**算法**(中大厂会考察,小厂考察的比较少)+**八股文** + + + +最后,附上一年工作经验需要掌握的技能(可以参考一下): + +1、Java基础扎实,理解IQ、线程、集合等基础框架,对JVM原理有一定的了解; + +2、熟悉使用MySQL或者Oracle等关系型数据库; + +3、熟悉使用主流框架(SpringBoot、Mybatis、Springmvc、Zookeeper、Dubbo等); + +4、熟悉Redis缓存、ElasticSearch等开发经验; + +5、具备定位解决生产环境问题的能力; + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/zsxq/question/java-or-bigdata.md b/docs/zsxq/question/java-or-bigdata.md new file mode 100644 index 0000000..cf98ec1 --- /dev/null +++ b/docs/zsxq/question/java-or-bigdata.md @@ -0,0 +1,62 @@ +--- +sidebar: heading +title: 24届校招,Java开发和大数据开发怎么选 +category: 分享 +tag: + - 星球 +head: + - - meta + - name: keywords + content: 职业规划,岗位选择,Java还是大数据 + - - meta + - name: description + content: 星球问题摘录 +--- + +## 24届校招,Java开发和大数据开发怎么选 + +最近在大彬的[学习圈](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d#rd)中,有小伙伴提了一个关于方向选择的问题:**24届校招,Java开发和大数据开发怎么选**? + +**原问题如下**: + +想请教一下大彬,对于**java开发和大数据开发**的明年秋招情况的预测。java岗位多卷度也高,每家公司基本都有相关的职位,大数据开发的话培训班比岗位多,只有大公司和数据公司会有这类岗位。24年秋招的话**选择哪个方向**会比较合适呢? + +--- + +**大彬的回答**: + +建议选大数据吧。就这几年校招来看,大数据岗位拿offer的**难度**相比Java还是比较小一些的。而且距离24年秋招还有一年多时间,转大数据完全来得及,而且有了Java基础,再来学大数据,应该会比较快入门。 + +再说下大数据和后端的**差异**。大数据门槛比Java高,除了熟悉数据库的操作之外,还要学习大数据整个生态,需要会分布式、数仓、数据分析统计等知识。因为大数据的学习门槛比 Java 高,所以市场上培训大数据的相比Java会少一些,**竞争也相对小**,没有Java那么卷。 + +另外,大数据**薪资**总体会比Java开发高一些(同一家公司同一级别,普通开发岗比大数据开发薪资会少一点),这也是大数据方向的一个优势。 + +不过呢,小点的公司,可能没有大数据的需求,毕竟业务量不大(小公司通常也不建议去,坑多)。 + +综上所述,还是建议你选择**大数据**方向。 + + + +--- + +最后,推荐大家加入我的[学习圈](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247492252&idx=1&sn=8fc12e97763e3b994b0dd0e717a4b674&chksm=ce9b1fdaf9ec96cca6c03cb6e7b61156d3226dbb587f81cea27b71be6671b81b537c9b7e9b2d&scene=21#wechat_redirect),目前已经有140多位小伙伴加入了,文末有50元的**优惠券**,**扫描文末二维码**领取优惠券加入。 + +学习圈提供以下这些**服务**: + +1、学习圈内部**知识图谱**,汇总了**优质资源、面试高频问题、大厂面经、踩坑分享**,让你少走一些弯路 + +2、四个**优质专栏**、Java**面试手册完整版**(包含场景设计、系统设计、分布式、微服务等),持续更新 + +3、**一对一答疑**,我会尽自己最大努力为你答疑解惑 + +4、**免费的简历修改、面试指导服务**,绝对赚回门票 + +5、各个阶段的优质**学习资源**(新手小白到架构师),超值 + +6、打卡学习,**大学自习室的氛围**,一起蜕变成长 + + + +**加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ + +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git "a/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" new file mode 100644 index 0000000..debf2c5 --- /dev/null +++ "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" @@ -0,0 +1,63 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [放弃大厂去外包...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495584&idx=1&sn=55ea6cba584d0e2d0a585ccdb0f437a2&chksm=ce9b12e6f9ec9bf02105572f7a4d432edf8c5c635fc876001d6590cd2b67e321932dbc0d9a20&token=1557067692&lang=zh_CN#rd) +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,您好,有个问题咨询您,offer选择,都是 Java 开发岗。 + +**小红书**,**直播技术**,北京,试用期后定级, base+大小周双倍加班费+1.5km 房补,年包差不多50w。作息 10:30~21:30 ,大小周。argue 了一下 base 、签字费和期权,可能会有一点点上涨空间。 + +**淘天**,**交易技术**,杭州,p4, 薪资组成( base+车补和两年的无距离限制房补+两年杭州市的补贴+签字费),算补贴不算签字费年包 40w+。作息为 10105 。 + +业务方面,我和两边的leader都聊过,淘天那边是架构很成熟了,最近组里是在用 DDD 领域驱动设计**重构旧代码**,负责预售、价保、赠品等产品,技术这块我是放心的。 + +小红书那边是处于业务的**增长期**,虽然 ld 知道成熟的架构是什么样的,表示“没有壁垒”,但是现有的架构还需要迭代;组里的 20+开发大多来自抖音快手,校招社招会再招人。有三个方向可选,tob 的商家-主播推荐平台、toc 的直播间互动和直播架构。 + +**跳槽**方面的担忧 本人背景是**双一流本**,在小红书可能会有更多崭露头角的机会,但是去淘天能有大厂光环。考虑到晋升速度,若干年后如果需要从淘天跳槽,与从小红书跳槽相比,哪边会更吃香一些呢? + +**技术**方面的担忧 我算是自驱力比较强的,在开源社区也很活跃。我担心小红书的技术不如淘天,直播的流量目前也不大,学到的技术会比淘天少很多? + +关于**提前实习** 如果要选择小红书,那么有没有必要在毕业前去淘天实习镀金?怎么对 HR 开口比较好呢? 另外 + +--- + +**大彬的回答**: + +师弟你好,恭喜恭喜,能拿到这两个offer很优秀啊! + +我认为新人,**一开始在成长期的中厂比单纯的大厂更好**,主要是几个方面的原因: + +**职业发展**来说,大厂基本上工作内容、技术架构和晋升通道已经**固化**了,大部分情况都是按部就班堆工作量;中厂还有很多“不完善”的地方可以优化并且做出成绩,有快速脱颖而出的机会。 + +**技术成长**来说,对于新人,用一个已有的技术可以快速学到一些东西,但也只能让你从初级工成长为熟练工;只有自己设计或者优化了某个技术,才能晋升到职业成长的下个阶段,比如技术负责人或架构师。 + +大厂的流程体系更完善,但是有一定体量的中厂也不会差到哪里去,这部分不会有质的区别。 + +总的来说,大厂和中厂的头衔其实不是特别重要,新人找工作其实更多应该看重成长速度,而技术人员的成长绝大部分都依赖于解决问题。在目前的环境下,大厂的增长相对停滞,工作中面对的更多是细碎的**存量优化**问题,或者无效内卷;少数还有增长空间的中厂无疑对于技术成长的价值更大一些。 + +除了公司,还有业务方向的问题也需要考虑一下,“直播技术”的范围很大,从编解码器到流媒体工程到前后端工程都有可能,如果没有特殊教育背景或者偏好,我建议新人选一个更通用的业务方向,尽量去**核心业务**的核心流程,见到的东西会更多一些。 + +第三,**城市**也需要考虑,虽然年轻人换个城市的代价不大,但是一旦有了对象或者想要常住某个城市之后,那么之前不是问题的问题就都是问题。比如购房成本北京会高一些等 + +最后,工资这东西当然也需要考虑,但也**不是追求越高越好**。一般来说,现在这个行业内工资水平都是相对稳定的,如果某一家工资特别高,那么要么是赶在业务扩张前急招(后面有可能会裁)、要么是工作强度明显更大;当然,相对的,在裁员的时候,也更偏向于裁工资更高的人。 + + diff --git a/docs/zsxq/question/personality-test.md b/docs/zsxq/question/personality-test.md new file mode 100644 index 0000000..df5b17e --- /dev/null +++ b/docs/zsxq/question/personality-test.md @@ -0,0 +1,43 @@ +## 想进大厂的年轻人,有多少败在了性格测试? + +在互联网公司招聘技术岗位人才时,除了技术能力的考察,越来越多的公司开始注重应聘者的个人**性格测试**。这一趋势源自于互联网公司对员工全面发展和协同工作的需求,只有具有不同特点和性格的人才一起协作,才能产生更多的创新点和独特的想法。 + +互联网公司所进行的技术岗位性格测试大都是基于心理学理论和经验,以分析应聘者的人格、态度、价值观等方面的特点,评估其是否适合公司的团队文化和价值观,并为公司提供精准的人才匹配。 + +最近[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于性格测试的疑问: + +**提问原文**: + +大彬哥,问下性格测评,给了三个选项都是好的品质(比如具有领导力,能够洞察事物本质,做事情很有计划条理),选一个**最符合**的,一个**最不符合**的,这种题要怎么选啊,不明白目的是想选拔什么样的人才呢? + +还有一种题,如果自己手头有比较繁重的任务,领导因为项目赶工期又派来了新任务,这种情况选择提前说明情况延期保证工作质量,还是选择接受任务努力把两个项目都干好呢? + +**听说性格测评真的会挂人**,所以比较慌。 + +--- + +**大彬的回答**: + +1. 首先性格没有**好坏之分**,内向外向也并不完全是考察的重点,不同的公司会有不同的标准。 +2. 关于性格测试怎么做的问题,其实没有一个确定的方法,我觉得最关键的是根据自己**真实情况**来作答。在性格测试中,对于同一个维度的考察会出现内容不同、位置不相连的多道题目,如果在测试结果中出现了很多矛盾的回答,系统可能会判定测试者是在**作弊**。比如华为、滴滴的性格测试题就是这样的。 +3. 你说的那两道题也没有对错之分,按照你自身情况作答即可,**保持前后回答一致**。 +4. 另外也不建议**刻意去迎合**应聘公司的文化价值观,伪装往往可能会适得其反,很容易前后回答不一致导致测试不通过。 +5. 如果涉及一些**原则性**的问题,比如是否一遇到困难就退缩,团队协作能力怎么样,这种还是要往正面的方向回答,没有公司会招一个团队协作能力差、遇到困难就退缩的员工。 + +当然,性格测试不仅是企业了解应聘者的方式,也是应聘者**了解自己**的方式。通过性格测试可以帮助应聘者全面认识自己的优点和缺陷,发现自己的潜力和不足,进而定位自己的特长和优势。所以应聘者在完成性格测试之后,可以进行**自我分析**,了解自己,精准面向优势和弱点,为自己的职业规划建立更好的基础。 + +更多信息可以查看原文(在**星球精华帖**):https://t.zsxq.com/0eJD15817 + +![](http://img.topjavaer.cn/img/202305141809523.png) + +--- + +[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)置顶帖汇总了所有球友的**提问**,内容包括**职业规划、技术问题、面试问题、岗位选择、学习路线**等等,现在很多现在困扰你的问题,在这里都能找到**答案**。 + +![](http://img.topjavaer.cn/img/202305141824858.png) + +![](http://img.topjavaer.cn/img/202305141813807.png) + +加入的同学一定要看看[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)的置顶内容,相信对你会有帮助的! + +> 点击【阅读原文】直达星球! \ No newline at end of file diff --git a/docs/zsxq/question/qa-or-java.md b/docs/zsxq/question/qa-or-java.md new file mode 100644 index 0000000..919a3da --- /dev/null +++ b/docs/zsxq/question/qa-or-java.md @@ -0,0 +1,29 @@ +大彬老师您好 + +我是参加今年24届秋招的研二学生,本科就读于某211信息安全专业,硕士就读于中国科学院大学 网络空间安全专业,最近开始了秋招,但是在职业规划上十分迷茫,想听听您的建议。 + +我得求职期望是,在北京找一个高薪的岗位,工作几年后回家乡,找一个国企躺平。 + +目前我已经排除了算法岗,虽然已经发表了两篇论文,但是研究方向小众,不足以支撑我找到一个算法岗的工作。所以我在以下几个岗位中有所纠结: + +1. java开发岗:为了准备开发岗,我跟着教程自己做了一个项目,整体的感受是工程能力很薄弱,并且只有一个学习项目,没有真正的工程项目。 +2. 安全岗位:虽然我本硕都是安全专业,但是研究方向都比较小众,目前互联网需要的安全人才都是攻防方向的,我的竞争力不够。 +3. 测试开发岗:这是我比较有把握的一个求职方向,因为之前有过一段在字节的测开实习经历,加上一直准备java开发,很多八股都是通用的。但是我比较担心测开岗位会不会不太好跳槽到国企,对未来比较担忧。 + +所以我目前采取的策略是:大厂投测开、中小厂投开发、国企/银行投安全 + +这令我十分地疲惫。很羞愧,已经7月份了,我竟让自己陷入了如此被动的局面,加上这两年互联网寒冬,我对秋招很担心。实际上我从很早之前就在准备找工作了,科研之余每天刷题、学java基础、学框架、写项目,但是时至今日我还是不够,始终比不上那些硕士阶段一直在参加工程的同学。 + +以上是我的基本情况 +- 想听听老师对我的现状有没有什么建议 +- 您是否了解测试开发岗位的发展前景,它的薪资和开发是差不多的嘛? +- 测开有没有可能从互联网顺利转国企 +- 现阶段我是继续补充java基础卷java岗,还是把重点放在测开上呢? + +原谅我现在处于焦虑状态因此问题比较多,图片是我的简历方便老师了解我的基本情况,期待老师回复🍓 +谢谢老师~ + +1、建议投测开,你这个简历在测开岗里面算是比较好的,相反在Java开发岗中算是比较一般的,没有很大的竞争力。有了字节的测开实习,在测开岗里面应该领先一大波人了。 +2、测开发展前景个人觉得也是不错的,随着互联网行业的发展,用户对产品的质量要求也越来越高,软件的性能测试、需求测试等方面的需求目前看是只增不减的。薪资方面,同职级测开跟开发基本持平。 +3、测开有没有可能从互联网顺利转国企?完全可以,没问题 +4、把重点放在测开,不建议同时准备两个岗位,可能顾此失彼 \ No newline at end of file diff --git a/docs/zsxq/question/tech/service-expansion.md b/docs/zsxq/question/tech/service-expansion.md new file mode 100644 index 0000000..b4a5b1d --- /dev/null +++ b/docs/zsxq/question/tech/service-expansion.md @@ -0,0 +1,7 @@ +## 怎么做到增加机器能线性增加性能的? + +线性扩容有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +对于有状态服务,除了要解决服务发现问题之外,还要解决状态(数据)迁移问题,迁移又分两步:先是数据拆分,常见的用哈希把数据打散。然后是迁移,常见的办法有快照和日志两类迁移方式。也有一些数据库直接实现了开发无感知的状态迁移功能,比如 hbase。 \ No newline at end of file diff --git "a/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" new file mode 100644 index 0000000..b8e25d6 --- /dev/null +++ "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" @@ -0,0 +1,61 @@ +大彬,您好! + +首先介绍下基本情况:211本科,信息工程专业。 + +本人毕业快三年,第一年做了一些功能测试之类的活,后来,岗位调动去做低代码平台的开发,拖拉拽的那种,主要是写SQL,所以自学了MySQL,所以数据库这块看看面试题应该差不了太多。 + +从22年开始转Java后端,边工作边学习,看的都是B站黑马程序员的课程,目前已经学完了Javase,Javaweb,Spring,SSM,Springboot,Git,Maven等,也算比较系统的学了一遍。 + +Javase课程基本是跟着用手敲了一遍,后面的视频就只认真看了一遍来赶进度。 + +大学里课程选修了linux操作系统,算法与数据结构,计算机网络,不过也快忘得差不多了,除了记得一些linux常用命令,因为驻场有用到。 + +目前主要是在学redis和mq等中间件,八股文也在准备,但是算法没有刷过题。 + +公司客户主要是一些金融机构,因为积累了差不多两年的业务经验,所以本人也初步打算继续往金融科技这块发展(不一定,哪钱多去哪)。 + +后面打算跳槽甲方,但是目前除了相关技术学了一轮,还没有做过后端开发的项目。 + +请问: + +1、现在这个状态找测开怎么样,有什么需要重点准备的吗。虽然本人更想做开发。 + +2、现在提离职,估计要过一阵放我走。现在边工作边找,但是工作完没有太多时间学习,并且没有java项目开发的经历,应该只能投测开,长远看,本人更想做后端开发,还得准备刷下算法,背背八股。 + +因为我现在到手也就1w出头,所以如果三月底离职了还没找到理想的开发或测开工作,我是想直接找个比如黑马的瑞吉外卖项目,把已学的融汇贯通,认真做个把月冲击后端咋样? + +3、继续骑驴找马,直到找到才提离职。但是这样准备项目的时间不太够,做完一个项目估计起码四五月份,还不说背其它的八股和算法。 + +目前继续做这份工作我也没有太多成长,温水煮青蛙。 + +4、如何面试包装下涨薪最快呢,想尽可能的提升下月base,目标是2w+,但是又好像有不超过30%的限制?问上家月薪的时候可以虚报吗,或者怎么说? + +5、金融行业不少公司比较看重学历,请问咱做技术的,是否有必要花一年时间读个海外硕投资自己呢? + + + +------ + +大彬的回答: + +1、 遵从你的内心。如果确实想做开发,先按照开发的方向去努力吧,尽量投开发。后面又去做测开,可能你会有不甘心。当然3年经验转方向,可能会 + +2、已经提离职的话,工作上的内容 就推就推,马上就要走人了,身上就别挂那么多活了,特别是那种紧急上线,什么三月份之前上线,就是趁你走之前 赶紧开发完的那种, 都推一推,多给自己争取点自学的时间。 + +自己做一个项目是可以的,好好冲后端。 + +3、建议是尽量一边工作一边准备,如果时间确实挤不开,可以提离职,也是背水一战了。 + +4、入职的时候 上家公司的月薪 是要 银行流水截图发个hr的,不过有的人 是把 银行流水给p图了,改的薪资。 + +这么干只能说 有风险(可能被hr发现,终止offer),但确实不少人这么干的,然后也顺利入职了。 + +要不要这么搞 还需要你自己权衡一下。 + +你是 测试 转开发,double还是有难度。如果你面试表现足够好的话,要个double 也是有希望的,这个薪资 基本要去 北上深 这样的城市了。 + +5、自己攒够钱去 读个 香港 一年制 是可以的,提升一下自己,重新校招,学校认可度在大陆整体也还行,入学门槛相对足够低了。 + +不过花费不少,可能要50w起步, 这不是小数目,估计可以在老家买套不错的房子了。 + +不过如果你已经跳槽到一家不错的企业的话,我倾向于好好赚钱就好,毕竟 海外一年制学费不低,还要耽误 一年多的赚钱时间,后面能不能把这个钱赚回来,不太确定。 \ No newline at end of file diff --git "a/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" new file mode 100644 index 0000000..e76be5f --- /dev/null +++ "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**出国读研**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,有个问题咨询你 + +基本情况: 25岁,**二本非科班**程序员,在上海,最近想**出国读研** + +本科读的工商管理,还挂过科,辅修了计算机,现在工资税前 17k 吧,感觉到瓶颈期了,想出去见见世面,回来有个应届生的 buff ,**冲冲大厂**。 + +不知道这个决定是否可靠?有没有建议? + +--- + +**大彬的回答**: + +你好。按照目前互联网的行情,我**不建议**你这样做。 + +除非你出国是为了**留在国外**,国外文凭拿到国内**认可度**不见得比国内的高,或者你能够得上那种国内都认识的,不然 HR 最多参考一下 QS 排名,不然不认识的可能就被当成野鸡大学了。 + +还有一种为了拿**上海户口**,海归有优惠政策,可以去读个水硕回来就业落户 + +留学回来冲大厂最好就不要想了,**只有留学背景是不够的**,就现在应届生很多很多,如果你**本科学历和专业技能**都不够硬,现在再投校招岗位弄不好连简历筛选都过不去,连面试的机会都没有,而且出国读研是没法随时回来实习的,一些企业要求学生**实习**,如果你无法提前到岗位进行实习,可能直接被淘汰。对留学生而言在这方面没有任何优势。 + +唯一优势就是你留学后提升了你的**英语水平**,回来可以冲一些外企,英语好特别是口语好是有一定优势。 + +结论:如果出国留学是为了回国发展,你需要了解国内对于外国文凭的**认可度**问题,同时还要注重本科学历和专业技能的提高。出国留学的经历可以帮助你提升英语水平,学习不同的文化和思维方式,但并不一定就能为你在就业市场上带来直接的**竞争力**的。 + diff --git "a/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" new file mode 100644 index 0000000..54129bb --- /dev/null +++ "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" @@ -0,0 +1,42 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何谈薪**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥好,本人是今年参与校招的一名应届硕士生,目前已经拿到一些 offer 意向,到10月底可能就要陆陆续续谈薪了,请问彬哥在与 hr 沟通谈薪的时候有没有什么技巧或者要注意的呢?因为之前都注意的是面试方面,真正沟通谈薪的发现网上也很少有教,希望彬哥可以分享一下 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. 你可以到 OfferShow 上查一下你面试的这家公司对应岗位的薪资水平就知道你拿到的是高还是低了。一般校招都是企业单方面开价,除非你觉得你面试中水平远超其他参与该岗位竞争的人,或已经拿到其他公司比该公司开出的薪资更高的 offer ,你就可以跟 HR argue 薪资了。 +2. 还可以到牛客上去打探下消息今年校招薪资的情况,看看其他人拿到的 package 是多少,但在网上给别人透露你的薪资时注意保持隐藏身份,以免被介意公开薪资的 HR 抓到收回 offer +3. 想要在谈薪中掌握主动权,首先你要足够优秀,面试评价很高,部门很看好你,这样才有议价的余地。如果你的面试结果一般,和其他候选人相比没有什么优势,这样是基本不会给你 argue 的机会的,毕竟 HR 开完价后再重新调整,公司内部也要走很多流程,甚至大领导点头才可能给你调,手上备胎又多,根本就不关心你接不接这个 offer + + + +总之,argue 薪资的前提是你手上有多个备胎可以选择,如果 HR 不接受,就放弃该 offer 吧,选择你更满意的 + + + + + + + diff --git "a/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" new file mode 100644 index 0000000..9714a26 --- /dev/null +++ "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" @@ -0,0 +1,39 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【如何找实习工作】的提问。 + + + +**球友提问**: + +目前状况:学校是**双非一本**,目前阶段是大三上开始,9月份中旬提问。 然后希望能在十月,十一月份的时候**找一份寒假时期实习或者日常实习**,希望能够为简历添加一份实习经历,也为了熟悉面试流程,为明年秋招做好准备。 + +提问目的:大彬哥你好,麻烦大彬哥帮我看看,我目前的**个人总结**,麻烦你针对性的给出目前我和实习的差距,因为时间就这一两个月,想知道具体准备哪些部分才能找到一份实习。问题很多,麻烦大彬哥了! + +然后这里看图片。 + +![](http://img.topjavaer.cn/img/202309280816580.png) + + 最后提问 + +1. 麻烦大彬哥根据以上,针对性的给出我目前和寒假日常实习的**差距**,目标是通过面试,因为时间就这一两个月,想知道具体**重点准备**哪些部分才能找到一份实习。 +2. 如果有机会还是希望能去大厂实习,是否差距是太大了? +3. 因为没有突出优势,基础部分没有系统学习如计网,操作系统,**只背面试题**够用吗。 +4. 项目部分还要学习spring boot和redis和spring cloud 这些是必要的吗,学完这些做项目会不会太慢了。如果和大厂太遥远,目标放在小厂的话哪些知识点是重点,哪些是可以先放放的。 + +--- + +**大彬的回答**: + +学弟你好。 + +1. 按照你目前的情况,想要一份实习工作,之前需要补充学习这些内容:Redis,SpringBoot,微服务,消息队列,Nginx,至少一个项目。如果时间比较紧的话,建议刚开始学**先掌握基本的api用法**,等到后面有时间再去深入学习、看源码。 +2. 关于项目,很多同学刚开始时,总想自己从零开始敲代码,或者以从零开始搭建一个项目为学习目标。其实刚开始学的时候,会遇到很多坑,比如一个分号一个单词拼错都可能会导致卡进展,从而影响到学习效率和学习积极性。正确的做法是,先运行跑通现有代码,运行时通过结果理解关键性语法和技能点,然后再尝试修改人家的代码看结果,这样就能达到边学边进步的效果。 +3. 做什么项目呢?可以参考星球**置顶帖**分享的一些优质项目(https://t.zsxq.com/12mxN9kMJ) + +2. 想进大厂的话,力扣算法之前刷300道左右,**计算机基础要扎实**,计算机网络、操作系统这些单靠背面试题是不够的,需要花时间去看看书,系统学习一下。 + +3. SpringBoot、Redis、Springcloud这些有必要学吗?如第一点所说的,肯定要学的,这些都是Java开发必备的知识了,其实学习简单使用还是挺快的,原理那些可以后面有时间在捡起来 + +4. **建议你先冲一下中小厂**,后面还有暑期实习和秋招,进大厂还是有机会的。优先把上面提到的那些知识点先学了,找个项目做做,然后准备下面试题和力扣(中小厂力扣前200就够了) +5. 一定别只学技术,也要去“背“面试题。建议把[**星球**](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)的**面试手册**(在**星球置顶帖**:**https://t.zsxq.com/12mxN9kMJ**)过一遍,基本包括绝大部分常考面试题了。实习岗位不可能因为你技术到位自己跑过来,而是要你通过面试证明你的能力才能争取到,所以掌握面试技巧也很重要。面试前先找小公司练练手,亲历面试,并在面试中进一步学习面试技巧、查漏补缺。 \ No newline at end of file diff --git "a/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" new file mode 100644 index 0000000..861f0a8 --- /dev/null +++ "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" @@ -0,0 +1,38 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何提升技术能力**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,我学历是**普通一本**,工作**一年多**,想跳槽进大厂做java后端,平时应该练什么才能**提升技术**?感谢! + +--- + +**大彬的回答**: + +你好,挺多同学都有这样的问题,分享一下我的建议: + +显然刚开始工作的新人是很难接触到一些比较大的锻炼机会的,比如写一些**底层框架**,或者**项目重构**等一些0-1的设计等等,这种一般是交给有一定经验的人去负责。 + +我觉得你这个阶段要做的是多**注重平时的积累**。主要是**实战和理论**两方面并行,比如**代码扩展、复用性、架构方面的理论**等。通过书籍或者课程学习相关的理论,然后在日常工作中,有意识地运用这些理论进行实际锻炼。 + +即使你一开始没机会设计一个大工程,那么就把一个小功能当成一个架构去做设计考量。代码的**设计模式、扩展、复用性**等,平时积累锻炼的机会还是有的,自己多去思考,平时写的业务代码,有没有优化空间之类的。 + +这样,比较理想的路线是 : 通过平时积累锻炼 -> leader 发现你相关能力不错 -> 尝试派给你更大更有挑战的任务 ->得到更好地锻炼。 + +当然如果你觉得自己可以承担一些比较复杂的任务了,也可以自己**主动**去争取一些锻炼机会。别会怕搞砸,开发的时候方案让组里的大佬帮你把把关,这个过程会是非常好的**成长**机会。 + + + diff --git "a/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" new file mode 100644 index 0000000..60ab0d6 --- /dev/null +++ "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" @@ -0,0 +1,70 @@ +大家好,我是大彬~ + +最近陆续有同学找我报喜了,有些同学拿了好几个offer,但是不知道该如何**抉****择。**这确实是个值得认真考虑的问题。 + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬哥好,向您咨询**offer选择**的问题 + +最近找工作有两家 offer + +一家是**外派**给传统行业外企的岗位(公司1) + +a. 工作时间:早 9 晚 6 **不加班**,可居家办公 + +b. 薪资年包:五险一金按**最低**的交,到手比目前降了**10%** + + + +另一家是规模不错的**知名互联网公司**(公司2) + +a. 工作时间:比较**卷**,暂且按每天 11 个小时来算 + +b. 薪资年包:到手基本持平,**五险一金正常交** + +公司 1 有充足的属于自己的时间,工作上有一定的**主导权**,技术栈也比较**符合**自己 + +公司 2 会让自己的履历好看一些,能接触到技术方面更多的业务场景,有助于学习提高,但是技术栈**不太符合**自己,也比较**卷** + +两家公司如果算到手薪资时薪的话,外派的反而稍微高一点,想问问彬哥有什么建议没 + +(内心里是更**倾向**于去 1 ,第一想要主导权和自己的时间,其实就是花钱买时间,第二比较烦大厂的官僚风,讲鬼话,现在最主要是怕不稳定,但是公司 1 合同里边还分基础薪资和绩效薪资,基础薪资压的老低,感觉太不保险了,风险太大) + +--- + +**大彬的回答**: + +你好,不知道你是不是理解错了,外派外企就不是外企,是外企**外包**。五险一金按最低交基本就不用考虑了,真外企不会克扣这个。项目结束合作就结束了,到时候你就会派到下一个公司了。 + +所谓的早 9 晚 6 不加班,有可能只是hr一家之言,加不加班都是项目组说了算,不是外派公司能决定的。 + +虽然大厂这两年裁员很凶,不过再怎么说**稳定性**也好过外包的。这两者其实没有可比性。 + +如果从长远职业发展考虑,可见的将来还是一直干这行,家里没资本,**果断选 2** 就行了;后续可能改行,或者家庭条件比较好,哪里舒服选哪。 + +再有一点,比工资也不能光看到手,**公积金**完全可以算自己的钱,社保虽然是将来的事但多总比少要好的。 + +最后,建议你要么找个**真外企**,要么就选 2 ,降薪去外包不值得。 + + + + + diff --git "a/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 0000000..12ac7c0 --- /dev/null +++ "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,116 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴提出的一些技术问题和解答。 + + + +**球友提问**: + +彬哥,我们项目组要求用户敏感数据加密存储,主要的问题: + +1 、加密后如何查询才能击中**索引**; + +2 、用户身份如何验证(手机号、身份证); + +大佬有什么建议吗? + +**大彬的回答**: + +首先需要一个专用于加密解密的服务。 + +所有加密解密操作都访问该服务。 可访问数据库的人, 秘钥管理人,加密服务管理人,三者不能有任何一方有办法获取到所有信息。 + +接下来回答你的两个问题: + +1、加密后如何查询才能击中索引? + +使用 **hash** 查询。加密字段只允许精准查询,不允许模糊。如果一定要模糊,那也是有限制的模糊,提前定好**模糊规则**,根据模糊规则提前预留相关数据,再进行加密。 + +2 、用户身份如何验证(手机号、身份证); 验证后再加密,或者允许解密。 + + + +需要注意的是: + +1. 不同业务场景对脱敏的要求是不一样的。我们公司的要求是,任何人任何时候都不能看到明文,明文只出现在内存中。 +2. 解密后,明文只能存在**内存**中,调用解密必须要有日志。 + +--- + +**球友提问**: + +大彬哥,请教您技术方案,我们项目有个场景,需要在一段时间内发了**n个相同的mq消息**,去更新数据,只需要**保留最后一个消息**就可以,前面的都可以不要,应该怎么实现? + +**大彬的回答**: + +收到消息后先往redis写,如果存在就直接**覆盖**,如果不存在就先写入,并且发送一条延迟消费的消息到mq,具体延迟多久可以是大于等于你说的一段时间内这个值。然后收到延迟消费的消息时直接去取redis的消息,这条消息基本就是这段时间内的最后一条消息了 + +--- + +**球友提问**: + +大佬你好,请教一个问题。 有两个百万条数据的表以及三张万以内数据表,取数据的时候不可避免要 **join**。 客户端可以自由调整日期,来查看数据。 因为表数据十分钟同步更新一次以及用户自由调整日期,感觉没办法做缓存。 现在每次查询都要 join 一次,导致查询数据很慢,要 10s 多,请问一下要怎么优化? + +**大彬的回答**: + +如果能改表结构的话,把索引和内容分开存,其实百万级的数据全放内存里应该是没问题的。如果非要 join,要么降低 join 的数据规模,要么提前算好数据,要么把能缓存的数据(比如那 3 张表)**缓存**起来,尽量降低查询时的磁盘消耗。 + +--- + +> 往期**星球提问整理**: + +**球友提问**: + +大彬哥,请教一个问题(之前面试问到的),怎么做到**增加机器线性增加性能**的? + +**大彬的回答**: + +你好。**线性扩容**有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +--- + +**球友提问**: + +彬哥,请教你两个问题: + +1. 新功能提测之类的,流程是怎样的,是各个功能特性分开提测还是一起合并了提测?另外比较偏向业务的,单元测试集成测试这些要求怎样,还是会在合入的时候进行限制,不到一定比率不允许合入? +2. 一般线上出事故了,就是开会总结、发邮件改进流程之类的。彬哥有没啥经验之类的可以分享? + +**大彬的回答**: + +1.像我们项目组的话,服务有一定程度的拆分,每周都会上线,冲突的概率不高。如果冲突了,那就合并测。单元测试有要求,但是最高就要求到40%,再高维护成本太高。 + +2.**总结发邮件**肯定是少不了的,我分享一下我们这边的流程,你可以参考一下。 + +a.发现线上事件后,需第一时间报告运营经理,项目经理。 + +b.判断线上事件的性质,对外报告处理故障进展,按流程要求通知相关负责人。 + +c.问题处理。如果问题比较紧急(故障分为A/B/C三个等级),需要立即通知相关部门的领导,由各部门领导协调事故处理,一般要在2小时内给予解决,保证系统恢复正常。 + +d.线上故障处理后都需要测试人员进行跟进,问题修复后第一时间验证,然后按照上线管理流程进行程序发布 + +e.通知业务方 + + + +持续更新中... + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa&token=1033350733&lang=zh_CN#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + diff --git "a/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" new file mode 100644 index 0000000..d9e7718 --- /dev/null +++ "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" @@ -0,0 +1,44 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**是否有必要参加培训班**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,向您请教 + +我的基本情况: 本科**末流211**,**非计算机专业**,打算找Java方向的工作,目前**秋招还没拿到offer**,面试官反馈是基础比较差,打算**去上培训班**了。 + +我去打听了,周围一家培训机构收费是**15800**,5800可以分期付款,另外10000找到工作后再给,时间是 6-8 个月。听了两天试听课感觉那个老师讲的还不错。不知道彬哥是否建议我去上培训班?穷学生有点怕翻车 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. **金钱成本和时间成本**,一般培训班周期在6个月以上,而且学员水平参差不齐,机构老师没办法根据每个学员的能力**定制化学习路线**,只能跟着大部队走。另外**培训学费**也比较贵,对很多学生来说都是一笔很大的开销。 +2. **目的是否明确**,培训班的作用是快速入门,注意只是**入门**。培训班老师会告诉你最常用的技术是什么,怎么用。至于深度就不用想了,很难培训出高级程序员的 +3. **自学能力**怎么样。培训班仅仅是帮助你将知识整理出一个体系,这个体系给到你了,至于能不能学会还是两码事。建议你可以去 B 站先看黑马或者尚学堂的视频,一般都有二三百集,先自己看几十集,看看自己是否**适合** + +ps: 我的经历跟你类似,非科班转行,当初没有选择进培训班学习,一方面**学费很贵**,对于穷学生来说,真的贵,另一方面我的**自律能力和自学能力**还算可以,感觉也没有参加培训班的必要。 + +另外如果想在计算机行业拿到高薪的话,需要多学很多东西,培训班教的那些是远远不够的。 + + + + + diff --git "a/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" new file mode 100644 index 0000000..f9375bb --- /dev/null +++ "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" @@ -0,0 +1,22 @@ +前段时间[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于读博还是找工作的疑问,跟大家分享一下: + +**学弟提问**: + +大彬老师好,我是参加今年24届秋招的**研二**学生,本科是川大数学专业,硕士在xxx研究所读研,计算机应用技术专业。在所里主要是研究无人机的路径规划和强化学习。 + +所里因为不给出去实习,所以我也**没有实习项目**,所里给的福利也就是最后可以留所在央企研究所工作,但是我不太想留,所以想出去找工作。自己有2篇无人机相关的论文,但是以后也不想去做无人机相关。只想做纯计算机相关的工作。后面就想转java,可以说除了高中的竞赛基础,完全没有别的基础,java就跟着机构的视频在学。但是感觉学的太慢了,从4月才开始算0基础学,秋招感觉是完全赶不上了,学了2个月去看看面经发现基本都答不上。所以很迷茫到底是继续java开发往后学下去,反正最后有个保底。还是不去找工作了,凭着2篇论文去找个博士读,博士毕业再出去找企业。希望能给个建议,谢谢! + +--- + +**大彬的回答**: + +你好,你现在研二了,过了两年的研究生生活,如果你适应这种探索性研究生活、**对科研有热情、有想法**想要去**探索**一些问题,而且没有太大生活压力,家里不需要你的经济支持,那建议你去申请读博。读完博士你的选择会更多更多。 + +如果你是处于**懵懂**状态,感觉硕士毕业找不到好工作就去读博,也没有科研热情,就这样盲目进行的话,这样只是**浪费时间**走一些弯路,之后也可能会后悔,建议你还是直接去找工作。因为读博也是有很大的挑战性的,需要克服焦虑和长时间的孤独,可能比你想象中的要困难一些。 + +另外如果想要找Java开发方向岗位的话,目前你的进度确实稍微慢了一些,秋招马上就要开始了,建议你抓重点去学,参考[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)置顶帖的最新Java学习路线,加快学习节奏。 + + + + + diff --git "a/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" new file mode 100644 index 0000000..23e4bb2 --- /dev/null +++ "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【非科班转码如何补基础】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [读博?找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬大佬,想问下非科班要补哪些基础? 求推荐视频,国内国外都行。 + +--- + +**大彬的回答**: + +你好,我也是非科班转码的,Java方向,不知道你打算想往哪个方向发展。不过没关系,无论哪个方向,计算机基础都是相通的,下面分享一下我的经验: + +1. 数据结构:程序员可以不关注硬件,软件部分就是代码的逻辑实现,其中数据结构是基础,推荐橘黄色的算法书,想进中大厂就刷 leetcode ;这部分我觉得熟悉常见数据结构,了解常见算法就够了。 +2. 操作系统:推荐电子科技大学的蒲晓蓉老师的操作系统课程,看完觉得意犹未尽再去翻翻现代操作系统或者 csapp 吧,这部分主要看下进程、内存、文件系统。 +3. 计算机网络:推荐自顶向下,重点看两章就够了,应用层和传输层,更下层的说实话用不到。这里工作用到的更多的是 http,看下图解 http 之类的,有需要的可以看下图解密码学。 +4. 数据库:推荐伯克利的 CS168 课程。国内的推荐中国人民大学王珊老师的《数据库系统概论》 +5. 编译原理:不推荐太早看,代码写多了再来看,前期直接跳过。如果你是前端程序员,至少接触过 babel 这一类工具,了解过原理之后再来学习,这门课太早接触我觉得真的没用,晦涩难懂 +6. 最后补充下个人理解:这个阶段最重要的不是深入细节,熟悉原理这一类的,看到不懂的部分直接跳过就行了,先大概过一遍建立计算机的一些基本思想和概念,比如分层和抽象、时间和空间、接口和实现、分治等等等等,先悟到这一层,再回头看书能快很多,接下来再去深入一些感兴趣的细节部分,我觉得就差不多了 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git "a/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" new file mode 100644 index 0000000..66d63ea --- /dev/null +++ "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" @@ -0,0 +1,332 @@ +## 前言 + +开发过程中其实有很多小细节要去注意,只有不断去抠细节,写出精益求精的代码,从量变中收获质变。 + +技术的进步并非一蹴而就,而是通过无数次的量变,才能引发质的飞跃。我们始终坚信,只有对每一个细节保持敏锐的触觉,才能绽放出完美的技术之花。 + +从一行行代码中,我们品味到了追求卓越的滋味。每一个小小的优化,每一个微妙的改进,都是我们追求技艺的印记。我们知道,只有更多的关注细节,才能真正理解技术的本质,洞察其中的玄机。正是在对细节的把握中,我们得以成就更好的技术人生。 + +耐心看完,你一定会有所收获。 + +### 1. **代码风格一致性**: + +代码风格一致性可以提高代码的可读性和可维护性。例如,使用Java编程中普遍遵循的命名约定(驼峰命名法),使代码更易于理解。 + +```ini +ini复制代码// 不好的代码风格 +int g = 10; +String S = "Hello"; + +// 好的代码风格 +int count = 10; +String greeting = "Hello"; +``` + +### 2. **使用合适的数据结构和集合**: + +选择适当的数据结构和集合类可以改进代码的性能和可读性。例如,使用HashSet来存储唯一的元素。 + +```csharp +csharp复制代码// 不好的例子 - 使用ArrayList存储唯一元素 +List list = new ArrayList<>(); +list.add(1); +list.add(2); +list.add(1); // 重复元素 + +// 好的例子 - 使用HashSet存储唯一元素 +Set set = new HashSet<>(); +set.add(1); +set.add(2); +set.add(1); // 自动忽略重复元素 +``` + +### 3. **避免使用魔法数值**: + +使用常量或枚举来代替魔法数值可以提高代码的可维护性和易读性。 + +```ini +ini复制代码// 不好的例子 - 魔法数值硬编码 +if (status == 1) { + // 执行某些操作 +} + +// 好的例子 - 使用常量代替魔法数值 +final int STATUS_ACTIVE = 1; +if (status == STATUS_ACTIVE) { + // 执行某些操作 +} +``` + +### 4. **异常处理**: + +正确处理异常有助于代码的健壮性和容错性,避免不必要的try-catch块可以提高代码性能。 + +```php +php复制代码// 不好的例子 - 捕获所有异常,没有具体处理 +try { + // 一些可能抛出异常的操作 +} catch (Exception e) { + // 空的异常处理块 +} + +// 好的例子 - 捕获并处理特定异常,或向上抛出 +try { + // 一些可能抛出异常的操作 +} catch (FileNotFoundException e) { + // 处理文件未找到异常 +} catch (IOException e) { + // 处理其他IO异常 +} +``` + +### 5. **及时关闭资源**: + +使用完资源后,及时关闭它们可以避免资源泄漏,特别是对于文件流、数据库连接等资源。 + +更好的处理方式参见第16条,搭配`try-with-resources`食用最佳 + +```ini +ini复制代码// 不好的例子 - 未及时关闭数据库连接 +Connection conn = null; +Statement stmt = null; +try { + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + stmt = conn.createStatement(); + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} finally { + // 数据库连接未关闭 +} + +// 好的例子 - 使用try-with-resources确保资源及时关闭,避免了数据库连接资源泄漏的问题 +try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + Statement stmt = conn.createStatement()) { + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} +``` + +### 6. **避免过度使用全局变量**: + +过度使用全局变量容易引发意外的副作用和不可预测的结果,建议尽量避免使用全局变量。 + +```csharp +csharp复制代码// 不好的例子 - 过度使用全局变量 +public class MyClass { + private int count; + + // 省略其他代码 +} + +// 好的例子 - 使用局部变量或实例变量 +public class MyClass { + public void someMethod() { + int count = 0; + // 省略其他代码 + } +} +``` + +### 7. **避免不必要的对象创建**: + +避免在循环或频繁调用的方法中创建不必要的对象,可以使用对象池、StringBuilder等技术。 + +```arduino +arduino复制代码// 不好的例子 - 频繁调用方法创建不必要的对象 +public String formatData(int year, int month, int day) { + String formattedDate = String.format("%d-%02d-%02d", year, month, day); // 每次调用方法都会创建新的String对象 + return formattedDate; +} + +// 好的例子 - 避免频繁调用方法创建不必要的对象 +private static final String DATE_FORMAT = "%d-%02d-%02d"; +public String formatData(int year, int month, int day) { + return String.format(DATE_FORMAT, year, month, day); // 重复使用同一个String对象 +} +``` + +### 8. **避免使用不必要的装箱和拆箱**: + +避免频繁地在基本类型和其对应的包装类型之间进行转换,可以提高代码的性能和效率。 + +```dart +dart复制代码// 不好的例子 +Integer num = 10; // 不好的例子,自动装箱 +int result = num + 5; // 不好的例子,自动拆箱 + +// 好的例子 - 避免装箱和拆箱 +int num = 10; // 好的例子,使用基本类型 +int result = num + 5; // 好的例子,避免装箱和拆箱 +``` + +### 9. **使用foreach循环遍历集合**: + +使用foreach循环可以简化集合的遍历,并提高代码的可读性。 + +```ini +ini复制代码// 不好的例子 - 可读性不强,并且增加了方法调用的开销 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (int i = 0; i < names.size(); i++) { + System.out.println(names.get(i)); // 不好的例子 +} + +// 好的例子 - 更加简洁,可读性更好,性能上也更优 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (String name : names) { + System.out.println(name); // 好的例子 +} +``` + +### 10. **使用StringBuilder或StringBuffer拼接大量字符串**: + +在循环中拼接大量字符串时,使用StringBuilder或StringBuffer可以避免产生大量临时对象,提高性能。 + +```ini +ini复制代码// 不好的例子 - 每次循环都产生新的字符串对象 +String result = ""; +for (int i = 0; i < 1000; i++) { + result += "Number " + i + ", "; +} + +// 好的例子 - StringBuilder不会产生大量临时对象 +StringBuilder result = new StringBuilder(); +for (int i = 0; i < 1000; i++) { + result.append("Number ").append(i).append(", "); +} +``` + +### 11. **使用equals方法比较对象的内容**: + +老生常谈的问题,在比较对象的内容时,使用equals方法而不是==操作符,确保正确比较对象的内容。 + +```ini +ini复制代码// 不好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1 == name2) { + // 不好的例子,使用==比较对象的引用,而非内容 +} + +// 好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1.equals(name2)) { + // 好的例子,使用equals比较对象的内容 +} +``` + +### 12. **避免使用多个连续的空格或制表符**: + +多个连续的空格或制表符会使代码看起来杂乱不堪,建议使用合适的缩进和空格,保持代码的清晰可读。 + +```ini +ini复制代码// 不好的例子 +int a = 10; // 不好的例子,多个连续的空格和制表符 +String name = "John"; // 不好的例子,多个连续的空格和制表符 + +// 好的例子 +int a = 10; // 好的例子,适当的缩进和空格 +String name = "John"; // 好的例子,适当的缩进和空格 +``` + +### 13. **使用日志框架记录日志**: + +在代码中使用日志框架(如Log4j、SLF4J)来记录日志,而不是直接使用System.out.println(),可以更灵活地管理日志输出和级别。 + +```go +go复制代码// 不好的例子: +System.out.println("Error occurred"); // 不好的例子,直接输出日志到控制台 + +// 好的例子: +logger.error("Error occurred"); // 好的例子,使用日志框架记录日志 +``` + +### 14. **避免在循环中创建对象**: + +在循环中频繁地创建对象会导致大量的内存分配和垃圾回收,影响性能。尽量在循环外部创建对象,或使用对象池来复用对象,从而减少对象的创建和销毁开销。 + +```ini +ini复制代码// 不好的例子 - 在循环过程中频繁地创建和销毁对象,增加了垃圾回收的负担 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, i + 1); + Date date = calendar.getTime(); // 在循环中频繁创建Calendar和Date对象 + dates.add(date); + } + return dates; +} + + +// 好的例子 - 在循环外部创建对象,减少内存分配和垃圾回收的开销 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + Calendar calendar = Calendar.getInstance(); + Date date = new Date(); // 在循环外部创建Date对象 + for (int i = 0; i < 7; i++) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + date.setTime(calendar.getTimeInMillis()); // 复用Date对象 + dates.add(date); + } + return dates; +} +``` + +### 15. **使用枚举替代常量**: + +这条其实和第3条一个道理,使用枚举可以更清晰地表示一组相关的常量,并且能够提供更多的类型安全性和功能性。 + +```arduino +arduino复制代码// 不好的例子 - 使用常量表示颜色 +public static final int RED = 1; +public static final int GREEN = 2; +public static final int BLUE = 3; + +// 好的例子 - 使用枚举表示颜色 +public enum Color { + RED, GREEN, BLUE +} +``` + +### 16. **使用try-with-resources语句**: + +在处理需要关闭的资源(如文件、数据库连接等)时,使用try-with-resources语句可以自动关闭资源,避免资源泄漏。 + +```java +java复制代码// 不好的例子 - 没有使用try-with-resources +FileReader reader = null; +try { + reader = new FileReader("file.txt"); + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // 处理关闭异常 + } + } +} + +// 好的例子 - 使用try-with-resources自动关闭资源 +try (FileReader reader = new FileReader("file.txt")) { + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} +``` + + + + + +这16个小建议,希望对你有所帮助。 + + + +参考链接:https://juejin.cn/post/7261835383201726523 diff --git a/docs/zsxq/share/completable-future-bug.md b/docs/zsxq/share/completable-future-bug.md new file mode 100644 index 0000000..6ff1be0 --- /dev/null +++ b/docs/zsxq/share/completable-future-bug.md @@ -0,0 +1,250 @@ +--- +sidebar: heading +title: 记一次生产中使用CompletableFuture遇到的坑 +category: 优质文章 +tag: + - 生产问题 +head: + - - meta + - name: keywords + content: CompletableFuture,生产问题,bug,异步调用 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +#### 为什么使用CompletableFuture + +业务功能描述:有一个功能是需要调用基础平台接口组装我们需要的数据,在这个功能里面我们要调用多次基础平台的接口,我们的入参是一个id,但是这个id是一个集合。我们都是使用RPC调用,一般常规的想法去遍历循环这个idList,但是呢这个id集合里面的数据可能会有500个左右。说多不多,说少也不少,主要是在for循环里面多次去RPC调用是一件特别费时的事情。 + +我用代码大致描述一下这个需求: + +```java +public List buildBasicInfo(List ids) { + List basicInfoList = new ArrayList<>(); + for (Long id : ids) { + getBasicData(basicInfoList, id); + } + } + + private List getBasicData(List basicInfoList, Long id) { + BasicInfo basicInfo = rpcGetBasicInfo(id); + return basicInfoList.add(basicInfo); + } + + public BasicInfo rpcGetBasicInfo(Long id) { + // 第一次RPC 调用 + rpcInvoking_1()........... + + // 拿到第一次的结果进行第二次RPC 调用 + rpcInvoking_2()........... + + // 拿到第二次的结果进行第三次RPC 调用、 + rpcInvoking_3()........... + + // 拿到第三次的结果进行第四次RPC 调用、 + rpcInvoking_4()........... + + // 组装结果返回 + + return BasicInfo; + } +``` + +是的,这个数据的获取就是这么的扯淡。。。如果使用循环的方式,当ids数据量在500个左右的时候,这个接口返回的时间再8s左右,这是万万不能接受的,那如果ids数据更多呢?所以不能用for循环去遍历ids呀,这样确实是太费时了。 + +既然远程调用避免不了,那就想办法让这个接口快一点,这时候就想到了多线程去处理,然后就想到使用CompletableFuture异步调用: + +#### CompletableFuture多线程异步调用 + +```java +List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + ids.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + try { + List basicInfos = future.get(); + } catch (Exception e) { + e.printStackTrace(); + } +``` + +> 这里补充一点:**CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数减1大于1时,才会使用默认的线程池(ForkJoinPool),否则将会为每个CompletableFuture的任务创建一个新线程去执行**。即,CompletableFuture的默认线程池,只有在**双核以上的机器**内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,**等于没有使用线程池,且有资源耗尽的风险**。 + +默认线程池,**池内的核心线程数,也为机器核心数减1**,这里我们的机器是8核的,也就是会创建7个线程去执行。 + +上面这种方式虽然实现了多线程异步执行,但是如果ids集合很多话,依然会很慢,因为`future.get();`也是堵塞的,必须等待所有的线程执行完成才能返回结果。 + +#### 改进CompletableFuture多线程异步调用 + +想让速度更快一点,就想到了把ids进行分隔: + +```ini +ini复制代码 int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionAssetsIdList = Lists.partition(ids, pageSize); +``` + +因为我们CPU核数为8核,所有当ids的大小小于8时,就开启8个线程,每个线程分一个。这里的>>3(右移运算)相当于ids的大小除以2的3次方也就是除以8;右移运算符相比除效率会高。毕竟现在是在优化提升速度。 + +如果这里的ids的大小是500个,就是开启9个线程,其中8个线程是处理62个数据,另一个线程处理4个数据,因为有余数会另开一个线程处理。具体代码如下: + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + //如果ids为500,这里会分隔成9份,也就是partitionIdList.size()=9;遍历9次,也相当于创建了9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据。第9个处理4个数据。 + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +如果ids的大小等于500,就会被分隔成9份,创建9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据(id),第9个处理4个数据(id)。这62个数据又会被分成7个线程去执行(CPU核数减1个线程)。经过分隔之后充分利用了CPU。速度也从8s减到1-2s。得到了总监和同事的夸赞,同时也被写到正向事件中;哈哈哈哈。 + +#### 在生产环境中遇到的坑 + +上面说了那么多还没有说到坑在哪里,下面我们就说说坑在哪里? + +本地和测试都没有啥问题,那就找个时间上生产呗,升级到生产环境,发现这个接口堵塞了,超时了。。。 + +![](http://img.topjavaer.cn/img/202308070010341.png) + +刚被记录到正向事件,可不想在被记录个负向时间。感觉去看日志。 + +发现日志就执行了将ids进行分隔,后面循环去创建CompletableFuture对象之后的代码都没有在执行了。然后我第一感觉测试是future.get()获取结果的时候堵塞了,所以一直没有结果返回。 + +#### 排查问题过程 + +我们要解决这个问题就要看看问题出现在哪里? + +当执行到这个接口时候我们第一时间看了看CPU的使用率: + +![](http://img.topjavaer.cn/img/202308070010837.png) + +这是访问接口之前: + +![](http://img.topjavaer.cn/img/202308070010992.png) + +发现执行这个接口时PID为10348的这个进程的CPU突然的高了起来。 + +紧接着使用`jps -l` :打印出我们服务进程的PID + +![](http://img.topjavaer.cn/img/202308070011114.png) + +PID为10348正式我们现在执行这个服务。 + +接着我就详细的看一下这个PID为10348的进程下哪里线程占用的高: + +发现这几个占用的相对高一点: + +![](http://img.topjavaer.cn/img/202308070011121.png) + +![](http://img.topjavaer.cn/img/202308070011008.png) + +紧接着使用jstack命令生成java虚拟机当前时刻的线程快照,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源 + +`jstack -l 10348 >/tmp/10348.log`,使用此命令将PID为10348的进程下所有线程快照输出到log文件中。 + +同时我们将线程比较的PID转换成16进制:printf "%x\n" 10411 + +![](http://img.topjavaer.cn/img/202308070012886.png) + +我们将转换成16进制的数值28ab,28a9在10348.log中搜索一下: + +![](http://img.topjavaer.cn/img/202308070012229.png) + +![](http://img.topjavaer.cn/img/202308070012075.png) + +看到线程的快照发现这不是本次修改的接口呀。看到日志4处但是也是用了CompletableFuture。找到对应4处的代码发现这是监听mq消息,然后异步去执行,代码类型这样: + +![](http://img.topjavaer.cn/img/202308070013815.png) + +经过查看日志发现这个mq消息处理很频繁,每秒都会有很多的数据上来。 + +![](http://img.topjavaer.cn/img/202308070013136.png) + +我们知道CompletableFuture默认是使用ForkJoinPool作为线程池。难道mq使用ForkJoinPool和我当前接口使用的都是同一个线程池中的线程?难道是共用的吗? + +MQ监听使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013857.png) + +我们当前接口使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013168.png) + +![](http://img.topjavaer.cn/img/202308070014642.png) + +![](http://img.topjavaer.cn/img/202308070014684.png) + +![](http://img.topjavaer.cn/img/202308070014348.png) + +它们使用的都是ForkJoinPool.commonPool()公共线程池中的线程! + +看到这个结论就很好理解了,我们目前修改的接口使用的线程池中的线程全部都被MQ消息处理占用,我们修改优化的接口得不到资源,所以一直处于等待。 + +同时我们在线程快照10348.log日志中也看到我们优化的接口对应的线程处于WAITING状态! + +![image-20230807001443599](http://img.topjavaer.cn/img/202308070014683.png) + +这里`- parking to wait for <0x00000000fe2081d8>`肯定也是MQ消费线程中的某一个。由于MQ消费消息比较多,每秒都会监听到大量的数据,线程的快照日志收集不全。所以在10348.log中没有找到,这不影响我们修改bug。问题的原因已经找到了。 + +#### 解决问题 + +上面我们知道两边使用的都是公共静态线程池,我们只要让他们各用各的就行了:自定义一个线程池:`ForkJoinPool pool = new ForkJoinPool();` + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + //重新创建一个ForkJoinPool对象就可以了 + ForkJoinPool pool = new ForkJoinPool(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getMonitoringCoverage(basicInfoList, id); + }); + return basicInfoList; + //在这里使用 + },pool); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +这样他们就各自用各自的线程池中的线程了。不会存在资源的等待现场了。 + +#### 总结: + +之所以测试环境和开发环境没有出现这样的问题是因为这两个环境mq没有监听到消息。大量的消息都在生产环境中才会出现。由于测试环境的数据量达不到生产环境的数据量,所以有些问题在测试环境体验不出来。 + + + +> 原文链接:https://juejin.cn/post/7165704556540755982 \ No newline at end of file diff --git a/docs/zsxq/share/oom.md b/docs/zsxq/share/oom.md new file mode 100644 index 0000000..d378bbf --- /dev/null +++ b/docs/zsxq/share/oom.md @@ -0,0 +1,13 @@ +记录一次内存泄露排查的过程。 + +最近经常告警有空指针异常报出,于是找到运维查日志定位到具体是哪一行代码抛出的空指针异常,发现是在解析cookie的一个方法内,调用`HttpServletRequest.getServerName()`获取不到抛出的NPE,这个获取服务名获取不到,平时都没有出现过的问题,最近也没有发版,那初步怀疑应该前端或者传输过程有问题导致获取不到参数。 + +后续找了运维查了ng的日志,确实存在状态码为**499**的错误码,查了一下这个是**客户端主动关闭请求或者客户端网络断掉时**报的错误码,那也就是前端断开了请求。 + +继续排查为啥前端会中断请求的原因,问了前端同学说是超时时间设置了10秒,又看了日志,确实是有处理时间超过10秒的,那问题大概定位到了。 + +接下来就是分析处理时长为什么会那么长,看了报错的时候请求量并没有很大,后续让运维查了机器有几台cpu用量处于30%左右,明显高于另外几台3%,而且499错误的集中在cpu用量高的几台,怀疑是否是内存问题导致,让运维跑了`jstat -gcutil`看了一下,确实存在full GC问题,又跑了`jmap -dump`下了dump文件,定位到是ip限流的方法,有一个清除Map的方法在多线程并发情况下没有生效,导致内存泄露。 + +知道问题后反推感觉就一切的疑问有了结果,ng报499是前置超时,超时是服务频繁full gc导致stw,无法处理请求导致耗时增加,ng探活接口在机器stw期间无法响应产生了error.log,而有几台机器cpu不高是因为之前重启过所以释放掉了内存没有触发full gc。 + +后续处理是改进了ip限流的方法,测试环境复现问题和改动限流方法,通过guaua的LoadingCache监听器的方式过期自动处理,这次问题嵌套问题比较多,由于上报量低,ng侧报了499没有纳入监控范围,而且机器由于重启没有快速发现问题,后续改进是代码侧能复用规范代码最好复用,不要重复造轮子。 \ No newline at end of file diff --git a/docs/zsxq/share/slow-query.md b/docs/zsxq/share/slow-query.md new file mode 100644 index 0000000..4d3ab7f --- /dev/null +++ b/docs/zsxq/share/slow-query.md @@ -0,0 +1,62 @@ +**最近的生产慢查询问题分析与总结** + +**1.问题描述** + +前几天凌晨出现大量慢查询告警,经DBA定位为某个子系统涉及的一条查询语句出现慢查询,引起数据库服务器的cpu使用率突增,触发大量告警,查看生产执行计划发现慢查询为索引跳变引起;具体出现问题的sql语句如下: + +``` +select * from ( select t.goods id as cardid,p.validate as validate, p.create timecreateTime + p.repay_coupon type as repaycoupontype,p.require anount as requireAmount, p.goods tag as goodsTag, + p.deduction anount as deductionAmount, p.repay coupon remark as repay(ouponkemark, + p.sale_point as salepoint from user_prizes p join trans_order t + on p.user_id = t.user_id and p.order_no = t.order_no + where p.user_id = #{user_id} and p.wmkt_type = '6' and t.status = 's' and p.equity_code_status = 'N' + and p.create_time >= #{beginDate} + order by p.create_time dese limit 1000) as cc group by cc.cardid: +``` + + 该sql为查询三个月内满足条件的还款代金券列表,其中user_prizes表和trans_order表都是大表,数据量达到亿级别,user_prizes表有如下几个索引: + +```sql +PRIMARY KEY ('merchant_order_no ,partition key ) USING BTREE, +KEY “thirdOrderNo" ("third_order_no","app_id"), +KEY "createTime" ("create_time"), +KEY "userId" ("user_id"), +KEY "time_activity" ("create_time', 'activity_name') +``` + + 正常情况下该语句走的userId索引,当天零点后该sql语句出现索引跳变,走了createTime索引,导致出现慢查询。 + +**2.问题处理方式** + + 生产定位到问题后,因该sql的查询场景为前端触发,当天为账单日,请求量大,DBA通过kill脚本临时进行处理,同时准备增加强制索引优化的紧急版本,通过加上强制索引force index(userId)处理索引跳变,DBA也同步在备库删除createTime索引观察效果,准备进行主备切换尝试解决,但在备库执行索引删除后查看执行计划发现又走了time_activity索引,最后通过发布增加强制索引优化的紧急版本进行解决。 + +**3.问题分析** + +MySQL优化器选择索引的目的是找到一个最优的执行方案,并用最小的代价去执行语句,扫描行数是影响执行代价的重要因素,扫描行数越少,意味着访问磁盘数据次数越少,消耗的cpu资源越少,除此之外,优化器还会结合是否使用临时表,以及是否排序等因素综合判断。 + +出现索引跳变的这个sql有order by create_time,且create_time为索引,user_prizes表是按月分区的,数据总量为1亿400多万,通过统计生产数据量分布情况发现,近几个月的分区数据量如下: + +![](http://img.topjavaer.cn/img/202307102303308.png) + + 该sql7月1日零点后查询的是4月1日之后的数据,3月份分区的数据量为958万多,4月之后分区数量都保持在500多万。4月份之后分区数据减少了,可能是这个原因导致优化器认为走时间索引createTime的区分度更高,同时还可以避免排序,因而选择了时间索引。查看索引跳变后的执行计划如下: + +![](http://img.topjavaer.cn/img/202307102303994.png) + +走createTime索引虽然可以避免排序,但从执行计划的type=range可看出为索引范围的扫描,根据索引createTime扫描记录,通过索引叶子节点的主键值回表查找完整记录,然后判断记录中满足sql过滤条件的数据,再将结果进行返回,而该语句为查找满足条件的1000条数据,正常情况下一个user_id满足条件的数据量不会超过1000条,要找到所有满足条件的记录就是索引范围的扫描加回表查询,加上查询范围内的数据量大,因此走createTime索引就会非常慢。 + +**4.总结** + +由于mysql在真正执行语句的时候,并不能准确的知道满足这个条件的记录有多少,只能通过统计信息来估算记录,而优化器并不是非常智能的,就有可能发生索引跳变的情况,这种情况很难在测试的时候复现出来,生产也可能是突然出现,所以我们只能在使用上尽量的去降低索引发生跳变的可能性,尽量避免出现该问题。我们可以在创建索引和使用sql的时候通过以下几个点进行检视。 + +(1) 索引的创建 + + 创建索引的时候要注意尽量避免创建单列的时间字段(createTime、updateTime)索引,避免留坑,因为很多场景都可能用到时间字段进行排序,有排序的情况若排序字段又是单列索引字段,就可能引起索引跳变,如果需要使用时间字段作为索引时,尽量使用联合索引,且时间字段放在后面;高效的索引应遵循高区分度字段+避免排序的原则。 + + 创建索引的时候也要尽量避免索引重复,且一张表的索引个数也要控制好,索引过多也会影响增删改的效率。 + +(2) sql的检视 + + 检视历史和新增的sql是否有order by,且order by的第一个字段是否有单列索引,这种存在索引跳变的风险,需要具体分析后进行优化; + + 写sql语句的时候,尽可能简单化,像union、排序等尽量少在sql中实现,减少sql慢查询的风险。 \ No newline at end of file diff --git a/docs/zsxq/share/spring-upgrade-copy-problem.md b/docs/zsxq/share/spring-upgrade-copy-problem.md new file mode 100644 index 0000000..07b2f5c --- /dev/null +++ b/docs/zsxq/share/spring-upgrade-copy-problem.md @@ -0,0 +1,146 @@ +最近内部组件升级到spring5.3.x的时候对象拷贝内容不全,定位分析总结如下(可直接拉到最后看结论和解决办法): + +**1.现象:** + +源对象的类里有个内部类的成员变量,是List类型,,List的元素类型是自己的内部静态类 + +目标对象的类里有个内部类的成员变量,也是List类型,List的元素类型是自己的内部静态类 + +源对象的代码示例如下(省略了get set方法): + +```java +/** + * 源对象 + * @author dabin + * + */ +public class Rsp_07300240_01 { + private int totals; + private List contracts;//合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_07300240_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +目标对象的代码示例如下(省略了get set方法): + +```java +public class Rsp_04301099_01 { + @RmbField(seq = 1, title = "总条数") + private int totals; + @RmbField(seq = 2, title = "合同列表") + // 这里是自己的内部类 + private List contracts;// 合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash +@Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_04301099_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +单元测试验证代码如下: + +```java +public class SpringBeanCopyUtilTest { + @Test + public void testBeanCopy() { + Rsp_07300240_01 orgResp = new Rsp_07300240_01(); + orgResp.setTotals(100); + List contracts = new ArrayList<>(); + Rsp_07300240_01.Contract cc = new Rsp_07300240_01.Contract(); + cc.setConstractId("aaa"); + contracts.add(cc); + orgResp.setContracts(contracts); + Rsp_04301099_01 destResp = new Rsp_04301099_01(); + System.out.println("源对象的值:" + orgResp); + System.out.println("复制前的值:" + destResp); + BeanUtils.copyProperties(orgResp, destResp); + System.out.println("Spring版本" + BeanUtils.class.getPackage().getImplementationVersion()); + System.out.println("复制后的值:" + destResp); +// if (destResp.getContracts() != null && destResp.getContracts().size() > 0) { +// System.out.println("复制后的list成员类型是:" + destResp.getContracts().get(0)); +// } + } +} +``` + +分别依赖spring 5.2.4和5.3.9版本,运行结果如下: + +```java +源对象的值:Rsp_07300240_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] +复制前的值:Rsp_04301099_01 [totals=0, contracts=null] + +Spring版本5.2.4.RELEASE +复制后的值:Rsp_04301099_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] + +Spring版本5.3.9 +复制后的值:Rsp_04301099_01 [totals=100, contracts=null] +``` + +**2.分析** + +可以看到在依赖spring 5.3.x的时候,contracts的值是没有复制过来的。 + +但是也可以看到在依赖spring 5.2.x的时候,contracts的值是直接设置的引用,List的成员变量类型是 Rsp_07300240_01.Contract,Rsp_04301099_01.Contract。 + +这个其实也是有问题的。但是为啥在业务逻辑中没有暴雷呢? + +经核实,业务代码中,是在返回应答对象之前执行的 org.springframework.beans.BeanUtils.copyProperties 操作,执行完之后,立即返回了对象,然后内部使用的框架,直接使用jackson进行系列化,此时类型信息已经擦除,不涉及类型转换,所以正常生成了json字符串。 + +而上面示例中被注释的代码里,如果启用的话,测试的时候就会立即报错,提示类型转换异常。 + +对比代码可以发现: + +![](https://article-images.zsxq.com/FvjCdaKbUj3E3fLVKRkvtve2yIa6) + +经过一番搜索,原来是在2019年的时候就有人向Spring社区提了bug,然后spring增加了泛型判断逻辑,杜绝了错误的赋值,在5.3.x中修复了这个bug。 + +https://github.com/spring-projects/spring-framework/issues/24187 + +由于平时大部分使用场景都是执行BeanUtils.copyProperties后立即取出里面的对象进行操作,这种情况下,就会提前触发bug,然后调用方自己想办法规避掉spring bug。 + +恰好内部使用的xx框架在BeanUtils.copyProperties之后没有显式的操作成员对象,因此一直没有触发bug,直到升级到spring 5.3.x时才暴雷。 + +**3.解决办法:** + +先回退版本,然后检视所有调用BeanUtils.copyProperties的地方,针对触发bug的这种场景优化代码,比如把两个内部静态class合并使用一个公共的class。 \ No newline at end of file diff --git "a/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" new file mode 100644 index 0000000..1812f1d --- /dev/null +++ "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" @@ -0,0 +1,219 @@ +本文主要介绍不同业务所对应的 SQL 写法进行归纳总结而来。在这里分享给大家。 + +- 本文所讲述 sql 语法都是基于 MySQL 8.0 + +# 一、ORDER BY FIELD() 自定义排序逻辑 + +MySql 中的排序 ORDER BY 除了可以用 ASC 和 DESC,还可以通过 **ORDER BY FIELD(str,str1,...)** 自定义字符串/数字来实现排序。这里用 order_diy 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150751904.png) + + ORDER BY FIELD(str,str1,...) 自定义排序sql如下: + +```sql +SELECT * from order_diy ORDER BY FIELD(title,'九阴真经', +'降龙十八掌','九阴白骨爪','双手互博','桃花岛主', +'全真内功心法','蛤蟆功','销魂掌','灵白山少主'); +``` + +查询结果如下: + +![](http://img.topjavaer.cn/img/202309150752762.png) + + + + 如上,我们设置自定义排序字段为 title 字段,然后将我们自定义的排序结果跟在 title 后面。 + +# 二、CASE 表达式 + +**case when then else end**表达式功能非常强大可以帮助我们解决 `if elseif else` 这种问题,这里继续用 order_diy 表举例,假如我们想在 order_diy 表加一列 level 列,根据money 判断大于60就是高级,大于30就是中级,其余显示低级,sql 如下: + +```sql +SELECT *, +case when money > 60 then '高级' +when money > 30 then '中级' +else '低级' END level +from order_diy; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150752538.png) + +> 需要注意的就是 **case when then** 语句不匹配如果没有写 **else end** 会返回 null,影响数据展示。 + +# 三、EXISTS 用法 + +我猜大家在日常开发中,应该都对关键词 exists 用的比较少,估计使用 in 查询偏多。这里给大家介绍一下 exists 用法,引用官网文档: + +![](http://img.topjavaer.cn/img/202309150752021.png) + +可知 exists 后面是跟着一个子查询语句,它的作用是**根据主查询的数据,每一行都放到子查询中做条件验证,根据验证结果(TRUE 或者 FALSE),TRUE的话该行数据就会保留**,下面用 emp 表和 dept 表进行举例,表结构以及数据展示: + +![](http://img.topjavaer.cn/img/202309150753317.png) + +计入我们现在想找到 emp 表中 dept_name 与 dept表 中 dept_name 对应不上员工数据,sql 如下: + +```sql +SELECT * from emp e where exists ( +SELECT * from dept p where e.dept_id = p.dept_id +and e.dept_name != p.dept_name +) +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753427.png) + +我们通过 exists 语法将外层 emp 表全部数据 放到子查询中与一一与 dept 表全部数据进行比较,只要有一行记录返回true。画个图展示主查询所有记录与子查询交互如下: + +![](http://img.topjavaer.cn/img/202309150753553.png) + +- 第一条记录与子查询比较时,全部返回 false,所以第一行不展示。 +- 第二行记录与子查询比较时,发现 `销售部门` 与 dept 表第二行 `销售部` 对应不上,返回 true,所以主查询该行记录会返回。 +- 第二行以后记录执行结果同第一条。 + +# 四、GROUP_CONCAT(expr) 组连接函数 + +**GROUP_CONCAT(expr)** 组连接函数可以返回分组后指定字段的字符串连接形式,并且可以指定排序逻辑,以及连接字符串,默认为英文逗号连接。这里继续用 order_diy 表举例:sql 如下: + +```sql +SELECT name, GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-') +from order_diy GROUP BY name ORDER BY NULL; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753797.png) + +如上我们通过 **GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-')** 语句,指定分组连接 title 字段并按照 id 排序,设置连接字符串为 `-`。 + +# 五、自连接查询 + +自连接查询是 sql 语法里常用的一种写法,掌握了自连接的用法我们可以在 sql 层面轻松解决很多问题。这里用 tree 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150753344.png) + +tree 表中通过 pid 字段与 id 字段进行父子关联,假如现在有一个需求,我们想按照父子层级将 tree 表数据转换成 `一级职位 二级职位 三级职位` 三个列名进行展示,sql 如下: + +```sql +SELECT t1.job_name '一级职位', t2.job_name '二级职位', t3.job_name '三级职位' +from tree t1 join tree t2 on t1.id = t2.pid left join tree t3 on t2.id = t3.pid +where t1.pid = 0; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150753141.png) + +我们通过 **tree t1 join tree t2 on t1.id = t2.pid** 自连接展示 `一级职位 二级职位`,再用 **left join tree t3 on t2.id = t3.pid** 自连接展示 `二级职位 三级职位`,最后通过**where 条件 t1.pid = 0**过滤掉非一级职位的展示,完成这个需求。 + +# 六、更新 emp 表和 dept 表关联数据 + +这里继续使用上文提到的 emp 表和 dept 表,数据如下: + +![](http://img.topjavaer.cn/img/202309150754679.png) + +可以看到上述 emp 表中 jack 的部门名称与 dept 表实际不符合,现在我们想将 jack 的部门名称更新成 dept 表的正确数据,sql 如下: + +```sql +update emp, dept set emp.dept_name = dept.dept_name +where emp.dept_id = dept.dept_id; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754517.png) + +我们可以直接关联 emp 表和 dept 表并设置关联条件,然后更新 emp 表的 dept_name 为 dept 表的 dept_name。 + +# 七、ORDER BY 空值 NULL 排序 + +ORDER BY 字句中可以跟我们要排序的字段名称,但是当字段中存在 null 值时,会对我们的排序结果造成影响。我们可以通过 **ORDER BY IF(ISNULL(title), 1, 0)** 语法将 null 值转换成0或1,来达到将 null 值放到前面还是后面进行排序的效果。这里继续用 order_diy 表举例,sql 如下: + +```sql +SELECT * FROM order_diy ORDER BY IF(ISNULL(title), 0, 1), money; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754872.png) + +# 八、with rollup 分组统计数据的基础上再进行统计汇总 + +MySql 中可以使用 with rollup 在分组统计数据的基础上再进行统计汇总,即用来得到 group by 的汇总信息。这里继续用order_diy 表举例,sql 如下: + +```sql +SELECT name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754016.png) + +可以看到通过 **GROUP BY name WITH ROLLUP** 语句,查询结果最后一列显示了分组统计的汇总结果。但是 name 字段最后显示为 null,我们可以通过 `coalesce(val1, val2, ...)` 函数,这个函数会返回参数列表中的第一个非空参数。 + +```sql +SELECT coalesce(name, '总金额') name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754845.png) + +# 九、with as 提取临时表别名 + +with as 语法需要 MySql 8.0以上版本,它有一个别名叫做 CTE,官方对它的说明如下 + +> 公用表表达式 (CTE) 是一个命名的临时结果集,它存在于单个语句的范围内,稍后可以在该语句中引用,可以多次引用。 + +它的作用主要是提取子查询,方便后续共用,更多情况下会用在数据分析的场景上。 + +如果一整句查询中**多个子查询都需要使用同一个子查询**的结果,那么就可以用 with as,将共用的子查询提取出来,加个别名。后面查询语句可以直接用,对于大量复杂的SQL语句起到了很好的优化作用。这里继续用 order_diy 表举例,这里使用 with as 给出 sql 如下: + +```sql +-- 使用 with as +with t1 as (SELECT * from order_diy where money > 30), +t2 as (SELECT * from order_diy where money > 60) +SELECT * from t1 +where t1.id not in (SELECT id from t2) and t1.name = '周伯通'; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754613.png) + +这个 sql 查询了 order_diy 表中 money 大于30且小于等于60之间并且 name 是周伯通的记录。可以看到使用 CTE 语法后,sql写起来会简洁很多。 + +# 10、存在就更新,不存在就插入 + +MySql 中通过**on duplicate key update**语法来实现存在就更新,不存在就插入的逻辑。插入或者更新时,它会根据表中主键索引或者唯一索引进行判断,如果主键索引或者唯一索引有冲突,就会执行**on duplicate key update**后面的赋值语句。 这里通过 news 表举例,表结构和说数据展示,其中 news_code 字段有唯一索引: + +![](http://img.topjavaer.cn/img/202309150754511.png) + +添加sql: + +```sql +-- 第一次执行添加语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻3', '小花', 'wx-0003') +on duplicate key update news_title = '新闻3'; +-- 第二次执行修改语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻4', '小花', 'wx-0003') +on duplicate key update news_title = '新闻4'; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150755137.png) + +# 总结 + +到这里,本文所分享的10个高级sql写法就全部介绍完了,希望对大家日常开发 sql 编写有所帮助。 + + + +参考链接:https://juejin.cn/post/7209625823580766264 \ No newline at end of file diff --git "a/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" new file mode 100644 index 0000000..40a8c05 --- /dev/null +++ "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" @@ -0,0 +1,49 @@ +# 由“ YYYY-MM-dd ”引发的bug + +跟大家分享网上看到的一篇文章。 + +## 前言 + +在使用一些 App 的时候,竟然被我发现了一个应该是由于前端粗心而导致的 bug,在 2019.12.30 出发,结果 App 上显示的是 2020.12.30(吓得我以为我的订单下错了,此处是不是该把程序员拉去祭天了)。 + +鉴于可能会有程序员因此而被拉去祭天,而我以前学 Java 的时候就有留意过这个问题,所以我还是把这个问题拿出来说一下,希望能尽量避免这方面的粗心大意(毕竟这种问题也很难测出来)。 + +## 正文 + +```java +public class DateTest { + public static void main(String[] args) { + Calendar calendar = Calendar.getInstance(); + calendar.set(2019, Calendar.AUGUST, 31); + Date strDate = calendar.getTime(); + DateFormat formatUpperCase = new SimpleDateFormat("yyyy-MM-dd"); + System.out.println("2019-08-31 to yyyy-MM-dd: " + formatUpperCase.format(strDate)); + formatUpperCase = new SimpleDateFormat("YYYY-MM-dd"); + System.out.println("2019-08-31 to YYYY/MM/dd: " + formatUpperCase.format(strDate)); + } +} +``` + +我们来看下运行结果: + +``` +2019-08-31 to yyyy-MM-dd: 2019-08-31 +2019-08-31 to YYYY/MM/dd: 2019-08-31 +``` + +如果我们日期改成 12.31: + +``` +2019-12-31 to yyyy-MM-dd: 2019-12-31 +2019-12-31 to YYYY-MM-dd: 2020-12-31 +``` + +问题就出现了是吧,虽然是一个小小的细节,但是用户看了也会一脸懵,但是我们作为开发者,不能懵啊,赶紧文档查起来: + +![](http://img.topjavaer.cn/img/202309081043248.png) + +y:year-of-era;正正经经的年,即元旦过后; + +Y:week-based-year;只要本周跨年,那么这周就算入下一年;就比如说今年(2019-2020) 12.31 这一周是跨年的一周,而 12.31 是周二,那使用 YYYY 的话会显示 2020,使用 yyyy 则会从 1.1 才开始算是 2020。 + +这虽然是个很小的知识点,但是也有很多人栽到坑里,在此记录一下~ \ No newline at end of file diff --git a/docs/zsxq/springboot-inner-material.md b/docs/zsxq/springboot-inner-material.md new file mode 100644 index 0000000..e69de29 diff --git "a/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" new file mode 100644 index 0000000..72208d6 --- /dev/null +++ "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" @@ -0,0 +1,95 @@ +你好,我是大彬~ + +这段时间春招陆续开始了,很多大厂(美团、阿里等)开启了24届春招补录,也有公司开始暑期实习招聘流程了。 + +看了小破站访问量也是嘎嘎上涨,看来大家都在偷偷卷啊。。 + +![](http://img.topjavaer.cn/img/202403020934945.png) + +最近也有挺多同学私信我,我总结了一些比较常见的问题,比如有没有内推渠道?怎么写好简历?面试八股文应该怎么准备等等。在这里分享一下~ + +## 1、有没有内推渠道? + +内推其实是换一种方式投简历,好处是可以**免简历筛选**,有时还能免笔试(比较少)。 + +为了感谢大家一直以来的支持,我特地新建了各个城市的**内推群**。 + +你可以在群里发布内推信息、招聘信息,也可以在群里交流面试、工作问题。 + +![](http://img.topjavaer.cn/img/202403021124419.png) + +**进群方式** + +添加大彬的个人微信 dabinjava 或者 i_am_dabin,备注:**内推+工作城市**,我拉你进群 + +![](http://img.topjavaer.cn/img/202403021107571.png) + +> **被添加频繁的话,请半个小时之后重试** + +## 2、怎么写好简历 + +分成以下几个方面来编写: + +- **简历模板** +- **基本信息** +- **教育经历** +- **专业技能** +- **项目经验(实习经历、工作经历)** +- **奖项荣誉** + +其中最重要的应该是项目经验这一模块,要写清楚你在项目中的角色,以及个人职责,具体负责哪一块业务等。可以重点突出**攻克的技术问题**,或者参与过的**性能优化**。 + +最好能有**数据支撑**,**数据是最有说服力的**,比如SQL执行时间从xx秒到xx毫秒、xx优化给公司节省了多少万的服务器成本等,这些在面试中很加分,比较能体现个人的实战能力。 + +在这里也吐槽一下,我帮[知识星球](https://mp.weixin.qq.com/s/VdKnOvkQa5rrqgdllSJtUw)小伙伴修改了大概**220**多份简历,很多同学简历上的项目经验,真的写的跟流水账一样。比如: + +1. 实现了xx功能 +2. 做了xx页面 +3. ... + +说实话,看完简历印象分就大打折扣了,简历关可能就被pass了。面试机会不多,特别是现在大环境不好,更要认真对待,尽力把每一个环节做好。 + +建议大家在写项目经验的时候,可以按照我整理的**模板**(出于 xx 考虑,使用 xx 解决了 xx 问题 ,达到了 xxx 效果等),这样去写,效果会好很多。 + +如果你确实不知道该如何写自己的简历,可以看看我星球的文章——**手把手教你写好一份简历** + +> 链接:https://t.zsxq.com/09wZgDO7v + +如果你还是觉得自己简历写的不够好,可以找我帮你**修改**简历,需要的可以加我微信 i_am_dabin,备注【**简历修改**】(**付费**的,介意的勿扰)。 + +![](http://img.topjavaer.cn/img/202403021121570.png) + +下面是我修改简历的一些案例。 + +![](http://img.topjavaer.cn/img/202403031119562.png) + +![](http://img.topjavaer.cn/img/202403031120321.png) + +![](http://img.topjavaer.cn/img/image-20230111224753836.png) + +## 3、先面试大公司还是小公司? + +大彬建议把自己比较**心仪的大厂放在后面**,拿一些小公司或者意愿不大的中大公司试手,积攒经验,把面试技术问题范围记录下来,查漏补缺,最后冲自己的目标公司。面试前期就是广撒网,捞小鱼积累经验, 把想捞的大鱼放在后面。 + +## 4、面试八股文应该怎么准备? + +在这里当然要推荐一下我的小破站了。 + +> 地址:topjavaer.cn + +这是一个优质的八股文网站,内容涵盖**Java基础、并发、JVM、MySQL、Springboot、MyBatis、Redis、消息队列、微服务**等,非常全面,建议面试前刷上两遍! + +![](http://img.topjavaer.cn/img/202403021025198.png) + +不少同学靠着网站上面整理的面试题找到了工作,质量还是不错的。 + +建议大家面试前先把网站上面的面试题刷上2-3遍~ + +![](http://img.topjavaer.cn/img/202403021036763.png) + +![](http://img.topjavaer.cn/img/202403021113383.png) + + + +最后祝大家春招顺利~ + diff --git "a/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" new file mode 100644 index 0000000..57a099a --- /dev/null +++ "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" @@ -0,0 +1,95 @@ +线上CPU飙升100%问题排查 + +对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),特此整理排查方法一篇,供大家参考讨论提高。 + +## 二、问题复现 + +线上系统突然运行缓慢,CPU飙升,甚至到100%,以及Full GC次数过多,接着就是各种报警:例如接口超时报警等。此时急需快速线上排查问题。 + +## 三、问题排查 + +不管什么问题,既然是CPU飙升,肯定是查一下耗CPU的线程,然后看看GC。 + +### 3.1 核心排查步骤 + +1.执行“top”命令``:查看所有进程占系统CPU的排序。极大可能排第一个的就是咱们的java进程(COMMAND列)。PID那一列就是进程号。`` + +2.执行“top -Hp 进程号”命令:查看java进程下的所有线程占CPU的情况。 + +3.执行“printf "%x\n 10"命令 :后续查看线程堆栈信息展示的都是十六进制,为了找到咱们的线程堆栈信息,咱们需要把线程号转成16进制。例如,printf "%x\n 10-》打印:a,那么在jstack中线程号就是0xa. + +4.执行 “jstack 进程号 | grep 线程ID” 查找某进程下-》线程ID(jstack堆栈信息中的nid)=0xa的线程状态。如果“"VM Thread" os_prio=0 tid=0x00007f871806e000 nid=0xa runnable”,第一个双引号圈起来的就是线程名,如果是“VM Thread”这就是虚拟机GC回收线程了 + +5.执行“jstat -gcutil 进程号 统计间隔毫秒 统计次数(缺省代表一致统计)”,查看某进程GC持续变化情况,如果发现返回中FGC很大且一直增大-》确认Full GC! 也可以使用“jmap -heap 进程ID”查看一下进程的堆内从是不是要溢出了,特别是老年代内从使用情况一般是达到阈值(具体看垃圾回收器和启动时配置的阈值)就会进程Full GC。 + +6.执行“jmap -dump:format=b,file=filename 进程ID”,导出某进程下内存heap输出到文件中。可以通过eclipse的mat工具查看内存中有哪些对象比较多 + +### 3.2 原因分析 + +#### 1.内存消耗过大,导致Full GC次数过多 + +执行步骤1-5: + +- 多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程-》上一节步骤2 +- 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。--》上一节步骤5 + +确定是Full GC,接下来找到**具体原因**: + +- 生成大量的对象,导致内存溢出-》执行步骤6,查看具体内存对象占用情况。 +- 内存占用不高,但是Full GC次数还是比较多,此时可能是代码中手动调用 System.gc()导致GC次数过多,这可以通过添加 -XX:+DisableExplicitGC来禁用JVM对显示GC的响应。 + +#### 2.代码中有大量消耗CPU的操作,导致CPU过高,系统运行缓慢; + +执行步骤1-4:在步骤4jstack,可直接定位到代码行。例如某些复杂算法,甚至算法BUG,无限循环递归等等。 + +#### 3.由于锁使用不当,导致死锁。 + +执行步骤1-4: 如果有死锁,会直接提示。关键字:deadlock.步骤四,会打印出业务死锁的位置。 + +造成死锁的原因:最典型的就是2个线程互相等待对方持有的锁。 + +#### 4.随机出现大量线程访问接口缓慢。 + +代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;平时消耗的CPU不多,而且占用的内存也不高。 + +思路: + +首先找到该接口,通过压测工具不断加大访问力度,大量线程将阻塞于该阻塞点。 + +执行步骤1-4: + +``` +"http-nio-8080-exec-4" #31 daemon prio=5 os_prio=31 tid=0x00007fd08d0fa000 nid=0x6403 waiting on condition [0x00007000033db000] + + java.lang.Thread.State: TIMED_WAITING (sleeping)-》期限等待 + + at java.lang.Thread.sleep(Native Method) + + at java.lang.Thread.sleep(Thread.java:340) + + at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) + + at com.*.user.controller.UserController.detail(UserController.java:18)-》业务代码阻塞点 +``` + +如上图,找到业务代码阻塞点,这里业务代码使用了TimeUnit.sleep()方法,使线程进入了TIMED_WAITING(期限等待)状态。 + +#### 5.某个线程由于某种原因而进入WAITING状态,此时该功能整体不可用,但是无法复现; + +执行步骤1-4:jstack多查询几次,每次间隔30秒,对比一直停留在parking 导致的WAITING状态的线程。例如CountDownLatch倒计时器,使得相关线程等待->AQS->LockSupport.park()。 + +``` +"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9de08c7000 nid=0x5603 waiting on condition [0x0000700001f89000] +java.lang.Thread.State: WAITING (parking) ->无期限等待 +at sun.misc.Unsafe.park(Native Method) +at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) +at com.*.SyncTask.lambda$main$0(SyncTask.java:8)-》业务代码阻塞点 +at com.*.SyncTask$$Lambda$1/1791741888.run(Unknown Source) +at java.lang.Thread.run(Thread.java:748) +``` + + + +## 四、总结 + +按照3.1节的6个步骤走下来,基本都能找到问题所在。 \ No newline at end of file diff --git "a/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" new file mode 100644 index 0000000..db9492d --- /dev/null +++ "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" @@ -0,0 +1,34 @@ +大家好,我是大彬。 + +最近是面试高峰期,有挺多读者来咨询我有没有一些**大厂的面试题目**,想在面试前重点复习一下,“抱佛腿”~ + +![](http://img.topjavaer.cn/img/202403161558009.png) + +![](http://img.topjavaer.cn/img/202403161559190.png) + +刚好我这几天有空,整理了近**300**家公司的面试题目,按照**社招、校招、实习**分类。 + +![](http://img.topjavaer.cn/img/202403161603239.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161616030.png) + +![](http://img.topjavaer.cn/img/202403161618740.png) + +![](http://img.topjavaer.cn/img/202403161620835.png) + +![](http://img.topjavaer.cn/img/202403161624295.png) + +![](http://img.topjavaer.cn/img/202403161649106.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161626248.png) + +![](http://img.topjavaer.cn/img/202403161628679.png) + +面试题目**获取方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试题目**】。PS:有偿,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202403161641601.png) + diff --git "a/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" new file mode 100644 index 0000000..a4850d8 --- /dev/null +++ "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" @@ -0,0 +1,59 @@ +为方便大家交流,大彬新建了**面试真题共享群,**群里可以讨论面试问题、发布内推信息,群成员可以共享各大公司的面试真题,同时可以**永久**查看大彬整理的**面试真题VIP**手册(目前整理了超过**300**家公司的面试题目)。 + +群友**评价**: + +![](http://img.topjavaer.cn/img/202404221801755.png) + +![](http://img.topjavaer.cn/img/202404221801618.png) + +![](http://img.topjavaer.cn/img/202404221801810.png) + +面试真题分享群截图: + +![](http://img.topjavaer.cn/img/202404221802888.png) + +![](http://img.topjavaer.cn/img/202404221802316.png) + +![](http://img.topjavaer.cn/img/202404221802739.png) + +![](http://img.topjavaer.cn/img/202404221802506.png) + +![](http://img.topjavaer.cn/img/202404221802781.png) + +![](http://img.topjavaer.cn/img/202404221802200.png) + +![](http://img.topjavaer.cn/img/202404221803753.png) + +![](http://img.topjavaer.cn/img/202404221817070.png) + + + +**面试真题VIP**手册截图: + +![](http://img.topjavaer.cn/img/202404221808531.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221807102.png) + +![](http://img.topjavaer.cn/img/202404221807282.png) + +![](http://img.topjavaer.cn/img/202404221806214.png) + +![](http://img.topjavaer.cn/img/202404221806220.png) + +![](http://img.topjavaer.cn/img/202404221806613.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221806258.png) + +![](http://img.topjavaer.cn/img/202404221806239.png) + + + +面试共享群**进群方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试真题**】。 + +PS:**有偿**,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202404221805547.png) \ No newline at end of file diff --git a/img/1588431199776.png b/img/1588431199776.png deleted file mode 100644 index c28de4d..0000000 Binary files a/img/1588431199776.png and /dev/null differ diff --git a/img/ConcurrentHashMap-segment.jpg b/img/ConcurrentHashMap-segment.jpg deleted file mode 100644 index 4b709a3..0000000 Binary files a/img/ConcurrentHashMap-segment.jpg and /dev/null differ diff --git a/img/JDK1.8-ConcurrentHashMap-Structure.jpg b/img/JDK1.8-ConcurrentHashMap-Structure.jpg deleted file mode 100644 index a443bb3..0000000 Binary files a/img/JDK1.8-ConcurrentHashMap-Structure.jpg and /dev/null differ diff --git a/img/Java-Collections.jpeg b/img/Java-Collections.jpeg deleted file mode 100644 index cf9071f..0000000 Binary files a/img/Java-Collections.jpeg and /dev/null differ diff --git a/img/all-table-search.jpg b/img/all-table-search.jpg deleted file mode 100644 index 8c0605a..0000000 Binary files a/img/all-table-search.jpg and /dev/null differ diff --git a/img/aqs.png b/img/aqs.png deleted file mode 100644 index 08ceae5..0000000 Binary files a/img/aqs.png and /dev/null differ diff --git a/img/bean-life-cycle.jpg b/img/bean-life-cycle.jpg deleted file mode 100644 index 628017f..0000000 Binary files a/img/bean-life-cycle.jpg and /dev/null differ diff --git a/img/bloom-filter.jpg b/img/bloom-filter.jpg deleted file mode 100644 index 96e3f88..0000000 Binary files a/img/bloom-filter.jpg and /dev/null differ diff --git a/img/cms-collector.png b/img/cms-collector.png deleted file mode 100644 index 3ed3bd8..0000000 Binary files a/img/cms-collector.png and /dev/null differ diff --git a/img/concurrent/executors-ali.png b/img/concurrent/executors-ali.png deleted file mode 100644 index a1a3858..0000000 Binary files a/img/concurrent/executors-ali.png and /dev/null differ diff --git a/img/concurrent/synchronized-block.png b/img/concurrent/synchronized-block.png deleted file mode 100644 index 9c905bb..0000000 Binary files a/img/concurrent/synchronized-block.png and /dev/null differ diff --git a/img/concurrent/synchronized-method.png b/img/concurrent/synchronized-method.png deleted file mode 100644 index 2b471ef..0000000 Binary files a/img/concurrent/synchronized-method.png and /dev/null differ diff --git a/img/condition-await.png b/img/condition-await.png deleted file mode 100644 index 014b97b..0000000 Binary files a/img/condition-await.png and /dev/null differ diff --git a/img/condition-signal.png b/img/condition-signal.png deleted file mode 100644 index ea388d9..0000000 Binary files a/img/condition-signal.png and /dev/null differ diff --git a/img/condition-wait-queue.png b/img/condition-wait-queue.png deleted file mode 100644 index 9d2f803..0000000 Binary files a/img/condition-wait-queue.png and /dev/null differ diff --git a/img/cover-index.png b/img/cover-index.png deleted file mode 100644 index f36d390..0000000 Binary files a/img/cover-index.png and /dev/null differ diff --git a/img/direct-pointer.png b/img/direct-pointer.png deleted file mode 100644 index 2449ab7..0000000 Binary files a/img/direct-pointer.png and /dev/null differ diff --git a/img/docker/docker.jpg b/img/docker/docker.jpg deleted file mode 100644 index a64ca63..0000000 Binary files a/img/docker/docker.jpg and /dev/null differ diff --git a/img/explain-all.png b/img/explain-all.png deleted file mode 100644 index 7b49bf4..0000000 Binary files a/img/explain-all.png and /dev/null differ diff --git a/img/explain-const.png b/img/explain-const.png deleted file mode 100644 index 80a4fb4..0000000 Binary files a/img/explain-const.png and /dev/null differ diff --git a/img/explain-id.png b/img/explain-id.png deleted file mode 100644 index 86fe346..0000000 Binary files a/img/explain-id.png and /dev/null differ diff --git a/img/explain-range.png b/img/explain-range.png deleted file mode 100644 index b2a4d3e..0000000 Binary files a/img/explain-range.png and /dev/null differ diff --git a/img/explain-ref.png b/img/explain-ref.png deleted file mode 100644 index 5e52fc8..0000000 Binary files a/img/explain-ref.png and /dev/null differ diff --git a/img/g1-region.jpg b/img/g1-region.jpg deleted file mode 100644 index 99f595a..0000000 Binary files a/img/g1-region.jpg and /dev/null differ diff --git a/img/gc-root-refer.png b/img/gc-root-refer.png deleted file mode 100644 index f326103..0000000 Binary files a/img/gc-root-refer.png and /dev/null differ diff --git a/img/git-status.png b/img/git-status.png deleted file mode 100644 index f93b1c3..0000000 Binary files a/img/git-status.png and /dev/null differ diff --git a/img/heap-structure.png b/img/heap-structure.png deleted file mode 100644 index 7934357..0000000 Binary files a/img/heap-structure.png and /dev/null differ diff --git a/img/image-20200520234137916.png b/img/image-20200520234137916.png deleted file mode 100644 index 8ea464e..0000000 Binary files a/img/image-20200520234137916.png and /dev/null differ diff --git a/img/image-20200520234200868.png b/img/image-20200520234200868.png deleted file mode 100644 index 54d8b42..0000000 Binary files a/img/image-20200520234200868.png and /dev/null differ diff --git a/img/image-20200520234231001.png b/img/image-20200520234231001.png deleted file mode 100644 index 5867656..0000000 Binary files a/img/image-20200520234231001.png and /dev/null differ diff --git a/img/image-20200608232749393.png b/img/image-20200608232749393.png deleted file mode 100644 index 55e28ff..0000000 Binary files a/img/image-20200608232749393.png and /dev/null differ diff --git a/img/image-20200608232951873.png b/img/image-20200608232951873.png deleted file mode 100644 index 9ae4e7e..0000000 Binary files a/img/image-20200608232951873.png and /dev/null differ diff --git a/img/image-20200608233400458.png b/img/image-20200608233400458.png deleted file mode 100644 index 980e322..0000000 Binary files a/img/image-20200608233400458.png and /dev/null differ diff --git a/img/image-20200614165333479.png b/img/image-20200614165333479.png deleted file mode 100644 index aba7127..0000000 Binary files a/img/image-20200614165333479.png and /dev/null differ diff --git a/img/image-20200614165432775.png b/img/image-20200614165432775.png deleted file mode 100644 index 69dea69..0000000 Binary files a/img/image-20200614165432775.png and /dev/null differ diff --git a/img/index-search-range.jpg b/img/index-search-range.jpg deleted file mode 100644 index f4ab778..0000000 Binary files a/img/index-search-range.jpg and /dev/null differ diff --git a/img/index-search.jpg b/img/index-search.jpg deleted file mode 100644 index 3273df7..0000000 Binary files a/img/index-search.jpg and /dev/null differ diff --git a/img/java-basic/exception.png b/img/java-basic/exception.png deleted file mode 100644 index ab9b49e..0000000 Binary files a/img/java-basic/exception.png and /dev/null differ diff --git a/img/java-basic/field-method.png b/img/java-basic/field-method.png deleted file mode 100644 index cb566fc..0000000 Binary files a/img/java-basic/field-method.png and /dev/null differ diff --git a/img/java-basic/io.jpg b/img/java-basic/io.jpg deleted file mode 100644 index 9bd463e..0000000 Binary files a/img/java-basic/io.jpg and /dev/null differ diff --git a/img/java-ram-region.png b/img/java-ram-region.png deleted file mode 100644 index c5088be..0000000 Binary files a/img/java-ram-region.png and /dev/null differ diff --git a/img/jmeter/http-request-param.png b/img/jmeter/http-request-param.png deleted file mode 100644 index 098ea41..0000000 Binary files a/img/jmeter/http-request-param.png and /dev/null differ diff --git a/img/jmeter/jmeter-aggregate-report.png b/img/jmeter/jmeter-aggregate-report.png deleted file mode 100644 index 5a2668e..0000000 Binary files a/img/jmeter/jmeter-aggregate-report.png and /dev/null differ diff --git a/img/jmeter/jmeter-default-request.png b/img/jmeter/jmeter-default-request.png deleted file mode 100644 index ed4ad06..0000000 Binary files a/img/jmeter/jmeter-default-request.png and /dev/null differ diff --git a/img/jmeter/jmeter-request-header.png b/img/jmeter/jmeter-request-header.png deleted file mode 100644 index 3191795..0000000 Binary files a/img/jmeter/jmeter-request-header.png and /dev/null differ diff --git a/img/jmeter/jmeter-thread-group.png b/img/jmeter/jmeter-thread-group.png deleted file mode 100644 index a2869a2..0000000 Binary files a/img/jmeter/jmeter-thread-group.png and /dev/null differ diff --git a/img/jvm/string-equal.png b/img/jvm/string-equal.png deleted file mode 100644 index 1dc11f3..0000000 Binary files a/img/jvm/string-equal.png and /dev/null differ diff --git a/img/jvm/string-intern.png b/img/jvm/string-intern.png deleted file mode 100644 index 935adc7..0000000 Binary files a/img/jvm/string-intern.png and /dev/null differ diff --git a/img/jvm/string-new.png b/img/jvm/string-new.png deleted file mode 100644 index 2c0a320..0000000 Binary files a/img/jvm/string-new.png and /dev/null differ diff --git a/img/left-match.jpg b/img/left-match.jpg deleted file mode 100644 index 66ca509..0000000 Binary files a/img/left-match.jpg and /dev/null differ diff --git a/img/middleware/redis-transaction.png b/img/middleware/redis-transaction.png deleted file mode 100644 index 5234b99..0000000 Binary files a/img/middleware/redis-transaction.png and /dev/null differ diff --git a/img/middleware/redis-usage.png b/img/middleware/redis-usage.png deleted file mode 100644 index cea1fa6..0000000 Binary files a/img/middleware/redis-usage.png and /dev/null differ diff --git a/img/mysql-architecture.png b/img/mysql-architecture.png deleted file mode 100644 index 16597c9..0000000 Binary files a/img/mysql-architecture.png and /dev/null differ diff --git a/img/mysql-clustered-index.png b/img/mysql-clustered-index.png deleted file mode 100644 index 6b83fd0..0000000 Binary files a/img/mysql-clustered-index.png and /dev/null differ diff --git a/img/mysql/arch.png b/img/mysql/arch.png deleted file mode 100644 index f2c94c6..0000000 Binary files a/img/mysql/arch.png and /dev/null differ diff --git a/img/mysql/current-read.png b/img/mysql/current-read.png deleted file mode 100644 index 56d2db2..0000000 Binary files a/img/mysql/current-read.png and /dev/null differ diff --git a/img/mysql/mvcc-impl.png b/img/mysql/mvcc-impl.png deleted file mode 100644 index cc3f58a..0000000 Binary files a/img/mysql/mvcc-impl.png and /dev/null differ diff --git a/img/mysql/myisam-innodb-index.png b/img/mysql/myisam-innodb-index.png deleted file mode 100644 index 308f01a..0000000 Binary files a/img/mysql/myisam-innodb-index.png and /dev/null differ diff --git a/img/mysql/mysql-archpng.png b/img/mysql/mysql-archpng.png deleted file mode 100644 index e7e8d2a..0000000 Binary files a/img/mysql/mysql-archpng.png and /dev/null differ diff --git a/img/mysql/nested-loop.png b/img/mysql/nested-loop.png deleted file mode 100644 index 1ba69ca..0000000 Binary files a/img/mysql/nested-loop.png and /dev/null differ diff --git a/img/mysql/orderby.png b/img/mysql/orderby.png deleted file mode 100644 index 1b1283e..0000000 Binary files a/img/mysql/orderby.png and /dev/null differ diff --git a/img/net/bio.png b/img/net/bio.png deleted file mode 100644 index a2740f1..0000000 Binary files a/img/net/bio.png and /dev/null differ diff --git a/img/net/cdn.png b/img/net/cdn.png deleted file mode 100644 index 1694d81..0000000 Binary files a/img/net/cdn.png and /dev/null differ diff --git a/img/net/https-certificate-chain.jpg b/img/net/https-certificate-chain.jpg deleted file mode 100644 index 79b5120..0000000 Binary files a/img/net/https-certificate-chain.jpg and /dev/null differ diff --git a/img/net/https-certificate.png b/img/net/https-certificate.png deleted file mode 100644 index ba526b5..0000000 Binary files a/img/net/https-certificate.png and /dev/null differ diff --git a/img/net/https-client-hello.jpg b/img/net/https-client-hello.jpg deleted file mode 100644 index e994177..0000000 Binary files a/img/net/https-client-hello.jpg and /dev/null differ diff --git a/img/net/https-server-hello.jpg b/img/net/https-server-hello.jpg deleted file mode 100644 index 72ef9e2..0000000 Binary files a/img/net/https-server-hello.jpg and /dev/null differ diff --git a/img/net/nio.png b/img/net/nio.png deleted file mode 100644 index 6aa8959..0000000 Binary files a/img/net/nio.png and /dev/null differ diff --git a/img/oauth2.png b/img/oauth2.png deleted file mode 100644 index c7ccdc2..0000000 Binary files a/img/oauth2.png and /dev/null differ diff --git a/img/object-dead.png b/img/object-dead.png deleted file mode 100644 index 092dc12..0000000 Binary files a/img/object-dead.png and /dev/null differ diff --git a/img/object-handle.png b/img/object-handle.png deleted file mode 100644 index e794881..0000000 Binary files a/img/object-handle.png and /dev/null differ diff --git a/img/optimistic-lock.jpg b/img/optimistic-lock.jpg deleted file mode 100644 index 7a3c464..0000000 Binary files a/img/optimistic-lock.jpg and /dev/null differ diff --git a/img/others/json-web-token.png b/img/others/json-web-token.png deleted file mode 100644 index cfae5cd..0000000 Binary files a/img/others/json-web-token.png and /dev/null differ diff --git a/img/others/seckill.jpg b/img/others/seckill.jpg deleted file mode 100644 index f8e03ed..0000000 Binary files a/img/others/seckill.jpg and /dev/null differ diff --git a/img/parnew-collector.png b/img/parnew-collector.png deleted file mode 100644 index c79c76f..0000000 Binary files a/img/parnew-collector.png and /dev/null differ diff --git a/img/rabbitmq-direct.png b/img/rabbitmq-direct.png deleted file mode 100644 index b93f4a8..0000000 Binary files a/img/rabbitmq-direct.png and /dev/null differ diff --git a/img/rabbitmq-fanout.png b/img/rabbitmq-fanout.png deleted file mode 100644 index 485a4a0..0000000 Binary files a/img/rabbitmq-fanout.png and /dev/null differ diff --git a/img/rabbitmq-return-listener.png b/img/rabbitmq-return-listener.png deleted file mode 100644 index c4b6874..0000000 Binary files a/img/rabbitmq-return-listener.png and /dev/null differ diff --git a/img/rabbitmq-topic.png b/img/rabbitmq-topic.png deleted file mode 100644 index 1cc325b..0000000 Binary files a/img/rabbitmq-topic.png and /dev/null differ diff --git a/img/rabbitmq.png b/img/rabbitmq.png deleted file mode 100644 index 18e05e2..0000000 Binary files a/img/rabbitmq.png and /dev/null differ diff --git a/img/redis/cache-consist.png b/img/redis/cache-consist.png deleted file mode 100644 index b74c9e2..0000000 Binary files a/img/redis/cache-consist.png and /dev/null differ diff --git a/img/redis/evalsha.png b/img/redis/evalsha.png deleted file mode 100644 index ce00e50..0000000 Binary files a/img/redis/evalsha.png and /dev/null differ diff --git a/img/redis/list-api.png b/img/redis/list-api.png deleted file mode 100644 index ca79514..0000000 Binary files a/img/redis/list-api.png and /dev/null differ diff --git a/img/redis/redis-replication.png b/img/redis/redis-replication.png deleted file mode 100644 index 057306a..0000000 Binary files a/img/redis/redis-replication.png and /dev/null differ diff --git a/img/redis/redis-skiplist.png b/img/redis/redis-skiplist.png deleted file mode 100644 index eb9fd26..0000000 Binary files a/img/redis/redis-skiplist.png and /dev/null differ diff --git a/img/redis/time-complexity.png b/img/redis/time-complexity.png deleted file mode 100644 index 88a7713..0000000 Binary files a/img/redis/time-complexity.png and /dev/null differ diff --git "a/img/redis/\347\274\223\345\255\230\345\207\273\347\251\277\345\212\240\344\272\222\346\226\245\351\224\201.png" "b/img/redis/\347\274\223\345\255\230\345\207\273\347\251\277\345\212\240\344\272\222\346\226\245\351\224\201.png" deleted file mode 100644 index 16bd977..0000000 Binary files "a/img/redis/\347\274\223\345\255\230\345\207\273\347\251\277\345\212\240\344\272\222\346\226\245\351\224\201.png" and /dev/null differ diff --git a/img/scheduled-task.jpg b/img/scheduled-task.jpg deleted file mode 100644 index c56521d..0000000 Binary files a/img/scheduled-task.jpg and /dev/null differ diff --git a/img/serial-collector.png b/img/serial-collector.png deleted file mode 100644 index 2145dce..0000000 Binary files a/img/serial-collector.png and /dev/null differ diff --git a/img/singleton-class-init.png b/img/singleton-class-init.png deleted file mode 100644 index 4887284..0000000 Binary files a/img/singleton-class-init.png and /dev/null differ diff --git "a/img/spring/\346\226\271\346\263\225\347\272\247\345\210\253\346\263\250\350\247\243.png" "b/img/spring/\346\226\271\346\263\225\347\272\247\345\210\253\346\263\250\350\247\243.png" deleted file mode 100644 index a1ad6f0..0000000 Binary files "a/img/spring/\346\226\271\346\263\225\347\272\247\345\210\253\346\263\250\350\247\243.png" and /dev/null differ diff --git "a/img/springboot/SpringBoot\347\232\204\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.jpg" "b/img/springboot/SpringBoot\347\232\204\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.jpg" deleted file mode 100644 index f4b8c47..0000000 Binary files "a/img/springboot/SpringBoot\347\232\204\350\207\252\345\212\250\351\205\215\347\275\256\345\216\237\347\220\206.jpg" and /dev/null differ diff --git a/img/thread-pool.png b/img/thread-pool.png deleted file mode 100644 index bc66194..0000000 Binary files a/img/thread-pool.png and /dev/null differ diff --git a/img/thread-status.jpeg b/img/thread-status.jpeg deleted file mode 100644 index e44c970..0000000 Binary files a/img/thread-status.jpeg and /dev/null differ diff --git a/img/threadlocal-oom.png b/img/threadlocal-oom.png deleted file mode 100644 index f146346..0000000 Binary files a/img/threadlocal-oom.png and /dev/null differ diff --git a/img/threadlocal.png b/img/threadlocal.png deleted file mode 100644 index 4c235ba..0000000 Binary files a/img/threadlocal.png and /dev/null differ diff --git a/img/tree-visit.png b/img/tree-visit.png deleted file mode 100644 index 44d075c..0000000 Binary files a/img/tree-visit.png and /dev/null differ diff --git a/img/two-index.jpg b/img/two-index.jpg deleted file mode 100644 index e5b9371..0000000 Binary files a/img/two-index.jpg and /dev/null differ diff --git a/img/web/servlet-container.jpg b/img/web/servlet-container.jpg deleted file mode 100644 index f7e6310..0000000 Binary files a/img/web/servlet-container.jpg and /dev/null differ diff --git "a/img/\345\271\273\350\257\2731.png" "b/img/\345\271\273\350\257\2731.png" deleted file mode 100644 index 50fc31c..0000000 Binary files "a/img/\345\271\273\350\257\2731.png" and /dev/null differ diff --git "a/img/\346\213\245\345\241\236\346\216\247\345\210\266.jpg" "b/img/\346\213\245\345\241\236\346\216\247\345\210\266.jpg" deleted file mode 100644 index 941f50f..0000000 Binary files "a/img/\346\213\245\345\241\236\346\216\247\345\210\266.jpg" and /dev/null differ diff --git "a/img/\350\257\267\346\261\202\344\277\235\346\212\244\351\205\215\347\275\256\346\226\271\346\263\225.png" "b/img/\350\257\267\346\261\202\344\277\235\346\212\244\351\205\215\347\275\256\346\226\271\346\263\225.png" deleted file mode 100644 index cfe3855..0000000 Binary files "a/img/\350\257\267\346\261\202\344\277\235\346\212\244\351\205\215\347\275\256\346\226\271\346\263\225.png" and /dev/null differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..57fab08 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14322 @@ +{ + "name": "vuepress-theme-hope-template", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "vuepress-theme-hope-template", + "version": "2.0.0", + "license": "MIT", + "devDependencies": { + "@vuepress/client": "^2.0.0-beta.49", + "@vuepress/plugin-docsearch": "^2.0.0-beta.49", + "@vuepress/plugin-google-analytics": "^1.9.7", + "@vuepress/plugin-nprogress": "^2.0.0-beta.49", + "@vuepress/plugin-search": "^2.0.0-beta.49", + "vue": "^3.2.36", + "vuepress": "^2.0.0-beta.49", + "vuepress-plugin-autometa": "^0.1.13", + "vuepress-plugin-baidu-autopush": "^1.0.1", + "vuepress-plugin-copyright2": "^2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "^2.0.0-beta.87", + "vuepress-plugin-reading-time2": "^2.0.0-beta.87", + "vuepress-plugin-sitemap2": "^2.0.0-beta.87", + "vuepress-theme-hope": "^2.0.0-beta.87" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.1.tgz", + "integrity": "sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.7.1" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.1.tgz", + "integrity": "sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.7.1" + }, + "peerDependencies": { + "@algolia/client-search": "^4.9.1", + "algoliasearch": "^4.9.1" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.1.tgz", + "integrity": "sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg==", + "dev": true + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz", + "integrity": "sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-common/-/cache-common-4.14.2.tgz", + "integrity": "sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==", + "dev": true + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz", + "integrity": "sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-account/-/client-account-4.14.2.tgz", + "integrity": "sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-analytics/-/client-analytics-4.14.2.tgz", + "integrity": "sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-common/-/client-common-4.14.2.tgz", + "integrity": "sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-personalization/-/client-personalization-4.14.2.tgz", + "integrity": "sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/client-search": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-search/-/client-search-4.14.2.tgz", + "integrity": "sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/@algolia/logger-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-common/-/logger-common-4.14.2.tgz", + "integrity": "sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==", + "dev": true + }, + "node_modules/@algolia/logger-console": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-console/-/logger-console-4.14.2.tgz", + "integrity": "sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==", + "dev": true, + "dependencies": { + "@algolia/logger-common": "4.14.2" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz", + "integrity": "sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-common/-/requester-common-4.14.2.tgz", + "integrity": "sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==", + "dev": true + }, + "node_modules/@algolia/requester-node-http": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz", + "integrity": "sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==", + "dev": true, + "dependencies": { + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/transporter/-/transporter-4.14.2.tgz", + "integrity": "sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==", + "dev": true, + "dependencies": { + "@algolia/cache-common": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/requester-common": "4.14.2" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "dependencies": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.18.8.tgz", + "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.12", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.10", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", + "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz", + "integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz", + "integrity": "sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz", + "integrity": "sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", + "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", + "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz", + "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz", + "integrity": "sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz", + "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.18.9", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.18.9.tgz", + "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", + "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.18.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", + "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz", + "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz", + "integrity": "sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz", + "integrity": "sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz", + "integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz", + "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz", + "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz", + "integrity": "sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-identifier": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz", + "integrity": "sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", + "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", + "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "regenerator-transform": "^0.15.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz", + "integrity": "sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.18.9", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.18.9", + "@babel/plugin-transform-classes": "^7.18.9", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.18.9", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.18.9", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.18.9", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", + "core-js-compat": "^3.22.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.18.11.tgz", + "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.18.11", + "@babel/types": "^7.18.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.18.10.tgz", + "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", + "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==", + "dev": true + }, + "node_modules/@docsearch/css": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.2.0.tgz", + "integrity": "sha512-jnNrO2JVYYhj2pP2FomlHIy6220n6mrLn2t9v2/qc+rM7M/fbIcKMgk9ky4RN+L/maUEmteckzg6/PIYoAAXJg==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.2.0.tgz", + "integrity": "sha512-FEgXW8a+ZKBjSDteFPsKQ7Hlzk6+18A2Y7NffjV+VTsE7P3uTvHPKHKDCeYMnAgXTatRCGHWCfP7YImTSwEFQA==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.2.0", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.2.0.tgz", + "integrity": "sha512-ATS3w5JBgQGQF0kHn5iOAPfnCCaoLouZQMmI7oENV//QMFrYbjhUZxBU9lIwAT7Rzybud+Jtb4nG5IEjBk3Ixw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.7.1", + "@algolia/autocomplete-preset-algolia": "1.7.1", + "@docsearch/css": "3.2.0", + "algoliasearch": "^4.0.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz", + "integrity": "sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lit/reactive-element": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/@lit/reactive-element/-/reactive-element-1.3.4.tgz", + "integrity": "sha512-I1wz4uxOA52zSBhKmv4KQWLJpCyvfpnDg+eQR6mjpRgV+Ldi14HLPpSUpJklZRldz0fFmGCC/kVmuc/3cPFqCg==", + "dev": true + }, + "node_modules/@mdit-vue/plugin-component": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.6.0.tgz", + "integrity": "sha512-S/Dd0eoOipbUAMdJ6A7M20dDizJxbtGAcL6T1iiJ0cEzjTrHP1kRT421+JMGPL8gcdsrIxgVSW8bI/R6laqBtA==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-frontmatter": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-frontmatter/-/plugin-frontmatter-0.6.0.tgz", + "integrity": "sha512-cRunxy0q1gcqxUHAAiV8hMKh2qZOTDKXt8YOWfWNtf7IzaAL0v/nCOfh+O7AsHRmyc25Th8sL3H85HKWnNJtdw==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "gray-matter": "^4.0.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-headers": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-headers/-/plugin-headers-0.6.0.tgz", + "integrity": "sha512-pg56w9/UooYuIZIoM0iQ021hrXt450fuRG3duxcwngw3unmE80rkvG3C0lT9ZnNXHSSYC9vGWUJh6EEN4nB34A==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-sfc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-sfc/-/plugin-sfc-0.6.0.tgz", + "integrity": "sha512-R7mwUz2MxEopVQwpcOqCcqqvKx3ibRNcZ7QC31w4VblRb3Srk1st1UuGwHJxZ6Biro8ZWdPpMfpSsSk+2G+mIg==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-title": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-title/-/plugin-title-0.6.0.tgz", + "integrity": "sha512-K2qUIrHmCp9w+/p1lWfkr808+Ge6FksM1ny/siiXHMHB0enArUd7G7SaEtro8JRb/hewd9qKq5xTOSWN2Q5jow==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/plugin-toc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-toc/-/plugin-toc-0.6.0.tgz", + "integrity": "sha512-5pgKY2++3w2/9Pqpgz7mZUiXs6jDcEyFPcf14QdiqSZ2eL+4VLuupcoC4JIDF+mAFHt+TJCfhk3oeG8Y6s6TBg==", + "dev": true, + "dependencies": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/shared": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/shared/-/shared-0.6.0.tgz", + "integrity": "sha512-RtV1P8jrEV/cl0WckOvpefiEWScw7omCQrIEtorlagG2XmnI9YbxMkLD53ETscA7lTVzqhGyzfoSrAiPi0Sjnw==", + "dev": true, + "dependencies": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "node_modules/@mdit-vue/types": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/types/-/types-0.6.0.tgz", + "integrity": "sha512-2Gf6MkEmoHrvO/IJsz48T+Ns9lW17ReC1vdhtCUGSCv0fFCm/L613uu/hpUrHuT3jTQHP90LcbXTQB2w4L1G8w==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.30", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/highlight.js": { + "version": "9.12.4", + "resolved": "https://registry.npmmirror.com/@types/highlight.js/-/highlight.js-9.12.4.tgz", + "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", + "dev": true + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==", + "dev": true, + "dependencies": { + "@types/markdown-it": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, + "node_modules/@types/mermaid": { + "version": "8.2.9", + "resolved": "https://registry.npmmirror.com/@types/mermaid/-/mermaid-8.2.9.tgz", + "integrity": "sha512-f1i8fNoVFVJXedk+R7GcEk4KoOWzWAU3CzFqlVw1qWKktfsataBERezCz1pOdKy8Ec02ZdPQXGM7NU2lPHABYQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.6.4", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.6.4.tgz", + "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "dependencies": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "node_modules/@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, + "node_modules/@types/uglify-js": { + "version": "3.16.0", + "resolved": "https://registry.npmmirror.com/@types/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.14", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz", + "integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==", + "dev": true + }, + "node_modules/@types/webpack": { + "version": "4.41.32", + "resolved": "https://registry.npmmirror.com/@types/webpack/-/webpack-4.41.32.tgz", + "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-dev-server": { + "version": "3.11.6", + "resolved": "https://registry.npmmirror.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", + "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", + "dev": true, + "dependencies": { + "@types/connect-history-api-fallback": "*", + "@types/express": "*", + "@types/serve-static": "*", + "@types/webpack": "^4", + "http-proxy-middleware": "^1.0.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz", + "integrity": "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "vite": "^2.5.10", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz", + "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz", + "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz", + "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-ssr": "3.2.37", + "@vue/reactivity-transform": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz", + "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz", + "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==", + "dev": true + }, + "node_modules/@vue/reactivity": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz", + "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==", + "dev": true, + "dependencies": { + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz", + "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz", + "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz", + "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==", + "dev": true, + "dependencies": { + "@vue/runtime-core": "3.2.37", + "@vue/shared": "3.2.37", + "csstype": "^2.6.8" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz", + "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.2.37", + "@vue/shared": "3.2.37" + }, + "peerDependencies": { + "vue": "3.2.37" + } + }, + "node_modules/@vue/shared": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz", + "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==", + "dev": true + }, + "node_modules/@vuepress/bundler-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/bundler-vite/-/bundler-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-6AK3HuFHQKMWefTasyS+wsvb0wLufWBdQ/eHMDxZudE63dU7mSwCvV0kpX2uFzhlpdE/ug/8NuQbOlh4zZayvA==", + "dev": true, + "dependencies": { + "@vitejs/plugin-vue": "^2.3.3", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "autoprefixer": "^10.4.7", + "connect-history-api-fallback": "^2.0.0", + "postcss": "^8.4.14", + "rollup": "^2.76.0", + "vite": "~2.9.14", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/cli": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/cli/-/cli-2.0.0-beta.49.tgz", + "integrity": "sha512-3RtuZvtLIGXEtsLgc3AnDr4jxiFeFDWfNw6MTb22YwuttBr5h5pZO/F8XMyP9+tEi73q3/l4keNQftU4msHysQ==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "cac": "^6.7.12", + "chokidar": "^3.5.3", + "envinfo": "^7.8.1", + "esbuild": "^0.14.49" + }, + "bin": { + "vuepress-cli": "bin/vuepress.js" + } + }, + "node_modules/@vuepress/client": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/client/-/client-2.0.0-beta.49.tgz", + "integrity": "sha512-zfGlCAF/LwDOrZXZPqADsMgWRuH/2GFOGSOCvt7ZUZHnSrYBdK2FOez/ksWL8EwGNLsRLB8ny1IachMwTew5og==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/shared": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/core": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/core/-/core-2.0.0-beta.49.tgz", + "integrity": "sha512-40J74qGOPqF9yGdXdzPD1kW9mv5/jfJenmhsH1xaErPsr6qIM8jcraVRC+R7NoVTIecRk9cC9MJcDRnLmDDiAg==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/markdown": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/markdown/-/markdown-2.0.0-beta.49.tgz", + "integrity": "sha512-aAw41NArV5leIpZOFmElxzRG29LDdEQe7oIcZtIvKPhVmEfg9/mgx4ea2OqY5DaBvEhkG42SojjKvmHiJKrwJw==", + "dev": true, + "dependencies": { + "@mdit-vue/plugin-component": "^0.6.0", + "@mdit-vue/plugin-frontmatter": "^0.6.0", + "@mdit-vue/plugin-headers": "^0.6.0", + "@mdit-vue/plugin-sfc": "^0.6.0", + "@mdit-vue/plugin-title": "^0.6.0", + "@mdit-vue/plugin-toc": "^0.6.0", + "@mdit-vue/shared": "^0.6.0", + "@mdit-vue/types": "^0.6.0", + "@types/markdown-it": "^12.2.3", + "@types/markdown-it-emoji": "^2.0.2", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-anchor": "^8.6.4", + "markdown-it-emoji": "^2.0.2", + "mdurl": "^1.0.1" + } + }, + "node_modules/@vuepress/plugin-active-header-links": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-2.0.0-beta.49.tgz", + "integrity": "sha512-p69WE1eQwUoe1FtlVf029ZsdS44pLLkxXsq8+XRi3TRGbhK3kcUy7m6Amjj3imV2iJm2CYtQWpNjs22O1jjMMw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-back-to-top": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-back-to-top/-/plugin-back-to-top-2.0.0-beta.49.tgz", + "integrity": "sha512-fDwU916nLLnS7Pye2XR1Hf9c/4Vc8YdldwXWECtpBybdk/1h8bWb/qMOmL84W39ZF4k3XbZX24ld3uw2JQm52A==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-container": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-container/-/plugin-container-2.0.0-beta.49.tgz", + "integrity": "sha512-PWChjwDVci4UMrzT4z4eYooXikf60+PseMuUioLF5lB6/6AYfL5QrzXOq7znRtG/IXtE8jIjid962eFJDvw/iA==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^12.2.3", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-container": "^3.0.0" + } + }, + "node_modules/@vuepress/plugin-docsearch": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-docsearch/-/plugin-docsearch-2.0.0-beta.49.tgz", + "integrity": "sha512-580pQ9AyOjTe64YH8h3MHsvj+EfxCmJ6IJ/3kp51tT0/zL59mE8aLyveyvgwJrvhBdki5PMOGgBx95tOT7QVwQ==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.1.1", + "@docsearch/js": "^3.1.1", + "@docsearch/react": "^3.1.1", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-external-link-icon": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-external-link-icon/-/plugin-external-link-icon-2.0.0-beta.49.tgz", + "integrity": "sha512-ZwmLJAp3xF+0yJNeqaTwc17Nw0RyMk8DsNfoecyRgzHud8OxrcJj+NLF8Tpw+t1k22cfIfaIIyWJbGcGZOzVCw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-git": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-git/-/plugin-git-2.0.0-beta.49.tgz", + "integrity": "sha512-CjaBYWBAkQmlpx5v+mp2vsoRxqRTi/mSvXy8im/ftc8zX/sVT4V1LBWX1IsDQn1VpWnArlfAsFd+BrmxzPFePA==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "execa": "^5.1.1" + } + }, + "node_modules/@vuepress/plugin-google-analytics": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz", + "integrity": "sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q==", + "dev": true, + "dependencies": { + "@vuepress/types": "1.9.7" + } + }, + "node_modules/@vuepress/plugin-medium-zoom": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-medium-zoom/-/plugin-medium-zoom-2.0.0-beta.49.tgz", + "integrity": "sha512-Z80E/BhHnTQeC208Dw9D1CpyxONGJ3HVNd3dU3qJfdjX9o8GzkRqdo17aq4aHOeEPn0DQ04I/7sHFVgv41KGgw==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "medium-zoom": "^1.0.6", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/plugin-nprogress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-nprogress/-/plugin-nprogress-2.0.0-beta.49.tgz", + "integrity": "sha512-SBnOQMMxhdzdbB4yCxCzFGpZUxTV4BvexauLXfZNqm128WwXRHk6MJltFIZIFODJldMpSuCCrkm0Uj7vC5yDUA==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-palette": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-palette/-/plugin-palette-2.0.0-beta.49.tgz", + "integrity": "sha512-88zeO8hofW+jl+GyMXXRW8t5/ibBoUUVCp4ctN+dJvDNADbBIVVQOkwQhDnPUyVwoEni/dQ4b879YyZXOhT5MA==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3" + } + }, + "node_modules/@vuepress/plugin-prismjs": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-prismjs/-/plugin-prismjs-2.0.0-beta.49.tgz", + "integrity": "sha512-/XK+Gjs92SEoqHL1XGaspMxv0sMMEPrR+YisSQn3KzaWE59yylsD3I7fMOkJI7D02n9Cw8pejGoR3XOH0M8Q2Q==", + "dev": true, + "dependencies": { + "@vuepress/core": "2.0.0-beta.49", + "prismjs": "^1.28.0" + } + }, + "node_modules/@vuepress/plugin-search": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-search/-/plugin-search-2.0.0-beta.49.tgz", + "integrity": "sha512-XkI5FfqJUODh5V7ic/hjja4rjVJQoT29xff63hDFvm+aVPG9FwAHtMSqUHutWO92WtlqoDi9y2lTbpyDYu6+rQ==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/@vuepress/plugin-theme-data": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-theme-data/-/plugin-theme-data-2.0.0-beta.49.tgz", + "integrity": "sha512-zwbnDKPOOljSz7nMQXCNefp2zpDlwRIX5RTej9JQlCdcPXyLkFfvDgIMVpKNx6/5/210tKxFsCpmjLR8i+DbgQ==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "node_modules/@vuepress/shared": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/shared/-/shared-2.0.0-beta.49.tgz", + "integrity": "sha512-yoUgOtRUrIfe0O1HMTIMj0NYU3tAiUZ4rwVEtemtGa7/RK7qIZdBpAfv08Ve2CUpa3wrMb1Pux1aBsiz1EQx+g==", + "dev": true, + "dependencies": { + "@vue/shared": "^3.2.37" + } + }, + "node_modules/@vuepress/theme-default": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/theme-default/-/theme-default-2.0.0-beta.49.tgz", + "integrity": "sha512-HUhDT7aWdtsZTRmDDWgWc9vRWGKGLh8GB+mva+TQABTgXV4qPmvuKzRi0yOU3FX1todRifxVPJTiJYVfh7zkPQ==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-back-to-top": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-medium-zoom": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.7.5", + "sass": "^1.53.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/@vuepress/types": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/types/-/types-1.9.7.tgz", + "integrity": "sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog==", + "dev": true, + "dependencies": { + "@types/markdown-it": "^10.0.0", + "@types/webpack-dev-server": "^3", + "webpack-chain": "^6.0.0" + } + }, + "node_modules/@vuepress/types/node_modules/@types/markdown-it": { + "version": "10.0.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-10.0.3.tgz", + "integrity": "sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==", + "dev": true, + "dependencies": { + "@types/highlight.js": "^9.7.0", + "@types/linkify-it": "*", + "@types/mdurl": "*", + "highlight.js": "^9.7.0" + } + }, + "node_modules/@vuepress/utils": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/utils/-/utils-2.0.0-beta.49.tgz", + "integrity": "sha512-t5i0V9FqpKLGlu2kMP/Y9+wdgEmsD2yQAMGojxpMoFhJBmqn2L9Rkk4WYzHKzPGDkm1KbBFzYQqjAhZQ7xtY1A==", + "dev": true, + "dependencies": { + "@types/debug": "^4.1.7", + "@types/fs-extra": "^9.0.13", + "@vuepress/shared": "2.0.0-beta.49", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "globby": "^11.0.4", + "hash-sum": "^2.0.0", + "ora": "^5.4.1", + "upath": "^2.0.1" + } + }, + "node_modules/@vueuse/core": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-8.9.4.tgz", + "integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.14", + "@vueuse/metadata": "8.9.4", + "@vueuse/shared": "8.9.4", + "vue-demi": "*" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.9.4.tgz", + "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==", + "dev": true + }, + "node_modules/@vueuse/shared": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.9.4.tgz", + "integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==", + "dev": true, + "dependencies": { + "vue-demi": "*" + }, + "peerDependencies": { + "@vue/composition-api": "^1.1.0", + "vue": "^2.6.0 || ^3.2.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@waline/client": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/@waline/client/-/client-2.6.2.tgz", + "integrity": "sha512-yQUPRXF8Om+YQCeqZY4BWHKOpHAouGYlRiqBWsen/hUgdRs5eMcsnaYtrcluQHFzPb/Mv2HPOVKQCJys6oSJpw==", + "dev": true, + "dependencies": { + "@vueuse/core": "^8.9.4", + "autosize": "^5.0.1", + "marked": "^4.0.18", + "vue": "^3.2.37" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "node_modules/algoliasearch": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/algoliasearch/-/algoliasearch-4.14.2.tgz", + "integrity": "sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==", + "dev": true, + "dependencies": { + "@algolia/cache-browser-local-storage": "4.14.2", + "@algolia/cache-common": "4.14.2", + "@algolia/cache-in-memory": "4.14.2", + "@algolia/client-account": "4.14.2", + "@algolia/client-analytics": "4.14.2", + "@algolia/client-common": "4.14.2", + "@algolia/client-personalization": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/logger-console": "4.14.2", + "@algolia/requester-browser-xhr": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/requester-node-http": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.8", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/autosize": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/autosize/-/autosize-5.0.1.tgz", + "integrity": "sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA==", + "dev": true + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", + "integrity": "sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.2", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.2", + "core-js-compat": "^3.21.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/balloon-css": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz", + "integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "node_modules/bcrypt-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/bcrypt-ts/-/bcrypt-ts-1.0.0.tgz", + "integrity": "sha512-7CwTSYmfIPdP/CR2uKMajK4eNByTIZqJgrF2j4yozv8BDMbBgCzzxy6iKy6WLueZH+ZcFpyWDa6ddsDH9Yf5FA==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.3", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.5" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cac": { + "version": "6.7.12", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.12.tgz", + "integrity": "sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001374", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz", + "integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/convert-source-map/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.24.1", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.24.1.tgz", + "integrity": "sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.3", + "semver": "7.0.0" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==", + "dev": true + }, + "node_modules/d3": { + "version": "7.6.1", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "dev": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dev": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "dev": true + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dev": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dev": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dev": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dev": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dev": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "dev": true + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dev": true, + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "dev": true, + "dependencies": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dagre-d3/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3": { + "version": "5.16.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "dev": true, + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dev": true, + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dev": true, + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "dev": true, + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/dagre-d3/node_modules/d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "dev": true, + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dev": true, + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dev": true, + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dev": true, + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dev": true, + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/dagre-d3/node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dev": true, + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dev": true, + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "dev": true + }, + "node_modules/dagre-d3/node_modules/d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "dev": true, + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/dagre-d3/node_modules/d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "dev": true, + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre-d3/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.4", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.4.tgz", + "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dev": true, + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-2.3.10.tgz", + "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g==", + "dev": true + }, + "node_modules/echarts": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.3.3.tgz", + "integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", + "dev": true, + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.3.2" + } + }, + "node_modules/ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.211", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz", + "integrity": "sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==", + "dev": true + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.14.53.tgz", + "integrity": "sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.53", + "esbuild-android-64": "0.14.53", + "esbuild-android-arm64": "0.14.53", + "esbuild-darwin-64": "0.14.53", + "esbuild-darwin-arm64": "0.14.53", + "esbuild-freebsd-64": "0.14.53", + "esbuild-freebsd-arm64": "0.14.53", + "esbuild-linux-32": "0.14.53", + "esbuild-linux-64": "0.14.53", + "esbuild-linux-arm": "0.14.53", + "esbuild-linux-arm64": "0.14.53", + "esbuild-linux-mips64le": "0.14.53", + "esbuild-linux-ppc64le": "0.14.53", + "esbuild-linux-riscv64": "0.14.53", + "esbuild-linux-s390x": "0.14.53", + "esbuild-netbsd-64": "0.14.53", + "esbuild-openbsd-64": "0.14.53", + "esbuild-sunos-64": "0.14.53", + "esbuild-windows-32": "0.14.53", + "esbuild-windows-64": "0.14.53", + "esbuild-windows-arm64": "0.14.53" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz", + "integrity": "sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz", + "integrity": "sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz", + "integrity": "sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz", + "integrity": "sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz", + "integrity": "sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz", + "integrity": "sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz", + "integrity": "sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz", + "integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz", + "integrity": "sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz", + "integrity": "sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz", + "integrity": "sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz", + "integrity": "sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz", + "integrity": "sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz", + "integrity": "sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz", + "integrity": "sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz", + "integrity": "sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz", + "integrity": "sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz", + "integrity": "sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz", + "integrity": "sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz", + "integrity": "sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eve-raphael": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/eve-raphael/-/eve-raphael-0.5.0.tgz", + "integrity": "sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flowchart.js": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/flowchart.js/-/flowchart.js-1.17.1.tgz", + "integrity": "sha512-zphTaxdyqvHHu+8Cdf6HvamhArXpq9SyNe1zQ61maCIfTenaj3cMvjS1e/0gfPj7QTLTx3HroSzVqDXpL8naoQ==", + "dev": true, + "dependencies": { + "raphael": "2.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giscus": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/giscus/-/giscus-1.2.0.tgz", + "integrity": "sha512-IpfWvU0/hYbMGQKuoPlED8wWmluRYIOjtrBCnL7logsWjMpPRxiAC2pUIC0+SC0pDMOqXrk1onTYMHgwgRpRzg==", + "dev": true, + "dependencies": { + "lit": "^2.2.8" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "node_modules/highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "deprecated": "Support has ended for 9.x series. Upgrade to @latest", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": "*" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", + "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/idb/-/idb-7.0.2.tgz", + "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==", + "dev": true + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jake": { + "version": "10.8.5", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js-yaml/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/katex": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "dev": true, + "dependencies": { + "commander": "^8.0.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lit": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/lit/-/lit-2.2.8.tgz", + "integrity": "sha512-QjeNbi/H9LVIHR+u0OqsL+hs62a16m02JlJHYN48HcBuXyiPYR8JvzsTp5dYYS81l+b9Emp3UaGo82EheV0pog==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-element": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/lit-element/-/lit-element-3.2.2.tgz", + "integrity": "sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==", + "dev": true, + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-html": { + "version": "2.2.7", + "resolved": "https://registry.npmmirror.com/lit-html/-/lit-html-2.2.7.tgz", + "integrity": "sha512-JhqiAwO1l03kRe68uBZ0i2x4ef2S5szY9vvP411nlrFZIpKK4/hwnhA/15bqbvxe1lV3ipBdhaOzHmyOk7QIRg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "node_modules/lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha512-9er6Ccz6sEST3bHFtUrCFWk14nE8cdL/RoW1RRDV1BxqN3qsmsT56L14jhfctAqhVPVcdJw4MRxEaVoAK+JVvw==", + "dev": true + }, + "node_modules/lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha512-lsD+k73XztDsMBKPKvzHXRKFNMohTjoTKIIo4ADLn5dA65LZ1BqlAvSXhR2rPEC3BgAUQnzMnorqDtqn2z4IHA==", + "dev": true + }, + "node_modules/lodash.trimstart": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz", + "integrity": "sha512-b/+D6La8tU76L/61/aN0jULWHkT0EeJCmVstPBn/K9MtD2qBW83AsBNrr63dKuWYwVMO7ucv13QNO/Ek/2RKaQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.4", + "resolved": "https://registry.npmmirror.com/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", + "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it-container": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz", + "integrity": "sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==", + "dev": true + }, + "node_modules/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==", + "dev": true + }, + "node_modules/marked": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "node_modules/medium-zoom": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.0.6.tgz", + "integrity": "sha512-UdiUWfvz9fZMg1pzf4dcuqA0W079o0mpqbTnOz5ip4VGYX96QjmbM+OgOU/0uOzAytxC0Ny4z+VcYQnhdifimg==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "9.1.4", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-9.1.4.tgz", + "integrity": "sha512-QgQTpIIzJfV/Ob7FZTDzxmWjFzCciij4C8RbbQbamsadf2gHrNrfqAoWLF6ALfQlW5ZqOefvlogDdWcFZRnifg==", + "dev": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "d3": "^7.0.0", + "dagre": "^0.8.5", + "dagre-d3": "^0.6.4", + "dompurify": "2.3.10", + "graphlib": "^2.1.8", + "khroma": "^2.0.0", + "moment-mini": "^2.24.0", + "stylis": "^4.0.10" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true + }, + "node_modules/moment-mini": { + "version": "2.24.0", + "resolved": "https://registry.npmmirror.com/moment-mini/-/moment-mini-2.24.0.tgz", + "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/photoswipe": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/photoswipe/-/photoswipe-5.3.0.tgz", + "integrity": "sha512-vZMwziQorjiagzX7EvWimVT0YHO0DWNtR9UT6cv3yW1FA199LgsTpj4ziB2oJ/X/197gKmi56Oux5PudWUAmuw==", + "dev": true, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/preact": { + "version": "10.10.1", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.10.1.tgz", + "integrity": "sha512-cXljG59ylGtSLismoLojXPAGvnh2ipQr3BYz9KZQr+1sdASCT+sR/v8dSMDS96xGCdtln2wHfAHCnLJK+XcBNg==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/prismjs": { + "version": "1.28.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.28.0.tgz", + "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raphael": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/raphael/-/raphael-2.3.0.tgz", + "integrity": "sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==", + "dev": true, + "dependencies": { + "eve-raphael": "0.5.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz", + "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-5.1.0.tgz", + "integrity": "sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/register-service-worker": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/register-service-worker/-/register-service-worker-1.7.2.tgz", + "integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==", + "dev": true + }, + "node_modules/regjsgen": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.8.4", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==", + "dev": true + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/reveal.js": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/reveal.js/-/reveal.js-4.3.1.tgz", + "integrity": "sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==", + "dev": true + }, + "node_modules/rollup": { + "version": "2.77.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.77.2.tgz", + "integrity": "sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.54.3", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.54.3.tgz", + "integrity": "sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sitemap": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/sitemap/-/sitemap-7.1.1.tgz", + "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", + "dev": true, + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha512-3HVl+cOkJOlNUDAYdoCAfGx/fzUzG53YvJAl3RYlTvAcBdPqSp1Uv4wrmHymm7oEypTijSQqcqplW8cz0/r/YA==", + "dev": true + }, + "node_modules/stylis": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", + "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==", + "dev": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser": { + "version": "5.14.2", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/ts-debounce": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/ts-debounce/-/ts-debounce-4.0.0.tgz", + "integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + }, + "node_modules/twikoo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/twikoo/-/twikoo-1.6.4.tgz", + "integrity": "sha512-QC34vA037Pg2ENc05kqH3u9RrJbaf3UV9EfMUHdRurhL5frAekIU03X9TZ3JBdcx5NhvgiCxeZ9wOPA3szHtgw==", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "dev": true, + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "2.9.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-2.9.14.tgz", + "integrity": "sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz", + "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-sfc": "3.2.37", + "@vue/runtime-dom": "3.2.37", + "@vue/server-renderer": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "node_modules/vue-demi": { + "version": "0.13.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.6.tgz", + "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.3.tgz", + "integrity": "sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==", + "dev": true, + "dependencies": { + "@vue/devtools-api": "^6.1.4" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuepress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress/-/vuepress-2.0.0-beta.49.tgz", + "integrity": "sha512-dxbgCNn+S9DDUu4Ao/QqwfdQF3e6IgpKhqQxYPPO/xVYZbnQnmXbzh0uGdtKUAyKKgP8UouWbp4Qdk1/Z6ay9Q==", + "dev": true, + "dependencies": { + "vuepress-vite": "2.0.0-beta.49" + }, + "bin": { + "vuepress": "bin/vuepress.js" + } + }, + "node_modules/vuepress-plugin-autometa": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-autometa/-/vuepress-plugin-autometa-0.1.13.tgz", + "integrity": "sha512-yhd8smLhbCO0+gc3FRNjOyffdGl12gUkv60UqdtEUCojEy4s7APDJ2pt85cSLASMdnWaY7EcMibMRkqeUOVAKg==", + "dev": true, + "dependencies": { + "lodash.defaultsdeep": "4.6.1", + "lodash.findindex": "4.6.0", + "lodash.isempty": "4.4.0", + "lodash.trimend": "^4.5.1", + "lodash.trimstart": "^4.5.1", + "remove-markdown": "0.3.0", + "striptags": "3.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vuepress-plugin-baidu-autopush": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-baidu-autopush/-/vuepress-plugin-baidu-autopush-1.0.1.tgz", + "integrity": "sha512-KVQkrmMgPY+GG8dtI2wcRxUv1n2h5DM8aFs75ltsSlFBSS9C/vfLb2LmywXAsoCXk2EHya2p66cpn7BxofK+Mw==", + "dev": true + }, + "node_modules/vuepress-plugin-blog2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-blog2/-/vuepress-plugin-blog2-2.0.0-beta.87.tgz", + "integrity": "sha512-NbuxiWfTLV4hDSHj5PxBrsmv5Bdh3Gkwc3z36hcETmrLPf39uasxnwgHlDuhjuau9yqsilX3oqg/xRzB6nrLYg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-comment2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-comment2/-/vuepress-plugin-comment2-2.0.0-beta.87.tgz", + "integrity": "sha512-QbeCil40itjoEj6SaWtWSaZQ0YakCEz9CBG53n4/misW2JHgEbKQCpimm3hrWD3AgqZ2N2bFyuGH42hcy+k9vw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@waline/client": "^2.6.1", + "giscus": "^1.0.6", + "twikoo": "^1.5.11", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-components": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-components/-/vuepress-plugin-components-2.0.0-beta.87.tgz", + "integrity": "sha512-X4KkINr4llIHPMb/YCnlxqwRabT4VY/MZSEtWloNYtvIsAw/j95mgVyLlQGyk/xtW3DFv85IyuqMf0eaWxpj9w==", + "deprecated": "Please use latest beta version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-copy-code2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copy-code2/-/vuepress-plugin-copy-code2-2.0.0-beta.87.tgz", + "integrity": "sha512-SdIhcjCJ8aXFtzmKbP9+eeDh3nw6EPTFgu1EAmoS2NrhZDOminxnaTQgYuFjLrzBcky+d+RBWPcWEKhZCEJ9cg==", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-copyright2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copyright2/-/vuepress-plugin-copyright2-2.0.0-beta.87.tgz", + "integrity": "sha512-LUo7L+bU8iK4/BxCV2xv0yOYxooXbb0vSn9iDIBG16+Y5y2NdrFwRV2N9pa5Dao5RbVFvPVXHnlmktZO6CTwRQ==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "vue": "^3.2.37", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-feed2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-feed2/-/vuepress-plugin-feed2-2.0.0-beta.87.tgz", + "integrity": "sha512-J2A9o+gviuAwRPlwuOHsmtL2THY7FguaFfL+ooaGwAFo8UXdRvUvOIWof3DwQZOazOACkySyLCrWy6YnVnMapw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vuepress-shared": "2.0.0-beta.87", + "xml-js": "^1.6.11" + } + }, + "node_modules/vuepress-plugin-md-enhance": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-md-enhance/-/vuepress-plugin-md-enhance-2.0.0-beta.87.tgz", + "integrity": "sha512-HT0rbp3s3RY/JVfdx5UlyFKp6LH/QHuMx562UlbaWi8KF4mW/PkGXULk9O9B+He5BdT3EhvbJ56K0euNBT4TCA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@babel/core": "*", + "@types/katex": "^0.14.0", + "@types/markdown-it": "^12.2.3", + "@types/mermaid": "^8.2.9", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "chart.js": "^3.8.0", + "echarts": "^5.3.3", + "flowchart.js": "^1.17.1", + "katex": "^0.16.0", + "markdown-it": "^13.0.1", + "mermaid": "^9.1.3", + "reveal.js": "^4.3.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-photo-swipe": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-photo-swipe/-/vuepress-plugin-photo-swipe-2.0.0-beta.87.tgz", + "integrity": "sha512-kXdzjWfyV0xXB0N5M9jJNsg5tV5AjxPiQc8oRf6aiHG+mDf2GsljrNXeAjTYx93T4ahPGKs2M/Z3PSiYN8ONAg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "photoswipe": "^5.2.8", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-pwa2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-pwa2/-/vuepress-plugin-pwa2-2.0.0-beta.87.tgz", + "integrity": "sha512-XkWUYhu0kviogUCnCSzLZn7BC6JaWUYtGgLgVV9qpEK4jCqAHYMyijSsr6XL8hT4c+MCySU15nmtoJPoagPRIA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "mitt": "^3.0.0", + "register-service-worker": "^1.7.2", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87", + "workbox-build": "^6.5.3" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-reading-time2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-reading-time2/-/vuepress-plugin-reading-time2-2.0.0-beta.87.tgz", + "integrity": "sha512-LrEQmfYBpnd8U36jZvzrobVvx5Pl6dSL/kN1m/s/DlKm60hsHuHytLv8l1DYIwRNq9mE082P9mywd1FddaCJQg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-sass-palette": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sass-palette/-/vuepress-plugin-sass-palette-2.0.0-beta.87.tgz", + "integrity": "sha512-Z8RlqLIJnCGFG0ukHvCG8FGIvSzShbD05ISlNm7kxOf6Em/6xVkVMvYgwCL5KAc4EfLGjFm4rHuHbuDj8vpdBA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "sass": "^1.53.0", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress-plugin-seo2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-seo2/-/vuepress-plugin-seo2-2.0.0-beta.87.tgz", + "integrity": "sha512-QqEpnM9zCrMRneOq/NS3JZWLLwslgvoeL6jtq1VbJGtPElzx9ZxBrDrik/9nAiUaFX8Yc42GNUcgNDxKFXvXhg==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "gray-matter": "^4.0.3", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-plugin-sitemap2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sitemap2/-/vuepress-plugin-sitemap2-2.0.0-beta.87.tgz", + "integrity": "sha512-F0F0qlZ5Svr+w90+lI/vNwfojcTAp6B6aBq4qdj5EBX7uIfw4QQQSyvzN9jBKX9vD1cgM3BGILK7ddCqtknERw==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "sitemap": "^7.1.1", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "node_modules/vuepress-shared": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-shared/-/vuepress-shared-2.0.0-beta.87.tgz", + "integrity": "sha512-NbmjEiuBbMR/7GIhQVuPqFr3Kjq5RkliVocjZapyTNBx+9afevjEoDcBZ3VRmxZCir38cxW1Pc9j0FWjnfZnXA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "dayjs": "^1.11.3", + "execa": "^5.1.1", + "ora": "^5.4.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "node_modules/vuepress-theme-hope": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-theme-hope/-/vuepress-theme-hope-2.0.0-beta.87.tgz", + "integrity": "sha512-CTP4JJBSvsBD/LkJv+ePEPLqWRnmrwMXxuriPgqic9BF4v0TTuOu026KAd4eEfeY/Bd6LcVxCNurBrR0v7AueA==", + "deprecated": "Please use latest version", + "dev": true, + "dependencies": { + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "bcrypt-ts": "^1.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-blog2": "2.0.0-beta.87", + "vuepress-plugin-comment2": "2.0.0-beta.87", + "vuepress-plugin-components": "2.0.0-beta.87", + "vuepress-plugin-copy-code2": "2.0.0-beta.87", + "vuepress-plugin-copyright2": "2.0.0-beta.87", + "vuepress-plugin-feed2": "2.0.0-beta.87", + "vuepress-plugin-md-enhance": "2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "2.0.0-beta.87", + "vuepress-plugin-pwa2": "2.0.0-beta.87", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-plugin-seo2": "2.0.0-beta.87", + "vuepress-plugin-sitemap2": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + }, + "peerDependencies": { + "sass-loader": "^13.0.0" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + } + } + }, + "node_modules/vuepress/node_modules/vuepress-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress-vite/-/vuepress-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-iA0pBpjlonksEUbpyEKcTQH0r64mqWj+gHhFAur0/xzjsR8MYxU20b6gpEacDxyKLJr/zRja+XVPp6NSRnCCUg==", + "dev": true, + "dependencies": { + "@vuepress/bundler-vite": "2.0.0-beta.49", + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/theme-default": "2.0.0-beta.49" + }, + "bin": { + "vuepress": "bin/vuepress.js", + "vuepress-vite": "bin/vuepress.js" + }, + "peerDependencies": { + "@vuepress/client": "^2.0.0-beta.42", + "vue": "^3.2.36" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "dev": true, + "dependencies": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-chain/node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "node_modules/workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dev": true, + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workbox-build/node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==", + "dev": true + }, + "node_modules/workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dev": true, + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "dev": true, + "dependencies": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dev": true, + "dependencies": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "node_modules/workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4" + } + }, + "node_modules/workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dev": true, + "dependencies": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "node_modules/workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==", + "dev": true + }, + "node_modules/workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dev": true, + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/zrender": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz", + "integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", + "dev": true, + "dependencies": { + "tslib": "2.3.0" + } + } + }, + "dependencies": { + "@algolia/autocomplete-core": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.1.tgz", + "integrity": "sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg==", + "dev": true, + "requires": { + "@algolia/autocomplete-shared": "1.7.1" + } + }, + "@algolia/autocomplete-preset-algolia": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.1.tgz", + "integrity": "sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg==", + "dev": true, + "requires": { + "@algolia/autocomplete-shared": "1.7.1" + } + }, + "@algolia/autocomplete-shared": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.1.tgz", + "integrity": "sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg==", + "dev": true + }, + "@algolia/cache-browser-local-storage": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.2.tgz", + "integrity": "sha512-FRweBkK/ywO+GKYfAWbrepewQsPTIEirhi1BdykX9mxvBPtGNKccYAxvGdDCumU1jL4r3cayio4psfzKMejBlA==", + "dev": true, + "requires": { + "@algolia/cache-common": "4.14.2" + } + }, + "@algolia/cache-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-common/-/cache-common-4.14.2.tgz", + "integrity": "sha512-SbvAlG9VqNanCErr44q6lEKD2qoK4XtFNx9Qn8FK26ePCI8I9yU7pYB+eM/cZdS9SzQCRJBbHUumVr4bsQ4uxg==", + "dev": true + }, + "@algolia/cache-in-memory": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.2.tgz", + "integrity": "sha512-HrOukWoop9XB/VFojPv1R5SVXowgI56T9pmezd/djh2JnVN/vXswhXV51RKy4nCpqxyHt/aGFSq2qkDvj6KiuQ==", + "dev": true, + "requires": { + "@algolia/cache-common": "4.14.2" + } + }, + "@algolia/client-account": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-account/-/client-account-4.14.2.tgz", + "integrity": "sha512-WHtriQqGyibbb/Rx71YY43T0cXqyelEU0lB2QMBRXvD2X0iyeGl4qMxocgEIcbHyK7uqE7hKgjT8aBrHqhgc1w==", + "dev": true, + "requires": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "@algolia/client-analytics": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-analytics/-/client-analytics-4.14.2.tgz", + "integrity": "sha512-yBvBv2mw+HX5a+aeR0dkvUbFZsiC4FKSnfqk9rrfX+QrlNOKEhCG0tJzjiOggRW4EcNqRmaTULIYvIzQVL2KYQ==", + "dev": true, + "requires": { + "@algolia/client-common": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "@algolia/client-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-common/-/client-common-4.14.2.tgz", + "integrity": "sha512-43o4fslNLcktgtDMVaT5XwlzsDPzlqvqesRi4MjQz2x4/Sxm7zYg5LRYFol1BIhG6EwxKvSUq8HcC/KxJu3J0Q==", + "dev": true, + "requires": { + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "@algolia/client-personalization": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-personalization/-/client-personalization-4.14.2.tgz", + "integrity": "sha512-ACCoLi0cL8CBZ1W/2juehSltrw2iqsQBnfiu/Rbl9W2yE6o2ZUb97+sqN/jBqYNQBS+o0ekTMKNkQjHHAcEXNw==", + "dev": true, + "requires": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "@algolia/client-search": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/client-search/-/client-search-4.14.2.tgz", + "integrity": "sha512-L5zScdOmcZ6NGiVbLKTvP02UbxZ0njd5Vq9nJAmPFtjffUSOGEp11BmD2oMJ5QvARgx2XbX4KzTTNS5ECYIMWw==", + "dev": true, + "requires": { + "@algolia/client-common": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "@algolia/logger-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-common/-/logger-common-4.14.2.tgz", + "integrity": "sha512-/JGlYvdV++IcMHBnVFsqEisTiOeEr6cUJtpjz8zc0A9c31JrtLm318Njc72p14Pnkw3A/5lHHh+QxpJ6WFTmsA==", + "dev": true + }, + "@algolia/logger-console": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/logger-console/-/logger-console-4.14.2.tgz", + "integrity": "sha512-8S2PlpdshbkwlLCSAB5f8c91xyc84VM9Ar9EdfE9UmX+NrKNYnWR1maXXVDQQoto07G1Ol/tYFnFVhUZq0xV/g==", + "dev": true, + "requires": { + "@algolia/logger-common": "4.14.2" + } + }, + "@algolia/requester-browser-xhr": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.2.tgz", + "integrity": "sha512-CEh//xYz/WfxHFh7pcMjQNWgpl4wFB85lUMRyVwaDPibNzQRVcV33YS+63fShFWc2+42YEipFGH2iPzlpszmDw==", + "dev": true, + "requires": { + "@algolia/requester-common": "4.14.2" + } + }, + "@algolia/requester-common": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-common/-/requester-common-4.14.2.tgz", + "integrity": "sha512-73YQsBOKa5fvVV3My7iZHu1sUqmjjfs9TteFWwPwDmnad7T0VTCopttcsM3OjLxZFtBnX61Xxl2T2gmG2O4ehg==", + "dev": true + }, + "@algolia/requester-node-http": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/requester-node-http/-/requester-node-http-4.14.2.tgz", + "integrity": "sha512-oDbb02kd1o5GTEld4pETlPZLY0e+gOSWjWMJHWTgDXbv9rm/o2cF7japO6Vj1ENnrqWvLBmW1OzV9g6FUFhFXg==", + "dev": true, + "requires": { + "@algolia/requester-common": "4.14.2" + } + }, + "@algolia/transporter": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/@algolia/transporter/-/transporter-4.14.2.tgz", + "integrity": "sha512-t89dfQb2T9MFQHidjHcfhh6iGMNwvuKUvojAj+JsrHAGbuSy7yE4BylhLX6R0Q1xYRoC4Vvv+O5qIw/LdnQfsQ==", + "dev": true, + "requires": { + "@algolia/cache-common": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/requester-common": "4.14.2" + } + }, + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@apideck/better-ajv-errors": { + "version": "0.3.6", + "resolved": "https://registry.npmmirror.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz", + "integrity": "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==", + "dev": true, + "requires": { + "json-schema": "^0.4.0", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.18.8.tgz", + "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", + "dev": true + }, + "@babel/core": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + } + }, + "@babel/generator": { + "version": "7.18.12", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.10", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", + "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.9.tgz", + "integrity": "sha512-WvypNAYaVh23QcjpMR24CwZY2Nz6hqdOcFdPbNpV56hL5H6KiFheO7Xm1aPdlLQ7d5emYZX7VZwPp9x3z+2opw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.18.6.tgz", + "integrity": "sha512-7LcpH1wnQLGrI+4v+nPp+zUvIkF9x0ddv1Hkdue10tg3gmRnLy97DXh4STiOf1qeIInyD69Qv5kKSZzKD8B/7A==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.1.0" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.2.tgz", + "integrity": "sha512-r9QJJ+uDWrd+94BSPcP6/de67ygLtvVy6cK4luE6MOuDsZIdoaPBnfSpbO/+LTifjPckbKXRuI9BB/Z2/y3iTg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", + "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "dev": true, + "requires": { + "@babel/template": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", + "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", + "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz", + "integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz", + "integrity": "sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz", + "integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==", + "dev": true, + "requires": { + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz", + "integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", + "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.18.11.tgz", + "integrity": "sha512-oBUlbv+rjZLh2Ks9SKi4aL7eKaAXBWleHzU89mP0G6BMUlRxSckk9tSIkgDGydhgFxHuGSlBQZfnaD47oBEB7w==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.18.9", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.11", + "@babel/types": "^7.18.10" + } + }, + "@babel/helpers": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.18.9.tgz", + "integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==", + "dev": true, + "requires": { + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", + "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-proposal-optional-chaining": "^7.18.9" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", + "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", + "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", + "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.18.8" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", + "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", + "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.18.6.tgz", + "integrity": "sha512-/DU3RXad9+bZwrgWJQKbr39gYbJpLJHezqEzRzi/BHRlJ9zsQb4CK2CA/5apllXNomwA1qHwzvHl+AdEmC5krQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", + "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-remap-async-to-generator": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz", + "integrity": "sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz", + "integrity": "sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-replace-supers": "^7.18.9", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", + "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz", + "integrity": "sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", + "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.18.6.tgz", + "integrity": "sha512-Pra5aXsmTsOnjM3IajS8rTaLCy++nGM4v3YR4esk5PCsyg9z8NA5oQLwxzMUtDBd8F+UmVza3VxoAaWCbzH1rg==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.18.6.tgz", + "integrity": "sha512-Qfv2ZOWikpvmedXQJDSbxNqy7Xr/j2Y8/KfijM0iJyKkBTmWuvCA1yeH1yDM7NJhBW/2aXxeucLj6i80/LAJ/Q==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz", + "integrity": "sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-identifier": "^7.18.6", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.18.6.tgz", + "integrity": "sha512-UmEOGF8XgaIqD74bC8g7iV3RYj8lMf0Bw7NJzvnS9qQhM4mg+1WHKotUIdjxgD2RGrgFLZZPCFPFj3P/kVDYhg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.18.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz", + "integrity": "sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", + "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "regenerator-transform": "^0.15.0" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz", + "integrity": "sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.18.8", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-validator-option": "^7.18.6", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-async-generator-functions": "^7.18.10", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.18.6", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.18.9", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.18.9", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.18.6", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.18.6", + "@babel/plugin-transform-async-to-generator": "^7.18.6", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.18.9", + "@babel/plugin-transform-classes": "^7.18.9", + "@babel/plugin-transform-computed-properties": "^7.18.9", + "@babel/plugin-transform-destructuring": "^7.18.9", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.18.8", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.18.6", + "@babel/plugin-transform-modules-commonjs": "^7.18.6", + "@babel/plugin-transform-modules-systemjs": "^7.18.9", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.18.8", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.18.6", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.18.9", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.18.10", + "babel-plugin-polyfill-corejs2": "^0.3.2", + "babel-plugin-polyfill-corejs3": "^0.5.3", + "babel-plugin-polyfill-regenerator": "^0.4.0", + "core-js-compat": "^3.22.1", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + } + }, + "@babel/traverse": { + "version": "7.18.11", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.18.11.tgz", + "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.18.11", + "@babel/types": "^7.18.10", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.18.10", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.18.10.tgz", + "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + } + }, + "@braintree/sanitize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz", + "integrity": "sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w==", + "dev": true + }, + "@docsearch/css": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/css/-/css-3.2.0.tgz", + "integrity": "sha512-jnNrO2JVYYhj2pP2FomlHIy6220n6mrLn2t9v2/qc+rM7M/fbIcKMgk9ky4RN+L/maUEmteckzg6/PIYoAAXJg==", + "dev": true + }, + "@docsearch/js": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/js/-/js-3.2.0.tgz", + "integrity": "sha512-FEgXW8a+ZKBjSDteFPsKQ7Hlzk6+18A2Y7NffjV+VTsE7P3uTvHPKHKDCeYMnAgXTatRCGHWCfP7YImTSwEFQA==", + "dev": true, + "requires": { + "@docsearch/react": "3.2.0", + "preact": "^10.0.0" + } + }, + "@docsearch/react": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@docsearch/react/-/react-3.2.0.tgz", + "integrity": "sha512-ATS3w5JBgQGQF0kHn5iOAPfnCCaoLouZQMmI7oENV//QMFrYbjhUZxBU9lIwAT7Rzybud+Jtb4nG5IEjBk3Ixw==", + "dev": true, + "requires": { + "@algolia/autocomplete-core": "1.7.1", + "@algolia/autocomplete-preset-algolia": "1.7.1", + "@docsearch/css": "3.2.0", + "algoliasearch": "^4.0.0" + } + }, + "@esbuild/linux-loong64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz", + "integrity": "sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==", + "dev": true, + "optional": true + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@lit/reactive-element": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/@lit/reactive-element/-/reactive-element-1.3.4.tgz", + "integrity": "sha512-I1wz4uxOA52zSBhKmv4KQWLJpCyvfpnDg+eQR6mjpRgV+Ldi14HLPpSUpJklZRldz0fFmGCC/kVmuc/3cPFqCg==", + "dev": true + }, + "@mdit-vue/plugin-component": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.6.0.tgz", + "integrity": "sha512-S/Dd0eoOipbUAMdJ6A7M20dDizJxbtGAcL6T1iiJ0cEzjTrHP1kRT421+JMGPL8gcdsrIxgVSW8bI/R6laqBtA==", + "dev": true, + "requires": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/plugin-frontmatter": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-frontmatter/-/plugin-frontmatter-0.6.0.tgz", + "integrity": "sha512-cRunxy0q1gcqxUHAAiV8hMKh2qZOTDKXt8YOWfWNtf7IzaAL0v/nCOfh+O7AsHRmyc25Th8sL3H85HKWnNJtdw==", + "dev": true, + "requires": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "gray-matter": "^4.0.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/plugin-headers": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-headers/-/plugin-headers-0.6.0.tgz", + "integrity": "sha512-pg56w9/UooYuIZIoM0iQ021hrXt450fuRG3duxcwngw3unmE80rkvG3C0lT9ZnNXHSSYC9vGWUJh6EEN4nB34A==", + "dev": true, + "requires": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/plugin-sfc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-sfc/-/plugin-sfc-0.6.0.tgz", + "integrity": "sha512-R7mwUz2MxEopVQwpcOqCcqqvKx3ibRNcZ7QC31w4VblRb3Srk1st1UuGwHJxZ6Biro8ZWdPpMfpSsSk+2G+mIg==", + "dev": true, + "requires": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/plugin-title": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-title/-/plugin-title-0.6.0.tgz", + "integrity": "sha512-K2qUIrHmCp9w+/p1lWfkr808+Ge6FksM1ny/siiXHMHB0enArUd7G7SaEtro8JRb/hewd9qKq5xTOSWN2Q5jow==", + "dev": true, + "requires": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/plugin-toc": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-toc/-/plugin-toc-0.6.0.tgz", + "integrity": "sha512-5pgKY2++3w2/9Pqpgz7mZUiXs6jDcEyFPcf14QdiqSZ2eL+4VLuupcoC4JIDF+mAFHt+TJCfhk3oeG8Y6s6TBg==", + "dev": true, + "requires": { + "@mdit-vue/shared": "0.6.0", + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/shared": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/shared/-/shared-0.6.0.tgz", + "integrity": "sha512-RtV1P8jrEV/cl0WckOvpefiEWScw7omCQrIEtorlagG2XmnI9YbxMkLD53ETscA7lTVzqhGyzfoSrAiPi0Sjnw==", + "dev": true, + "requires": { + "@mdit-vue/types": "0.6.0", + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1" + } + }, + "@mdit-vue/types": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/@mdit-vue/types/-/types-0.6.0.tgz", + "integrity": "sha512-2Gf6MkEmoHrvO/IJsz48T+Ns9lW17ReC1vdhtCUGSCv0fFCm/L613uu/hpUrHuT3jTQHP90LcbXTQB2w4L1G8w==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "requires": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.30", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz", + "integrity": "sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/highlight.js": { + "version": "9.12.4", + "resolved": "https://registry.npmmirror.com/@types/highlight.js/-/highlight.js-9.12.4.tgz", + "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", + "dev": true + }, + "@types/http-proxy": { + "version": "1.17.9", + "resolved": "https://registry.npmmirror.com/@types/http-proxy/-/http-proxy-1.17.9.tgz", + "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/katex": { + "version": "0.14.0", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.14.0.tgz", + "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==", + "dev": true + }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-2ln8Wjbcj/0oRi/6VnuMeWEHHuK8uapFttvcLmDIe1GKCsFBLOLBX+D+xhDa9oWOQV0IpvxwrSfKKssAqqroog==", + "dev": true, + "requires": { + "@types/markdown-it": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", + "dev": true + }, + "@types/mermaid": { + "version": "8.2.9", + "resolved": "https://registry.npmmirror.com/@types/mermaid/-/mermaid-8.2.9.tgz", + "integrity": "sha512-f1i8fNoVFVJXedk+R7GcEk4KoOWzWAU3CzFqlVw1qWKktfsataBERezCz1pOdKy8Ec02ZdPQXGM7NU2lPHABYQ==", + "dev": true + }, + "@types/mime": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-3.0.1.tgz", + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, + "@types/node": { + "version": "18.6.4", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.6.4.tgz", + "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + } + } + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/@types/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, + "@types/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, + "requires": { + "@types/mime": "*", + "@types/node": "*" + } + }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, + "@types/tapable": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/tapable/-/tapable-1.0.8.tgz", + "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", + "dev": true + }, + "@types/trusted-types": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.2.tgz", + "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.16.0", + "resolved": "https://registry.npmmirror.com/@types/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-0yeUr92L3r0GLRnBOvtYK1v2SjqMIqQDHMl7GLb+l2L8+6LSFWEEWEIgVsPdMn5ImLM8qzWT8xFPtQYpp8co0g==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, + "@types/web-bluetooth": { + "version": "0.0.14", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz", + "integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==", + "dev": true + }, + "@types/webpack": { + "version": "4.41.32", + "resolved": "https://registry.npmmirror.com/@types/webpack/-/webpack-4.41.32.tgz", + "integrity": "sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "@types/webpack-dev-server": { + "version": "3.11.6", + "resolved": "https://registry.npmmirror.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", + "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", + "dev": true, + "requires": { + "@types/connect-history-api-fallback": "*", + "@types/express": "*", + "@types/serve-static": "*", + "@types/webpack": "^4", + "http-proxy-middleware": "^1.0.0" + } + }, + "@types/webpack-sources": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", + "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "@vitejs/plugin-vue": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-2.3.3.tgz", + "integrity": "sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==", + "dev": true, + "requires": {} + }, + "@vue/compiler-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz", + "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==", + "dev": true, + "requires": { + "@babel/parser": "^7.16.4", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz", + "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==", + "dev": true, + "requires": { + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "@vue/compiler-sfc": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz", + "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==", + "dev": true, + "requires": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-ssr": "3.2.37", + "@vue/reactivity-transform": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7", + "postcss": "^8.1.10", + "source-map": "^0.6.1" + } + }, + "@vue/compiler-ssr": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz", + "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "@vue/devtools-api": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.2.1.tgz", + "integrity": "sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==", + "dev": true + }, + "@vue/reactivity": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.2.37.tgz", + "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==", + "dev": true, + "requires": { + "@vue/shared": "3.2.37" + } + }, + "@vue/reactivity-transform": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz", + "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==", + "dev": true, + "requires": { + "@babel/parser": "^7.16.4", + "@vue/compiler-core": "3.2.37", + "@vue/shared": "3.2.37", + "estree-walker": "^2.0.2", + "magic-string": "^0.25.7" + } + }, + "@vue/runtime-core": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.2.37.tgz", + "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==", + "dev": true, + "requires": { + "@vue/reactivity": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "@vue/runtime-dom": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz", + "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==", + "dev": true, + "requires": { + "@vue/runtime-core": "3.2.37", + "@vue/shared": "3.2.37", + "csstype": "^2.6.8" + } + }, + "@vue/server-renderer": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.2.37.tgz", + "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==", + "dev": true, + "requires": { + "@vue/compiler-ssr": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "@vue/shared": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.2.37.tgz", + "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==", + "dev": true + }, + "@vuepress/bundler-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/bundler-vite/-/bundler-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-6AK3HuFHQKMWefTasyS+wsvb0wLufWBdQ/eHMDxZudE63dU7mSwCvV0kpX2uFzhlpdE/ug/8NuQbOlh4zZayvA==", + "dev": true, + "requires": { + "@vitejs/plugin-vue": "^2.3.3", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "autoprefixer": "^10.4.7", + "connect-history-api-fallback": "^2.0.0", + "postcss": "^8.4.14", + "rollup": "^2.76.0", + "vite": "~2.9.14", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/cli": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/cli/-/cli-2.0.0-beta.49.tgz", + "integrity": "sha512-3RtuZvtLIGXEtsLgc3AnDr4jxiFeFDWfNw6MTb22YwuttBr5h5pZO/F8XMyP9+tEi73q3/l4keNQftU4msHysQ==", + "dev": true, + "requires": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "cac": "^6.7.12", + "chokidar": "^3.5.3", + "envinfo": "^7.8.1", + "esbuild": "^0.14.49" + } + }, + "@vuepress/client": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/client/-/client-2.0.0-beta.49.tgz", + "integrity": "sha512-zfGlCAF/LwDOrZXZPqADsMgWRuH/2GFOGSOCvt7ZUZHnSrYBdK2FOez/ksWL8EwGNLsRLB8ny1IachMwTew5og==", + "dev": true, + "requires": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/shared": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/core": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/core/-/core-2.0.0-beta.49.tgz", + "integrity": "sha512-40J74qGOPqF9yGdXdzPD1kW9mv5/jfJenmhsH1xaErPsr6qIM8jcraVRC+R7NoVTIecRk9cC9MJcDRnLmDDiAg==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "@vuepress/markdown": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/markdown/-/markdown-2.0.0-beta.49.tgz", + "integrity": "sha512-aAw41NArV5leIpZOFmElxzRG29LDdEQe7oIcZtIvKPhVmEfg9/mgx4ea2OqY5DaBvEhkG42SojjKvmHiJKrwJw==", + "dev": true, + "requires": { + "@mdit-vue/plugin-component": "^0.6.0", + "@mdit-vue/plugin-frontmatter": "^0.6.0", + "@mdit-vue/plugin-headers": "^0.6.0", + "@mdit-vue/plugin-sfc": "^0.6.0", + "@mdit-vue/plugin-title": "^0.6.0", + "@mdit-vue/plugin-toc": "^0.6.0", + "@mdit-vue/shared": "^0.6.0", + "@mdit-vue/types": "^0.6.0", + "@types/markdown-it": "^12.2.3", + "@types/markdown-it-emoji": "^2.0.2", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-anchor": "^8.6.4", + "markdown-it-emoji": "^2.0.2", + "mdurl": "^1.0.1" + } + }, + "@vuepress/plugin-active-header-links": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-2.0.0-beta.49.tgz", + "integrity": "sha512-p69WE1eQwUoe1FtlVf029ZsdS44pLLkxXsq8+XRi3TRGbhK3kcUy7m6Amjj3imV2iJm2CYtQWpNjs22O1jjMMw==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/plugin-back-to-top": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-back-to-top/-/plugin-back-to-top-2.0.0-beta.49.tgz", + "integrity": "sha512-fDwU916nLLnS7Pye2XR1Hf9c/4Vc8YdldwXWECtpBybdk/1h8bWb/qMOmL84W39ZF4k3XbZX24ld3uw2JQm52A==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37" + } + }, + "@vuepress/plugin-container": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-container/-/plugin-container-2.0.0-beta.49.tgz", + "integrity": "sha512-PWChjwDVci4UMrzT4z4eYooXikf60+PseMuUioLF5lB6/6AYfL5QrzXOq7znRtG/IXtE8jIjid962eFJDvw/iA==", + "dev": true, + "requires": { + "@types/markdown-it": "^12.2.3", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "markdown-it": "^13.0.1", + "markdown-it-container": "^3.0.0" + } + }, + "@vuepress/plugin-docsearch": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-docsearch/-/plugin-docsearch-2.0.0-beta.49.tgz", + "integrity": "sha512-580pQ9AyOjTe64YH8h3MHsvj+EfxCmJ6IJ/3kp51tT0/zL59mE8aLyveyvgwJrvhBdki5PMOGgBx95tOT7QVwQ==", + "dev": true, + "requires": { + "@docsearch/css": "^3.1.1", + "@docsearch/js": "^3.1.1", + "@docsearch/react": "^3.1.1", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "ts-debounce": "^4.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/plugin-external-link-icon": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-external-link-icon/-/plugin-external-link-icon-2.0.0-beta.49.tgz", + "integrity": "sha512-ZwmLJAp3xF+0yJNeqaTwc17Nw0RyMk8DsNfoecyRgzHud8OxrcJj+NLF8Tpw+t1k22cfIfaIIyWJbGcGZOzVCw==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/markdown": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "@vuepress/plugin-git": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-git/-/plugin-git-2.0.0-beta.49.tgz", + "integrity": "sha512-CjaBYWBAkQmlpx5v+mp2vsoRxqRTi/mSvXy8im/ftc8zX/sVT4V1LBWX1IsDQn1VpWnArlfAsFd+BrmxzPFePA==", + "dev": true, + "requires": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "execa": "^5.1.1" + } + }, + "@vuepress/plugin-google-analytics": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz", + "integrity": "sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q==", + "dev": true, + "requires": { + "@vuepress/types": "1.9.7" + } + }, + "@vuepress/plugin-medium-zoom": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-medium-zoom/-/plugin-medium-zoom-2.0.0-beta.49.tgz", + "integrity": "sha512-Z80E/BhHnTQeC208Dw9D1CpyxONGJ3HVNd3dU3qJfdjX9o8GzkRqdo17aq4aHOeEPn0DQ04I/7sHFVgv41KGgw==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "medium-zoom": "^1.0.6", + "vue": "^3.2.37" + } + }, + "@vuepress/plugin-nprogress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-nprogress/-/plugin-nprogress-2.0.0-beta.49.tgz", + "integrity": "sha512-SBnOQMMxhdzdbB4yCxCzFGpZUxTV4BvexauLXfZNqm128WwXRHk6MJltFIZIFODJldMpSuCCrkm0Uj7vC5yDUA==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/plugin-palette": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-palette/-/plugin-palette-2.0.0-beta.49.tgz", + "integrity": "sha512-88zeO8hofW+jl+GyMXXRW8t5/ibBoUUVCp4ctN+dJvDNADbBIVVQOkwQhDnPUyVwoEni/dQ4b879YyZXOhT5MA==", + "dev": true, + "requires": { + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3" + } + }, + "@vuepress/plugin-prismjs": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-prismjs/-/plugin-prismjs-2.0.0-beta.49.tgz", + "integrity": "sha512-/XK+Gjs92SEoqHL1XGaspMxv0sMMEPrR+YisSQn3KzaWE59yylsD3I7fMOkJI7D02n9Cw8pejGoR3XOH0M8Q2Q==", + "dev": true, + "requires": { + "@vuepress/core": "2.0.0-beta.49", + "prismjs": "^1.28.0" + } + }, + "@vuepress/plugin-search": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-search/-/plugin-search-2.0.0-beta.49.tgz", + "integrity": "sha512-XkI5FfqJUODh5V7ic/hjja4rjVJQoT29xff63hDFvm+aVPG9FwAHtMSqUHutWO92WtlqoDi9y2lTbpyDYu6+rQ==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/plugin-theme-data": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/plugin-theme-data/-/plugin-theme-data-2.0.0-beta.49.tgz", + "integrity": "sha512-zwbnDKPOOljSz7nMQXCNefp2zpDlwRIX5RTej9JQlCdcPXyLkFfvDgIMVpKNx6/5/210tKxFsCpmjLR8i+DbgQ==", + "dev": true, + "requires": { + "@vue/devtools-api": "^6.2.0", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vue": "^3.2.37" + } + }, + "@vuepress/shared": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/shared/-/shared-2.0.0-beta.49.tgz", + "integrity": "sha512-yoUgOtRUrIfe0O1HMTIMj0NYU3tAiUZ4rwVEtemtGa7/RK7qIZdBpAfv08Ve2CUpa3wrMb1Pux1aBsiz1EQx+g==", + "dev": true, + "requires": { + "@vue/shared": "^3.2.37" + } + }, + "@vuepress/theme-default": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/theme-default/-/theme-default-2.0.0-beta.49.tgz", + "integrity": "sha512-HUhDT7aWdtsZTRmDDWgWc9vRWGKGLh8GB+mva+TQABTgXV4qPmvuKzRi0yOU3FX1todRifxVPJTiJYVfh7zkPQ==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-back-to-top": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-medium-zoom": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.7.5", + "sass": "^1.53.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "@vuepress/types": { + "version": "1.9.7", + "resolved": "https://registry.npmmirror.com/@vuepress/types/-/types-1.9.7.tgz", + "integrity": "sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog==", + "dev": true, + "requires": { + "@types/markdown-it": "^10.0.0", + "@types/webpack-dev-server": "^3", + "webpack-chain": "^6.0.0" + }, + "dependencies": { + "@types/markdown-it": { + "version": "10.0.3", + "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-10.0.3.tgz", + "integrity": "sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==", + "dev": true, + "requires": { + "@types/highlight.js": "^9.7.0", + "@types/linkify-it": "*", + "@types/mdurl": "*", + "highlight.js": "^9.7.0" + } + } + } + }, + "@vuepress/utils": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/@vuepress/utils/-/utils-2.0.0-beta.49.tgz", + "integrity": "sha512-t5i0V9FqpKLGlu2kMP/Y9+wdgEmsD2yQAMGojxpMoFhJBmqn2L9Rkk4WYzHKzPGDkm1KbBFzYQqjAhZQ7xtY1A==", + "dev": true, + "requires": { + "@types/debug": "^4.1.7", + "@types/fs-extra": "^9.0.13", + "@vuepress/shared": "2.0.0-beta.49", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "globby": "^11.0.4", + "hash-sum": "^2.0.0", + "ora": "^5.4.1", + "upath": "^2.0.1" + } + }, + "@vueuse/core": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-8.9.4.tgz", + "integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==", + "dev": true, + "requires": { + "@types/web-bluetooth": "^0.0.14", + "@vueuse/metadata": "8.9.4", + "@vueuse/shared": "8.9.4", + "vue-demi": "*" + } + }, + "@vueuse/metadata": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.9.4.tgz", + "integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==", + "dev": true + }, + "@vueuse/shared": { + "version": "8.9.4", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.9.4.tgz", + "integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==", + "dev": true, + "requires": { + "vue-demi": "*" + } + }, + "@waline/client": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/@waline/client/-/client-2.6.2.tgz", + "integrity": "sha512-yQUPRXF8Om+YQCeqZY4BWHKOpHAouGYlRiqBWsen/hUgdRs5eMcsnaYtrcluQHFzPb/Mv2HPOVKQCJys6oSJpw==", + "dev": true, + "requires": { + "@vueuse/core": "^8.9.4", + "autosize": "^5.0.1", + "marked": "^4.0.18", + "vue": "^3.2.37" + } + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "algoliasearch": { + "version": "4.14.2", + "resolved": "https://registry.npmmirror.com/algoliasearch/-/algoliasearch-4.14.2.tgz", + "integrity": "sha512-ngbEQonGEmf8dyEh5f+uOIihv4176dgbuOZspiuhmTTBRBuzWu3KCGHre6uHj5YyuC7pNvQGzB6ZNJyZi0z+Sg==", + "dev": true, + "requires": { + "@algolia/cache-browser-local-storage": "4.14.2", + "@algolia/cache-common": "4.14.2", + "@algolia/cache-in-memory": "4.14.2", + "@algolia/client-account": "4.14.2", + "@algolia/client-analytics": "4.14.2", + "@algolia/client-common": "4.14.2", + "@algolia/client-personalization": "4.14.2", + "@algolia/client-search": "4.14.2", + "@algolia/logger-common": "4.14.2", + "@algolia/logger-console": "4.14.2", + "@algolia/requester-browser-xhr": "4.14.2", + "@algolia/requester-common": "4.14.2", + "@algolia/requester-node-http": "4.14.2", + "@algolia/transporter": "4.14.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "dev": true + }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.8", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.8.tgz", + "integrity": "sha512-75Jr6Q/XpTqEf6D2ltS5uMewJIx5irCU1oBYJrWjFenq/m12WRRrz6g15L1EIoYvPLXTbEry7rDOwrcYNj77xw==", + "dev": true, + "requires": { + "browserslist": "^4.21.3", + "caniuse-lite": "^1.0.30001373", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "autosize": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/autosize/-/autosize-5.0.1.tgz", + "integrity": "sha512-UIWUlE4TOVPNNj2jjrU39wI4hEYbneUypEqcyRmRFIx5CC2gNdg3rQr+Zh7/3h6egbBvm33TDQjNQKtj9Tk1HA==", + "dev": true + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.2.tgz", + "integrity": "sha512-LPnodUl3lS0/4wN3Rb+m+UK8s7lj2jcLRrjho4gLw+OJs+I4bvGXshINesY5xx/apM+biTnQ9reDI8yj+0M5+Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.2", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.2", + "core-js-compat": "^3.21.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.0.tgz", + "integrity": "sha512-RW1cnryiADFeHmfLS+WW/G431p1PsW5qdRdz0SDRi7TKcUgc7Oh/uXkT7MZ/+tGsT1BkczEAmD5XjUyJ5SWDTw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "balloon-css": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/balloon-css/-/balloon-css-1.2.0.tgz", + "integrity": "sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bcrypt-ts": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/bcrypt-ts/-/bcrypt-ts-1.0.0.tgz", + "integrity": "sha512-7CwTSYmfIPdP/CR2uKMajK4eNByTIZqJgrF2j4yozv8BDMbBgCzzxy6iKy6WLueZH+ZcFpyWDa6ddsDH9Yf5FA==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.21.3", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.21.3.tgz", + "integrity": "sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001370", + "electron-to-chromium": "^1.4.202", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.5" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, + "cac": { + "version": "6.7.12", + "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.12.tgz", + "integrity": "sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "caniuse-lite": { + "version": "1.0.30001374", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz", + "integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.7.0.tgz", + "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", + "dev": true + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "core-js-compat": { + "version": "3.24.1", + "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.24.1.tgz", + "integrity": "sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==", + "dev": true, + "requires": { + "browserslist": "^4.21.3", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "csstype": { + "version": "2.6.20", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-2.6.20.tgz", + "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==", + "dev": true + }, + "d3": { + "version": "7.6.1", + "resolved": "https://registry.npmmirror.com/d3/-/d3-7.6.1.tgz", + "integrity": "sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw==", + "dev": true, + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.0.tgz", + "integrity": "sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g==", + "dev": true, + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "dev": true + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true + }, + "d3-contour": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.0.tgz", + "integrity": "sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw==", + "dev": true, + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "dev": true, + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true + }, + "d3-geo": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.0.1.tgz", + "integrity": "sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA==", + "dev": true, + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.0.1.tgz", + "integrity": "sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w==", + "dev": true + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dev": true, + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true + }, + "d3-shape": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.1.0.tgz", + "integrity": "sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ==", + "dev": true, + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ==", + "dev": true, + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-voronoi": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz", + "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", + "dev": true + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "dev": true, + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "dagre-d3": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/dagre-d3/-/dagre-d3-0.6.4.tgz", + "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==", + "dev": true, + "requires": { + "d3": "^5.14", + "dagre": "^0.8.5", + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "d3": { + "version": "5.16.0", + "resolved": "https://registry.npmmirror.com/d3/-/d3-5.16.0.tgz", + "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==", + "dev": true, + "requires": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true + }, + "d3-axis": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-1.0.12.tgz", + "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==", + "dev": true + }, + "d3-brush": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-1.1.6.tgz", + "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "d3-chord": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-1.0.6.tgz", + "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "dev": true, + "requires": { + "d3-array": "1", + "d3-path": "1" + } + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==", + "dev": true + }, + "d3-contour": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-1.3.2.tgz", + "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", + "dev": true, + "requires": { + "d3-array": "^1.1.1" + } + }, + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==", + "dev": true + }, + "d3-drag": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-1.2.5.tgz", + "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "d3-dsv": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-1.2.0.tgz", + "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==", + "dev": true, + "requires": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + } + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==", + "dev": true + }, + "d3-fetch": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-1.2.0.tgz", + "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==", + "dev": true, + "requires": { + "d3-dsv": "1" + } + }, + "d3-force": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-1.2.1.tgz", + "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==", + "dev": true, + "requires": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "d3-format": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "dev": true + }, + "d3-geo": { + "version": "1.12.1", + "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-1.12.1.tgz", + "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==", + "dev": true, + "requires": { + "d3-array": "1" + } + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "dev": true + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dev": true, + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true + }, + "d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==", + "dev": true + }, + "d3-quadtree": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz", + "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==", + "dev": true + }, + "d3-random": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-1.1.2.tgz", + "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==", + "dev": true + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "dev": true, + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "dev": true, + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==", + "dev": true + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==", + "dev": true + }, + "d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dev": true, + "requires": { + "d3-time": "1" + } + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==", + "dev": true + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "dev": true, + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "d3-zoom": { + "version": "1.8.3", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-1.8.3.tgz", + "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==", + "dev": true, + "requires": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "dayjs": { + "version": "1.11.4", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.4.tgz", + "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dev": true, + "requires": { + "robust-predicates": "^3.0.0" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dompurify": { + "version": "2.3.10", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-2.3.10.tgz", + "integrity": "sha512-o7Fg/AgC7p/XpKjf/+RC3Ok6k4St5F7Q6q6+Nnm3p2zGWioAY6dh0CbbuwOhH2UcSzKsdniE/YnE2/92JcsA+g==", + "dev": true + }, + "echarts": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.3.3.tgz", + "integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", + "dev": true, + "requires": { + "tslib": "2.3.0", + "zrender": "5.3.2" + } + }, + "ejs": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.4.211", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz", + "integrity": "sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==", + "dev": true + }, + "entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true + }, + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmmirror.com/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.14.53.tgz", + "integrity": "sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw==", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.53", + "esbuild-android-64": "0.14.53", + "esbuild-android-arm64": "0.14.53", + "esbuild-darwin-64": "0.14.53", + "esbuild-darwin-arm64": "0.14.53", + "esbuild-freebsd-64": "0.14.53", + "esbuild-freebsd-arm64": "0.14.53", + "esbuild-linux-32": "0.14.53", + "esbuild-linux-64": "0.14.53", + "esbuild-linux-arm": "0.14.53", + "esbuild-linux-arm64": "0.14.53", + "esbuild-linux-mips64le": "0.14.53", + "esbuild-linux-ppc64le": "0.14.53", + "esbuild-linux-riscv64": "0.14.53", + "esbuild-linux-s390x": "0.14.53", + "esbuild-netbsd-64": "0.14.53", + "esbuild-openbsd-64": "0.14.53", + "esbuild-sunos-64": "0.14.53", + "esbuild-windows-32": "0.14.53", + "esbuild-windows-64": "0.14.53", + "esbuild-windows-arm64": "0.14.53" + } + }, + "esbuild-android-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz", + "integrity": "sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz", + "integrity": "sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz", + "integrity": "sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz", + "integrity": "sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz", + "integrity": "sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz", + "integrity": "sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz", + "integrity": "sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz", + "integrity": "sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz", + "integrity": "sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz", + "integrity": "sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz", + "integrity": "sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz", + "integrity": "sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz", + "integrity": "sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz", + "integrity": "sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz", + "integrity": "sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz", + "integrity": "sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz", + "integrity": "sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz", + "integrity": "sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz", + "integrity": "sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.53", + "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz", + "integrity": "sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "eve-raphael": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/eve-raphael/-/eve-raphael-0.5.0.tgz", + "integrity": "sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "minimatch": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "flowchart.js": { + "version": "1.17.1", + "resolved": "https://registry.npmmirror.com/flowchart.js/-/flowchart.js-1.17.1.tgz", + "integrity": "sha512-zphTaxdyqvHHu+8Cdf6HvamhArXpq9SyNe1zQ61maCIfTenaj3cMvjS1e/0gfPj7QTLTx3HroSzVqDXpL8naoQ==", + "dev": true, + "requires": { + "raphael": "2.3.0" + } + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "giscus": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/giscus/-/giscus-1.2.0.tgz", + "integrity": "sha512-IpfWvU0/hYbMGQKuoPlED8wWmluRYIOjtrBCnL7logsWjMpPRxiAC2pUIC0+SC0pDMOqXrk1onTYMHgwgRpRzg==", + "dev": true, + "requires": { + "lit": "^2.2.8" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dev": true, + "requires": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-middleware": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", + "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "idb": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/idb/-/idb-7.0.2.tgz", + "integrity": "sha512-jjKrT1EnyZewQ/gCBb/eyiYrhGzws2FeY92Yx8qT9S9GeQAmo4JFVIiWRIfKW/6Ob9A+UDAOW9j9jn58fy2HIg==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmmirror.com/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true + }, + "katex": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.0.tgz", + "integrity": "sha512-wPRB4iUPysfH97wTgG5/tRLYxmKVq6Q4jRAWRVOUxXB1dsiv4cvcNjqabHkrOvJHM1Bpk3WrgmllSO1vIvP24w==", + "dev": true, + "requires": { + "commander": "^8.0.0" + } + }, + "khroma": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.0.0.tgz", + "integrity": "sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==", + "dev": true + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dev": true, + "requires": { + "uc.micro": "^1.0.1" + } + }, + "lit": { + "version": "2.2.8", + "resolved": "https://registry.npmmirror.com/lit/-/lit-2.2.8.tgz", + "integrity": "sha512-QjeNbi/H9LVIHR+u0OqsL+hs62a16m02JlJHYN48HcBuXyiPYR8JvzsTp5dYYS81l+b9Emp3UaGo82EheV0pog==", + "dev": true, + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "lit-element": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/lit-element/-/lit-element-3.2.2.tgz", + "integrity": "sha512-6ZgxBR9KNroqKb6+htkyBwD90XGRiqKDHVrW/Eh0EZ+l+iC+u+v+w3/BA5NGi4nizAVHGYvQBHUDuSmLjPp7NQ==", + "dev": true, + "requires": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "lit-html": { + "version": "2.2.7", + "resolved": "https://registry.npmmirror.com/lit-html/-/lit-html-2.2.7.tgz", + "integrity": "sha512-JhqiAwO1l03kRe68uBZ0i2x4ef2S5szY9vvP411nlrFZIpKK4/hwnhA/15bqbvxe1lV3ipBdhaOzHmyOk7QIRg==", + "dev": true, + "requires": { + "@types/trusted-types": "^2.0.2" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmmirror.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "lodash.findindex": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/lodash.findindex/-/lodash.findindex-4.6.0.tgz", + "integrity": "sha512-9er6Ccz6sEST3bHFtUrCFWk14nE8cdL/RoW1RRDV1BxqN3qsmsT56L14jhfctAqhVPVcdJw4MRxEaVoAK+JVvw==", + "dev": true + }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "lodash.trimend": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimend/-/lodash.trimend-4.5.1.tgz", + "integrity": "sha512-lsD+k73XztDsMBKPKvzHXRKFNMohTjoTKIIo4ADLn5dA65LZ1BqlAvSXhR2rPEC3BgAUQnzMnorqDtqn2z4IHA==", + "dev": true + }, + "lodash.trimstart": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz", + "integrity": "sha512-b/+D6La8tU76L/61/aN0jULWHkT0EeJCmVstPBn/K9MtD2qBW83AsBNrr63dKuWYwVMO7ucv13QNO/Ek/2RKaQ==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "markdown-it": { + "version": "13.0.1", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.1.tgz", + "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==", + "dev": true, + "requires": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "markdown-it-anchor": { + "version": "8.6.4", + "resolved": "https://registry.npmmirror.com/markdown-it-anchor/-/markdown-it-anchor-8.6.4.tgz", + "integrity": "sha512-Ul4YVYZNxMJYALpKtu+ZRdrryYt/GlQ5CK+4l1bp/gWXOG2QWElt6AqF3Mih/wfUKdZbNAZVXGR73/n6U/8img==", + "dev": true, + "requires": {} + }, + "markdown-it-container": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/markdown-it-container/-/markdown-it-container-3.0.0.tgz", + "integrity": "sha512-y6oKTq4BB9OQuY/KLfk/O3ysFhB3IMYoIWhGJEidXt1NQFocFK2sA2t0NYZAMyMShAGL6x5OPIbrmXPIqaN9rw==", + "dev": true + }, + "markdown-it-emoji": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==", + "dev": true + }, + "marked": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/marked/-/marked-4.0.18.tgz", + "integrity": "sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw==", + "dev": true + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, + "medium-zoom": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/medium-zoom/-/medium-zoom-1.0.6.tgz", + "integrity": "sha512-UdiUWfvz9fZMg1pzf4dcuqA0W079o0mpqbTnOz5ip4VGYX96QjmbM+OgOU/0uOzAytxC0Ny4z+VcYQnhdifimg==", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "mermaid": { + "version": "9.1.4", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-9.1.4.tgz", + "integrity": "sha512-QgQTpIIzJfV/Ob7FZTDzxmWjFzCciij4C8RbbQbamsadf2gHrNrfqAoWLF6ALfQlW5ZqOefvlogDdWcFZRnifg==", + "dev": true, + "requires": { + "@braintree/sanitize-url": "^6.0.0", + "d3": "^7.0.0", + "dagre": "^0.8.5", + "dagre-d3": "^0.6.4", + "dompurify": "2.3.10", + "graphlib": "^2.1.8", + "khroma": "^2.0.0", + "moment-mini": "^2.24.0", + "stylis": "^4.0.10" + } + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + } + } + }, + "mitt": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.0.tgz", + "integrity": "sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==", + "dev": true + }, + "moment-mini": { + "version": "2.24.0", + "resolved": "https://registry.npmmirror.com/moment-mini/-/moment-mini-2.24.0.tgz", + "integrity": "sha512-9ARkWHBs+6YJIvrIp0Ik5tyTTtP9PoV0Ssu2Ocq5y9v8+NOOpWiRshAp8c4rZVWTOe+157on/5G+zj5pwIQFEQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmmirror.com/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "photoswipe": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/photoswipe/-/photoswipe-5.3.0.tgz", + "integrity": "sha512-vZMwziQorjiagzX7EvWimVT0YHO0DWNtR9UT6cv3yW1FA199LgsTpj4ziB2oJ/X/197gKmi56Oux5PudWUAmuw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "preact": { + "version": "10.10.1", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.10.1.tgz", + "integrity": "sha512-cXljG59ylGtSLismoLojXPAGvnh2ipQr3BYz9KZQr+1sdASCT+sR/v8dSMDS96xGCdtln2wHfAHCnLJK+XcBNg==", + "dev": true + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "prismjs": { + "version": "1.28.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.28.0.tgz", + "integrity": "sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "raphael": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/raphael/-/raphael-2.3.0.tgz", + "integrity": "sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ==", + "dev": true, + "requires": { + "eve-raphael": "0.5.0" + } + }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.0", + "resolved": "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz", + "integrity": "sha512-LsrGtPmbYg19bcPHwdtmXwbW+TqNvtY4riE3P83foeHRroMbH6/2ddFBfab3t7kbzc7v7p4wbkIecHImqt0QNg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpu-core": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-5.1.0.tgz", + "integrity": "sha512-bb6hk+xWd2PEOkj5It46A16zFMs2mv86Iwpdu94la4S3sJ7C973h2dHpYKwIBGaWSO7cIRJ+UX0IeMaWcO4qwA==", + "dev": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + } + }, + "register-service-worker": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/register-service-worker/-/register-service-worker-1.7.2.tgz", + "integrity": "sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==", + "dev": true + }, + "regjsgen": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", + "dev": true + }, + "regjsparser": { + "version": "0.8.4", + "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, + "remove-markdown": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/remove-markdown/-/remove-markdown-0.3.0.tgz", + "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "reveal.js": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/reveal.js/-/reveal.js-4.3.1.tgz", + "integrity": "sha512-1kyEnWeUkaCdBdX//XXq9dtBK95ppvIlSwlHelrP8/wrX6LcsYp4HT9WTFoFEOUBfVqkm8C2aHQ367o+UKfcxw==", + "dev": true + }, + "robust-predicates": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.1.tgz", + "integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==", + "dev": true + }, + "rollup": { + "version": "2.77.2", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.77.2.tgz", + "integrity": "sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sass": { + "version": "1.54.3", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.54.3.tgz", + "integrity": "sha512-fLodey5Qd41Pxp/Tk7Al97sViYwF/TazRc5t6E65O7JOk4XF8pzwIW7CvCxYVOfJFFI/1x5+elDyBIixrp+zrw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "sitemap": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/sitemap/-/sitemap-7.1.1.tgz", + "integrity": "sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg==", + "dev": true, + "requires": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "dependencies": { + "@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "dev": true + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "dev": true + }, + "strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha512-3HVl+cOkJOlNUDAYdoCAfGx/fzUzG53YvJAl3RYlTvAcBdPqSp1Uv4wrmHymm7oEypTijSQqcqplW8cz0/r/YA==", + "dev": true + }, + "stylis": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz", + "integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true + }, + "tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + } + }, + "terser": { + "version": "5.14.2", + "resolved": "https://registry.npmmirror.com/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + } + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "ts-debounce": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/ts-debounce/-/ts-debounce-4.0.0.tgz", + "integrity": "sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==", + "dev": true + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true + }, + "twikoo": { + "version": "1.6.4", + "resolved": "https://registry.npmmirror.com/twikoo/-/twikoo-1.6.4.tgz", + "integrity": "sha512-QC34vA037Pg2ENc05kqH3u9RrJbaf3UV9EfMUHdRurhL5frAekIU03X9TZ3JBdcx5NhvgiCxeZ9wOPA3szHtgw==", + "dev": true + }, + "type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true + }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "upath": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz", + "integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "vite": { + "version": "2.9.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-2.9.14.tgz", + "integrity": "sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + } + }, + "vue": { + "version": "3.2.37", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.2.37.tgz", + "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==", + "dev": true, + "requires": { + "@vue/compiler-dom": "3.2.37", + "@vue/compiler-sfc": "3.2.37", + "@vue/runtime-dom": "3.2.37", + "@vue/server-renderer": "3.2.37", + "@vue/shared": "3.2.37" + } + }, + "vue-demi": { + "version": "0.13.6", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.6.tgz", + "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==", + "dev": true, + "requires": {} + }, + "vue-router": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.3.tgz", + "integrity": "sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==", + "dev": true, + "requires": { + "@vue/devtools-api": "^6.1.4" + } + }, + "vuepress": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress/-/vuepress-2.0.0-beta.49.tgz", + "integrity": "sha512-dxbgCNn+S9DDUu4Ao/QqwfdQF3e6IgpKhqQxYPPO/xVYZbnQnmXbzh0uGdtKUAyKKgP8UouWbp4Qdk1/Z6ay9Q==", + "dev": true, + "requires": { + "vuepress-vite": "2.0.0-beta.49" + }, + "dependencies": { + "vuepress-vite": { + "version": "2.0.0-beta.49", + "resolved": "https://registry.npmmirror.com/vuepress-vite/-/vuepress-vite-2.0.0-beta.49.tgz", + "integrity": "sha512-iA0pBpjlonksEUbpyEKcTQH0r64mqWj+gHhFAur0/xzjsR8MYxU20b6gpEacDxyKLJr/zRja+XVPp6NSRnCCUg==", + "dev": true, + "requires": { + "@vuepress/bundler-vite": "2.0.0-beta.49", + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/theme-default": "2.0.0-beta.49" + } + } + } + }, + "vuepress-plugin-autometa": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-autometa/-/vuepress-plugin-autometa-0.1.13.tgz", + "integrity": "sha512-yhd8smLhbCO0+gc3FRNjOyffdGl12gUkv60UqdtEUCojEy4s7APDJ2pt85cSLASMdnWaY7EcMibMRkqeUOVAKg==", + "dev": true, + "requires": { + "lodash.defaultsdeep": "4.6.1", + "lodash.findindex": "4.6.0", + "lodash.isempty": "4.4.0", + "lodash.trimend": "^4.5.1", + "lodash.trimstart": "^4.5.1", + "remove-markdown": "0.3.0", + "striptags": "3.1.1" + } + }, + "vuepress-plugin-baidu-autopush": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-baidu-autopush/-/vuepress-plugin-baidu-autopush-1.0.1.tgz", + "integrity": "sha512-KVQkrmMgPY+GG8dtI2wcRxUv1n2h5DM8aFs75ltsSlFBSS9C/vfLb2LmywXAsoCXk2EHya2p66cpn7BxofK+Mw==", + "dev": true + }, + "vuepress-plugin-blog2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-blog2/-/vuepress-plugin-blog2-2.0.0-beta.87.tgz", + "integrity": "sha512-NbuxiWfTLV4hDSHj5PxBrsmv5Bdh3Gkwc3z36hcETmrLPf39uasxnwgHlDuhjuau9yqsilX3oqg/xRzB6nrLYg==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-comment2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-comment2/-/vuepress-plugin-comment2-2.0.0-beta.87.tgz", + "integrity": "sha512-QbeCil40itjoEj6SaWtWSaZQ0YakCEz9CBG53n4/misW2JHgEbKQCpimm3hrWD3AgqZ2N2bFyuGH42hcy+k9vw==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@waline/client": "^2.6.1", + "giscus": "^1.0.6", + "twikoo": "^1.5.11", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-components": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-components/-/vuepress-plugin-components-2.0.0-beta.87.tgz", + "integrity": "sha512-X4KkINr4llIHPMb/YCnlxqwRabT4VY/MZSEtWloNYtvIsAw/j95mgVyLlQGyk/xtW3DFv85IyuqMf0eaWxpj9w==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-copy-code2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copy-code2/-/vuepress-plugin-copy-code2-2.0.0-beta.87.tgz", + "integrity": "sha512-SdIhcjCJ8aXFtzmKbP9+eeDh3nw6EPTFgu1EAmoS2NrhZDOminxnaTQgYuFjLrzBcky+d+RBWPcWEKhZCEJ9cg==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "balloon-css": "^1.2.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-copyright2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-copyright2/-/vuepress-plugin-copyright2-2.0.0-beta.87.tgz", + "integrity": "sha512-LUo7L+bU8iK4/BxCV2xv0yOYxooXbb0vSn9iDIBG16+Y5y2NdrFwRV2N9pa5Dao5RbVFvPVXHnlmktZO6CTwRQ==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "vue": "^3.2.37", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-feed2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-feed2/-/vuepress-plugin-feed2-2.0.0-beta.87.tgz", + "integrity": "sha512-J2A9o+gviuAwRPlwuOHsmtL2THY7FguaFfL+ooaGwAFo8UXdRvUvOIWof3DwQZOazOACkySyLCrWy6YnVnMapw==", + "dev": true, + "requires": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "vuepress-shared": "2.0.0-beta.87", + "xml-js": "^1.6.11" + } + }, + "vuepress-plugin-md-enhance": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-md-enhance/-/vuepress-plugin-md-enhance-2.0.0-beta.87.tgz", + "integrity": "sha512-HT0rbp3s3RY/JVfdx5UlyFKp6LH/QHuMx562UlbaWi8KF4mW/PkGXULk9O9B+He5BdT3EhvbJ56K0euNBT4TCA==", + "dev": true, + "requires": { + "@babel/core": "*", + "@types/katex": "^0.14.0", + "@types/markdown-it": "^12.2.3", + "@types/mermaid": "^8.2.9", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "chart.js": "^3.8.0", + "echarts": "^5.3.3", + "flowchart.js": "^1.17.1", + "katex": "^0.16.0", + "markdown-it": "^13.0.1", + "mermaid": "^9.1.3", + "reveal.js": "^4.3.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-photo-swipe": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-photo-swipe/-/vuepress-plugin-photo-swipe-2.0.0-beta.87.tgz", + "integrity": "sha512-kXdzjWfyV0xXB0N5M9jJNsg5tV5AjxPiQc8oRf6aiHG+mDf2GsljrNXeAjTYx93T4ahPGKs2M/Z3PSiYN8ONAg==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "photoswipe": "^5.2.8", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-pwa2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-pwa2/-/vuepress-plugin-pwa2-2.0.0-beta.87.tgz", + "integrity": "sha512-XkWUYhu0kviogUCnCSzLZn7BC6JaWUYtGgLgVV9qpEK4jCqAHYMyijSsr6XL8hT4c+MCySU15nmtoJPoagPRIA==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "mitt": "^3.0.0", + "register-service-worker": "^1.7.2", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87", + "workbox-build": "^6.5.3" + } + }, + "vuepress-plugin-reading-time2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-reading-time2/-/vuepress-plugin-reading-time2-2.0.0-beta.87.tgz", + "integrity": "sha512-LrEQmfYBpnd8U36jZvzrobVvx5Pl6dSL/kN1m/s/DlKm60hsHuHytLv8l1DYIwRNq9mE082P9mywd1FddaCJQg==", + "dev": true, + "requires": { + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-sass-palette": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sass-palette/-/vuepress-plugin-sass-palette-2.0.0-beta.87.tgz", + "integrity": "sha512-Z8RlqLIJnCGFG0ukHvCG8FGIvSzShbD05ISlNm7kxOf6Em/6xVkVMvYgwCL5KAc4EfLGjFm4rHuHbuDj8vpdBA==", + "dev": true, + "requires": { + "@vuepress/utils": "2.0.0-beta.49", + "chokidar": "^3.5.3", + "sass": "^1.53.0", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-seo2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-seo2/-/vuepress-plugin-seo2-2.0.0-beta.87.tgz", + "integrity": "sha512-QqEpnM9zCrMRneOq/NS3JZWLLwslgvoeL6jtq1VbJGtPElzx9ZxBrDrik/9nAiUaFX8Yc42GNUcgNDxKFXvXhg==", + "dev": true, + "requires": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "gray-matter": "^4.0.3", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-plugin-sitemap2": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-plugin-sitemap2/-/vuepress-plugin-sitemap2-2.0.0-beta.87.tgz", + "integrity": "sha512-F0F0qlZ5Svr+w90+lI/vNwfojcTAp6B6aBq4qdj5EBX7uIfw4QQQSyvzN9jBKX9vD1cgM3BGILK7ddCqtknERw==", + "dev": true, + "requires": { + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "sitemap": "^7.1.1", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "vuepress-shared": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-shared/-/vuepress-shared-2.0.0-beta.87.tgz", + "integrity": "sha512-NbmjEiuBbMR/7GIhQVuPqFr3Kjq5RkliVocjZapyTNBx+9afevjEoDcBZ3VRmxZCir38cxW1Pc9j0FWjnfZnXA==", + "dev": true, + "requires": { + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "dayjs": "^1.11.3", + "execa": "^5.1.1", + "ora": "^5.4.1", + "vue": "^3.2.37", + "vue-router": "^4.1.2" + } + }, + "vuepress-theme-hope": { + "version": "2.0.0-beta.87", + "resolved": "https://registry.npmmirror.com/vuepress-theme-hope/-/vuepress-theme-hope-2.0.0-beta.87.tgz", + "integrity": "sha512-CTP4JJBSvsBD/LkJv+ePEPLqWRnmrwMXxuriPgqic9BF4v0TTuOu026KAd4eEfeY/Bd6LcVxCNurBrR0v7AueA==", + "dev": true, + "requires": { + "@vuepress/cli": "2.0.0-beta.49", + "@vuepress/client": "2.0.0-beta.49", + "@vuepress/core": "2.0.0-beta.49", + "@vuepress/plugin-active-header-links": "2.0.0-beta.49", + "@vuepress/plugin-container": "2.0.0-beta.49", + "@vuepress/plugin-external-link-icon": "2.0.0-beta.49", + "@vuepress/plugin-git": "2.0.0-beta.49", + "@vuepress/plugin-nprogress": "2.0.0-beta.49", + "@vuepress/plugin-palette": "2.0.0-beta.49", + "@vuepress/plugin-prismjs": "2.0.0-beta.49", + "@vuepress/plugin-theme-data": "2.0.0-beta.49", + "@vuepress/shared": "2.0.0-beta.49", + "@vuepress/utils": "2.0.0-beta.49", + "@vueuse/core": "^8.9.2", + "balloon-css": "^1.2.0", + "bcrypt-ts": "^1.0.0", + "vue": "^3.2.37", + "vue-router": "^4.1.2", + "vuepress-plugin-blog2": "2.0.0-beta.87", + "vuepress-plugin-comment2": "2.0.0-beta.87", + "vuepress-plugin-components": "2.0.0-beta.87", + "vuepress-plugin-copy-code2": "2.0.0-beta.87", + "vuepress-plugin-copyright2": "2.0.0-beta.87", + "vuepress-plugin-feed2": "2.0.0-beta.87", + "vuepress-plugin-md-enhance": "2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "2.0.0-beta.87", + "vuepress-plugin-pwa2": "2.0.0-beta.87", + "vuepress-plugin-reading-time2": "2.0.0-beta.87", + "vuepress-plugin-sass-palette": "2.0.0-beta.87", + "vuepress-plugin-seo2": "2.0.0-beta.87", + "vuepress-plugin-sitemap2": "2.0.0-beta.87", + "vuepress-shared": "2.0.0-beta.87" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmmirror.com/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "dev": true, + "requires": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "dependencies": { + "deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true + } + } + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "workbox-background-sync": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz", + "integrity": "sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==", + "dev": true, + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-broadcast-update": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-broadcast-update/-/workbox-broadcast-update-6.5.4.tgz", + "integrity": "sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-build": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-build/-/workbox-build-6.5.4.tgz", + "integrity": "sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==", + "dev": true, + "requires": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.11.1", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^11.2.1", + "@rollup/plugin-replace": "^2.4.1", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^7.1.6", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.43.1", + "rollup-plugin-terser": "^7.0.0", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "6.5.4", + "workbox-broadcast-update": "6.5.4", + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-google-analytics": "6.5.4", + "workbox-navigation-preload": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-range-requests": "6.5.4", + "workbox-recipes": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4", + "workbox-streams": "6.5.4", + "workbox-sw": "6.5.4", + "workbox-window": "6.5.4" + }, + "dependencies": { + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "upath": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true + } + } + }, + "workbox-cacheable-response": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-cacheable-response/-/workbox-cacheable-response-6.5.4.tgz", + "integrity": "sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-core": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-core/-/workbox-core-6.5.4.tgz", + "integrity": "sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==", + "dev": true + }, + "workbox-expiration": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-expiration/-/workbox-expiration-6.5.4.tgz", + "integrity": "sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==", + "dev": true, + "requires": { + "idb": "^7.0.1", + "workbox-core": "6.5.4" + } + }, + "workbox-google-analytics": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-google-analytics/-/workbox-google-analytics-6.5.4.tgz", + "integrity": "sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==", + "dev": true, + "requires": { + "workbox-background-sync": "6.5.4", + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-navigation-preload": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-navigation-preload/-/workbox-navigation-preload-6.5.4.tgz", + "integrity": "sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-precaching": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-precaching/-/workbox-precaching-6.5.4.tgz", + "integrity": "sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==", + "dev": true, + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-range-requests": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-range-requests/-/workbox-range-requests-6.5.4.tgz", + "integrity": "sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-recipes": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-recipes/-/workbox-recipes-6.5.4.tgz", + "integrity": "sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==", + "dev": true, + "requires": { + "workbox-cacheable-response": "6.5.4", + "workbox-core": "6.5.4", + "workbox-expiration": "6.5.4", + "workbox-precaching": "6.5.4", + "workbox-routing": "6.5.4", + "workbox-strategies": "6.5.4" + } + }, + "workbox-routing": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-routing/-/workbox-routing-6.5.4.tgz", + "integrity": "sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-strategies": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-strategies/-/workbox-strategies-6.5.4.tgz", + "integrity": "sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==", + "dev": true, + "requires": { + "workbox-core": "6.5.4" + } + }, + "workbox-streams": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-streams/-/workbox-streams-6.5.4.tgz", + "integrity": "sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==", + "dev": true, + "requires": { + "workbox-core": "6.5.4", + "workbox-routing": "6.5.4" + } + }, + "workbox-sw": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-sw/-/workbox-sw-6.5.4.tgz", + "integrity": "sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==", + "dev": true + }, + "workbox-window": { + "version": "6.5.4", + "resolved": "https://registry.npmmirror.com/workbox-window/-/workbox-window-6.5.4.tgz", + "integrity": "sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==", + "dev": true, + "requires": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "6.5.4" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "requires": { + "sax": "^1.2.4" + } + }, + "zrender": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz", + "integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", + "dev": true, + "requires": { + "tslib": "2.3.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b5876c5 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "vuepress-theme-hope-template", + "version": "2.0.0", + "description": "A project of vuepress-theme-hope", + "license": "MIT", + "scripts": { + "docs:build": "vuepress build docs", + "docs:clean-dev": "vuepress dev docs --clean-cache", + "docs:dev": "vuepress dev docs" + }, + "devDependencies": { + "@vuepress/client": "^2.0.0-beta.49", + "@vuepress/plugin-docsearch": "^2.0.0-beta.49", + "@vuepress/plugin-google-analytics": "^1.9.7", + "@vuepress/plugin-nprogress": "^2.0.0-beta.49", + "@vuepress/plugin-search": "^2.0.0-beta.49", + "vue": "^3.2.36", + "vuepress": "^2.0.0-beta.49", + "vuepress-plugin-autometa": "^0.1.13", + "vuepress-plugin-baidu-autopush": "^1.0.1", + "vuepress-plugin-copyright2": "^2.0.0-beta.87", + "vuepress-plugin-photo-swipe": "^2.0.0-beta.87", + "vuepress-plugin-reading-time2": "^2.0.0-beta.87", + "vuepress-plugin-sitemap2": "^2.0.0-beta.87", + "vuepress-theme-hope": "^2.0.0-beta.87" + } +} diff --git "a/\344\270\255\351\227\264\344\273\266/Elasticsearch\345\205\245\351\227\250.md" "b/\344\270\255\351\227\264\344\273\266/Elasticsearch\345\205\245\351\227\250.md" deleted file mode 100644 index 7e2b094..0000000 --- "a/\344\270\255\351\227\264\344\273\266/Elasticsearch\345\205\245\351\227\250.md" +++ /dev/null @@ -1,2438 +0,0 @@ - - - - -- [基础](#%E5%9F%BA%E7%A1%80) - - [概念](#%E6%A6%82%E5%BF%B5) - - [启动和关闭](#%E5%90%AF%E5%8A%A8%E5%92%8C%E5%85%B3%E9%97%AD) - - [配置文件](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) - - [PUT](#put) - - [GET](#get) - - [全文搜索](#%E5%85%A8%E6%96%87%E6%90%9C%E7%B4%A2) - - [高亮搜索](#%E9%AB%98%E4%BA%AE%E6%90%9C%E7%B4%A2) - - [分析](#%E5%88%86%E6%9E%90) - - [分布式特性](#%E5%88%86%E5%B8%83%E5%BC%8F%E7%89%B9%E6%80%A7) -- [集群原理](#%E9%9B%86%E7%BE%A4%E5%8E%9F%E7%90%86) - - [术语](#%E6%9C%AF%E8%AF%AD) - - [节点](#%E8%8A%82%E7%82%B9) - - [分片](#%E5%88%86%E7%89%87) - - [集群健康](#%E9%9B%86%E7%BE%A4%E5%81%A5%E5%BA%B7) - - [索引](#%E7%B4%A2%E5%BC%95) - - [故障转移](#%E6%95%85%E9%9A%9C%E8%BD%AC%E7%A7%BB) - - [单机多节点](#%E5%8D%95%E6%9C%BA%E5%A4%9A%E8%8A%82%E7%82%B9) -- [数据输入与输出](#%E6%95%B0%E6%8D%AE%E8%BE%93%E5%85%A5%E4%B8%8E%E8%BE%93%E5%87%BA) - - [文档元数据](#%E6%96%87%E6%A1%A3%E5%85%83%E6%95%B0%E6%8D%AE) - - [创建新文档](#%E5%88%9B%E5%BB%BA%E6%96%B0%E6%96%87%E6%A1%A3) - - [取回文档](#%E5%8F%96%E5%9B%9E%E6%96%87%E6%A1%A3) - - [取回多个文档](#%E5%8F%96%E5%9B%9E%E5%A4%9A%E4%B8%AA%E6%96%87%E6%A1%A3) - - [删除文档](#%E5%88%A0%E9%99%A4%E6%96%87%E6%A1%A3) - - [检查文档是否存在](#%E6%A3%80%E6%9F%A5%E6%96%87%E6%A1%A3%E6%98%AF%E5%90%A6%E5%AD%98%E5%9C%A8) - - [更新文档](#%E6%9B%B4%E6%96%B0%E6%96%87%E6%A1%A3) - - [更新和冲突](#%E6%9B%B4%E6%96%B0%E5%92%8C%E5%86%B2%E7%AA%81) - - [批量操作](#%E6%89%B9%E9%87%8F%E6%93%8D%E4%BD%9C) -- [分布式文档存储](#%E5%88%86%E5%B8%83%E5%BC%8F%E6%96%87%E6%A1%A3%E5%AD%98%E5%82%A8) - - [路由文档到分片](#%E8%B7%AF%E7%94%B1%E6%96%87%E6%A1%A3%E5%88%B0%E5%88%86%E7%89%87) -- [映射和分析](#%E6%98%A0%E5%B0%84%E5%92%8C%E5%88%86%E6%9E%90) - - [核心简单域类型](#%E6%A0%B8%E5%BF%83%E7%AE%80%E5%8D%95%E5%9F%9F%E7%B1%BB%E5%9E%8B) - - [查看映射](#%E6%9F%A5%E7%9C%8B%E6%98%A0%E5%B0%84) - - [自定义映射器](#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%98%A0%E5%B0%84%E5%99%A8) - - [index](#index) - - [analyzer](#analyzer) - - [更新映射](#%E6%9B%B4%E6%96%B0%E6%98%A0%E5%B0%84) - - [测试映射](#%E6%B5%8B%E8%AF%95%E6%98%A0%E5%B0%84) - - [分析器](#%E5%88%86%E6%9E%90%E5%99%A8) - - [测试分析器](#%E6%B5%8B%E8%AF%95%E5%88%86%E6%9E%90%E5%99%A8) - - [复杂核心域类型](#%E5%A4%8D%E6%9D%82%E6%A0%B8%E5%BF%83%E5%9F%9F%E7%B1%BB%E5%9E%8B) - - [多值域](#%E5%A4%9A%E5%80%BC%E5%9F%9F) - - [内部对象](#%E5%86%85%E9%83%A8%E5%AF%B9%E8%B1%A1) -- [搜索](#%E6%90%9C%E7%B4%A2) - - [空搜索](#%E7%A9%BA%E6%90%9C%E7%B4%A2) - - [多索引多类型](#%E5%A4%9A%E7%B4%A2%E5%BC%95%E5%A4%9A%E7%B1%BB%E5%9E%8B) - - [轻量搜索](#%E8%BD%BB%E9%87%8F%E6%90%9C%E7%B4%A2) -- [请求体查询](#%E8%AF%B7%E6%B1%82%E4%BD%93%E6%9F%A5%E8%AF%A2) - - [空查询](#%E7%A9%BA%E6%9F%A5%E8%AF%A2) - - [提升权重](#%E6%8F%90%E5%8D%87%E6%9D%83%E9%87%8D) - - [explain](#explain) - - [查询和过滤器的区别](#%E6%9F%A5%E8%AF%A2%E5%92%8C%E8%BF%87%E6%BB%A4%E5%99%A8%E7%9A%84%E5%8C%BA%E5%88%AB) -- [排序与相关性](#%E6%8E%92%E5%BA%8F%E4%B8%8E%E7%9B%B8%E5%85%B3%E6%80%A7) - - [按照字段的值排序](#%E6%8C%89%E7%85%A7%E5%AD%97%E6%AE%B5%E7%9A%84%E5%80%BC%E6%8E%92%E5%BA%8F) - - [多级排序](#%E5%A4%9A%E7%BA%A7%E6%8E%92%E5%BA%8F) - - [多值字段的排序](#%E5%A4%9A%E5%80%BC%E5%AD%97%E6%AE%B5%E7%9A%84%E6%8E%92%E5%BA%8F) - - [字符串排序](#%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%8E%92%E5%BA%8F) -- [索引管理](#%E7%B4%A2%E5%BC%95%E7%AE%A1%E7%90%86) - - [创建索引](#%E5%88%9B%E5%BB%BA%E7%B4%A2%E5%BC%95) - - [删除索引](#%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95) - - [索引设置](#%E7%B4%A2%E5%BC%95%E8%AE%BE%E7%BD%AE) - - [配置分析器](#%E9%85%8D%E7%BD%AE%E5%88%86%E6%9E%90%E5%99%A8) - - [自定义分析器](#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%86%E6%9E%90%E5%99%A8) - - [类型和映射](#%E7%B1%BB%E5%9E%8B%E5%92%8C%E6%98%A0%E5%B0%84) - - [根对象](#%E6%A0%B9%E5%AF%B9%E8%B1%A1) -- [分片内部原理](#%E5%88%86%E7%89%87%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86) - - [倒排索引](#%E5%80%92%E6%8E%92%E7%B4%A2%E5%BC%95) -- [深入搜索](#%E6%B7%B1%E5%85%A5%E6%90%9C%E7%B4%A2) - - [精确值查找](#%E7%B2%BE%E7%A1%AE%E5%80%BC%E6%9F%A5%E6%89%BE) - - [term 查询文本](#term-%E6%9F%A5%E8%AF%A2%E6%96%87%E6%9C%AC) - - [terms 查询](#terms-%E6%9F%A5%E8%AF%A2) - - [全文搜索](#%E5%85%A8%E6%96%87%E6%90%9C%E7%B4%A2-1) - - [match 查询](#match-%E6%9F%A5%E8%AF%A2) - - [range 查询](#range-%E6%9F%A5%E8%AF%A2) - - [数字范围](#%E6%95%B0%E5%AD%97%E8%8C%83%E5%9B%B4) - - [日期范围](#%E6%97%A5%E6%9C%9F%E8%8C%83%E5%9B%B4) - - [分页](#%E5%88%86%E9%A1%B5) - - [exists 查询](#exists-%E6%9F%A5%E8%AF%A2) - - [bool 组合查询](#bool-%E7%BB%84%E5%90%88%E6%9F%A5%E8%AF%A2) - - [如何使用 bool 查询](#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-bool-%E6%9F%A5%E8%AF%A2) - - [constant_score 查询](#constant_score-%E6%9F%A5%E8%AF%A2) - - [多字段搜索](#%E5%A4%9A%E5%AD%97%E6%AE%B5%E6%90%9C%E7%B4%A2) - - [多字符串查询](#%E5%A4%9A%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9F%A5%E8%AF%A2) - - [multi_match 查询](#multi_match-%E6%9F%A5%E8%AF%A2) - - [多字段映射](#%E5%A4%9A%E5%AD%97%E6%AE%B5%E6%98%A0%E5%B0%84) - - [copy_to 定制组合 field](#copy_to-%E5%AE%9A%E5%88%B6%E7%BB%84%E5%90%88-field) -- [springboot 集成 es](#springboot-%E9%9B%86%E6%88%90-es) -- [mall](#mall) - - - -[Elasticsearch 权威指南](https://www.elastic.co/guide/cn/elasticsearch/guide/current/_talking_to_elasticsearch.html) - -## 基础 - -Elasticsearch 是一个开源的搜索引擎,建立在全文搜索引擎库 lucene 基础之上。Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单, 通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。 - -ES与mysql的对应关系: - -- index –> DB -- type –> Table -- Document –> row - -### 概念 - -[参考--阮一峰es教程](http://www.ruanyifeng.com/blog/2017/08/elasticsearch.html) - -1. node 和 cluster - - Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。 - -2. Index - - 数据库的同义词。每个 Index (即数据库)的名字必须是小写。查看当前节点的所有index:`GET _cat/indices` - -3. Type - - 类似于表结构。不同的 Type 应该有相似的结构(schema),举例来说,`id`字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的区别。性质完全不同的数据(比如`products`和`logs`)应该存成两个 Index,而不是一个 Index 里面的两个 Type。 - -4. Document - - 单条的记录称为 Document。Document 可以分组,比如`weather`这个 Index 里面,可以按城市分组(北京和上海),这种分组就是 Type。同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。 - - -### 启动和关闭 - -不用安装,解压即可。 - -启动:`bin\elasticsearch.bat` - -关闭:ctrl+c/`curl -XPOST http://localhost:9200/_cluster/nodes/_shutdown `关掉整个集群 - -启动 head 插件:到 head 安装目录下运行`grunt server` - -### 配置文件 - -安装目录config下的 elasticsearch.yml 可以配置集群的信息,如cluster.name 和 node.name。 - -```yaml -# ---------------------------------- MyConfig ---------------------------------- -#cluster.name: xxx -node.name: node-2 -#指明该节点可以是主节点,也可以是数据节点 -node.master: true -node.data: true -network.host: 127.0.0.1 -http.port: 9200 -transport.tcp.port: 9300 -``` - -http.port 是elasticsearch对外提供服务的http端口配置。 - -transport.tcp.port 指定了elasticsearch集群内数据通讯使用的端口,默认情况下为9300。 - - -### PUT - -```json -PUT /company/employee/1 -{ - "first_name" : "Tyson", - "last_name" : "dai", - "age" : 23, - "interests" : ["sport", "music"], - "hire_date" : "2014-01-01" -} -``` - -company:索引名称,employee:类型名称,1是 ID。 - -### GET - -```json -GET /company/employee/1 -``` - -搜索所有雇员: - -```json -GET /company/employee/_search -``` - -搜索结果如下,雇员数据放在 hits 数组中: - -```json -{ - "took": 413, - "timed_out": false, - "_shards": { - "total": 5, - "successful": 5, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 2, - "max_score": 1, - "hits": [ - { - "_index": "company", - "_type": "employee", - "_id": "2", - "_score": 1, - "_source": { - "first_name": "Lily", - "last_name": "dai", - "age": 23, - "interests": [ - "sport", - "music" - ] - } - }, - { - "_index": "company", - "_type": "employee", - "_id": "1", - "_score": 1, - "_source": { - "first_name": "Tyson", - "last_name": "dai", - "age": 23, - "interests": [ - "sport", - "music" - ] - } - } - ] - } -} -``` - -按特定条件搜索: - -```json -GET /company/employee/_search?q=first_name:Tyson -``` - -使用查询表达式搜索: - -```json -GET /company/employee/_search -{ - "query": { - "match": { - "first_name": "Tyson" - } - } -} -``` - -复杂查询: - -姓氏为 Dai的雇员,但这次我们只需要年龄大于 20 的。 - -```json -GET /company/employee/_search -{ - "query": { - "bool" : { - "must" : { - "match" : { - "last_name" : "dai" - } - }, - "filter": { - "range": { - "age": {"gt" : 20} - } - } - } - } -} -``` - -### 全文搜索 - -传统数据库确实很难搞定的任务。 - -```json -GET /company/employee/_search -{ - "query": { - "match": { - "about": "rock climbing" - } - } -} -``` - -返回`"about": "rock climbing"`和`"about": "rock albums"`两条记录,默认按照每个文档跟查询的匹配程度排序。 - -### 高亮搜索 - -```json -GET /company/employee/_search -{ - "query": { - "match_phrase": { - "about": "climbing" - } - }, - "highlight": { - "fields": { - "about": {} - } - } -} -``` - -返回结果多 `highlight` 的部分。这个部分包含了 `about` 属性匹配的文本片段,并以 HTML 标签 `` 封装。 - -```json -{ - "took": 43, - "timed_out": false, - "_shards": { - "total": 5, - "successful": 5, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 0.2876821, - "hits": [ - { - "_index": "company", - "_type": "employee", - "_id": "3", - "_score": 0.2876821, - "_source": { - "first_name": "Tyson", - "last_name": "dai", - "age": 23, - "interests": [ - "sport", - "music" - ], - "about": "rock climbing" - }, - "highlight": { - "about": [ - "rock climbing" - ] - } - } - ] - } -} -``` - -### 分析 - -Elasticsearch 有一个功能叫聚合(aggregations),允许我们基于数据生成一些精细的分析结果。聚合与 SQL 中的 `GROUP BY` 类似但更强大。 - -```json -GET /company/employee/_search -{ - "aggs": { - "all_interests": { - "terms": { "field": "interests" } - } - } -} -``` - -直接执行上面的代码会报错,原因是5.x后对排序,聚合这些操作用单独的数据结构(fielddata)缓存到内存里了,需要单独开启。 - -```json -PUT company/_mapping/employee/ -{ - "properties": { - "interests": { - "type": "text", - "fielddata": true - } - } -} -``` - -搜索结果: - -```json -{ - ... - "hits": { ... }, - "aggregations": { - "all_interests": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "music", - "doc_count": 4 - }, - { - "key": "sport", - "doc_count": 4 - } - ] - } - } -} -``` - -如果想知道叫 Tyson的雇员中最受欢迎的兴趣爱好,可以直接添加适当的查询来组合查询: - -```json -GET company/employee/_search -{ - "query": { - "match": { - "first_name": "Tyson" - } - }, - "aggs": { - "all_interests": { - "terms": { - "field": "interests" - } - } - } -} -``` - -搜索结果: - -```json -{ - "took": 33, - "timed_out": false, - "_shards": { - "total": 5, - "successful": 5, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 0.2876821, - "hits": [ - { - "_index": "company", - "_type": "employee", - "_id": "3", - "_score": 0.2876821, - "_source": { - "first_name": "Tyson", - "last_name": "dai", - "age": 23, - "interests": [ - "sport", - "music" - ], - "about": "rock climbing" - } - } - ] - }, - "aggregations": { - "all_interests": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "music", - "doc_count": 1 - }, - { - "key": "sport", - "doc_count": 1 - } - ] - } - } -} -``` - -聚合还支持分级汇总 。比如,查询特定兴趣爱好员工的平均年龄: - -```java -GET company/employee/_search -{ - "aggs": { - "all_interests": { - "terms": { - "field": "interests" - }, - "aggs": { - "avg_age": { - "avg": { - "field": "age" - } - } - } - } - } -} -``` - -搜索结果: - -```json -"aggregations": { - "all_interests": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 0, - "buckets": [ - { - "key": "music", - "doc_count": 4, - "avg_age": { - "value": 18.5 - } - }, - { - "key": "sport", - "doc_count": 4, - "avg_age": { - "value": 18.5 - } - } - ] - } - } -``` - -### 分布式特性 - -Elasticsearch 可以横向扩展至数百(甚至数千)的服务器节点,同时可以处理PB级数据。 - -## 集群原理 - -ElasticSearch 的主旨是随时可用和按需扩容。扩容可以通过购买性能更强大( *垂直扩容* ) 或者数量更多的服务器( *水平扩容* )来实现。 - -### 术语 - -#### 节点 - -一个运行中的 Elasticsearch 实例称为节点,而集群是由一个或者多个拥有相同 `cluster.name` 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。 - -当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。 - -我们可以将请求发送到集群中的任何节点,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。 - -获取节点信息:`http://localhost:9200/_cluster/state/nodes?pretty`,pretty 用于换行。 - -#### 分片 - -每个节点可以分配一个或多个分片。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。 - -一个分片可以是 *主* 分片或者 *副本* 分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。 - -一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。 - -### 集群健康 - -`GET /_cluster/health` - -返回内容: - -```json -{ - "cluster_name": "elasticsearch", - "status": "yellow", - "timed_out": false, - "number_of_nodes": 1, - "number_of_data_nodes": 1, - "active_primary_shards": 6, - "active_shards": 6, - "relocating_shards": 0, - "initializing_shards": 0, - "unassigned_shards": 5, - "delayed_unassigned_shards": 0, - "number_of_pending_tasks": 0, - "number_of_in_flight_fetch": 0, - "task_max_waiting_in_queue_millis": 0, - "active_shards_percent_as_number": 54.54545454545454 -} -``` - -status 字段表示当前集群总体是否正常,它有三个值: - -1. green,所有的主分片和副本分片都正常运行。 - -2. yellow,所有的主分片都正常运行,但不是所有的副本分片都正常运行。 -3. red,有主分片不能正常运行。 - -### 索引 - -往 elasticsearch 添加数据时需要用到索引,索引是指向一个或多个分片的逻辑命名空间。一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 - -当我们只开启一个节点时,索引在默认情况下会被分配5个主分片,每个主分片拥有一个副本分片。主分片会被分配到这个节点上,而副本分片不会被分配到任何节点。 - -```json -{ - "cluster_name": "elasticsearch", - "status": "yellow", - ... - "unassigned_shards": 5,//5个副本分片都是 unassigned,都没有被分配到任何节点 - ... -} -``` - -当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。 - -### 故障转移 - -可以在同一个目录下开启另一个节点,副本分片会被分配到这个节点上,此时计算有硬件故障也不会丢失数据。 - -### 单机多节点 - -node1 的 elasticsearch.yml 做如下配置: - -```yaml -# ---------------------------------- MyConfig ---------------------------------- -#cluster.name: xxx -node.name: node-1 -#指明该节点可以是主节点,也可以是数据节点 -#node.master: true -#node.data: true -network.host: 127.0.0.1 -http.port: 9200 -transport.tcp.port: 9300 -discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9301", "127.0.0.1:9302"] - -# 解决elasticsearch-head 集群健康值: 未连接问题 -http.cors.enabled: true -http.cors.allow-origin: "*" -``` - -node2 的 elasticsearch.yml 做如下配置: - -```yaml -# ---------------------------------- MyConfig ---------------------------------- -#cluster.name: xxx -node.name: node-2 -#指明该节点可以是主节点,也可以是数据节点 -#node.master: true -#node.data: true -network.host: 127.0.0.1 -http.port: 9201 -transport.tcp.port: 9301 -discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300", "127.0.0.1:9301", "127.0.0.1:9302"] - -# 解决elasticsearch-head 集群健康值: 未连接问题 -http.cors.enabled: true -http.cors.allow-origin: "*" -``` - -同理 node3 也做好配置。然后重启 elasticsearch 节点,就可以实现单机多节点集群搭建。 - - - -## 数据输入与输出 - -### 文档元数据 - -一个文档不仅包含数据,也包含元数据。三个必须的元数据元素如下:\_index、\_type和_id。 - -索引名字必须小写,不能以下划线开头,不能包含逗号。 - -\_type在索引中对数据进行逻辑分区,如产品下面还可以分为很多子类。一个 `_type` 命名可以是大写或者小写,但是不能以下划线或者句号开头,不应该包含逗号, 并且长度限制为256个字符。 - -### 创建新文档 - -使用 PUT 请求,需要定义 _id: - -```json -PUT /{index}/{type}/{id} -{ - "field": "value", - ... -} -``` - -id 为2的文档存在时会报错: document already exists - -```json -PUT company/employee/2/_create -{ - "first_name" : "Tyson", - "last_name" : "dai", - "age" : 23, - "interests" : ["sport", "music"] -} -``` - -不存在时才创建: - -```json -PUT company/employee/2?op_type=create -{ - "first_name" : "Tyson", - "last_name" : "dai", - "age" : 23, - "interests" : ["sport", "music"] -} -``` - -使用 `POST` 可以让 Elasticsearch 自动生成唯一 的_id。 - -```json -POST company/employee -{ - "first_name" : "Tyson", - "last_name" : "dai", - "age" : 23, - "interests" : ["sport", "music"] -} -``` - -### 取回文档 - -返回文档的一部分: - -```json -GET /company/employee/1?_source=first_name,interests -``` - -只得到_source 字段(即id为1的整个文档): - -```json -GET /company/employee/1/_source -``` - - -### 取回多个文档 - -mget api 要求传入一个 docs 数组作为参数,可以通过 _source 指定返回字段。 - -```json -GET /_mget -{ - "docs": [ - { - "_index": "company", - "_type": "employee", - "_id": 2 - }, - { - "_index": "class", - "_type": "student", - "_id": 1, - "_source": "about" - } - ] -} -``` - -如果获取的文档 _index 和 _type 相同,可以在 url 指定默认的 _index 和 _type,传入一个 ids 数组。 - -```json -GET /company/employee/_mget -{ - "ids": ["1", "2"] -} -``` - -通过单独请求可以覆盖默认的 _index 和 _type。 - -```json -GET /company/employee/_mget -{ - "docs" : [ - { "_id" : 2 }, - { "_index" : "class", "_type" : "student", "_id" : 1 } - ] -} -``` - -### 删除文档 - -`DELETE company/employee/1`,删除文档,版本号会增加。 - -```json -{ - "_index": "company", - "_type": "employee", - "_id": "1", - "_version": 2, - "result": "deleted", - "_shards": { - "total": 2, - "successful": 1, - "failed": 0 - }, - "_seq_no": 1, - "_primary_term": 4 -} -``` - -### 检查文档是否存在 - -`HEAD /company/employee/1` - -返回结果:`200 - OK` - -### 更新文档 - -文档是不可变的,不能被修改,只能被替换。 `update` API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, `update` API 简单使用 *检索-修改-重建索引* 的处理过程。 - -```json -POST company/employee/1/_update -{ - "doc": { - "first_name": "sophia", - "interests": ["sport", "chess"] - } -} -``` - -#### 更新和冲突 - -```json -POST company/employee/1/_update?retry_on_conflict=5 -{ - "doc": { - "first_name": "sophia", - "interests": ["sport", "chess"] - } -} -``` - -### 批量操作 - -语法: - -```json -{ action: { metadata }}\n -{ request body }\n -{ action: { metadata }}\n -{ request body }\n -... -``` - -action 有 create、delete、index 和 update。metadata 定义了\_index,\_type,_id 等信息。 - -create:如果文档不存在,那么就创建它。index:创建一个新文档或者替换一个现有的文档。create 和 index 不指定 `_id`的话 ,将会自动生成一个 ID 。 - -```json -POST /_bulk -{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }} -{ "create": { "_index": "website", "_type": "blog", "_id": "123" }} -{ "title": "My first blog post" } -{ "index": { "_index": "website", "_type": "blog" }} -{ "title": "My second blog post" } -{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} } -{ "doc" : {"title" : "My updated blog post"} } -``` - -bulk 请求不是原子的,不能用它来实现事务控制。每个请求是单独处理的,因此一个请求的成功或失败不会影响其他的请求。 - -**默认的 index 和 type** - -```json -POST /website/_bulk -{ "index": { "_type": "log" }} -{ "event": "User logged in" } -``` - - - -## 分布式文档存储 - -### 路由文档到分片 - -文档所在分片的位置通过这个公式计算:`shard = hash(routing) % number_of_primary_shards`,rounting 默认是文档的 _id,可以设置成自定义的值。创建索引的时候就确定好主分片的数量,并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。 - - - - -## 映射和分析 - -映射:为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。 - -分析:将一块文本分成适合于倒排索引的独立的词条,然后将这些词条统一化为标准格式以提高它们的可搜索性。 - -### 核心简单域类型 - -Elasticsearch 支持 如下简单域类型: - -- 字符串: `string` -- 整数 : `byte`, `short`, `integer`, `long` -- 浮点数: `float`, `double` -- 布尔型: `boolean` -- 日期: `date` - -当你索引一个新的文档,elasticsearch 会使用动态映射,通过 json 中的基本数据类型,尝试猜测域的类型。 - -| **JSON type** | **域 type** | -| ------------------------------ | ----------- | -| 布尔型: `true` 或者 `false` | `boolean` | -| 整数: `123` | `long` | -| 浮点数: `123.45` | `double` | -| 字符串,有效日期: `2014-09-15` | `date` | -| 字符串: `foo bar` | `string` | - -如果你通过引号( `"123"` )索引一个数字,它会被映射为 `string` 类型,而不是 `long` 。但是,如果这个域已经映射为 `long` ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。 - -### 查看映射 - -`GET /company/_mapping/employee` - -返回结果: - -```json -{ - "company": { - "mappings": { - "employee": { - "properties": { - "age": { - "type": "long" - }, - "first_name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "hire_date": { - "type": "date" - }, - "interests": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "last_name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - } - } - } -} -``` - -### 自定义映射器 - -`string` 类型域会被认为包含全文。它们的值在索引前,会通过 一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。string 域映射的两个最重要的属性是 index 和 analyzer。 - -#### index - -`index` 属性控制怎样索引字符串。它有三个值: - -- analyzed:默认,首先分析这个域,然后索引它; -- not_analyzed:索引这个域,索引的是精确值,不会对它进行分析; -- no:不要索引这个域,这个域不会被搜索到。 - -设置 tag 域的 index 为 not_analyzed: - -```json -{ - "tag": { - "type": "string", - "index": "not_analyzed" - } -} -``` - -其他简单类型(例如 `long` , `double` , `date` 等)也接受 `index` 参数,但有意义的值只有 `no` 和 `not_analyzed` , 因为它们永远不会被分析。 - -#### analyzer - -对于 `analyzed` 字符串域,用 `analyzer` 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 `standard` 分析器, 但你可以指定一个内置的分析器替代它,例如 `whitespace` 、 `simple` 和 `english`: - -```json -{ - "tweet": { - "type": "string", - "analyzer": "english" - } -} -``` - - -### 更新映射 - -我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 `analyzed` 改为 `not_analyzed` ,否则索引的数据可能会出错,数据不能被正常的搜索。 - -增加名为 tag 的 not_analyzed 的文本域: - -```json -PUT /gb/_mapping/tweet -{ - "properties" : { - "tag" : { - "type" : "text", - "index": "not_analyzed" - } - } -} -``` - -指定中文分词器: - -```json -PUT /gb/_mapping/tweet -{ - "properties" : { - "user": { - "type": "text", - "analyzer": "ik_max_word", - "search_analyzer": "ik_max_word" - } - } -} -``` - -`analyzer`是字段文本的分词器,`search_analyzer`是搜索词的分词器。`ik_max_word`分词器是插件`ik`提供的,可以对文本进行最大数量的分词。 - - -### 测试映射 - -```json -GET /gb/_analyze -{ - "field": "tag", - "text": "Black-cats" -} -``` - - `tag` 域产生单独的词条 `Black-cats` 。 - -### 分析器 - -分析器的三个功能: - -- 字符过滤。字符串按顺序通过每个字符过滤器。它们在分词前整理字符串。一个字符过滤器可以用来去掉HTML或者将 & 转化为 and。 -- 字符串被分词器分成单个的词条 -- token 过滤器。改变词条(Quick 小写),删除词条(a/the/and),增加词条(leap/jump这种同义词) - -对特定域和全文域_all 查询字符串时可能会返回不同的结果。 - -当你查询一个 *全文* 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。 - -当你查询一个 *精确值* 域时,不会分析查询字符串, 而是搜索你指定的精确值。 - -```json -GET /_search?q=2014 # 12 results -GET /_search?q=2014-09-15 # 12 results -GET /_search?q=date:2014-09-15 # 1 result -GET /_search?q=date:2014 # 0 results 没有"2014",只有"2014-09-15" -``` - -date 域包含一个精确值:单独的词条 `2014-09-15`。 - -_all 域是一个全文域,所以分词进程将日期转化为三个词条: `2014`, `09`, 和 `15`。 - -#### 测试分析器 - -```json -GET /_analyze -{ - "analyzer": "standard", - "text": "Text to analyze" -} -``` - -结果: - -```json -{ - "tokens": [ - { - "token": "text", - "start_offset": 0, - "end_offset": 4, - "type": "", - "position": 0 - }, - { - "token": "to", - "start_offset": 5, - "end_offset": 7, - "type": "", - "position": 1 - }, - { - "token": "analyze", - "start_offset": 8, - "end_offset": 15, - "type": "", - "position": 2 - } - ] -} -``` - -### 复杂核心域类型 - -#### 多值域 - -`{ "tag": [ "search", "nosql" ]}` - -### 内部对象 - -```json -{ - "tweet": "Elasticsearch is very flexible", - "user": { - "id": "@johnsmith", - "gender": "male", - "age": 26, - "name": { - "full": "John Smith", - "first": "John", - "last": "Smith" - } - } -} -``` - -Lucene 不理解内部对象。 Lucene 文档是由一组键值对列表组成的。为了能让 Elasticsearch 有效地索引内部类,它把我们的文档转化成这样: - -```json -{ - "tweet": [elasticsearch, flexible, very], - "user.id": [@johnsmith], - "user.gender": [male], - "user.age": [26], - "user.name.full": [john, smith], - "user.name.first": [john], - "user.name.last": [smith] -} -``` - - - - -## 搜索 - -### 空搜索 - -`GET /_search`,返回的 hits 数组包含所查询结果的前十个文档。 - -`GET /_search?timeout=10ms`,在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。 - - -### 多索引多类型 - -`GET /c*,g*/_search`,在以 c 开头和 g 开头的索引中搜索所有的类型。 - -`GET /_all/employee,student/_search?size=1&from=1`,在所有索引中搜索 employee 和 student 类型。elasticsearch 默认一次返回10条结果,size 可以指定返回返回结果数量,from 指定位移。 - - -### 轻量搜索 - -查询 employee 类型的 last_name 字段为 dai 的所有文档:`GET company/employee/_search?q=last_name:dai` - -查询包含 dai 的所有文档:`GET /_search?q=dai` - - - -## 请求体查询 - -### 空查询 - -```json -GET _search -{ - "query" : { - "match_all": {} - } -} -``` - - - - -### 提升权重 - -我们可以通过指定 `boost` 来控制任何查询语句的相对的权重, `boost` 的默认值为 `1` ,大于 `1` 会提升一个语句的相对权重。 - -```json -GET /company/employee/_search -{ - "query": { - "bool": { - "must": { - "match": { - "interests": { - "query": "sport music", - "operator": "and" - } - } - }, - "should": [ - { "match": { - "first_name": { - "query": "tyson", - "boost": 3 - } - }}, - { "match": { - "first_name": { - "query": "sophia", - "boost": 2 - } - }} - ] - } - } -} -``` - -### explain - -查询结果说明。 - -```json -GET /_validate/query?explain -{ - "query": { - "match" : { - "interests" : "sport shoes" - } - } -} -``` - -结果: - -```json -{ - "valid": true, - "_shards": { - "total": 3, - "successful": 3, - "failed": 0 - }, - "explanations": [ - { - "index": "class", - "valid": true, - "explanation": "interests:sport interests:shoes" - }, - { - "index": "company", - "valid": true, - "explanation": "interests:sport interests:shoes" - } - ] -} -``` - -### 查询和过滤器的区别 - -查询会计算得分,而过滤不计算得分,过滤器所需处理更少,所以过滤器可以比普通查询更快。而且过滤器可以被缓存。 - - - -## 排序与相关性 - -有时,_score 相关性评分对你来说并没有意义。例如,下面的查询返回所有 `user_id` 字段包含 `1` 的结果: - -```json -GET /_search -{ - "query" : { - "constant_score" : { - "filter" : { - "term" : { - "id" : 1 - } - } - } - } -} -``` - -### 按照字段的值排序 - -按照 hire_date 排序。 - -```json -GET _search -{ - "query" : { - "bool" : { - "filter" : { - "match" : { - "first_name" : "tyson" - } - } - } - }, - "sort" : { - "hire_date" : { "order" : "desc" } - } -} -``` - -### 多级排序 - -首先按第一个条件排序,仅当结果集的第一个 `sort` 值完全相同时才会按照第二个条件进行排序,以此类推。 - -```json -GET _search -{ - "query" : { - "bool" : { - "filter" : { - "match" : { - "first_name" : "tyson" - } - } - } - }, - "sort" : { - "hire_date" : { "order" : "desc" }, - "_score" : { "order" : "desc" } - } -} -``` - -Query-string 搜索 也支持自定义排序,可以在查询字符串中使用 `sort` 参数: - -```js -GET /_search?sort=hire_date:desc&sort=_score -``` - -### 多值字段的排序 - -对于数字或日期,你可以将多值字段减为单值,这可以通过使用 `min` 、 `max` 、 `avg` 或是 `sum` 排序模式。 例如你可以按照每个 hire_date 字段中的最早日期进行排序,通过以下方法: - -```json -"sort": { - "hire_date": { - "order": "asc", - "mode": "min" - } -} -``` - -### 字符串排序 - -为了对字符串字段进行排序,需要用两种方式对同一个字符串进行索引: `analyzed` 用于搜索, `not_analyzed` 用于排序。 - -用两种方式索引一个单字段: - -```json -"interests": { - "type": "string", - "analyzer": "english", - "fields": { - "raw": { - "type": "string", - "index": "not_analyzed" - } - } -} -``` - -interests.raw 子字段是 not_analyzed。interests 字段只在 _source 中出现一次。 - -使用 interests 字段用于搜索,interests.raw 字段用于排序: - -```json -GET /_search -{ - "query": { - "match": { - "interests": "elasticsearch" - } - }, - "sort": "interests.raw" -} -``` - -备注:以全文 `analyzed` 字段排序会消耗大量的内存。 - - - -## 索引管理 - -### 创建索引 - -```json -PUT /my_index -{ - "settings": { ... any settings ... }, - "mappings": { - "type_one": { ... any mappings ... }, - "type_two": { ... any mappings ... }, - ... - } -} -``` - -禁止自动创建索引,你 可以通过在 `config/elasticsearch.yml` 的每个节点下添加下面的配置: - -`action.auto_create_index: false` - -### 删除索引 - -`DELETE /company,student` - -`DELETE /index_*` - -`DELETE /_all` 或 `DELETE /*` - -### 索引设置 - -number_of_shards:每个索引的主分片数,默认值是 `5` 。这个配置在索引创建后不能修改。 - -number_of_replicas:每个主分片的副本数,默认值是 `1` 。对于活动的索引库,这个配置可以随时修改。 - -创建只有 一个主分片,没有副本的小索引: - -```json -PUT /my_temp_index -{ - "settings": { - "number_of_shards" : 1, - "number_of_replicas" : 0 - } -} -``` - - 动态修改副本数: - -```json -PUT /my_temp_index/_settings -{ - "number_of_replicas": 1 -} -``` - -### 配置分析器 - -#### 自定义分析器 - -,一个分析器组合了三种函数:字符过滤器、分词器和词单元过滤器, 三种函数按照顺序被执行。 - -```json -PUT /my_index -{ - "settings": { - "analysis": { - "char_filter": { - "&_to_and" : { - "type" : "mapping", - "mappings" : ["&=>and"] - } - }, - "filter": { - "my_stopwords" : { - "type" : "stop", - "stopwords" : ["the", "a"] - } - }, - "analyzer": { - "my_analyzer" : { - "type" : "custom", - "char_filter" : ["html_strip", "&_to_and"], - "tokenizer" : "standard", - "filter" : ["lowercase", "my_stopwords"] - } - } - } - } -} -``` - -测试: - -```json -GET /my_index/_analyze -{ - "analyzer" : "my_analyzer", - "text" : "The quick & brown fox" -} -``` - -结果: - -```json -{ - "tokens": [ - { - "token": "quick", - "start_offset": 4, - "end_offset": 9, - "type": "", - "position": 1 - }, - { - "token": "and", - "start_offset": 10, - "end_offset": 11, - "type": "", - "position": 2 - }, - { - "token": "brown", - "start_offset": 12, - "end_offset": 17, - "type": "", - "position": 3 - }, - { - "token": "fox", - "start_offset": 18, - "end_offset": 21, - "type": "", - "position": 4 - } - ] -} -``` - -把分析器应用在 string 字段上: - -```json -PUT /my_index/_mapping/my_type -{ - "properties": { - "title": { - "type": "string", - "analyzer": "my_analyzer" - } - } -} -``` - -### 类型和映射 - -同一个索引下,不同的类型type应该有相同的结构,映射也应该相同。因为 Lucene 会将同一个索引下的所有字段的映射扁平化,相同字段不同映射会导致冲突。 - -```json -{ - "data": { - "mappings": { - "people": { - "properties": { - "name": { - "type": "string", - }, - "address": { - "type": "string" - } - } - }, - "transactions": { - "properties": { - "timestamp": { - "type": "date", - "format": "strict_date_optional_time" - }, - "message": { - "type": "string" - } - } - } - } - } -} -``` - -后台 Lucene 将创建一个映射: - -```json -{ - "data": { - "mappings": { - "_type": { - "type": "string", - "index": "not_analyzed" - }, - "name": { - "type": "string" - } - "address": { - "type": "string" - } - "timestamp": { - "type": "long" - } - "message": { - "type": "string" - } - } - } -} -``` - -因此,类型不适合 *完全不同类型的数据* 。如果两个类型的字段集是互不相同的,这就意味着索引中将有一半的数据是空的(字段将是 *稀疏的* ),最终将导致性能问题。在这种情况下,最好是使用两个单独的索引。 - - -### 根对象 - -映射最高的一层被称为根对象。它可能包含以下几项: - -- properties 节点,列出文档每个字段的映射 -- 下划线开头的元数据字段,如 \_type,_id 和 \_source -- 设置项,控制如何动态处理新的字段,例如 `analyzer` 、 `dynamic_date_formats` 和`dynamic_templates` -- 其他设置,可以同时应用在根对象和其他 `object` 类型的字段上,例如 `enabled` 、 `dynamic` 和 `include_in_all` - - - -## 分片内部原理 - -### 倒排索引 - -倒排索引包含一个有序列表,列表包含所有文档出现过的不重复个体,或称为 *词项* ,对于每一个词项,包含了它所有曾出现过文档的列表。 - -| Term | doc1 | doc2 | doc3 | -| ----- | ---- | ---- | ---- | -| sport | x | | x | -| music | | x | | -| chess | x | x | x | - - - -## 深入搜索 - -### 精确值查找 - -term 查询被用于精确值匹配,这些精确值可能是数字、时间、布尔或者那些 `not_analyzed` 的字符串。 - -```json -GET /company/employee/_search -{ - "query" : { - "term" : { - "interests" : "listen" - } - } -} -``` - -hire_date 在过去四年内的文档: - -```json -GET company/employee/_search -{ - "query": { - "constant_score": { - "filter": { - "range": { - "hire_date": { - "gt": "now-4y" - } - } - } - } - } -} -``` - -#### term 查询文本 - -删除旧索引(因为旧索引字段都是 analyzed 的,它的映射不正确)然后创建一个能正确映射的新索引: - -```json -PUT /my_store -{ - "mappings" : { - "products" : { - "properties": { - "productID" : { - "type" : "keyword",//es6废除了string,index是boolean值,keyword不分词,text分词 - } - } - } - } -} -``` - -放进数据: - -```json -POST /my_store/products/_bulk -{ "index": { "_id": 1 }} -{ "price" : 10, "productID" : "XHDK-A-1293-#fJ3" } -{ "index": { "_id": 2 }} -{ "price" : 20, "productID" : "KDKE-B-9947-#kL5" } -{ "index": { "_id": 3 }} -{ "price" : 30, "productID" : "JODL-X-1937-#pV7" } -{ "index": { "_id": 4 }} -{ "price" : 30, "productID" : "QQPX-R-3956-#aD8" } -``` - -查询文本: - -```json -GET /my_store/products/_search -{ - "query" : { - "constant_score" : { - "filter" : { - "term" : { - "productID" : "XHDK-A-1293-#fJ3" - } - } - } - } -} -``` - -#### terms 查询 - -允许指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件。 - -```json -GET /company/employee/_search -{ - "query" : { - "terms" : { - "interests" : ["listen", "music", "sport"] - } - } -} -``` - -置入 `filter` 语句的常量评分查询: - -```json -GET /my_store/products/_search -{ - "query": { - "constant_score": { - "filter": { - "terms" : { - "interests" : ["listen", "music", "sport"] - } - }, - "boost": 1.2 - } - } -} -``` - -interests 为 listen、music 和 sport 的文档会被匹配。 - -### 全文搜索 - -#### match 查询 - -match 查询是对全文进行查询。 - -如果有多个搜索关键字, Elastic 认为它们是`or`关系。下面例子返回 interests 为 sport 或者 music 的文档。 - -```json -GET _search -{ - "query" : { - "match": { - "interests": "sport music" - } - } -} -``` - -指定词都必须匹配: - -```json -GET _search -{ - "query" : { - "match": { - "interests": { - "query": "sport music", - "operator": "and" - } - } - } -} -``` - -控制匹配的字段数目: - -```json -GET _search -{ - "query" : { - "match": { - "interests": { - "query": "sport music", - "minimum_should_match": 2 - } - } - } -} -``` - -### range 查询 - -查询找出那些落在指定区间内的数字或者时间。 - -#### 数字范围 - -```json -GET company/employee/_search -{ - "query": { - "constant_score": { - "filter": { - "range": { - "age": { - "gt": 20, - "lt": 100 - } - } - } - } - } -} -``` - -`gt`:大于;`gte`:大于或等于;`lt`:小于;`lte`:小于或等于。 - -#### 日期范围 - -```json -GET company/employee/_search -{ - "query": { - "constant_score": { - "filter": { - "range": { - "hire_date": { - "gt": "2015-01-01", - "lt": "2019-01-01" - } - } - } - } - } -} -``` - -### 分页 - -请求得到 1 到 3 页的结果: - -```json -GET /_search?size=5 -GET /_search?size=5&from=5 -GET /_search?size=5&from=10 -``` - -### exists 查询 - -查询字段 interests 中不为空的文档: - -```json -GET /company/employee/_search -{ - "query": { - "constant_score": { - "filter": { - "exists": { - "field": "interests" - } - } - } - } -} -``` - -字段 interests 为空的文档。es6 没有 missing api,直接用 bool 和 exists 实现。 - -```json -GET /company/employee/_search -{ - "query": { - "bool": { - "must_not": { - "exists": { - "field": "interests" - } - } - } - } -} -``` - -### bool 组合查询 - -bool 查询允许在单独的查询中组合任意数量的查询,适用于将不同查询字符串映射到不同字段的情况。bool 查询接收如下参数: - -- must:文档必须匹配这些条件才能被包含进来 -- must_not:文档必须不匹配这些条件才能被包含进来 -- should:如果满足这些语句中的任意语句,将增加 _score,否则没有影响。主要用来修正每个文档的相关性得分 -- filter:必须匹配,但是不影响评分 - -如果没有 `must` 语句,那么至少需要能够匹配其中的一条 `should` 语句。但,如果存在至少一条 `must` 语句,则对 `should` 语句的匹配没有要求。 - -```json -GET /company/employee/_search -{ - "query" : { - "bool" : { - "must" : { "match" : {"first_name" : "tyson"}}, - "must_not" : { "match" : {"last_name" : "tang"}}, - "should" : [ - {"match" : {"interest" : "music"}}, - {"range" : {"age" : { "gte" : 30 }}} - ] - } - } -} -``` - -如果不想因为文档的时间而影响得分,可以添加 filter 过滤掉某个时间段。 - -```json -GET _search -{ - "query" : { - "bool" : { - "must" : { - "match" : { "first_name" : "tyson"} - }, - "must_not" : { - "match" : {"last_name" : "tang"} - }, - "should" : [ - {"match" : { "interests": "sport"}}, - {"range" : { "age" : { "gt" : 20 }}} - ], - "filter" : { - "range" : { "hire_date" : { "gt" : "2015-01-01" }} - } - } - } -} -``` - -可以将查询移到 `bool` 查询的 `filter` 语句中,这样它就自动的转成一个不评分的查询了。 - -```json -{ - "bool": { - "must": { "match": { "title": "how to make millions" }}, - "must_not": { "match": { "tag": "spam" }}, - "should": [ - { "match": { "tag": "starred" }} - ], - "filter": { - "bool": { - "must": [ - { "range": { "date": { "gte": "2014-01-01" }}}, - { "range": { "price": { "lte": 29.99 }}} - ], - "must_not": [ - { "term": { "category": "ebooks" }} - ] - } - } - } -} -``` - -bool 查询使用`minimum_should_match`控制需要匹配的 should 语句的数量: - -```json -GET /my_index/my_type/_search -{ - "query": { - "bool": { - "should": [ - { "match": { "title": "brown" }}, - { "match": { "title": "fox" }}, - { "match": { "title": "dog" }} - ], - "minimum_should_match": 2 - } - } -} -``` - -### 如何使用 bool 查询 - -多次 match 查询: - -```json -GET /company/employee/_search -{ - "query": { - "match": { - "interests": "sport music" - } - } -} -``` - -等价的 bool 查询(存在`must`,则对 `should` 语句的匹配没有要求,否则至少需要能够匹配其中的一条 `should` 语句): - -```json -GET /company/employee/_search -{ - "query": { - "bool": { - "should": [ - { "term" : { "interests": "sport" } }, - { "term" : { "interests" : "music" } } - ] - } - } -} -``` - -使用 and 操作符,返回所有字段都匹配到的文档: - -```json -GET /company/employee/_search -{ - "query": { - "match": { - "interests": { - "query": "sport music", - "operator": "and" - } - } - } -} -``` - -等价的 bool 查询: - -```json -GET /company/employee/_search -{ - "query": { - "bool": { - "must": [ - { "term": { "interests": "sport" }}, - { "term": { "interests": "music" }} - ] - } - } -} -``` - -指定参数 minimum_should_match: - -```json -GET /company/employee/_search -{ - "query": { - "match": { - "interests": { - "query": "sport music chess", - "minimum_should_match" : 2 - } - } - } -} -``` - -等价的 bool 查询: - -```json -GET /company/employee/_search -{ - "query": { - "bool": { - "should": [ - {"term": { "interests": "sport" }}, - {"term": { "interests": "music" }}, - {"term": { "interests": "chess" }} - ], - "minimum_should_match": 2 - } - } -} -``` - -### constant_score 查询 - -constant_score 查询返回的文档`score`都是1。它经常用于只需要执行一个 filter 而没有其它查询的情况下。`score`会受到`boost`影响,出现 tyson 的文档`score`为1.2。 - -```json -GET _search -{ - "query" : { - "constant_score": { - "filter": { - "match" : { "first_name" : "tyson"} - }, - "boost": 1.2 - } - } -} -``` - - - - -### 多字段搜索 - -参考自:[best_fields most_fields cross_fields从内在实现看区别——本质就是前两者是以field为中心,后者是词条为中心](https://www.cnblogs.com/bonelee/p/6827068.html) - -最佳字段 best_fields:搜索结果中应该返回某一个字段匹配到了最多的关键词的文档。 - -多数字段 most_fields:返回匹配了更多的字段的文档,尽可能多地匹配文档。ES会为每个字段生成一个match查询,然后将它们包含在一个bool查询中。 - -跨字段 cross_fields:每个查询的单词都出现在不同的字段中。*cross_fields类型采用了一种以词条为中心(Term-centric)的方法,这种方法和best_fields及most_fields采用的以字段为中心(Field-centric)的方法有很大的区别。它将所有的字段视为一个大的字段,然后在任一字段中搜索每个词条。 - -#### 多字符串查询 - -```json -GET /_search -{ - "query": { - "bool": { - "should": [ - { "match": { // 权重:1/3 - "title": { - "query": "War and Peace", - "boost": 2 - }}}, - { "match": { // 权重:1/3 - "author": { - "query": "Leo Tolstoy", - "boost": 2 - }}}, - { "bool": { //译者信息用布尔查询,降低评分权重 权重:1/3 - "should": [ - { "match": { "translator": "Constance Garnett" }}, - { "match": { "translator": "Louise Maude" }} - ] - }} - ] - } - } -} -``` - -#### multi_match 查询 - -multi_match 查询可以在多个字段上执行相同的 match 查询。`multi_match` 多匹配查询的类型有三种:`best_fields` 、 `most_fields` 和 `cross_fields` (最佳字段、多数字段、跨字段)。 - -```json -GET company/employee/_search -{ - "query" : { - "multi_match": { - "query": "sport shoes", - "type": "best_fields", - "fields": [ "interests", "first_name" ], - "tie_breaker": 0.3, - "minimum_should_match": 1 - } - } -} -``` - -模糊匹配: - -```json -GET company/employee/_search -{ - "query" : { - "multi_match": { - "query": "tyson dai", - "type": "best_fields", - "fields": "*_name", - "tie_breaker": 0.3, - "minimum_should_match": "30%" - } - } -} -``` - -提升字段的权重,first_name 字段的 `boost` 值为 `2` : - -```json -GET company/employee/_search -{ - "query" : { - "multi_match": { - "query": "tyson dai", - "type": "best_fields", - "fields": ["*_name", "first_name^3"], - "tie_breaker": 0.3, - "minimum_should_match": "30%" - } - } -} -``` - -#### 多字段映射 - -对字段索引两次: 一次使用词干模式以及一次非词干模式。 - -```json -DELETE /my_index - -PUT /my_index -{ - "settings": { "number_of_shards": 1 }, - "mappings": { - "my_type": { - "properties": { - "title": { - "type": "string", - "analyzer": "english",//提取词干 - "fields": { - "std": { - "type": "string", - "analyzer": "standard" - } - } - } - } - } - } -} -``` - -`title` 字段使用 `english` 分析器来提取词干;`title.std` 字段使用 `standard` 标准分析器,所以没有词干提取。 - -索引文档: - -```json -PUT /my_index/my_type/1 -{ "title": "My rabbit jumps" } - -PUT /my_index/my_type/2 -{ "title": "Jumping jack rabbits" } -``` - -multi_match 查询: - -```json -GET /my_index/_search -{ - "query": { - "multi_match": { - "query": "jumping rabbits", - "type": "most_fields", - "fields": [ "title", "title.std" ] - } - } -} -``` - -文档2匹配度更高,因为 title.std 不会提取词干,只有文档2是匹配的。 - -设置`title` 字段的 `boost` 的值为 `10`,提升 title 字段的权重: - -```json -GET /my_index/_search -{ - "query": { - "multi_match": { - "query": "jumping rabbits", - "type": "most_fields", - "fields": [ "title^10", "title.std" ] - } - } -} -``` - -#### copy_to 定制组合 field - -定制组合 field 与 cross_field 跨字段查询类似,根据两者的实际性能选择具体方案。 - -创建映射: - -```json -PUT my_index1 -{ - "mappings": { - "my_type": { - "properties": { - "first_name": { - "type": "keyword", - "copy_to": "full_name" - }, - "last_name": { - "type": "keyword", - "copy_to": "full_name" - }, - "full_name": { - "type": "text", - "fielddata": true - } - } - } - } -} -``` - -插入数据: - -```json -PUT my_index1/my_type/1 -{ - "first_name": "John", - "last_name": "Smith" -} -``` - -校验查询: - -```json -GET my_index1/_search -{ - "query": { - "match": { - "full_name": { - "query": "John Smith", - "operator": "and" - } - } - } -} - -``` - -`copy_to` 设置对 multi-field 无效。如果尝试这样配置映射,Elasticsearch 会抛异常。多字段只是以不同方式简单索引主字段;它们没有自己的数据源。 - -```json -PUT /my_index -{ - "mappings": { - "person": { - "properties": { - "first_name": { - "type": "string", - "copy_to": "full_name", - "fields": { - "raw": { - "type": "string", - "index": "not_analyzed" - } - } - }, - "full_name": { - "type": "string" - } - } - } - } -} -``` - -first_name 是主字段,first_name.raw 是多字段。 - - - -## springboot 集成 es - -[springboot 集成 es](https://blog.csdn.net/cwenao/article/details/54943505) - -[springboot整合elasticsearch5.x以及IK分词器做全文检索](https://blog.csdn.net/chenxihua1/article/details/94546282) - - - -## mall - -创建文档: - -```json -POST /pms/product -{ - "productSn": "HNTBJ2E080A", - "brandId": 50, - "brandName": "海澜之家", - "productCategoryId": 8, - "productCategoryName": "T恤", - "pic": "http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180615/5ac98b64N70acd82f.jpg!cc_350x449.jpg", - "name": "HLA海澜之家蓝灰花纹圆领针织布短袖T恤", - "subTitle": "2018夏季新品短袖T恤男HNTBJ2E080A 蓝灰花纹80 175/92A/L80A 蓝灰花纹80 175/92A/L", - "keywords": "", - "price": 98, - "sale": 0, - "newStatus": 0, - "recommandStatus": 0, - "stock": 100, - "promotionType": 0, - "sort": 0, - "attrValueList": [ - { - "id": 183, - "productAttributeId": 24, - "value": null, - "type": 1, - "name": "商品编号" - }, - { - "id": 184, - "productAttributeId": 25, - "value": "夏季", - "type": 1, - "name": "适用季节" - } - ] -} -``` - -获取文档映射:`GET /pms/_mapping/product` - -按字段查询:`GET /pms/product/_search?q=subTitle:2018` - -match查询: - -```json -GET /pms/product/_search -{ - "query": { - "match": { - "brandName": "小米" - } - } -} -``` - - - - - diff --git "a/\345\205\266\344\273\226/code.md" "b/\345\205\266\344\273\226/code.md" deleted file mode 100644 index 074879c..0000000 --- "a/\345\205\266\344\273\226/code.md" +++ /dev/null @@ -1,77 +0,0 @@ - - - - -- [单例模式](#%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F) -- [Java8](#java8) - - - -## 单例模式 - -双重检查锁定: - -```java -public class Singleton { - private static volatile Singleton instance = null; //volatile - private Singleton(){} - public static Singleton getInstance() { - if (instance == null) { //两次检查,降低同步开销 - synchronized (Singleton.class) { - if (instance == null) { - instance = new Singleton(); - } - } - } - return instance; - } -} -``` - -静态内部类: - -```java -public class Singleton { - private static class SingletonHolder { - public static Singleton instance = new Singleton(); - } - private Singleton() {} - public static Singleton getSingleton() { - return SingletonHolder.instance;// 这里将导致SingletonHolder类被初始化 - } -} -``` - - - -## Java8 - -排序: - -```java -stringCollection - .stream() - .sorted() - .filter((s) -> s.startsWith("a")) - .forEach(System.out::println); - -// "aaa1", "aaa2" - -list.stream().sorted(Comparator.comparing(Student::getAge).reversed()); -list.stream().sorted(Comparator.comparing(Student::getAge)); -``` - -过滤: - -```java -stringCollection - .stream() - .sorted() - .filter((s) -> s.startsWith("a")) - .forEach(System.out::println); - -// "aaa1", "aaa2" -``` - - - diff --git "a/\345\205\266\344\273\226/linux.md" "b/\345\205\266\344\273\226/linux.md" deleted file mode 100644 index d2e1663..0000000 --- "a/\345\205\266\344\273\226/linux.md" +++ /dev/null @@ -1,417 +0,0 @@ - - - - -- [基本命令](#%E5%9F%BA%E6%9C%AC%E5%91%BD%E4%BB%A4) - - [与或](#%E4%B8%8E%E6%88%96) - - [help](#help) -- [文件](#%E6%96%87%E4%BB%B6) - - [当前路径](#%E5%BD%93%E5%89%8D%E8%B7%AF%E5%BE%84) - - [查看目录](#%E6%9F%A5%E7%9C%8B%E7%9B%AE%E5%BD%95) - - [查看文件](#%E6%9F%A5%E7%9C%8B%E6%96%87%E4%BB%B6) - - [文件操作](#%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C) - - [vim](#vim) - - [查找文件](#%E6%9F%A5%E6%89%BE%E6%96%87%E4%BB%B6) - - [whereis](#whereis) - - [locate](#locate) - - [which](#which) - - [find](#find) - - [权限](#%E6%9D%83%E9%99%90) -- [服务](#%E6%9C%8D%E5%8A%A1) -- [进程端口](#%E8%BF%9B%E7%A8%8B%E7%AB%AF%E5%8F%A3) -- [grep](#grep) - - [高亮查找](#%E9%AB%98%E4%BA%AE%E6%9F%A5%E6%89%BE) - - [环顾四周](#%E7%8E%AF%E9%A1%BE%E5%9B%9B%E5%91%A8) - - [打印行号](#%E6%89%93%E5%8D%B0%E8%A1%8C%E5%8F%B7) - - [统计行数](#%E7%BB%9F%E8%AE%A1%E8%A1%8C%E6%95%B0) - - [不区分大小写](#%E4%B8%8D%E5%8C%BA%E5%88%86%E5%A4%A7%E5%B0%8F%E5%86%99) - - [反查](#%E5%8F%8D%E6%9F%A5) - - [多文件查找](#%E5%A4%9A%E6%96%87%E4%BB%B6%E6%9F%A5%E6%89%BE) -- [sed](#sed) -- [磁盘](#%E7%A3%81%E7%9B%98) - - [mount](#mount) -- [内存](#%E5%86%85%E5%AD%98) - - - -## 基本命令 - -### 与或 - -方式:command1 && command2 -如果command1执行成功,则执行command2 -方式:command1 || command2 -如果command1执行失败,则执行command2 - -### help - -help 命令是用于显示 shell 内建命令的简要帮助信息 `help cd` -man得到的内容比用 help 更多更详细,而且man没有内建与外部命令的区分 - - - -## 文件 - -### 当前路径 - -查看当前路径:pwd - -### 查看目录 -目录下各个文件容量。 - -```bash -ls -lht -``` - --l:使用长格式列出文件及目录信息 - --t:根据最后的修改时间排序 - -### 查看文件 - -列出文件:`ls -l` 可以简写为 `ll`(别名,ubantu下不支持) - -cat:输出文件所有内容 - -tail -10 demo.txt:显示最后10行 - -tail -f -n 100 demo.txt:实时查看文件,显示最后100行 - -head -10 demo.txt - -head -20 nohup.out | tail -10:第11行到20行 - -less:分页显示工具,可以前后翻页查看文件。pagedown下一页,pageup上一页。 - -```bash -less demo.log -``` - -查看占用磁盘容量最多的文件: - -```bash - du -a /var | sort -n -r | head -n 10 -``` - -### 文件操作 - -新建文件夹: mkdir - -新建文件:touch - -编辑文件:vi - -删除文件:rm -rf (-f强制删除 -r 递归删除) - -删除空目录:`rmdir dir`。rmdir命令只能删除空目录。当要删除非空目录时,就要使用带有“-R”选项的rm命令。 - -复制文件:cp -r dir1 dir2 表示将dir1及其dir1下所包含的文件复制到dir2下。-r 复制目录 - -文件重命名:mv before.txt after.txt - -解压:tar -zxvf jdk-8u131-linux-x64.tar.gz -C /usr/lib/jvm (-C解压到指定的目录) - -解压带有中文字符的zip文件:unzip -O gbk filename.zip - -### vim - -退出编辑:输入i进入编辑模式,按下esc退出编辑,输入:w,保存不退出;:z/:wq,保存后退出;:q!,退出不保存(w:write,q:quit) - -搜索:输入/+搜索的词,回车,输入n跳转下一个匹配词 - -删除行:按esc进入命令模式,dd删除当前行,Ndd删除当前行以下的n行 - -撤销上一个操作:命令模式下,按 u - -恢复上一步被撤销的操作:ctrl+r - -当vim非正常关闭时,会产生swp文件,在当前目录删除:rm .xxx.swp - -### 查找文件 - -#### whereis - -简单快速,直接从数据库查找,whereis 只能搜索二进制文件(-b),man 帮助文件(-m)和源代码文件(-s) - -查找tomcat:`whereis tomcat` - -#### locate - -快而全,通过“ /var/lib/mlocate/mlocate.db ”数据库查找,数据库不是实时更新,递归查找指定目录下的不同文件类型,刚添加的文件需手 - -动执行updatedb。 - -查找图片:locate background.jpg - -查找某个目录下所有图片:locate -ic /usr/share/\*.jpg(-c统计数目,-i忽略大小写) - -#### which - -小而精,which 本身是shell內建的一个命令,它只从PATH环境变量指定的路径去搜索,通常使用which来确定是否安装了某个命令。 - -查看是否安装man:`which man` - -#### find - -最强大,速度慢,第一个参数是搜索的地方。 - -```bash -find / -name background.jpg -sudo find / -name .ssh //隐藏文件 -``` - -### 权限 - -```bash -chmod +x xxx -chmod u+x shutdown -sudo chmod 744 hello.c -sudo chown tyson hello.c -chown mysql slow.log -``` - - - -## 服务 - -启动服务: - -```bash -service mysqld start -/sbin/service rabbitmq-server start -redis-server /etc/redis.conf -redis-cli #启动redis客户端 -``` - -停止服务: - -```bash -service mysqld stop -/sbin/service rabbitmq-server stop #service是个文件,路径/usr/sbin/service,根目录下有/sbin链接到/usr/sbin,可以在任何文件夹使用此命令 -redis-cli -a password shutdown #关闭redis服务,没有设置密码则直接redis-cli shutdown -``` - -[redis安装](https://cloud.tencent.com/developer/article/1119337) - - - -## 进程端口 - -查看所有进程:`ps -ef` -e显示所有进程,-f显示进程对应的命令行 - -查看特定进程:`ps -ef | grep nginx` - -根据进程号查看进程信息:`ps aux | grep pid` - -根据进程号查找线程:`ps -T -p xxx` - -正常结束进程:`kill -15 pid` 默认-15(SIGTERM)可以省略 - -杀死进程:`kill -9 pid` -9(SIGKILL)强制中断 - -放通端口:iptables -I INPUT -p TCP --dport 8013 -j ACCEPT - -查看监听的端口:`netstat -tunlp` - -查看端口信息:`netstat -tunlp | grep 8000` - -- -t - 显示 TCP 端口。 -- -u - 显示 UDP 端口。 -- -n - 显示数字地址而不是主机名。 -- -l - 仅显示侦听端口。 -- -p - 显示进程的 PID 和名称。仅当以 root 或 sudo 用户身份运行命令时,才会显示此信息。 - - - -## grep - -global search regular expression and print out the line。全面搜索正则表达式,并将其打印出来。 - -### 高亮查找 - -```bash -grep --color "leo" /etc/passwd -``` - -### 环顾四周 - -`-A`表示除了展示匹配行之外,还要展示出匹配行下面的若干行。`-B`表示除了展示匹配行之外,还要展示出匹配行上面的若干行。`-C`表示除了展示匹配行之外,还要展示出匹配行上面和下面各若干行。 - -```bash -grep -C 1 leo passwd -``` - -### 打印行号 - -查找字符串mail,打印行号: - -```bash -[root@VM-0-7-centos etc]# grep -n mail /etc/passwd -9:mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -``` - -### 统计行数 - -```bash -[root@VM-0-7-centos etc]# grep -c mail /etc/passwd -1 -``` - -### 不区分大小写 - -```bash -[root@VM-0-7-centos etc]# grep -i mail /etc/passwd -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -``` - -### 反查 - -搜索不包含root的行,-v实现反查效果: - -```bash -[root@VM-0-7-centos etc]# grep -v "root" /etc/passwd -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -``` - -### 多文件查找 - -找出内容中含有first单词的文件: - -```bash -[root@VM-0-7-centos test]# grep -l "first" *.txt -1.txt -``` - -找出不含 first 单词的文件: - -```bash -[root@VM-0-7-centos test]# grep -L "first" *.txt -2.txt -3.txt -``` - -搜索/etc/passwd文件中开头是mail的行: - -```bash -[root@VM-0-7-centos test]# grep '^mail' /etc/passwd -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -``` - -搜索 /etc/passwd 文件中行尾是nologin的行: - -```bash -[root@VM-0-7-centos test]# grep 'nologin$' /etc/passwd -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -``` - -精确匹配某个词,单纯使用bin会把sbin等也搜索出来: - -```bash -[root@VM-0-7-centos test]# grep -w 'bin' /etc/passwd -root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -``` - -正则表达式: - -```mysql -# 将匹配以'z'开头以'o'结尾的所有字符串 -$ echo 'zero\nzo\nzoo' | grep 'z.*o' - -# 将匹配以'z'开头以'o'结尾,中间包含一个任意字符的字符串 -$ echo 'zero\nzo\nzoo' | grep 'z.o' - -# 将匹配以'z'开头,以任意多个'o'结尾的字符串 -$ echo 'zero\nzo\nzoo' | grep 'zo*' - - -# grep命令用于打印输出文本中匹配的模式串 -# grep默认是区分大小写的,这里将匹配所有的小写字母 -$ echo '1234\nabcd' | grep '[a-z]' - -# 将匹配所有的数字 -$ echo '1234\nabcd' | grep '[0-9]' - -# 将匹配所有的数字 -$ echo '1234\nabcd' | grep '[[:digit:]]' - -# 将匹配所有的小写字母 -$ echo '1234\nabcd' | grep '[[:lower:]]' - -# 将匹配所有的大写字母 -$ echo '1234\nabcd' | grep '[[:upper:]]' - -# 将匹配所有的字母和数字,包括0-9,a-z,A-Z -$ echo '1234\nabcd' | grep '[[:alnum:]]' - -# 将匹配所有的字母 -$ echo '1234\nabcd' | grep '[[:alpha:]]' -``` - - - -## sed - -处理、编辑文本文件。 - -```bash -# 打印2-5行 nl输出文件内容带上行号 -$ nl passwd | sed -n '2,5p' - -# 删除2-5行 -nl /etc/passwd | sed '2,5d' - -# 删除第三到最后一行 -nl /etc/passwd | sed '3,$d' -``` - - - -## 磁盘 - -以容易阅读的方式,显示磁盘使用情况: - -```bash -df -h -``` - -指定文件磁盘使用情况: - -```bash -[root@VM-0-7-centos etc]# df /etc/dhcp -Filesystem 1K-blocks Used Available Use% Mounted on -/dev/vda1 51539404 23774760 25567400 49% / -``` - -### mount - -挂载Linux系统外的文件。 - -挂载hda2到/mnt/hda2目录下。确定目录/mnt/hda2 已经存在。 - -```bash -mount /dev/hda2 /mnt/hda2 -``` - -卸载hda2硬盘。先从挂载点/mnt/hda2退出。 - -```bash -umount /dev/hda2 -``` - - - -## 内存 - -`free -h` -h以可读的方式展示。 - -``` - total used free shared buff/cache available -Mem: 1.8G 1.1G 107M 1.1M 562M 515M -Swap: 0B 0B 0B - -``` - diff --git "a/\345\205\266\344\273\226/note.md" "b/\345\205\266\344\273\226/note.md" deleted file mode 100644 index dad41bf..0000000 --- "a/\345\205\266\344\273\226/note.md" +++ /dev/null @@ -1,286 +0,0 @@ - - - - -- [RESTful](#restful) -- [JWT](#jwt) - - [原理](#%E5%8E%9F%E7%90%86) - - [数据结构](#%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) -- [金额使用decimal存储类型的缺点](#%E9%87%91%E9%A2%9D%E4%BD%BF%E7%94%A8decimal%E5%AD%98%E5%82%A8%E7%B1%BB%E5%9E%8B%E7%9A%84%E7%BC%BA%E7%82%B9) -- [Maven的作用](#maven%E7%9A%84%E4%BD%9C%E7%94%A8) -- [为什么使用消息队列](#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97) -- [秒杀系统](#%E7%A7%92%E6%9D%80%E7%B3%BB%E7%BB%9F) -- [shiro](#shiro) -- [分布式锁](#%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81) -- [dubbo](#dubbo) -- [分布式ID](#%E5%88%86%E5%B8%83%E5%BC%8Fid) - - [UUID](#uuid) - - [数据库自增ID](#%E6%95%B0%E6%8D%AE%E5%BA%93%E8%87%AA%E5%A2%9Eid) - - [基于Redis模式](#%E5%9F%BA%E4%BA%8Eredis%E6%A8%A1%E5%BC%8F) - - [雪花算法](#%E9%9B%AA%E8%8A%B1%E7%AE%97%E6%B3%95) -- [foeach和for的效率](#foeach%E5%92%8Cfor%E7%9A%84%E6%95%88%E7%8E%87) -- [epoll](#epoll) -- [防止重复请求](#%E9%98%B2%E6%AD%A2%E9%87%8D%E5%A4%8D%E8%AF%B7%E6%B1%82) -- [接口幂等性](#%E6%8E%A5%E5%8F%A3%E5%B9%82%E7%AD%89%E6%80%A7) -- [令牌桶算法](#%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95) -- [超卖问题](#%E8%B6%85%E5%8D%96%E9%97%AE%E9%A2%98) -- [TPS和QPS](#tps%E5%92%8Cqps) -- [微服务](#%E5%BE%AE%E6%9C%8D%E5%8A%A1) -- [进程通信](#%E8%BF%9B%E7%A8%8B%E9%80%9A%E4%BF%A1) -- [分布式事务](#%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1) - - - -## RESTful - -[RESTful API](https://www.zhihu.com/question/28557115) - -REST -- REpresentational State Transfer 直接翻译:表现层状态转移。URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。 - -REST描述的是在网络中client和server的一种交互形式。REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口)。 - - Server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。通过HTTP方法来实现资源的状态扭转: - -```java -DELETE http://api.qc.com/v1/friends/{id} -GET http://api.qc.com/v1/friends -``` - - - - - -## JWT - -[JWT教程](http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html) | [数字签名](https://blog.csdn.net/qq_21514303/article/details/82898984) - -JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。 - -session认证流程: - -1、用户向服务器发送用户名和密码。 - -2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。 - -3、服务器向用户返回一个 session_id,写入用户的 Cookie。 - -4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。 - -5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。 - -如果需要跨域认证,即用户登录了a网站,再访问b网站需要自动登录,则要求session数据共享。可以将session数据持久化,存到数据库来实现session共享。 - -JWT不在服务器端保存session数据,所有数据都保存在客户端,每次请求都发回服务器。它是通过算法实现对Token合法性的验证,不依赖数据库,只要密钥和算法相同,**不同服务器程序生成的Token可以互相验证**。 - -### 原理 - -用户使用用户名和密码登录服务器。服务器认证完成后,使用私钥生成一个 JSON 对象(包括用户名、权限等),发回给用户。 - -```json -{ - "username": "张三", - "roles": "管理员", - "expire": "2018年7月1日0点0分" -} -``` - -以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器依据JSON对象认证用户身份。为了防止用户篡改数据,服务器在生成JSON对象的时候会使用私钥进行加密生成签名。 - -![](../img/others/json-web-token.png) - -### 数据结构 - -JWT数据分为三个部分: - -- Header(头部) -- Payload(负载) -- Signature(签名) - -Header和Payload会使用 Base64URL 算法转成字符串,然后三部分用`.`进行分隔: - -```javascript -Header.Payload.Signature -``` - -Header 部分是一个 JSON 对象,描述 JWT 的元数据。 - -```javascript -{ - "alg": "HS256", - "typ": "JWT" -} -``` - -Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据,如用户名、权限等。 - -Signature 部分是对前两部分的签名,防止数据篡改。首先需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用 Header 里面指定的签名算法(默认是 HMAC SHA256),根据下面的公式产生签名。 - -```javascript -HMACSHA256( - base64UrlEncode(header) + "." + - base64UrlEncode(payload), - secret) -``` - - - -## 金额使用decimal存储类型的缺点 - -- 占用存储空间。浮点类型在存储同样范围的值时,通常比decimal使用更少的空间 -- 使用decimal计算效率不高 - -因为使用decimal时间和空间开销较大,选用int作为数据库存储格式比较合适,可以同时避免浮点存储计算的不精确和decimal的缺点。对于存储数值较大或者保留小数较多的数字,数据库存储结构可以选择bigint,可以同时避免浮点存储计算不精准和DECIMAL精度计算代价高的问题 - - - -## Maven的作用 - -管理jar包依赖;根据配置自动下载相应jar包; 热部署,编译* package命令完成了项目编译、单元测试、打包功能,但没有把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库* install命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库,但没有布署到远程maven私服仓库* deploy命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库  - - - -## 为什么使用消息队列 - -在高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。 -异步通信就是你发了一个请求,没收到回答的时候,你发了另一个请求。 - - - - - -## shiro - -作用: - -1. 验证用户身份 -2. 用户访问权限控制 -3. 支持提供`Remember Me`服务 - - - -## 分布式锁 - -在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式。 - -在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户发起一个数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插入操作,导致数据重复插入,对于某些不允许有多余数据的业务来说,这就会造成问题。而分布式锁机制就是为了解决类似这类问题,保证多个服务之间互斥的访问共享资源,如果一个服务抢占了分布式锁,其他服务没获取到锁,就不进行后续操作。 - -![](https://user-gold-cdn.xitu.io/2019/4/25/16a53749547937bb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -实现分布式锁有以下几种方式: - -- 基于数据库 - -- 基于Redis - - 使用lua优点,可以保证多个命令是一次行传输到Redis服务器并且是串行执行的,保证串行执行的命令中不行插入其他命令,防止并发问题。 - -- 基于zookeeper - - - -## dubbo - -分布式协调服务:在分布式系统中共享配置,协调锁资源,提供命名服务。 - - - -## 分布式ID - -`全局唯一ID`就叫`分布式ID`。 - -### UUID - -```java -String uuid = UUID.randomUUID().toString().replaceAll("-",""); -``` - -优点: - -- 生成足够简单,本地生成无网络消耗,具有唯一性 - -缺点: - -- 无序的字符串,不具备趋势自增特性 -- 没有具体的业务含义 - -### 数据库自增ID - -优点: - -- 简单方便,有序递增,方便排序和分页 - -缺点: - -- 分库分表会带来问题,需要进行改造。 - -### 基于Redis模式 - -利用`redis`的 `incr`命令实现ID的原子性自增。 - -### 雪花算法 - -![](https://user-gold-cdn.xitu.io/2020/2/16/1704bd6d27b09766?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - - - -## foeach和for的效率 - -1、foreach适用于数组或实现了iterator的集合类。foreach就是使用Iterator接口来实现对集合的遍历的。 - -2、在用foreach循环遍历一个集合时,不能改变集合中的元素,如增加元素、修改元素。否则会抛出ConcurrentModificationException异常。普通 for 循环遍历过程可以修改元素。 - - - -## epoll - -select、poll和epoll都是IO多路复用的机制。 - - - -## 防止重复请求 - -1. 前端提交按钮置灰几秒钟。 -2. 后端使用Redis保存请求的唯一id(业务参数或者前端自己生成,保证唯一),当第一次请求过来,从redis中取id,如果value为null,说明是第一次请求,将这个id存入redis;如果不为null,说明是重复请求,直接抛异常。 - - - -## 接口幂等性 - -幂等性:接口一次调用和多次调用的结果一致。 - -对于业务中需要考虑幂等性的地方一般都是接口的重复请求,重复请求是指同一个请求因为某些原因被多次提交。导致这个情况会有几种场景: - -- 前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。 -- 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。 -- 消息重复消费:MQ消息中间件,消息重复消费。 - - - -## 令牌桶算法 - -令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 - - - - - -## TPS和QPS - -**QPS**:Queries Per Second意思是“每秒查询率”,是一台服务器每秒处理的查询次数。 - -**TPS:**是TransactionsPerSecond的缩写,也就是事务数/秒。它是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。 - -Tps即每秒处理事务数,包括了三个过程: - -1)用户请求服务器 - -2)服务器自己的内部处理 - -3)服务器返回给用户 - -如果每秒能够完成N个这三个过程,Tps也就是N; - -对于一个页面的一次访问,形成一个Tps;但一次页面请求,可能产生多次对服务器的请求,这些请求可以计入Qps之中。例如访问一个页面会请求服务器3次,产生一个TPS,产生3个QPS。 - - - diff --git "a/\345\205\266\344\273\226/\345\256\236\346\210\230\347\257\207.md" "b/\345\205\266\344\273\226/\345\256\236\346\210\230\347\257\207.md" deleted file mode 100644 index 7d5164d..0000000 --- "a/\345\205\266\344\273\226/\345\256\236\346\210\230\347\257\207.md" +++ /dev/null @@ -1,93 +0,0 @@ -## OOM问题排查 - -[记一次OOM问题排查!](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486106&idx=1&sn=0d528efad06f0a1440e02983bf336bca&chksm=ce98f7dcf9ef7ecafee10115f1d29bfba56dd8bf694122dd2cb69a53f333b2242522f88aa92e&token=1360123733&lang=zh_CN#rd) - - - -## RT过长,排查思路 - -[Java诊断工具Arthas](https://mp.weixin.qq.com/s/TnLl2OW9XJLSZihcpgP7VQ) - - - -## 限流算法 - -限流是指在系统面临高并发、大流量请求的情况下,限制新的流量对系统的访问,从而保证系统服务的安全性。常用的限流算法有计数器固定窗口算法、滑动窗口算法、漏斗算法和令牌桶算法。 - -### 计数器固定窗口算法 - -计数器固定窗口算法是最基础也是最简单的一种限流算法。原理就是对一段固定时间窗口内的请求进行计数,如果请求数超过了阈值,则舍弃该请求;如果没有达到设定的阈值,则接受该请求,且计数加1。当时间窗口结束时,重置计数器为0。 - -**优点**:实现简单,容易理解。 - -**缺点**:流量曲线可能不够平滑,有“突刺现象”,这样会有两个问题: - -1. **一段时间内(不超过时间窗口)系统服务不可用**。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第1ms来了100个请求,然后第2ms-999ms的请求就都会被拒绝,这段时间用户会感觉系统服务不可用。 -2. **窗口切换时可能会产生两倍于阈值流量的请求**。比如窗口大小为1s,限流大小为100,然后恰好在某个窗口的第999ms来了100个请求,窗口前期没有请求,所以这100个请求都会通过。再恰好,下一个窗口的第1ms有来了100个请求,也全部通过了,那也就是在2ms之内通过了200个请求,而我们设定的阈值是100,通过的请求达到了阈值的两倍。 - -### 计数器滑动窗口算法 - -计数器滑动窗口算法是计数器固定窗口算法的改进,解决了固定窗口切换时可能会产生两倍于阈值流量请求的缺点。 - -滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。 - -将计时窗口划分成一个小窗口,滑动窗口算法就退化成了固定窗口算法。而滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。 - -**特点分析** - -1. 避免了计数器固定窗口算法固定窗口切换时可能会产生两倍于阈值流量请求的问题; -2. 和漏斗算法相比,新来的请求也能够被处理到,避免了漏斗算法的饥饿问题。 - -### 漏斗算法 - -漏斗算法的原理也很容易理解。请求来了之后会首先进到漏斗里,然后漏斗以恒定的速率将请求流出进行处理,从而起到平滑流量的作用。当请求的流量过大时,漏斗达到最大容量时会溢出,此时请求被丢弃。从系统的角度来看,我们不知道什么时候会有请求来,也不知道请求会以多大的速率来,这就给系统的安全性埋下了隐患。但是如果加了一层漏斗算法限流之后,就能够保证请求以恒定的速率流出。在系统看来,请求永远是以平滑的传输速率过来,从而起到了保护系统的作用。 - -**特点分析** - -1. **漏桶的漏出速率是固定的,可以起到整流的作用**。即虽然请求的流量可能具有随机性,忽大忽小,但是经过漏斗算法之后,变成了有固定速率的稳定流量,从而对下游的系统起到保护作用。 -2. **不能解决流量突发的问题**。假如设定的漏斗速率是2个/秒,漏斗限流算法的容量是5。然后突然来了10个请求,受限于漏斗的容量,只有5个请求被接受,另外5个被拒绝。你可能会说,漏斗速率是2个/秒,然后瞬间接受了5个请求,这不就解决了流量突发的问题吗?不,这5个请求只是被接受了,但是没有马上被处理,处理的速度仍然是我们设定的2个/秒,所以没有解决流量突发的问题。而接下来我们要谈的令牌桶算法能够在一定程度上解决流量突发的问题。 - -### 令牌桶算法 - -令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。在令牌桶算法中,存在一个令牌桶,算法中存在一种机制以恒定的速率向令牌桶中放入令牌。令牌桶也有一定的容量,如果满了令牌就无法放进去了。当请求来时,会首先到令牌桶中去拿令牌,如果拿到了令牌,则该请求会被处理,并消耗掉拿到的令牌;如果令牌桶为空,则该请求会被丢弃。 - -比如过来10个请求,令牌桶算法和漏斗算法一样,都是接受了5个请求,拒绝了5个请求。与漏斗算法不同的是,令牌桶算法马上处理了这5个请求,处理速度可以认为是5个/秒,超过了我们设定的2个/秒的速率,即**允许一定程度的流量突发**。这一点也是和漏斗算法的主要区别。 - -**特点分析** - -令牌桶算法是对漏桶算法的一种改进,除了能够在限制调用的平均速率的同时还允许一定程度的流量突发。 - -### 小结 - -**计数器固定窗口算法**实现简单,容易理解。和漏斗算法相比,新来的请求也能够被马上处理到。但是流量曲线可能不够平滑,有“突刺现象”,在窗口切换时可能会产生两倍于阈值流量的请求。而**计数器滑动窗口算法**作为计数器固定窗口算法的一种改进,有效解决了窗口切换时可能会产生两倍于阈值流量请求的问题。 - -**漏斗算法**能够对流量起到整流的作用,让随机不稳定的流量以固定的速率流出,但是不能解决**流量突发**的问题。**令牌桶算法**作为漏斗算法的一种改进,除了能够起到平滑流量的作用,还允许一定程度的流量突发。 - -令牌桶算法一般用于保护自身的系统,对调用者进行限流,保护自身的系统不被突发的流量打垮。如果自身的系统实际的处理能力强于配置的流量限制时,可以允许一定程度的流量突发,使得实际的处理速率高于配置的速率,充分利用系统资源。而漏斗算法一般用于保护第三方的系统,比如自身的系统需要调用第三方的接口,为了保护第三方的系统不被自身的调用打垮,便可以通过漏斗算法进行限流,保证自身的流量平稳的打到第三方的接口上。 - -参考:https://zhuanlan.zhihu.com/p/228412634 - - - -## 亿级数据分页查询难怎么解决? - -无深翻页需求的实时在线应用:禁止深翻页,数据库 offset < N。 - -确有深翻页需求的离线分析应用:es搜scroll,jdbc自己维护这个读取游标的长链接,持续流式读取。 - - - -## 秒杀场景 - -并发修改库存,会加行锁。rt较高,tps上不去。两种方案: - -1. 分库分表。减小锁粒度 -2. 降低锁的持有时间 - -可以在内存起一个队列,将用户请求放进队列。起一个异步线程,每200ms处理队列数据,批量扣库存。这样就不用每个请求都去扣减库存,减少行锁持有时间。 - -每个请求封装一个锁对象(锁里面包含购买数量),丢进队列,调用wait(200)。当扣库存完成后拿到锁对象执行notify,通知用户请求线程继续往下执行。即每个用户线程都会有一个锁对象,用来阻塞和通知,阻塞是最多200ms - - - -## MySQL表数据量大 diff --git "a/\345\205\266\344\273\226/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/\345\205\266\344\273\226/\350\256\276\350\256\241\346\250\241\345\274\217.md" deleted file mode 100644 index 3808e3a..0000000 --- "a/\345\205\266\344\273\226/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ /dev/null @@ -1,437 +0,0 @@ - - - - -- [单例模式](#%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F) - - [饿汉模式](#%E9%A5%BF%E6%B1%89%E6%A8%A1%E5%BC%8F) - - [双重检查锁定](#%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A) - - [静态内部类](#%E9%9D%99%E6%80%81%E5%86%85%E9%83%A8%E7%B1%BB) -- [装饰模式](#%E8%A3%85%E9%A5%B0%E6%A8%A1%E5%BC%8F) -- [适配器模式](#%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F) -- [观察者模式](#%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F) -- [代理模式](#%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F) - - [静态代理](#%E9%9D%99%E6%80%81%E4%BB%A3%E7%90%86) - - [动态代理](#%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86) - - [JDK动态代理](#jdk%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86) -- [工厂模式](#%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F) - - [简单工厂模式](#%E7%AE%80%E5%8D%95%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F) - - [工厂方法模式](#%E5%B7%A5%E5%8E%82%E6%96%B9%E6%B3%95%E6%A8%A1%E5%BC%8F) - - [抽象工厂模式](#%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82%E6%A8%A1%E5%BC%8F) -- [模板模式](#%E6%A8%A1%E6%9D%BF%E6%A8%A1%E5%BC%8F) -- [建造者模式](#%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F) - - - -## 设计模式的六大原则 - -- 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口。 -- 里氏替换原则:基类可以被子类替换,使用抽象类继承,不使用具体类继承。 -- 依赖倒转原则:要依赖于抽象,不要依赖于具体,针对接口编程,不针对实现编程。 -- 接口隔离原则:使用多个隔离的接口,比使用单个接口好,建立最小的接口。 -- 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系。 -- 合成复用原则:尽量使用合成/聚合,而不是使用继承。 - -## 单例模式 - -需要对实例字段使用线程安全的延迟初始化,使用双重检查锁定的方案;需要对静态字段使用线程安全的延迟初始化,使用静态内部类的方案。 - -### 饿汉模式 - -``` -public class Singleton { - private static Singleton instance = new Singleton(); - private Singleton() {} - public static Singleton newInstance() { - return instance; - } -} -``` -JVM在类的初始化阶段,会执行类的静态方法。在执行类的初始化期间,JVM会去获取Class对象的锁。这个锁可以同步多个线程对同一个类的初始化。 - -饿汉模式只在类加载的时候创建一次实例,没有多线程同步的问题。单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。 - -### 双重检查锁定 - -instance使用static修饰的原因:getInstance为静态方法,因为静态方法的内部不能直接使用非静态变量,只有静态成员才能在没有创建对象时进行初始化,所以返回的这个实例必须是静态的。 - -``` -public class Singleton { - private static volatile Singleton instance = null; //volatile - private Singleton(){} - public static Singleton getInstance() { - if (instance == null) { - synchronized (Singleton.class) { - if (instance == null) { - instance = new Singleton(); - } - } - } - return instance; - } -} -``` -为什么两次判断`instance == null`: - -| Time | Thread A | Thread B | -| ---- | -------------------- | -------------------- | -| T1 | 检查到`instance`为空 | | -| T2 | | 检查到`instance`为空 | -| T3 | | 初始化对象`A` | -| T4 | | 返回对象`A` | -| T5 | 初始化对象`B` | | -| T6 | 返回对象`B` | | - -`new Singleton()`会执行三个动作:分配内存空间、初始化对象和对象引用指向内存地址。 - -```java -memory = allocate();  // 1:分配对象的内存空间 -ctorInstance(memory);  // 2:初始化对象 -instance = memory;   // 3:设置instance指向刚分配的内存地址 -``` - -由于指令重排优化的存在,导致初始化对象和将对象引用指向内存地址的顺序是不确定的。在某个线程创建单例对象时,会为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的是未初始化的对象,程序就会出错。volatile 可以禁止指令重排序,保证了先初始化对象再赋值给instance变量。 - -| Time | Thread A | Thread B | -| :--- | :----------------------- | :--------------------------------------- | -| T1 | 检查到`instance`为空 | | -| T2 | 获取锁 | | -| T3 | 再次检查到`instance`为空 | | -| T4 | 为`instance`分配内存空间 | | -| T5 | 将`instance`指向内存空间 | | -| T6 | | 检查到`instance`不为空 | -| T7 | | 访问`instance`(此时对象还未完成初始化) | -| T8 | 初始化`instance` | | - -### 静态内部类 - -``` -public class Instance { - private static class InstanceHolder { - public static Instance instance = new Instance(); - } - private Instance() {} - public static Instance getInstance() { - return InstanceHolder.instance ;  // 这里将导致InstanceHolder类被初始化 - } -} -``` -它与饿汉模式一样,也是利用了类初始化机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。 - -![image-20200630092741672](../img/singleton-class-init.png) - -基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。 - -[参考:单例模式](https://blog.csdn.net/goodlixueyong/article/details/51935526) - - - -## 装饰模式 -装饰模式以对客户端透明的方式拓展对象的功能,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不创造更多子类的情况下,将对象的功能加以扩展。 - -``` -InputStream in = new LowerCaseInputStream( - new BufferedInputStream( - new FileInputStream("test.txt"))); -``` -设置FileInputStream,先用BufferedInputStream装饰它,再用自己写的LowerCaseInputStream过滤器去装饰它。 - -在装饰模式中的角色有: -抽象组件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。 -具体组件(ConcreteComponent)角色:定义一个将要接收附加责任的类。 -装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。 -具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。 -![在这里插入图片描述](https://img-blog.csdn.net/20180925095631732?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -## 适配器模式 -将现成的对象通过适配变成我们需要的接口。 -适配器模式有类的适配器模式和对象的适配器模式两种不同的形式。 - -对象适配器模式通过组合对象进行适配。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMC8xOS8xNjY4YWM5YTA2NzUzMjQ0?x-oss-process=image/format,png) - -类适配器通过继承来完成适配。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMC8xOS8xNjY4YWM5YTA2NTEyYjBj?x-oss-process=image/format,png) - -适配器模式的优点 - -1. 更好的复用性。系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。 -2. 更好的扩展性。在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。 - -## 观察者模式 -多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己。其作用是让主题对象和观察者松耦合。 - -观察者模式所涉及的角色有: -抽象主题(Subject)角色:抽象主题角色把所有对观察者对象的引用保存在一个聚集(比如ArrayList对象)里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象,抽象主题角色又叫做抽象被观察者(Observable)角色。 -具体主题(ConcreteSubject)角色:将有关状态存入具体观察者对象;在具体主题的内部状态改变时,给所有登记过的观察者发出通知。具体主题角色又叫做具体被观察者(Concrete Observable)角色。 -抽象观察者(Observer)角色:为所有的具体观察者定义一个接口,在得到主题的通知时更新自己,这个接口叫做更新接口。 -具体观察者(ConcreteObserver)角色:存储与主题的状态自恰的状态。具体观察者角色实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态相协调。如果需要,具体观察者角色可以保持一个指向具体主题对象的引用。 -Observer接口 Observable类 -被观察者类都是java.util.Observable类的子类。 -![在这里插入图片描述](https://img-blog.csdn.net/20180925095713437?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) -[观察者模式](https://www.cnblogs.com/luohanguo/p/7825656.html) - - - -## 代理模式 - -代理模式使用代理对象完成用户请求,屏蔽用户对真实对象的访问。 - -### 静态代理 - -静态代理:代理类在编译阶段生成,程序运行前就已经存在,在编译阶段将通知织入Java字节码中。 - -缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。同时,一旦接口增加方法,目标对象与代理对象都要维护。 - -### 动态代理 - -动态代理:代理类在程序运行时创建,在内存中临时生成一个代理对象,在运行期间对业务方法进行增强。 - -#### JDK动态代理 - -JDK实现代理只需要使用newProxyInstance方法: - -```java -static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h ) -``` - -ClassLoader loader:指定当前目标对象使用的类加载器 -Class[] interfaces:目标对象实现的接口的类型 -InvocationHandler h:当代理对象调用目标对象的方法时,会触发事件处理器的invoke方法() - -``` -public class DynamicProxyDemo { - - public static void main(String[] args) { - //被代理的对象 - MySubject realSubject = new RealSubject(); - - //调用处理器 - MyInvacationHandler handler = new MyInvacationHandler(realSubject); - - MySubject subject = (MySubject) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), - realSubject.getClass().getInterfaces(), handler); - - System.out.println(subject.getClass().getName()); - subject.rent(); - } -} - -interface MySubject { - public void rent(); -} -class RealSubject implements MySubject { - - @Override - public void rent() { - System.out.println("rent my house"); - } -} -class MyInvacationHandler implements InvocationHandler { - - private Object subject; - - public MyInvacationHandler(Object subject) { - this.subject = subject; - } - - @Override - public Object invoke(Object object, Method method, Object[] args) throws Throwable { - System.out.println("before renting house"); - //invoke方法会拦截代理对象的方法调用 - Object o = method.invoke(subject, args); - System.out.println("after rentint house"); - return o; - } -} -``` - - - -## 工厂模式 -工厂模式是用来封装对象的创建。 - -### 简单工厂模式 - -只有一个工厂类。适用于需要创建的对象较少的场景。 - -```java - public class ShapeFactory { - public static final String TAG = "ShapeFactory"; - public static Shape getShape(String type) { - Shape shape = null; - if (type.equalsIgnoreCase("circle")) { - shape = new CircleShape(); - } else if (type.equalsIgnoreCase("rect")) { - shape = new RectShape(); - } else if (type.equalsIgnoreCase("triangle")) { - shape = new TriangleShape(); - } - return shape; - } - } -``` - -优点:只需要一个工厂创建对象,代码量少。 - -缺点:系统扩展困难,新增产品需要修改工厂逻辑,当产品较多时,会造成工厂逻辑过于复杂,不利于系统扩展和维护。 - -### 工厂方法模式 - -针对不同的对象提供不同的工厂。每个对象都有一个与之对应的工厂。 - -```java -public interface Reader { - void read(); -} - -public class JpgReader implements Reader { - @Override - public void read() { - System.out.print("read jpg"); - } -} - -public class PngReader implements Reader { - @Override - public void read() { - System.out.print("read png"); - } -} - -public interface ReaderFactory { - Reader getReader(); -} - -public class JpgReaderFactory implements ReaderFactory { - @Override - public Reader getReader() { - return new JpgReader(); - } -} - -public class PngReaderFactory implements ReaderFactory { - @Override - public Reader getReader() { - return new PngReader(); - } -} -``` - -客户端通过子类来指定创建对应的对象。 - -```java -ReaderFactory factory=new JpgReaderFactory(); -Reader reader=factory.getReader(); -reader.read(); -``` - -优点:增加新的产品类时无须修改现有系统,只需增加新产品和对应的工厂类即可。 - -### 抽象工厂模式 - -多了一层抽象,减少了工厂的数量(HpMouseFactory和HpKeyboFactory合并为HpFactory)。 -![在这里插入图片描述](https://img-blog.csdn.net/2018092509574846?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - - - -## 模板模式 - -在抽象类中定义一个操作中算法的骨架,子类按照需要重写方法实现。 - -```java -public abstract class DodishTemplate { - //模板 - protected void dodish(){ - this.preparation(); - this.doing(); - this.carriedDishes(); - } - - public abstract void preparation(); - - public abstract void doing(); - - public abstract void carriedDishes (); -} -``` - - - -## 建造者模式 - -传统建造者模式: - -```java -public class Computer { - private final String cpu;//必须 - private final String ram;//必须 - private final int usbCount;//可选 - private final String keyboard;//可选 - private final String display;//可选 - - private Computer(Builder builder){ - this.cpu=builder.cpu; - this.ram=builder.ram; - this.usbCount=builder.usbCount; - this.keyboard=builder.keyboard; - this.display=builder.display; - } - public static class Builder{ - private String cpu;//必须 - private String ram;//必须 - private int usbCount;//可选 - private String keyboard;//可选 - private String display;//可选 - - public Builder(String cup,String ram){ - this.cpu=cup; - this.ram=ram; - } - public Builder setDisplay(String display) { - this.display = display; - return this; - } - //set... - public Computer build(){ - return new Computer(this); - } - } -} - -public class ComputerDirector { - public void makeComputer(ComputerBuilder builder){ - builder.setUsbCount(); - builder.setDisplay(); - builder.setKeyboard(); - } -} -``` - -传统建造者模式变种,链式调用: - -```java -public class LenovoComputerBuilder extends ComputerBuilder { - private Computer computer; - public LenovoComputerBuilder(String cpu, String ram) { - computer=new Computer(cpu,ram); - } - @Override - public void setUsbCount() { - computer.setUsbCount(4); - } - //... - @Override - public Computer getComputer() { - return computer; - } -} - -Computer computer=new Computer.Builder("因特尔","三星") - .setDisplay("三星24寸") - .setKeyboard("罗技") - .setUsbCount(2) - .build(); -``` - diff --git "a/\345\210\206\345\270\203\345\274\217/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" "b/\345\210\206\345\270\203\345\274\217/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" deleted file mode 100644 index 5ecd5d1..0000000 --- "a/\345\210\206\345\270\203\345\274\217/RocketMQ\345\256\236\347\216\260RPC\347\232\204\345\216\237\347\220\206.md" +++ /dev/null @@ -1,50 +0,0 @@ -一、背景 -基于RokcetMQ可以实现异步处理、流量削锋、业务解耦,通常是依赖RocketMQ的发布订阅模式。今天分享RocketMQ的新特性,基于Request/Reply模式来实现RPC的功能。该模式是在v4.6作为RocketMQ新特性引入,但在在v4.7.1上才比较完善。 - -二、设计思路 -从整个数据流的角度上来说,发布/订阅模式中生产者和消费者之间是异步处理的,生产者负责把消息投递到RocketMQ上,而消费者负责处理数据,如果把生产者当做上游系统,消费者是下游系统,那么上下游系统之间是没有任何的状态交流的。而我们知道,RPC上下游系统之间是需要状态交互的,简单来说,要想实现RPC功能,在整个数据链路上,原先上下游系统之间是异步交互的模式,首先需要把异步模式换成同步模式。 - -异步模式: - -把异步模式换成同步模式,需要在生产者发送消息到MQ之后,保持原有的状态,比如可以用一个Map集合去统一维护,等到消费者处理完数据返回响应后,再从Map集合中拿到对应的请求进行处理。其中涉及到怎么去标识一个请求,这里可以用UUID或者雪花id去标记。 - -同步模式: - -RocketMQ整体的处理思路跟上面是类似的,DefaultMQProducerImpl#request负责RPC消息的下发,而DefaultMQPushConsumer中负责消息的消费。具体用法可以看RocketMQ源码example中的RPC部分。 - - -三、结构定义 -RocketMQ中是依赖于Message的Properties来区分不同的请求,在调用DefaultMQProducerImpl#request进行消息下发之间会先给消息设置不同的属性,通过属性来保证上下游之间的处理是同一个请求。 - -设置的属性有: - -CORRELATION_ID:消息的标识Id,这里对应是一个UUID -REPLY_TO_CLIENT:消息下发的客户端Id -TTL:消息下发的超时时间,单位ms - -其实就类似于HTTP请求中的头部内容一样。 - -之后还会校验一下消息中对应Topic的一个合法性。 - -四、消息下发 -RocketMQ将下发的客户端封装成RequestResponseFuture,包含客户端Id,请求超时时间,同时根据客户端Id维护在ConcurrentHashMap,调用DefaultMQProducerImpl#sendDefaultImpl下发消息,根据下发消息的回调函数确认消息下发的状态。 - -消息下发后会调用waitResponse,waitResponse调用CountDownLatch进入阻塞状态,等待消息消费之后的响应。 - -CountDownLatch中的计数器是1,说明每个请求都会独立隔离阻塞。 - - -五、消息响应 -当服务端(消费者)收到消息处理完返回响应时,会调用ReplyMessageProcessor#pushReplyMessage封装响应的内容,处理响应的头部信息和返回body的参数,最终封装成一个PUSH_REPLY_MESSAGE_TO_CLIENT的请求命令发给客户端。 - -客户端(生产者)收到请求后,会调用ClientRemotingProcessor#processRequest,判断是PUSH_REPLY_MESSAGE_TO_CLIENT命令会调用receiveReplyMessage,将接收到的数据封装成新的消息,接着调用响应处理的处理器。 - -ClientRemotingProcessor#processReplyMessage中主要做的从消息中获取消息的Id,从ConcurrentHashMap中定位到具体的请求,将返回消息封装到RequestResponseFuture中,同时CountDownLatch的计数值减1,此时线程阻塞状态被释放,之后便将消息响应给到客户端。 - -六、总结 -所以整体上看来,RocketMQ的Request/Reply模式,其实是利用客户端线程阻塞来换取请求异步变同步以及RocketMQ的回调机制从而间接的实现了RPC效果,但是相比直接RPC调用,数据的链路更长,性能肯定是会有损耗,但是请求会持久化,所以给了重复下发提供了可能。 - - - -版权声明:本文为CSDN博主「林风自在」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 -原文链接:https://blog.csdn.net/lveex/article/details/122514893 \ No newline at end of file diff --git "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" "b/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" deleted file mode 100644 index 05ce1f0..0000000 --- "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" +++ /dev/null @@ -1,77 +0,0 @@ -分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务的特性。 - -## CAP理论 - -Consistency(一致性):数据一致更新,所有数据变动都是同步的(强一致性)。 - -Availability(可用性):好的响应性能 - -Partition tolerance(分区容错性) :可靠性 - -定理:任何分布式系统只可同时满足二点,没法三者兼顾。 - -CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。 - -CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。 - -AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。 - -## BASE理论 - -Basically Available基本可用:指分布式系统在出现不可预知的故障的时候,允许损失部分可用性——但不是系统不可用。 - -响应时间上的损失:假如正常一个在线搜索0.5秒之内返回,但由于故障(机房断电或网络不通),查询结果的响应时间增加到1—2秒。功能上的损失:如果流量激增或者一个请求需要多个服务间配合,而此时有的服务发生了故障,这时需要进行服务降级,进而保护系统稳定性。 - -Soft state软状态:允许系统在不同节点的数据副本之间进行数据同步的过程存在延迟。Eventually consistent最终一致:最终数据是一致的就可以了,而不是时时高一致。 - -BASE思想主要强调基本的可用性,如果你需要High 可用性,也就是纯粹的高性能,那么就要以一致性或容错性为牺牲。 - -## 实现方案 - -分布式事务的实现主要有以下 5 种方案: - -- XA 方案 -- TCC 方案 -- 可靠消息最终一致性方案 -- 最大努力通知方案 - -## 2PC/XA方案 - -所谓的 XA 方案,即:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。 - -这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。 - -一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的。如果你要操作别人的服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。 - -## TCC强一致性方案 - -TCC 的全称是:`Try`、`Confirm`、`Cancel`。 - -- **Try 阶段**:这个阶段说的是对各个服务的资源做检测以及对资源进行 **锁定或者预留**。 -- **Confirm 阶段**:这个阶段说的是在各个服务中执行实际的操作。 -- **Cancel 阶段**:如果任何一个服务的业务方法执行出错,那么这里就需要 **进行补偿**,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚) -- - -这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个 **事务回滚实际上是严重依赖于你自己写代码来回滚和补偿** 了,会造成补偿代码巨大,非常之恶心。 - - - -## 可靠消息最终一致性方案 - -基于 MQ 来实现事务。比如阿里的 RocketMQ 就支持消息事务。大概的意思就是: - -1. A 系统先发送一个 prepared 消息到 MQ,如果这个 prepared 消息发送失败那么就直接取消操作别执行了; -2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 MQ 发送确认消息,如果失败就告诉 MQ 回滚消息; -3. 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务; -4. mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。 -5. 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。 - -这个还是比较合适的,目前国内互联网公司大都是这么玩儿的,要不你举用 RocketMQ 支持的,要不你就自己基于类似 ActiveMQ?RabbitMQ?自己封装一套类似的逻辑出来,总之思路就是这样子的。 - -## 最大努力通知方案 - -这个方案的大致意思就是: - -1. 系统 A 本地事务执行完之后,发送个消息到 MQ; -2. 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口; -3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。 \ No newline at end of file diff --git "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" "b/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" deleted file mode 100644 index d19654d..0000000 --- "a/\345\210\206\345\270\203\345\274\217/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" +++ /dev/null @@ -1,98 +0,0 @@ -高并发环境下,例如典型的淘宝双11秒杀,几分钟内上亿的用户涌入淘宝,这个时候如果访问不加拦截,让大量的读写请求涌向数据库,由于磁盘的处理速度与内存显然不在一个量级,服务器马上就要宕机。**从减轻数据库的压力和提高系统响应速度两个角度来考虑,都会在数据库之前加一层缓存**,访问压力越大的,在缓存之前就开始 CDN 拦截图片等访问请求。 - -并且由于最早的单台机器的内存资源以及承载能力有限,如果大量使用本地缓存,也会使相同的数据被不同的节点存储多份,对内存资源造成较大的浪费,因此,才催生出了分布式缓存。 - -## 应用场景 - -1. **页面缓存**:用来缓存Web 页面的内容片段,包括HTML、CSS 和图片等; -2. **应用对象缓存**:缓存系统作为ORM 框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问;解决分布式Web部署的 session 同步问题,状态缓存.缓存包括Session 会话状态及应用横向扩展时的状态数据等,这类数据一般是难以恢复的,对可用性要求较高,多应用于高可用集群。 -3. **并行处理**:通常涉及大量中间计算结果需要共享; -4. **云计算领域提供分布式缓存服务** - -## 缓存常见的问题 - -### 缓存穿透 - -缓存穿透是指查询一个**不存在的数据**,由于缓存是不命中时被动写的,如果从DB查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。 - -1. **缓存空值**,不会查数据库。 -2. 采用**布隆过滤器**,将所有可能存在的数据哈希到一个足够大的`bitmap`中,查询不存在的数据会被这个`bitmap`拦截掉,从而避免了对`DB`的查询压力。 - -布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。 - -### 缓存雪崩 - -缓存雪崩是指在我们设置缓存时采用了相同的过期时间,**导致缓存在某一时刻同时失效**,请求全部转发到DB,DB瞬时压力过重挂掉。 - -解决方法:在原有的失效时间基础上**增加一个随机值**,使得过期时间分散一些。 - -### 缓存击穿 - -缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。**缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。** - -解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。 - -```java -public String get(String key) { - String value = redis.get(key); - if (value == null) { //缓存值过期 - String unique_key = systemId + ":" + key; - //设置30s的超时 - if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) { //设置成功 - value = db.get(key); - redis.set(key, value, expire_secs); - redis.del(unique_key); - } else { //其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值 - sleep(50); - get(key); //重试 - } - } else { - return value; - } -} -``` - -### 缓存预热 - -缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! - -解决方案: - -1. 直接写个缓存刷新页面,上线时手工操作一下; -2. 数据量不大,可以在项目启动的时候自动进行加载; -3. 定时刷新缓存; - -### 缓存降级 - -当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 - -缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 - -在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案: - -1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; -2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; -3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; -4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。 - -服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。 - -### 如何保证缓存与数据库双写时的数据一致性? - -**1、先删除缓存再更新数据库** - -进行更新操作时,先删除缓存,然后更新数据库,后续的请求再次读取时,会从数据库读取后再将新数据更新到缓存。 - -存在的问题:删除缓存数据之后,更新数据库完成之前,这个时间段内如果有新的读请求过来,就会从数据库读取旧数据重新写到缓存中,再次造成不一致,并且后续读的都是旧数据。 - -**2、先更新数据库再删除缓存** - -进行更新操作时,先更新MySQL,成功之后,删除缓存,后续读取请求时再将新数据回写缓存。 - -存在的问题:更新MySQL和删除缓存这段时间内,请求读取的还是缓存的旧数据,不过等数据库更新完成,就会恢复一致,影响相对比较小。 - -**3、异步更新缓存** - -数据库的更新操作完成后不直接操作缓存,而是把这个操作命令封装成消息扔到消息队列中,然后由Redis自己去消费更新数据,消息队列可以保证数据操作顺序一致性,确保缓存系统的数据正常。 - -以上几个方案都不完美,需要根据业务需求,评估哪种方案影响较小,然后选择相应的方案。 \ No newline at end of file diff --git "a/\345\211\215\347\253\257/vue.md" "b/\345\211\215\347\253\257/vue.md" deleted file mode 100644 index 74605b7..0000000 --- "a/\345\211\215\347\253\257/vue.md" +++ /dev/null @@ -1,401 +0,0 @@ - - - -- [基础](#%E5%9F%BA%E7%A1%80) - - [v-modal](#v-modal) - - [computed和method](#computed%E5%92%8Cmethod) -- [组件](#%E7%BB%84%E4%BB%B6) -- [深入组件](#%E6%B7%B1%E5%85%A5%E7%BB%84%E4%BB%B6) - - [prop](#prop) -- [vue-router](#vue-router) - - [动态匹配](#%E5%8A%A8%E6%80%81%E5%8C%B9%E9%85%8D) - - [导航](#%E5%AF%BC%E8%88%AA) - - [命名路由](#%E5%91%BD%E5%90%8D%E8%B7%AF%E7%94%B1) - - [命名视图](#%E5%91%BD%E5%90%8D%E8%A7%86%E5%9B%BE) - - [嵌套命名视图](#%E5%B5%8C%E5%A5%97%E5%91%BD%E5%90%8D%E8%A7%86%E5%9B%BE) - - [重定向](#%E9%87%8D%E5%AE%9A%E5%90%91) - - - -## 基础 - -### v-modal - -```vue - -

Message is: {{ message }}

-``` - -等价于: - -```vue - -``` - -在组件上使用: - -```vue -Vue.component('custom-input', { - props: ['value'], - template: ` - - ` -}) - - -``` - - - -### computed和method - -```vue -
-

message:${ message }

-

now (computed):${ now }

-

getNow (method):${ getNow() }

-
- -var vm = new Vue({ - el: '#app', - delimiters: ['${', '}'], - data: { - message: 'Hello World!', - }, - computed: { - now: function() { - return Date.now(); - }, - }, - methods: { - getNow: function() { - return Date.now(); - }, - }, -}); - -//改变了message,computed才会更新,即computed依赖于属性 -//getNow()方法每次调用都会更新 -vm.message = '999'; -``` - - - - - -## 组件 - -```vue -// 定义一个名为 button-counter 的新组件 -Vue.component('button-counter', { - data: function () { - return { - count: 0 - } - }, - template: '' -}) - -
- -
- -new Vue({ el: '#components-demo' }) -``` - -data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。 - -通过 Prop 向子组件传递数据,一个组件默认可以拥有任意数量的 prop。 - -```vue -Vue.component('blog-post', { - props: ['title'], - template: '

{{ title }}

' -}) - - - - -``` - -使用有约束条件的元素,table 内部只能有 tr: - -```vue - - -
-``` - -这个自定义组件 `` 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 `is` attribute 给了我们一个变通的办法: - -```vue - - -
-``` - - - -## 深入组件 - -全局注册,注册之后可以用在任何新创建的 Vue 根实例 (`new Vue`) 的模板中: - -```vue -Vue.component('component-a', { /* ... */ }) -Vue.component('component-b', { /* ... */ }) -Vue.component('component-c', { /* ... */ }) - -new Vue({ el: '#app' }) ---------------------- -
- - - -
-``` - -局部注册: - -```vue -var ComponentA = { /* ... */ } -var ComponentB = { /* ... */ } -var ComponentC = { /* ... */ } - -new Vue({ - el: '#app', - components: { - 'component-a': ComponentA, - 'component-b': ComponentB - } -}) -``` - -局部注册的组件在其子组件中不可用,如果要在 ComponentB 使用ComponentA,则应该这样写: - -```vue -var ComponentA = { /* ... */ } - -var ComponentB = { - components: { - 'component-a': ComponentA - }, - // ... -} - -//过 Babel 和 webpack 使用 ES2015 模块 -import ComponentA from './ComponentA.vue' - -export default { - components: { - ComponentA - }, - // ... -} -``` - -### prop - -使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名: - -```vue -Vue.component('blog-post', { - // 在 JavaScript 中是 camelCase 的 - props: ['postTitle'], - template: '

{{ postTitle }}

' -}) - - - -``` - -给 props 传入静态值: - -```vue -props: { - title: String, - likes: Number, - isPublished: Boolean, - commentIds: Array, - author: Object, - callback: Function, - contactsPromise: Promise // or any other constructor -} - - -``` - -可以通过 `v-bind` 动态赋值: - -```vue - - - - - -``` - - - - - -## vue-router - -### 动态匹配 - -```vue -const User = { - template: '
User
' -} - -const router = new VueRouter({ - routes: [ - // dynamic segments start with a colon - { path: '/user/:id', component: User } - ] -}) -``` - -/user/tyson 和 /user/tom 都会映射到同一个组件。路由匹配之后,每次组件都可以获取到 `this.$route.params`。 - -```vue -const User = { - template: '
User {{ $route.params.id }}
' -} -``` - -在一个路由中设置多段“路径参数”,对应的值都会设置到 `$route.params` 中。 - -| pattern | matched path | $route.params | -| ----------------------------- | ------------------- | -------------------------------------- | -| /user/:username | /user/evan | `{ username: 'evan' }` | -| /user/:username/post/:post_id | /user/evan/post/123 | `{ username: 'evan', post_id: '123' }` | - -### 导航 - -```js -// 字符串 -router.push('home') - -// 对象 -router.push({ path: 'home' }) - -// 命名的路由 -router.push({ name: 'user', params: { userId: '123' }}) - -// 带查询参数,变成 /register?plan=private -router.push({ path: 'register', query: { plan: 'private' }}) -``` - - history 记录中向前或者后退多少步: - -```vue -// 在浏览器记录中前进一步,等同于 history.forward() -router.go(1) - -// 后退一步记录,等同于 history.back() -router.go(-1) - -// 前进 3 步记录 -router.go(3) - -// 如果 history 记录不够用,那就默默地失败呗 -router.go(-100) -router.go(100) -``` - -### 命名路由 - -有时候,通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。 - -```vue -const router = new VueRouter({ - routes: [ - { - path: '/user/:userId', - name: 'user', - component: User - } - ] -}) -``` - -要链接到一个命名路由,可以给 `router-link` 的 `to` 属性传一个对象: - -```vue -User -``` - -等同于: - -```vue -router.push({ name: 'user', params: { userId: 123 }}) -``` - -### 命名视图 - -有时候想同时 (同级) 展示多个视图,可以使用命名视图。 - -```vue - - - -``` - -如果 `router-view` 没有设置名字,那么默认为 `default`。 - -多个视图需要使用多个组件渲染,使用 components 配置: - -```vue -const router = new VueRouter({ - routes: [ - { - path: '/', - components: { - default: Foo, - a: Bar, - b: Baz - } - } - ] -}) -``` - -### 嵌套命名视图 - -https://jsfiddle.net/posva/22wgksa3/ - -### 重定向 - -重定向也是通过 `routes` 配置来完成。 - -```vue -const router = new VueRouter({ - routes: [ - { path: '/a', redirect: '/b' } - ] -}) - -const router = new VueRouter({ - routes: [ - { path: '/a', redirect: { name: 'foo' }} //重定向目标可以是命名路由 - ] -}) -``` - -别名: - -```vue -const router = new VueRouter({ - routes: [ - { path: '/a', component: A, alias: '/b' } //访问路径是/b,路由匹配为/a - ] -}) -``` \ No newline at end of file diff --git "a/\345\267\245\345\205\267/GitHub\346\214\207\345\215\227.md" "b/\345\267\245\345\205\267/GitHub\346\214\207\345\215\227.md" deleted file mode 100644 index d78291f..0000000 --- "a/\345\267\245\345\205\267/GitHub\346\214\207\345\215\227.md" +++ /dev/null @@ -1,91 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [GitHub 的使用](#github-%E7%9A%84%E4%BD%BF%E7%94%A8) -- [GitHub 搜索技巧](#github-%E6%90%9C%E7%B4%A2%E6%8A%80%E5%B7%A7) -- [使用GitHub可以做什么](#%E4%BD%BF%E7%94%A8github%E5%8F%AF%E4%BB%A5%E5%81%9A%E4%BB%80%E4%B9%88) - - - -作为全球最大的~~代码托管~~同性交友平台,GitHub上面有着丰富的学习资源。 - -要使用GitHub,就要先学会使用Git。 - -Git又是什么呢?简单的说,**Git 是一个管理你的代码历史记录的工具。** - -# GitHub 的使用 - -首先看下怎么使用GitHub。 - -- 注册一个GitHub账号,这样比较简单,不详细展开讲了。 - -- 创建一个新的仓库,用来存放项目。 - - ![](http://img.dabin-coder.cn/image/image-20210822154700616.png) - -- 或者你在GitHub上看到别人有一个超级无敌有趣的项目,可以直接fork过来,可以理解成复制过来,变成你自己的。之后你想怎么改就怎么改! - - ![image-20210822155107839](http://img.dabin-coder.cn/image/image-20210822155107839.png) - -- 然后你可以通过Git命令行`git clone xxx`把项目clone到本地,在本地进行创作。 - - ![](http://img.dabin-coder.cn/image/image-20210822155359118.png) - -- 最后,在本地创作完成,可以使用`git commit -m xxx`提交到本地库,然后使用`git push`把修改推送到GitHub仓库。之后就可以在GitHub上面看到你修改的内容啦~ - -以上就是GitHub的基本使用方法,有一些细节没有写出来,但是关键的步骤都列举出来了~ - -# GitHub 搜索技巧 - -接下来说一下GitHub的搜索技巧,非常重要! - -- 评价GitHub项目的两个重要的参数:star和fork。 - -![](http://img.dabin-coder.cn/image/image-20210822161003170.png) - -比较优秀和热门的项目,star数目和fork数目都会比较多。我们可以根据这两个参数筛选出比较好的项目。使用`关键字 stars:>=xxx forks:>=xxx` 可以筛选出star和fork数目大于xxx的相关项目。 - -![](http://img.dabin-coder.cn/image/image-20210822161122586.png) - -- 使用 `awesome 关键字`,可以筛选出比较高质量的学习、书籍、工具类或者插件类的集合。 - -![](http://img.dabin-coder.cn/image/image-20210822161608599.png) - -- 在特定位置搜索关键词。有些关键词出现在项目的不同位置,比如项目名称、项目描述和README等。使用`关键词 in name/description/Readme`,可以搜索到相关的内容。比如使用`spring in name`,可以搜索到在项目名中包含spring的项目。 - -![image-20210822162144086](http://img.dabin-coder.cn/image/image-20210822162144086.png) - -- 指定条件搜索关键词。如`tool language:java`搜索到的是包含关键字tool的Java项目,`tool followers:>1000`可以搜索到包含关键字tool,且follower数量大于1000的项目。 - -![](http://img.dabin-coder.cn/image/image-20210822163111390.png) - - - -# 使用GitHub可以做什么 - -- 托管代码。GitHub 上记录这代码的修改历史,必要时可以进行回退; - -- 搜索牛逼的开源项目,参与开源项目开发。这里分享下自己的一个开源仓库,用于分享Java核心知识,包括Java基础、MySQL、SpringBoot、Mybatis、Redis、RabbitMQ等等,面试必备。 - - https://github.com/Tyson0314/Java-learning - -- 文档神器。可以为自己的项目建立wiki,可以用markdown语法写wiki; - -![](http://img.dabin-coder.cn/image/image-20210822172419760.png) - -- 使用GitHub pages建立个人静态网站,搞一个有自己域名的独立博客,想想都觉得开心。使用GitHub pages的好处是搭建简单而且免费,支持静态脚本,并且可以绑定自己的域名。具体可以参考:[GitHub Pages 建立个人网站详细教程 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/58229299) - -- 写书写文章,团队协作分工。 - - - -最后给大家分享一个GitHub仓库,上面放了200多本经典的计算机书籍,包括数据库、操作系统、计算机网络、数据结构和算法等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ - -GitHub地址:https://github.com/Tyson0314/java-books - -如果github访问不了,可以访问gitee仓库。 - -gitee地址:https://gitee.com/tysondai/java-books - - diff --git "a/\345\267\245\345\205\267/NPM.md" "b/\345\267\245\345\205\267/NPM.md" deleted file mode 100644 index f3f6108..0000000 --- "a/\345\267\245\345\205\267/NPM.md" +++ /dev/null @@ -1,48 +0,0 @@ - - - -- [安装](#%E5%AE%89%E8%A3%85) -- [卸载](#%E5%8D%B8%E8%BD%BD) -- [更新](#%E6%9B%B4%E6%96%B0) -- [搜索](#%E6%90%9C%E7%B4%A2) -- [创建模块](#%E5%88%9B%E5%BB%BA%E6%A8%A1%E5%9D%97) - - - -NPM 是随同NodeJS一起安装的包管理工具。 - -测试是否安装 npm:`npm -v` - -升级 npm 版本:`npm install npm -g` - -## 安装 - -本地安装:`npm install express` 安装包放在工程目录下的 ./node_modules,在代码中通过 require() 来引入本地安装的包。 - -全局安装:`npm install exprexss -g` 安装包放在 /usr/local 下或者你 node 的安装目录,可以直接在命令行里使用。 - -使用淘宝镜像的命令:`npm install -g cnpm --registry.npm.taobao.org`,接下来就可以使用 cnpm 命令来安装模块了:`cnpm install express` - -查看安装信息:`npm list -g` - -查看某个模块的版本号:`npm list grunt` - -## 卸载 - -`npm uninstall express` - -## 更新 - -`npm update express` - -## 搜索 - -`npm search express` - -## 创建模块 - -创建模块,package.json 文件是必不可少的:`npm init` - -注册用户:`npm adduser` - -发布模块:`npm publish` diff --git "a/\345\267\245\345\205\267/jenkins.md" "b/\345\267\245\345\205\267/jenkins.md" deleted file mode 100644 index 4f88a52..0000000 --- "a/\345\267\245\345\205\267/jenkins.md" +++ /dev/null @@ -1,120 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [简介](#%E7%AE%80%E4%BB%8B) -- [自动化部署](#%E8%87%AA%E5%8A%A8%E5%8C%96%E9%83%A8%E7%BD%B2) - - [安装jenkis镜像](#%E5%AE%89%E8%A3%85jenkis%E9%95%9C%E5%83%8F) - - [Jenkins配置](#jenkins%E9%85%8D%E7%BD%AE) - - [运行任务](#%E8%BF%90%E8%A1%8C%E4%BB%BB%E5%8A%A1) -- [问题记录](#%E9%97%AE%E9%A2%98%E8%AE%B0%E5%BD%95) - - [无法进入登录页面](#%E6%97%A0%E6%B3%95%E8%BF%9B%E5%85%A5%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2) - - [Cannot link to /mysql](#cannot-link-to-mysql) - - - -## 简介 - -Jenkins 提供超过1000个插件来支持构建、部署、自动化,满足任何项目的需要。我们可以用Jenkins来构建和部署我们的项目,比如说从我们的代码仓库获取代码,然后将代码打包成可执行的文件,通过远程的ssh工具执行脚本来运行我们的项目。 - - - -## 自动化部署 - -将生成docker镜像和运行docker容器的操作进行整合,实现[一键部署](https://mp.weixin.qq.com/s/tQqvgSc9cHBtnqRQSbI4aw)。 - -1. 基础环境搭建,安装mysql镜像等; -2. 服务器安装jenkins并启动; -3. 配置jenkins,添加代码仓路径、maven配置、git配置、ssh配置和启动docker容器的脚本(镜像打包完成后运行); -4. 在任务列表点击运行,一键部署。 - -### 安装jenkis镜像 - -下载jenkins镜像: - -``` -docker pull jenkins/jenkins:lts -``` - -在Docker容器中运行Jenkins: - -``` -docker run -p 80:8080 -p 50000:5000 --name jenkins \ --u root \ --v /mydata/jenkins_home:/var/jenkins_home \ --d jenkins/jenkins:lts -``` - -### Jenkins配置 - -访问http://192.168.6.132:8080/,登录Jenkins。 - -准备用于启动docker容器的脚本,上传到服务器。注意使用`--network network_name`指定与依赖服务(如mysql)在同一个网络下,不然会导致Cannot link to /mysql的异常。 - -``` -#!/usr/bin/env bash -app_name='mall-tiny-jenkins' -docker stop ${app_name} -echo'----stop container----' -docker rm ${app_name} -echo'----rm container----' -docker run -p 8088:8088 --name ${app_name} \ ---link mysql:db \ # 不同容器之间可以通过--link指定的服务别名互相访问,containName:alias ---network yml_default \ # 注意要与其他依赖的服务在同一个网络下,不然会导致Cannot link to /mysql的异常 --v /etc/localtime:/etc/localtime \ --v /mydata/app/${app_name}/logs:/var/logs \ --d mall-tiny/${app_name}:1.0-SNAPSHOT -echo'----start container----' -``` - -在jentins界面新建任务,配置maven、git、代码仓库、脚本路径等。 - -### 运行任务 - -可以在控制台界面查看任务执行的信息。 - - - -## 问题记录 - -### 无法进入登录页面 - -可能是机器网络配置改变,需要重启docker(systemctl restart docker),才能正常访问jenkins登录页面。 - -### Cannot link to /mysql - -mall-tiny-jenkins容器与mysql容器不在同一个网络下。应该在运行mall-tiny-jenkins的脚本中添加--network参数: - -``` -docker run -d --name mall-tiny-jenkins -p 8088:8088 --net yml_default mall-tiny-jenkins -``` - -查看mysql容器的networks: - -``` -docker inspect mysql -``` - -返回的networks信息: - -```yaml -"Networks": { - "yml_default": { - "IPAMConfig": null, - "Links": null, - "Aliases": [ - "mysql", - "3333d057ad33" - ], - "NetworkID": "0621fa711575b12f81015e7763733ac1db29c65d7abaf11d0b5da0484f5f70ea", - ... - } -} -``` - -查看所有容器的networks信息: - -``` -docker network ls -``` - diff --git "a/\345\267\245\345\205\267/jmeter.md" "b/\345\267\245\345\205\267/jmeter.md" deleted file mode 100644 index fe0f915..0000000 --- "a/\345\267\245\345\205\267/jmeter.md" +++ /dev/null @@ -1,39 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [线程组配置](#%E7%BA%BF%E7%A8%8B%E7%BB%84%E9%85%8D%E7%BD%AE) -- [Request 默认配置](#request-%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE) -- [HTTP头配置](#http%E5%A4%B4%E9%85%8D%E7%BD%AE) -- [聚合报告](#%E8%81%9A%E5%90%88%E6%8A%A5%E5%91%8A) -- [HTTP请求参数](#http%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0) - - - -## 线程组配置 - -![image-20201011235317624](../img/jmeter/jmeter-thread-group.png) - - - -## Request 默认配置 - -![image-20201011234642519](../img/jmeter/jmeter-default-request.png) - - - -## HTTP头配置 - -![image-20201011234827310](../img/jmeter/jmeter-request-header.png) - - - -## 聚合报告 - -![image-20201012000129480](../img/jmeter/jmeter-aggregate-report.png) - - - -## HTTP请求参数 - -![image-20201014232942952](../img/jmeter/http-request-param.png) \ No newline at end of file diff --git "a/\345\267\245\345\205\267/nginx\351\203\250\347\275\262\351\241\271\347\233\256.md" "b/\345\267\245\345\205\267/nginx\351\203\250\347\275\262\351\241\271\347\233\256.md" deleted file mode 100644 index 2b9f46f..0000000 --- "a/\345\267\245\345\205\267/nginx\351\203\250\347\275\262\351\241\271\347\233\256.md" +++ /dev/null @@ -1,325 +0,0 @@ - - - - -- [简介](#%E7%AE%80%E4%BB%8B) - - [基本命令](#%E5%9F%BA%E6%9C%AC%E5%91%BD%E4%BB%A4) - - [配置文件](#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) -- [应用](#%E5%BA%94%E7%94%A8) - - [动静分离](#%E5%8A%A8%E9%9D%99%E5%88%86%E7%A6%BB) -- [后端部署](#%E5%90%8E%E7%AB%AF%E9%83%A8%E7%BD%B2) -- [前端部署](#%E5%89%8D%E7%AB%AF%E9%83%A8%E7%BD%B2) -- [多站点配置](#%E5%A4%9A%E7%AB%99%E7%82%B9%E9%85%8D%E7%BD%AE) -- [HTTPS配置](#https%E9%85%8D%E7%BD%AE) - - - -## 简介 - -[Nginx入门指南](https://juejin.im/post/5e982d4b51882573b0474c07#heading-1) - -轻量级的 HTTP 服务器。 - -### 基本命令 - -nginx安装目录:使用whereis nginx查看安装路径 - -启动nginx:安装目录/sbin/nginx - -停止nginx:安装目录/sbin/nginx -s stop - -重启nginx:安装目录/sbin/nginx -s reload - -查看nginx是否正在运行:`netstat -anput | grep nginx` - -### 配置文件 - -配置文件路径:`/usr/local/nginx/conf/nginx.conf` - -日志路径:`/usr/local/nginx/logs/` - -配置文件介绍: - -``` -server { - # 当nginx接到请求后,会匹配其配置中的service模块 - # 匹配方法就是将请求携带的host和port去跟配置中的server_name和listen相匹配 - listen 8080; - server_name localhost; # 定义当前虚拟主机(站点)匹配请求的主机名 - - location / { - root html; # Nginx默认值 - # 设定Nginx服务器返回的文档名 - index index.html index.htm; # 先找根目录下的index.html,如果没有再找index.htm - } -} -``` - -server{ } 其实是包含在 http{ } 内部的。每一个 server{ } 是一个虚拟主机(站点)。 - -## 应用 - -### 动静分离 - -![1588431199776](..\img\1588431199776.png) - -静态请求直接从 nginx 服务器所设定的根目录路径去取对应的资源,动态请求转发给后端去处理。不仅能给应用服务器减轻压力,将后台api接口服务化,还能将前后端代码分开并行开发和部署。 - -## 后端部署 - -1. 修改配置文件,如端口、主机等; - -2. 执行 package 打包项目,在 target 目录下生成 jar 包,上传到服务器; - -3. jar 包目录下运行 `./start.sh` 启动服务; - - 启动脚本 start.sh:`nohup java -jar xxx.jar --spring.profiles.active=prod &` - - 停止脚本 stop.sh: - - ```shell - PID=$(ps -ef | grep eladmin-system-2.4.jar | grep -v grep | awk '{ print $2 }') - if [ -z "$PID" ] - then - echo Application is already stopped - else - echo kill $PID - kill $PID - fi - ``` - - 新建 log 文件 nohup.out:`touch nohup.out` - - 查看日志脚本 log.sh:`tail -f nohup.out` - - 所有脚本放在同一目录下。 - -4. 运行 `./stop.sh` 停止服务 - -5. 运行 `./log.sh`查看日志 - -6. 配置 nginx。使用 nginx 代理 Java 服务。 - - ```json - #auth|api|avatar开头的请求发送到后端服务上 - location ~* ^/(auth|api|avatar|file/) { - proxy_pass http://localhost:8000; - proxy_set_header Host $http_host; - proxy_connect_timeout 150s; - proxy_send_timeout 150s; - proxy_read_timeout 150s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - ``` - - - -## 前端部署 - -1. 修改后端接口地址 `http://xxx:80`,请求会被代理到`http://127.0.0.1:8000` - -2. `npm run rebuild:prod` 打包项目生成 dist 文件夹,上传到服务器项目目录下; - -3. 配置 nginx。 - - ``` - server { - listen 80; #localhost:80请求nginx服务器时,该请求就会被匹配进该代码块的 server{ } 中执行。 - server_name localhost; - root /home/eladmin/file/dist; - index index.html; - #charset koi8-r; - - #access_log logs/host.access.log main; - - location / { - try_files $uri $uri/ @router; - index index.html; - } - - location @router { - rewrite ^.*$ /index.html last; - } - #auth|api|avatar开头的请求发送到后端服务上 - location ~* ^/(auth|api|avatar|file/) { - proxy_pass http://localhost:8000; - proxy_set_header Host $http_host; - proxy_connect_timeout 150s; - proxy_send_timeout 150s; - proxy_read_timeout 150s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - } - ``` - - - -## 多站点配置 - -nginx 配置: - -1. /etc/nginx/nginx.conf 中 http 节点增加`include /etc/nginx/conf.d/*.conf` 不同站点使用不同的配置文件。 - - ``` - http { - include mime.types; - default_type application/octet-stream; - keepalive_timeout 65; - - include /etc/nginx/conf.d/*.conf; #配置多个站点 - - server { - xxx - } - xxx - } - ``` - -2. 新建/etc/nginx/conf.d/blog.conf,配置nginx: - - ``` - #blog - server { - listen 80; - server_name localhost; - #访问vue项目 - location / { - root /home/blog/dist; - index index.html; - } - #将api转发到后端 - location /api/ { - proxy_pass http://129.204.179.3:8001/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header REMOTE-HOST $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - #转发图片请求到后端 - location /img/ { - proxy_pass http://129.204.179.3:8001/img/; - } - } - ``` - -后端部署: - -1. idea--maven--project name--lifecycle--package,打包项目,在target目录生成 jar 包,拷贝到服务器。这里存放到 /home/blog 目录下。 - -2. 导入项目 sql 文件 `source blog.sql`。 - -3. 运行 jar 包。 - - ``` - #不挂断地运行命令;&表示后台运行;输出都将附加到当前目录的 nohup.out 文件 - nohup java -jar blog-springboot-1.0.jar & - - #结束运行 - PID=$(ps -ef | grep blog-springboot-1.0.jar | grep -v grep | awk '{ print $2 }') - if [ -z "$PID" ] - then - echo Application is already stopped - else - echo kill -9 $PID - kill -9 $PID - fi - ``` - -前端部署:运行 `npm run build` 生成 dist 文件夹,将 dist 文件夹下面的文件拷贝到 /home/blog/dist 目录。 - -环境准备:启动 mysql、redis、rabbitmq 等。 - -```bash -service mysqld start -/sbin/service rabbitmq-server start -``` - -启动 nginx:`安装目录/sbin/nginx` - - - -## HTTPS配置 - -[nginx配置HTTPS](https://www.cnblogs.com/ambition26/p/14077773.html) - -1. 安装 nginx ssl模块; - -2. 申请 ssl 证书,下载证书,复制到服务器某个目录下; - -3. 修改 /usr/local/nginx/conf/nginx.conf 文件 - - ```java - server { - listen 80; - server_name localhost; - rewrite ^(.*)$ https://$host$1 permanent; #http请求会被转发到https - #访问vue项目 - location / { - root /home/blog/dist; - index index.html; - } - #将api转发到后端 - location /api/ { - proxy_pass http://129.204.179.3:8001/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header REMOTE-HOST $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - #转发图片请求到后端 - location /img/ { - proxy_pass http://129.204.179.3:8001/img/; - } - } - server { - listen 443 ssl; # 1.1版本后这样写 - server_name tysonbin.com; #填写绑定证书的域名 - ssl_certificate /usr/local/nginx/cert/1_tysonbin.com_bundle.crt; # 指定证书的位置,绝对路径 - ssl_certificate_key /usr/local/nginx/cert/2_tysonbin.com.key; # 绝对路径 - ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照这个协议配置 - ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照这个套件配置 - ssl_prefer_server_ciphers on; - location / { - root /home/blog/dist; #站点目录,绝对路径 - index index.html; - } - - #将api转发到后端 - location /api/ { - proxy_pass https://tysonbin.com:8001/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header REMOTE-HOST $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - #转发图片请求到后端 - location /img/ { - proxy_pass https://tysonbin.com:8001/img/; - } - } - ``` - - 4. 执行`/usr/local/nginx/sbin/nginx -s reload`,重启nginx。 - -springboot配置https: - -1. 复制 tomcat 目录下的 tysonbin.com.jks 到 application.yml 同级目录。 - -2. 修改 application.yml: - - ```yaml - server: - port: 8001 - ssl: - #SSL证书路径 一定要加上classpath: - key-store: classpath:tysonbin.com.jks - #SSL证书密码,在证书tomcat目录下的keystorePass.txt或者是设置的私钥密码 - key-store-password: tx.123456 - #证书类型 - key-store-type: JKS - ``` - -3. 重新生成jar包,将jar包和tysonbin.com.jks文件一起放到服务器同一个目录下。 \ No newline at end of file diff --git "a/\346\225\260\346\215\256\345\272\223/MySQL\350\277\233\351\230\266.md" "b/\346\225\260\346\215\256\345\272\223/MySQL\350\277\233\351\230\266.md" deleted file mode 100644 index d66e18c..0000000 --- "a/\346\225\260\346\215\256\345\272\223/MySQL\350\277\233\351\230\266.md" +++ /dev/null @@ -1,811 +0,0 @@ - - - - -- [事务特性](#%E4%BA%8B%E5%8A%A1%E7%89%B9%E6%80%A7) -- [事务隔离级别](#%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB) -- [索引](#%E7%B4%A2%E5%BC%95) - - [什么是索引?](#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B4%A2%E5%BC%95) - - [索引的优缺点?](#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9) - - [索引的作用](#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%9C%E7%94%A8) - - [B+ 树](#b-%E6%A0%91) - - [索引实例](#%E7%B4%A2%E5%BC%95%E5%AE%9E%E4%BE%8B) - - [索引分类](#%E7%B4%A2%E5%BC%95%E5%88%86%E7%B1%BB) - - [最左匹配](#%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D) - - [聚集索引](#%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95) - - [覆盖索引](#%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95) - - [索引失效](#%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88) -- [存储引擎](#%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E) - - [InnoDB](#innodb) - - [MyISAM](#myisam) - - [MEMORY](#memory) - - [MyISAM和InnoDB区别](#myisam%E5%92%8Cinnodb%E5%8C%BA%E5%88%AB) -- [MVCC](#mvcc) - - [实现原理](#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) - - [read view](#read-view) - - [数据访问流程](#%E6%95%B0%E6%8D%AE%E8%AE%BF%E9%97%AE%E6%B5%81%E7%A8%8B) - - [快照读和当前读](#%E5%BF%AB%E7%85%A7%E8%AF%BB%E5%92%8C%E5%BD%93%E5%89%8D%E8%AF%BB) - - [select 读取锁定](#select-%E8%AF%BB%E5%8F%96%E9%94%81%E5%AE%9A) -- [分库分表](#%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) - - [垂直划分](#%E5%9E%82%E7%9B%B4%E5%88%92%E5%88%86) - - [水平划分](#%E6%B0%B4%E5%B9%B3%E5%88%92%E5%88%86) -- [日志](#%E6%97%A5%E5%BF%97) - - [bin log](#bin-log) - - [redo log](#redo-log) - - [undo Log](#undo-log) - - [查询日志](#%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97) -- [MySQL架构](#mysql%E6%9E%B6%E6%9E%84) - - [Server 层基本组件](#server-%E5%B1%82%E5%9F%BA%E6%9C%AC%E7%BB%84%E4%BB%B6) - - [语法解析器和预处理](#%E8%AF%AD%E6%B3%95%E8%A7%A3%E6%9E%90%E5%99%A8%E5%92%8C%E9%A2%84%E5%A4%84%E7%90%86) - - [查询优化器](#%E6%9F%A5%E8%AF%A2%E4%BC%98%E5%8C%96%E5%99%A8) - - [查询执行引擎](#%E6%9F%A5%E8%AF%A2%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E) - - [查询语句执行流程](#%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B) - - [更新语句执行过程](#%E6%9B%B4%E6%96%B0%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) -- [慢查询](#%E6%85%A2%E6%9F%A5%E8%AF%A2) - - [mysqldumpslow](#mysqldumpslow) -- [分区表](#%E5%88%86%E5%8C%BA%E8%A1%A8) - - [分区表类型](#%E5%88%86%E5%8C%BA%E8%A1%A8%E7%B1%BB%E5%9E%8B) - - [分区的问题](#%E5%88%86%E5%8C%BA%E7%9A%84%E9%97%AE%E9%A2%98) - - [查询优化](#%E6%9F%A5%E8%AF%A2%E4%BC%98%E5%8C%96) -- [其他](#%E5%85%B6%E4%BB%96) - - [processlist](#processlist) - - [exist和in](#exist%E5%92%8Cin) - - - -> PS:本文已经收录到github仓库,此仓库用于分享Java核心知识,包括Java基础、MySQL、SpringBoot、Mybatis、Redis、RabbitMQ等等,面试必备。 -> -> github地址:https://github.com/Tyson0314/Java-learning -> 如果github访问不了,可以访问gitee仓库。 -> gitee地址:https://gitee.com/tysondai/Java-learning - -## 事务特性 - -事务特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。 - -- 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。 -- 一致性是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。 -- 隔离性。跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。 -- 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。 - - - -## 事务隔离级别 - -先了解下几个概念:脏读、不可重复读、幻读。 - -- 脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。 -- 不可重复读是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。 -- 幻读是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。 - -不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。 -幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除。 - -事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。 - -MySQL数据库为我们提供的四种隔离级别: - -- Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。 -- Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。 -- Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。 -- Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。 - -查看隔离级别: - -```mysql -select @@transaction_isolation; -``` - -设置隔离级别: - -```mysql -set session transaction isolation level read uncommitted; -``` - - - -## 索引 - -### 什么是索引? - -索引是存储引擎用于提高数据库表的访问速度的一种数据结构。 - -### 索引的优缺点? - -特点:1、避免进行数据库全表的扫描,大多数情况,只需要扫描较少的索引页和数据页;提升查询语句的执行效率,但降低了新增、删除操作的速度,同时也会占用额外的存储空间。 - -### 索引的作用 - -数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。 - -什么情况下需要建索引: - -1. 经常用于查询的字段 -2. 经常用于连接的字段(如外键)建立索引,可以加快连接的速度 -3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度 - -什么情况下不建索引? - -1. where条件中用不到的字段不适合建立索引 -2. 表记录较少 -3. 需要经常增删改 -4. 参与列计算的列不适合建索引 -5. 区分度不高的字段不适合建立索引,性别等 - -### B+ 树 - -B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。 - -在 B+ 树中,节点中的 key 从左到右递增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 - -![](http://img.dabin-coder.cn/image/image-20210821165019147.png) - -进行查找操作时,首先在根节点进行二分查找,找到key所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的数据项。 - -MySQL 数据库使用最多的索引类型是BTREE索引,底层基于B+树数据结构来实现。 - -```mysql -mysql> show index from blog\G; -*************************** 1. row *************************** - Table: blog - Non_unique: 0 - Key_name: PRIMARY - Seq_in_index: 1 - Column_name: blog_id - Collation: A - Cardinality: 4 - Sub_part: NULL - Packed: NULL - Null: - Index_type: BTREE - Comment: -Index_comment: - Visible: YES - Expression: NULL -``` - -### 索引实例 - -下面来看看一个索引的例子: - -如下图,col1 是主键,col2和col3是普通字段。 - -![](http://img.dabin-coder.cn/image/image-20200520234137916.png) - -下图是主键索引对应的 B+树结构,每个节点对应磁盘的一页。 - -![](http://img.dabin-coder.cn/image/image-20200520234200868.png) - -对col3 建立一个单列索引,对应的B+树结构: - -![](http://img.dabin-coder.cn/image/image-20200520234231001.png) - -### 索引分类 -1. 主键索引:名为primary的唯一非空索引,不允许有空值。 - -2. 唯一索引:索引列中的值必须是唯一的,但是允许为空值。 - - 唯一索引和主键索引的区别是:UNIQUE 约束的列可以为null且可以存在多个null值。UNIQUE KEY的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。 - - 创建唯一索引: - - ```mysql - ALTER TABLE table_name - ADD CONSTRAINT constraint_name UNIQUE KEY(column_1,column_2,...); - ``` - -3. 组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀原则。 - -4. 全文索引:全文索引,只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引。 - -### 最左匹配 - -如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。 - -对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。 - -对(a,b,c,d)建立索引,查询条件为`a = 1 and b = 2 and c > 3 and d = 4`,那么,a,b,c三个字段能用到索引,而d就匹配不到。因为遇到了范围查询! - -对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会对b进行比较排序)。直接执行`b = 2`这种查询条件没有办法利用索引。 - -![最左匹配](http://img.dabin-coder.cn/image/image-20210821103313578.png) - -从局部来看,当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a=2时候,b的值为1,4也是有序状态。 因此,你执行`a = 1 and b = 2`是a,b字段能用到索引的。而你执行`a > 1 and b = 2`时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段用不上索引。 - -### 聚集索引 - -InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。 - -聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。 - -![](http://img.dabin-coder.cn/image/mysql-clustered-index.png) - -对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么innodb内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。 - -### 覆盖索引 - -select的数据列只用从索引中就能够取得,不需要到数据表进行二次查询,换句话说查询列要被所使用的索引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。 - -不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL只能使用b+树索引做覆盖索引。 - - 对于使用了覆盖索引的查询,在查询前面使用explain,输出的extra列会显示为using index。 - -比如user_like 用户点赞表,组合索引为(user_id, blog_id),user_id和blog_id都不为null。 - -```mysql -explain select blog_id from user_like where user_id = 13; -``` - -Extra中为`Using index`,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过**索引查找**就能直接找到符合条件的数据,不需要回表查询数据。 - -```mysql -explain select user_id from user_like where blog_id = 1; -``` - -Extra中为`Using where; Using index`, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过**索引扫描**找到符合条件的数据,也不需要回表查询数据。 - -![](http://img.dabin-coder.cn/image/cover-index.png) - -### 索引失效 - -导致索引失效的情况: - -- 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引 -- 以%开头的like查询如`%abc`,无法使用索引;非%开头的like查询如`abc%`,相当于范围查询,会使用索引 -- 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效 -- 判断索引列是否不等于某个值时 -- 对索引列进行运算 -- 使用or连接的条件,如果左边的字段有索引,右边的字段没有索引,那么左边的索引会失效 - -对于Java选手来说,基础知识非常重要,这里给大家分享一个超全面的Java知识总结,GitHub标星137k+,非常有用! - -![](https://pic3.zhimg.com/v2-cdfb49d3d6562191415ae9771055807a.jpg) - -有需要的小伙伴可以自行下载: - -http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=100000392&idx=1&sn=f6c8e84651ce48f6ef5b0d496f0f6adf&chksm=4e98ffce79ef76d8dcebdc4787ae8b37760ec193574da9036e46954ae8954ebd56c78792726f#rd - -## 存储引擎 - -MySQL 5.5版本后默认的存储引擎为InnoDB。 - -### InnoDB -InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。 - -**优点**:支持事务和崩溃修复能力。InnoDB引入了行级锁和外键约束。 - -**缺点**:占用的数据空间相对较大。 - -**适用场景**:需要事务支持,并且有较高的并发读写频率。 - -### MyISAM - -数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。 - -**优点**:访问速度快。 - -**缺点**:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。 - -**适用场景**:对事务完整性没有要求;只读的数据,或者表比较小,可以忍受修复repair操作。 - -MyISAM特性: - -1. MyISAM对整张表加锁,而不是针对行。读取数据时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在读取表记录的同时,可以往表中插入新的记录(并法插入)。 -2. 对于MyISAM表,MySQL可以手动或者自动执行检查和修复操作。执行表的修复可能会导致数据丢失,而且修复操作非常慢。可以通过`CHECK TABLE tablename`检查表的错误,如果有错误执行`REPAIR TABLE tablename`进行修复。 - -### MEMORY -MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。 - -MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。哈希索引使用拉链法来处理哈希冲突。 - -**优点**:访问速度较快。 - -**缺点**: - -1. 哈希索引数据不是按照索引值顺序存储,无法用于排序。 -2. 不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。 -3. 只支持等值比较,不支持范围查询。 -4. 当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的行。 - -### MyISAM和InnoDB区别 - -1. **是否支持行级锁** : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。 - -2. **是否支持事务和崩溃后的安全恢复**: MyISAM 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。 - -3. **是否支持外键:** MyISAM不支持,而InnoDB支持。 - -4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务,MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED` 和 `REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用乐观锁和悲观锁来实现;各数据库中MVCC实现并不统一。 - -5. MyISAM不支持聚集索引,InnoDB支持聚集索引。 - - myisam引擎主键索引和其他索引区别不大,叶子节点都包含索引值和行指针。 - innodb引擎二级索引叶子存储的是索引值和主键值(不是行指针),这样可以减少行移动和数据页分裂时二级索引的维护工作。 - - ![myisam-innodb-index](http://img.dabin-coder.cn/image/myisam-innodb-index.png) - - - -## MVCC - -MVCC(`Multiversion concurrency control`) 就是同一份数据保留多版本的一种方式,进而实现并发控制。可以认为MVCC是行级锁的变种。在查询的时候,通过read view和版本链找到对应版本的数据。 - -MVCC只适用于read committed和repeatable read。使用事务更新行记录时,会生成一个新的版本的行记录。 - -作用:提升并发性能。对于高并发场景,MVCC比行级锁更有效、开销更小。 - -### 实现原理 - -mvcc实现依赖于版本链,版本链是通过表的三个隐藏字段实现。 - -- 事务id:data_trx_id,当前事务id - -- 回滚指针:data_roll_ptr,指向当前行记录的上一个版本,通过这个指针将数据的多个版本连接在一起构成undo log版本链 - -- 主键:db_row_id,如果数据表没有主键,InnoDB会自动生成主键 - -使用事务更新行记录的时候,就会生成版本链: - -1. 用排他锁锁住该行; -2. 将该行原本的值拷贝到 undo log,作为旧版本用于回滚; -3. 修改当前行的值,生成一个新版本,更新事务id,使回滚指针指向旧版本的记录,这样就形成一条版本链; -4. 记录redo log; - -![](http://img.dabin-coder.cn/image/mvcc-impl.png) - -### read view - -read view就是在某一时刻给事务打snapshot快照。在read_view内部维护一个活跃事务链表,这个链表包含在创建read view之前还未提交的事务,不包含创建read view之后提交的事务。 - -不同隔离级别创建read view的时机不同。 - -read committed:每次执行select都会创建新的read_view,保证能读取到其他事务已经提交的修改。 - -repeatable read:在一个事务范围内,第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用之前的read_view。这样可以保证事务范围内每次读取的内容都一样,即可重复读。 - -### 数据访问流程 - -当访问数据行时,会先判断当前版本数据项是否可见,如果是不可见的,会通过版本链找到一个可见的版本。 - -- 如果数据行的当前版本 < read view最早的活跃事务id:说明在创建read_view时,修改该数据行的事务已提交,该版本的数据行可被当前事务读取到。 -- 如果数据行的当前版本 >= read view最晚的活跃事务id:说明当前版本的数据行的事务是在创建read_view之后生成的,该版本的数据行不可以被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本数据对当前事务的可见性。 -- 如果数据行的当前版本在最早的活跃事务id和最晚的活跃事务id之间: - 1. 需要在活跃事务链表中查找是否包含该数据行的最新事务id,即生成当前版本数据行的事务是否已经提交。 - 2. 如果存在,说明生成当前版本数据行的事务未提交,所以该版本的数据行不能被当前事务访问。此时需要通过版本链找到上一个版本,然后重新判断该版本的可见性。 - 3. 如果不存在,说明事务已经提交,可以直接读取该数据行。 - -**总结**:通过比较read view和数据行的当前版本,找到当前事务可见的版本,进而实现read commit和repeatable read的事务隔离级别。 - -### 快照读和当前读 - -记录的两种读取方式。 - -快照读:读取的是快照版本,也就是历史版本。普通的SELECT就是快照读。通过MVCC来进行控制的,不用加锁。 - -当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。 - -快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。 - -事务a和事务b同时开启事务,事务a插入数据然后提交,事务b执行全表的update,然后执行查询,查到了事务A中添加的数据。 - -![](http://img.dabin-coder.cn/image/幻读1.png) - -MySQL如何实现避免幻读: - -- 在快照读情况下,MySQL通过mvcc来避免幻读。 -- 在当前读情况下,MySQL通过next-key来避免幻读(加行锁和间隙锁来实现的)。 - -![](http://img.dabin-coder.cn/image/current-read.png) - -next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。 - -```mysql -select * from table where id<6 lock in share mode;--共享锁 锁定的是小于6的行和等于6的行 -select * from table where id<6 for update;--排他锁 -``` - -实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。 - -Serializable隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般很少使用。 - -### select 读取锁定 - -在SELECT 的读取锁定主要分为两种方式:共享锁和排他锁。 - -```mysql -SELECT ... LOCK IN SHARE MODE  -SELECT ... FOR UPDATE -``` - -这两种方式主要的不同在于LOCK IN SHARE MODE 多个事务同时更新同一个表单时很容易造成死锁。这种情况最好使用SELECT ...FOR UPDATE。 - -`select * from goods where id = 1 for update`:申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。 - -select... for update 使用注意事项 - -1. for update 仅适用于Innodb,且必须在事务范围内才能生效。 -2. 根据主键进行查询,查询条件为 like或者不等于,主键字段产生表锁。 -3. 根据非索引字段进行查询,name字段产生表锁。 - - - -## 分库分表 - -当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。 - -数据切分可以分为两种方式:垂直(纵向)划分和水平(横向)划分。 - -### 垂直划分 - -垂直划分数据库是根据业务进行划分,例如将shop库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能,但这种方式并没有解决高数据量带来的性能损耗。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。 - -![垂直划分](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg) - -优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。 - -缺点: - -- 主键出现冗余,需要管理冗余列; -- 会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力; -- 依然存在单表数据量过大的问题。 - -### 水平划分 - -水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。 - -![水平划分](https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg) - -优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。 - -缺点: - -- 分片事务一致性难以解决 -- 跨节点Join性能差,逻辑复杂 -- 数据分片在扩容时需要迁移 - -## 日志 - -MySQL日志 主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是二进制日志binlog和事务日志 redo log(重做日志)和 undo log(回滚日志)。 - -### bin log - -二进制日志(bin log)是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。 - -查看bin log是否开启,以及保存位置: - -```mysql -MySQL> show variables like '%log_bin%'; -+---------------------------------+----------------------------------------------------+ -| Variable_name | Value | -+---------------------------------+----------------------------------------------------+ -| log_bin | ON | -| log_bin_basename | F:\java\MySQL8\data\Data\DESKTOP-8F30VS1-bin | -| log_bin_index | F:\java\MySQL8\data\Data\DESKTOP-8F30VS1-bin.index | -| log_bin_trust_function_creators | OFF | -| log_bin_use_v1_row_events | OFF | -| sql_log_bin | ON | -+---------------------------------+----------------------------------------------------+ -``` - -关闭bin log,找到/etc/my.cnf文件,注释以下代码: - -```mysql -log-bin=MySQL-bin -binlog_format=mixed -``` - -### redo log - -重做日志(redo log)是Innodb引擎级别,用来记录Innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,InnoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数innodb_flush_log_at_tx_commit设置为1,那么在执行commit时将redo log同步写到磁盘。 - -bin log和redo log区别: - -1. bin log会记录所有日志记录,包括innoDB、MyISAM等存储引擎的日志;redo log只记录innoDB自身的事务日志 -2. bin log只在事务提交前写入到磁盘,一个事务只写一次,无论事务多大;而在事务进行过程,会有redo log不断写入磁盘 -3. binlog 是逻辑日志,记录的是SQL语句的原始逻辑;redo log 是物理日志,记录的是在某个数据页上做了什么修改。 - -### undo Log - -除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。 - -### 查询日志 - -记录所有对MySQL请求的信息,无论请求是否正确执行。 - -```mysql -MySQL> show variables like '%general_log%'; -+------------------+----------------------------------+ -| Variable_name | Value | -+------------------+----------------------------------+ -| general_log | OFF | -| general_log_file | /var/lib/MySQL/VM_0_7_centos.log | -+------------------+----------------------------------+ -``` - - - -## MySQL架构 - -MySQL主要分为 Server 层和存储引擎层: - -- **Server 层**:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。 -- **存储引擎**: 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。 - -![MySQL-archpng](http://img.dabin-coder.cn/image/mysql-archpng.png) - -### Server 层基本组件 - -- **连接器:** 当客户端连接 MySQL 时,server层会对其进行身份认证和权限校验。 -- **查询缓存:** 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除),先校验这个 sql 是否执行过,如果缓存 key (sql语句)被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作。MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,不推荐使用。 -- **分析器:** 没有命中缓存的话,SQL 语句就会经过分析器,主要分为两步,词法分析和语法分析,先看 SQL 语句要做什么,再检查 SQL 语句语法是否正确。 -- **优化器:** 优化器对查询进行优化,包括重写查询、决定表的读写顺序以及选择合适的索引等,生成执行计划。 -- **执行器:** 首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会根据执行计划去调用引擎的接口,返回结果。 - -#### 语法解析器和预处理 - -MySQL通过关键字将SQL语句进行解析,生成解析树。 - -MySQL解析器使用MySQL语法规则验证和解析查询,比如验证是否使用正确的关键字、关键字的次序是否正确和验证引号是否前后正确匹配。 - -预处理器会进一步检查解析树是否合法,如检查数据表和数据列是否存在,然后验证权限。 - -#### 查询优化器 - -优化器会找出一个它认为最优的执行计划。 - -MySQL 能够处理的优化类型: - -1. 重新定义表的关联顺序。数据表的关联并不是总按照查询中指定的顺序进行的。 -3. 使用等价变换,简化表达式。比如将 `5=5 AND a > 5` 转化为 `a > 5`。 -4. 优化COUNT/MIN/MAX。MIN查询最小值,对应的是b+树索引的第一行记录,优化器会将这个表达式作为一个常数对待。 -4. 列表IN()的比较。很多数据库系统,IN完成等价于多个OR子句。MySQL不一样,MySQL将IN列表的数据先进行排序,然后通过二分查找的方式确定列表的值是否符合要求,时间复杂度为O(logN),而OR查询的时间复杂度为O(N)。当IN列表有大量取值时,处理速度相比OR查询会更快。 -5. 覆盖索引扫描。 -6. 将外连接转化成内连接。某些情况下,外连接可能等价于一个内连接。 - -#### 查询执行引擎 - -在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划,调用存储引擎接口来完成整个查询。 - -### 查询语句执行流程 - -查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。 - -查询语句: - -```mysql -select * from user where id > 1 and name = '大彬'; -``` - -1. 检查权限,没有权限则返回错误; -2. MySQL以前会查询缓存,缓存命中则直接返回,没有则执行下一步; -3. 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误; -4. 两种执行方案,先查 `id > 1` 还是 `name = '大彬'`,优化器根据自己的优化算法选择执行效率最好的方案; -5. 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。 - -### 更新语句执行过程 - -更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo log(prepare 状态)、binlog、redo log(commit状态) - -更新语句: - -```mysql -update user set name = '大彬' where id = 1; -``` - -1. 先查询到 id 为1的记录,有缓存会使用缓存 -2. 拿到查询结果,将 name 更新为 大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。 -3. 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。 -4. 更新完成。 - -为什么记录完 redo log,不直接提交,先进入prepare状态? - -假设先写 redo log 直接提交,然后写 binlog,写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 binlog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。 - -假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。 - - - -## 慢查询 - -sql 语句查询时间超过(不包括等于) long_query_time,称为慢查询。 - -查看慢查询配置: - -```mysql -show variables like '%slow_query_log%'; #查看慢查询配置 -set global slow_query_log=1; #开启慢查询 -``` - -使用`set global slow_query_log=1`开启了慢查询日志只对当前数据库生效,如果MySQL重启后则会失效。如果要永久生效,就必须修改配置文件my.cnf。 - -```properties -slow_query_log =1 -slow_query_log_file=/tmp/MySQL_slow.log #系统默认会给一个缺省的文件host_name-slow.log -``` - -默认情况下long_query_time的值为10秒,可以使用命令修改,也可以在my.cnf参数里面修改。 - -```mysql -show variables like 'long_query_time%'; -set global long_query_time=4; #需要重新连接或新开一个会话才能看到修改值或者使用show global variables like 'long_query_time' -``` - -MySQL数据库支持同时两种日志存储方式,配置的时候以逗号隔开即可,如:log_output='FILE,TABLE'。 - -日志记录到系统的专用日志表中,要比记录到文件耗费更多的系统资源,因此对于需要启用慢查询日志,又需要能够获得更高的系统性能,那么建议优先记录到文件。 - -### mysqldumpslow - -如果自己手动查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow。 - -获取执行时间最长的10条sql语句: - -```mysql -mysqldumpslow -s al -n 10 /usr/local/MySQL/data/slow.log -``` - - - -## 分区表 - -分区表是一个独立的逻辑表,但是底层由多个物理子表组成。 - -当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。 - -### 分区表类型 - -1. 按照范围分区。 - - ```mysql - CREATE TABLE test_range_partition( - id INT auto_increment, - createdate DATETIME, - primary key (id,createdate) - ) - PARTITION BY RANGE (TO_DAYS(createdate) ) ( - PARTITION p201801 VALUES LESS THAN ( TO_DAYS('20210201') ), - PARTITION p201802 VALUES LESS THAN ( TO_DAYS('20210301') ), - PARTITION p201803 VALUES LESS THAN ( TO_DAYS('20210401') ), - PARTITION p201804 VALUES LESS THAN ( TO_DAYS('20210501') ), - PARTITION p201805 VALUES LESS THAN ( TO_DAYS('20210601') ), - PARTITION p201806 VALUES LESS THAN ( TO_DAYS('20210701') ), - PARTITION p201807 VALUES LESS THAN ( TO_DAYS('20210801') ), - PARTITION p201808 VALUES LESS THAN ( TO_DAYS('20210901') ), - PARTITION p201809 VALUES LESS THAN ( TO_DAYS('20211001') ), - PARTITION p201810 VALUES LESS THAN ( TO_DAYS('20211101') ), - PARTITION p201811 VALUES LESS THAN ( TO_DAYS('20211201') ) - ); - - insert into test_range_partition (createdate) values ('20210105'); - insert into test_range_partition (createdate) values ('20210205'); - ``` - - 在`/var/lib/mysql/data/`可以找到对应的数据文件,每个分区表都有一个使用#分隔命名的表文件: - - ``` - -rw-rw---- 1 mysql mysql 65 Aug 21 09:24 db.opt - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201801.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201802.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201803.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201804.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201805.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201806.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201807.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201808.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201809.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201810.ibd - -rw-rw---- 1 mysql mysql 98304 Aug 21 09:27 test_range_partition#P#p201811.ibd - -rw-rw---- 1 mysql mysql 8598 Aug 21 09:27 test_range_partition.frm - -rw-rw---- 1 mysql mysql 116 Aug 21 09:27 test_range_partition.par - ``` - -2. list分区。对于List分区,分区字段必须是已知的,如果插入的字段不在分区时枚举值中,将无法插入。 - - ```mysql - create table test_list_partiotion - ( - id int auto_increment, - data_type tinyint, - primary key(id,data_type) - )partition by list(data_type) - ( - partition p0 values in (0,1,2,3,4,5,6), - partition p1 values in (7,8,9,10,11,12), - partition p2 values in (13,14,15,16,17) - ); - ``` - -3. hash分区,可以将数据均匀地分布到预先定义的分区中。 - - ```mysql - drop table test_hash_partiotion; - create table test_hash_partiotion - ( - id int auto_increment, - create_date datetime, - primary key(id,create_date) - )partition by hash(year(create_date)) partitions 10; - ``` - -### 分区的问题 - -1. 打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、LOAD DATA INFILE和一次删除多行数据。 -2. 维护分区的成本可能很高。例如重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。 -3. 所有分区必须使用相同的存储引擎。 - -### 查询优化 - -分区最大的优点就是优化器可以根据分区函数过滤掉一些分区,可以让查询扫描更少的数据。在查询条件中加入分区列,就可以让优化器过滤掉无需访问的分区。如果查询条件没有分区列,MySQL会让存储引擎访问这个表的所有分区。需要注意的是,查询条件中的分区列不能使用表达式。 - - - -## 其他 - -### processlist - -`select * `会查询出不需要的、额外的数据,那么这些额外的数据在网络上进行传输,带来了额外的网络开销。 - -`show processlist` 或 `show full processlist` 可以查看当前 MySQL 是否有压力,正在运行的sql,有没有慢 SQL 正在执行。 - -- **id** - 线程ID,可以用:`kill id;` 杀死一个线程,很有用 - -- **db** - 数据库 - -- **user** - 用户 - -- **host** - 连库的主机IP - -- **command** - 当前执行的命令,比如最常见的:Sleep,Query,Connect 等 - -- **time** - 消耗时间,单位秒,很有用 - -- **state** - 执行状态 - - sleep,线程正在等待客户端发送新的请求 - - query,线程正在查询或者正在将结果发送到客户端 - - Sorting result,线程正在对结果集进行排序 - - Locked,线程正在等待锁 - -- **info** - 执行的SQL语句,很有用 - - - -### exist和in - -exists 用于对外表记录做筛选。 - -exists 会遍历外表,将外查询表的每一行,代入内查询进行判断。当 exists 里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。 - -```mysql -select a.* from A a -where exists(select 1 from B b where a.id=b.id) -``` - -in 是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。 - -```mysql -select * from A -where id in(select id from B) -``` - -子查询的表大的时候,使用EXISTS可以有效减少总的循环次数来提升速度;当外查询的表大的时候,使用IN可以有效减少对外查询表循环遍历来提升速度。 - - - -> 参考资料: -> -> 高性能MySQL书籍 -> -> MVCC实现原理:https://zhuanlan.zhihu.com/p/64576887 -> -> 多版本并发控制机制:https://www.cnblogs.com/axing-articles/p/11415763.html -> -> 排他锁分析:https://blog.csdn.net/claram/article/details/54023216 -> -> 分区表:https://www.cnblogs.com/wy123/p/9778590.html -> -> 一条SQL语句在MySQL中如何执行的:https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485097&idx=1&sn=84c89da477b1338bdf3e9fcd65514ac1&chksm=cea24962f9d5c074d8d3ff1ab04ee8f0d6486e3d015cfd783503685986485c11738ccb542ba7&token=79317275&lang=zh_CN#rd - diff --git "a/\346\241\206\346\236\266/SpringBoot\345\256\236\346\210\230.md" "b/\346\241\206\346\236\266/SpringBoot\345\256\236\346\210\230.md" deleted file mode 100644 index 2c1a423..0000000 --- "a/\346\241\206\346\236\266/SpringBoot\345\256\236\346\210\230.md" +++ /dev/null @@ -1,1028 +0,0 @@ - - - - -- [Spring Boot基础](#spring-boot%E5%9F%BA%E7%A1%80) - - [特点](#%E7%89%B9%E7%82%B9) -- [Spring Boot核心](#spring-boot%E6%A0%B8%E5%BF%83) - - [基本配置](#%E5%9F%BA%E6%9C%AC%E9%85%8D%E7%BD%AE) - - [外部配置](#%E5%A4%96%E9%83%A8%E9%85%8D%E7%BD%AE) - - [日志配置](#%E6%97%A5%E5%BF%97%E9%85%8D%E7%BD%AE) - - [Profile配置](#profile%E9%85%8D%E7%BD%AE) -- [Spring Boot的Web开发](#spring-boot%E7%9A%84web%E5%BC%80%E5%8F%91) - - [Thymeleaf 模板引擎](#thymeleaf-%E6%A8%A1%E6%9D%BF%E5%BC%95%E6%93%8E) - - [Thymeleaf 基础知识](#thymeleaf-%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86) - - [与 Spring MVC 集成](#%E4%B8%8E-spring-mvc-%E9%9B%86%E6%88%90) - - [Spring Boot 的 Thymeleaf 支持](#spring-boot-%E7%9A%84-thymeleaf-%E6%94%AF%E6%8C%81) - - [实战](#%E5%AE%9E%E6%88%98) - - [Web 相关配置](#web-%E7%9B%B8%E5%85%B3%E9%85%8D%E7%BD%AE) - - [Spring Boot 提供的自动配置](#spring-boot-%E6%8F%90%E4%BE%9B%E7%9A%84%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE) - - [实现自己的 MVC 配置](#%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%B7%B1%E7%9A%84-mvc-%E9%85%8D%E7%BD%AE) - - [注册 Servlet、Filter、Listener](#%E6%B3%A8%E5%86%8C-servletfilterlistener) - - [Tomcat配置](#tomcat%E9%85%8D%E7%BD%AE) - - [配置 Tomcat](#%E9%85%8D%E7%BD%AE-tomcat) - - [替换 Tomcat](#%E6%9B%BF%E6%8D%A2-tomcat) -- [Spring Boot 的数据访问](#spring-boot-%E7%9A%84%E6%95%B0%E6%8D%AE%E8%AE%BF%E9%97%AE) - - [Docker 常用命令及参数](#docker-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E5%8F%8A%E5%8F%82%E6%95%B0) - - [Docker 镜像命令](#docker-%E9%95%9C%E5%83%8F%E5%91%BD%E4%BB%A4) - - [Docker 容器命令](#docker-%E5%AE%B9%E5%99%A8%E5%91%BD%E4%BB%A4) - - [Spring Boot 对 Spring Data JPA 的支持](#spring-boot-%E5%AF%B9-spring-data-jpa-%E7%9A%84%E6%94%AF%E6%8C%81) - - [JDBC 的自动配置](#jdbc-%E7%9A%84%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE) - - [对 JPA 的自动配置](#%E5%AF%B9-jpa-%E7%9A%84%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE) - - [对 Spring Data JPA 的自动配置](#%E5%AF%B9-spring-data-jpa-%E7%9A%84%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE) -- [@Value原理](#value%E5%8E%9F%E7%90%86) -- [启动过程](#%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B) - - - -## Spring Boot基础 -理念:习惯优于配置,内置习惯性配置,无需手动进行配置。使用Spring boot可以很快创建一个独立运行、准生产级别的基于Spring框架的项目,不需要或者只需很少的Spring配置。 -### 特点 -- 内置servlet容器,不需要在服务器部署 tomcat。只需要将项目打成 jar 包,使用 java -jar xxx.jar一键式启动项目 -- SpringBoot提供了starter,把常用库聚合在一起,简化复杂的环境配置,快速搭建spring应用环境 -- 可以快速创建独立运行的spring项目,集成主流框架 -- 准生产环境的运行应用监控 - -## Spring Boot核心 - -### 基本配置 - -Spring Boot通常有个Application入口类: - -```java -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@SpringBootApplication -public class SpringbootDemoApplication { - public static void main(String[] args) { - SpringApplication.run(SpringbootDemoApplication.class, args); - } - -} -``` - -@SpringBootApplication是Spring Boot的核心注解,它是组合注解,源码如下: - -```java -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Inherited -@SpringBootConfiguration -@EnableAutoConfiguration -@ComponentScan( - excludeFilters = {@Filter( - type = FilterType.CUSTOM, - classes = {TypeExcludeFilter.class} -), @Filter( - type = FilterType.CUSTOM, - classes = {AutoConfigurationExcludeFilter.class} -)} -) -public @interface SpringBootApplication { -} -``` - -@SpringBootApplication注解组合了@Configuration、@EnableAutoConfiguration和@ComponentScan注解,若不使用@SpringBootApplication注解,则可以在入口类上直接使用@Configuration、@EnableAutoConfiguration和@ComponentScan。 - -@EnableAutoConfiguration作用是让Spring Boot根据类路径中的jar包依赖为当前项目进行自动配置。例如,添加了spring-boot-starter-web依赖,会自动添加Tomcat和Spring MVC依赖,那么Spring Boot会对Tomcat和Spring MVC进行自动配置。 - -@Configuration标注在类上,相当于把该类作为spring的xml配置文件中的``,作用是配置spring容器。 - -**关闭特定的自动配置** - -使用@SpringBootApplication的exclude参数关闭特定的自动配置。 - -```java -@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) -``` - -**Spring Boot的配置文件** - -Spring Boot使用一个全局的配置文件application.properties或application.yml。这个全局配置文件可以对一些默认配置的配置值进行修改。如修改Tomcat默认的端口号,并将默认的访问路径"/"修改为"/hello": - -```properties -server.port=9090 -server.context-path=/hello -``` - -**starter pom** - -Spring Boot为我们提供了简化企业级开发绝大多数场景的starter pom,只要使用了应用场景所需要的starter pom,相应的配置就可以消除,就可以得到Spring Boot为我们提供的自动配置的bean。 - -**xml配置** - -Spring Boot提倡零配置,但实际项目中可能需要使用xml配置,此时可以通过Spring提供的@ImportResource来加载xml配置。 - -```java -@ImportResource({"classpath:xxx-context.xml", "classpath:yyy-context.xml"}) -``` - -### 外部配置 - -**命令行参数配置** - -Spring Boot是基于jar包运行的,打成jar包的程序可以直接通过下面的命令运行: - -``` -java -jar xx.jar -``` - -可以通过以下命令修改端口号: - -``` -java -jar xx.jar --server.port=9090 -``` - -**常规属性配置** - -在Spring Boot里,我们只需在application.properties定义属性,直接使用@Value注入即可。 - -application.properties增加属性: - -```properties -book.author=tyson -book.name=life -``` - -修改入口类: - -```java -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@SpringBootApplication -public class SpringbootDemoApplication { - - @Value("${book.author}") - private String bookAuthor; - @Value("${book.name}") - private String bookName; - - @RequestMapping("/") - public String index() { - return "book name: " + bookName + ", written by: " + bookAuthor; - } - - public static void main(String[] args) { - SpringApplication.run(SpringbootDemoApplication.class, args); - } - -} -``` - -@RestController=@Controller + @ResponseBody,注解的类里面的方法以json格式输出。 - -**类型安全的配置** - -Spring Boot提供了基于类型安全的配置方式,通过@ConfigurationProperties将配置文件application.properties中配置的属性值映射到当前类的属性中,从而实现类型安全的配置。 - -类型安全的bean: - -```java -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@ConfigurationProperties(prefix = "book") -public class BookConfig { - private String name; - private String author; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } -} -``` - -通过@ConfigurationProperties加载properties文件内的配置,通过prefix属性指定前缀。 - -检验代码: - -```java -import com.tyson.springbootdemo.config.BookConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@SpringBootApplication -@EnableConfigurationProperties({BookConfig.class}) -public class SpringbootDemoApplication { - - @Autowired - public BookConfig bookConfig; - - @RequestMapping("/") - public String index() { - return "book name: " + bookConfig.getName() + ", written by: " + bookConfig.getAuthor(); - } - - public static void main(String[] args) { - SpringApplication.run(SpringbootDemoApplication.class, args); - } - -} -``` - -@EnableConfigurationProperties注解将带有@ConfigurationProperties注解的类注入为Spring容器的Bean。 - -### 日志配置 - -Spring Boot 支持 Log4J、Logback、Java Util Logging、Log4J2 作为日志框架,无论使用哪种日志框架,Spring Boot 已为当前使用日志框架的控制台输出及文件输出做好了配置。默认情况下,Spring Boot 使用 Logback 作为日志框架,日志级别为 INFO。 - -配置日志文件: - -```properties -logging.path=H:/log/ -logging.file=springbootdemo.log -``` - -配置日志级别,格式为 logging.level.包名=级别: - -```properties -logging.level.root=INFO #root级别,项目所有日志 -logging.level.org.springframework.web=DEBUG #package级别 -``` - -配置日志样式: - -```properties -logging.pattern.console=%d{yyyy/MM/dd-HH:mm:ss} [%thread] %-5level %logger- %msg%n -logging.pattern.file=%d{yyyy/MM/dd-HH:mm} [%thread] %-5level %logger- %msg%n -``` - -### Profile配置 - -Profile 是 Spring 用来针对不同环境对不同配置提供支持的,全局Profile配置使用 application-{profile}.properties(如application-prod.properties)。通过在 application.properties 中设置spring.profiles.active=prod 来制定活动的Profile。 - -假如有生产和开发环境,生产环境下端口号为80,开发环境下端口号为8888。配置文件如下: - -application-prod.properties: - -```properties -server.port=80 -``` - -application-dev.properties - -```properties -server.port=8888 -``` - -application.properties 增加: - -```properties -spring.profiles.active=prod -``` - -启动程序结果为: - -```java -2019-03-03 09:17:08.003 INFO 17812 --- [ main] c.t.s.SpringbootDemoApplication : The following profiles are active: prod -2019-03-03 09:17:11.007 INFO 17812 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8888 (http) -``` - -## Spring Boot的Web开发 - -spring-boot-starter-web 为我们提供了嵌入的 tomcat 和 Spring MVC 的依赖。 - -### Thymeleaf 模板引擎 - -JSP 在内嵌的 Servlet 容器上运行会存在一些问题(内嵌 Tomcat、Jetty 不支持以 jar 形式运行 JSP,Undertow 不支持 JSP)。Spring Boot 提供了大量的模板引擎,包含 FreeMarker、Groovy、Thymeleaf、Velocity 和 Mustache,Spring Boot 中推荐使用 Thymeleaf 作为模板引擎,因为 Thymeleaf 提供了完美的 Spring MVC 的支持。 - -#### Thymeleaf 基础知识 - -Thymeleaf 是 Java 类库,它是一个 xml/xhtml/html5 的模板引擎,可以作为 MVC 的 Web 应用的 view 层。Thymeleaf 还提供了额外的模块与 Spring MVC 集成,使用 Thymeleaf 完全可以替代 JSP。 - -1. 引入 Thymeleaf - -基本的 Thymeleaf 模板页面,引入了 Bootstrap(作为样式控制)和 jQuery(DOM 操作)。 - -```html - - - - - - - Demo - - - - - - -``` - -通过 xmlns:th="http://www.thymeleaf.org" 命名空间,将静态页面转换为动态视图。需要进行动态处理的元素需使用 “th:” 为前缀。通过 “@{}” 引用 Web 静态资源,这在 JSP 下是极易出错的。 - -2. 访问 model 中的数据 - -通过 “${}” 访问 model 中的属性,这和 JSP 很相似。需要处理的动态内容需要加上 “th:” 前缀。 - -```html -
-
-

访问model

-
-
- -
-
-``` - -3. model 中的数据迭代 - -```html -
-
-

列表

-
-
-
    -
  • - - -
  • -
-
-
-``` - -4. 数据判断 - -通过${not #lists.isEmpty(people)}表达式判断 people 是否为空。Thymeleaf 还支持 >、<、==、!= 等作为比较条件,同时也支持将 SpringEL 表达式语言用于条件中。 - -```html -
-
-
-

列表

-
-
-
    -
  • - - -
  • -
-
-
-
-``` - -5. 在 JavaScript 中访问 model - -```html - -``` - -通过 th:inline="javascript"添加到 script 标签,这样 JavaScript 代码即可访问 model 中的属性; - -通过“[[${person}]]”格式可以获得实际的值。 - -如果需要在 html 代码里访问 model 中的属性,比如我们需要在列表后面单击每一行后面的按钮获得 model 中的值,可做如下处理: - -```html -
  • - - - -
  • -``` - -#### 与 Spring MVC 集成 - -在 Spring MVC 中,若我们需要集成一个模板引擎的话,需要定义 ViewResolver,而 ViewResolver 需要定义一个 View。在 Spring MVC 中集成 Thymeleaf 非常简单,Thymeleaf 为我们定义好了 org.thymeleaf.spring4.view .ThymeleafView 和 org.thymeleaf.spring4.view.ThymeleafViewResolver(默认使用 ThymeleafView 作为 View)。Thymeleaf 给我们提供了一个 SpringTemplateEngine 类,用来驱动 Spring MVC 下使用 Thymeleaf 模板引擎,另外提供了一个 TemplateResolver 用来设置通用的模板引擎(包含前缀、后缀等)。 - -引入依赖: - -```xml - - - org.thymeleaf - thymeleaf-spring4 - ${thymeleaf.version} - - - org.thymeleaf - thymeleaf - ${thymeleaf.version} - -``` - -xml 配置: - -```java - - - - - - - - - - - - - - - - - -``` - -或者使用 JavaConfig 配置: - -```java -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.thymeleaf.spring4.SpringTemplateEngine; -import org.thymeleaf.spring4.view.ThymeleafViewResolver; -import org.thymeleaf.templateresolver.ServletContextTemplateResolver; -import org.thymeleaf.templateresolver.TemplateResolver; - -@Configuration -public class ThymeleafConfig { - @Bean - public TemplateResolver templateResolver() { - TemplateResolver templateResolver = new ServletContextTemplateResolver(); - templateResolver.setPrefix("/WEB-INF/template"); - templateResolver.setSuffix(".html"); - templateResolver.setTemplateMode("HTML5"); - return templateResolver; - } - - @Bean - public SpringTemplateEngine templateEngine(TemplateResolver templateResolver) { - SpringTemplateEngine templateEngine = new SpringTemplateEngine(); - templateEngine.setTemplateResolver(templateResolver); - return templateEngine; - } - - @Bean - public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) { - ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver(); - thymeleafViewResolver.setTemplateEngine(templateEngine); - //thymeleafViewResolver.setViewClass(ThymeleafView.class); - return thymeleafViewResolver; - } -} -``` - -#### Spring Boot 的 Thymeleaf 支持 - -Spring Boot 通过 org.springframework.boot.autoconfigure.thymeleaf 包对 Thymeleaf 进行了自动配置,通过ThymeleafAutoConfiguration 类对集成所需的 Bean 进行自动配置,包括 templateResolver、templateEngine 和 thymeleafViewResolvers 的配置。通过 ThymeleafProperties 来设置属性以及默认配置。 - -```java -@ConfigurationProperties( - prefix = "spring.thymeleaf" -) -public class ThymeleafProperties { - private static final Charset DEFAULT_ENCODING; - public static final String DEFAULT_PREFIX = "classpath:/templates/"; - public static final String DEFAULT_SUFFIX = ".html"; - private boolean checkTemplate = true; - private boolean checkTemplateLocation = true; - private String prefix = "classpath:/templates/"; - private String suffix = ".html"; - private String mode = "HTML"; - private Charset encoding; - private boolean cache; - private Integer templateResolverOrder; - private String[] viewNames; - private String[] excludedViewNames; - private boolean enableSpringElCompiler; - private boolean renderHiddenMarkersBeforeCheckboxes; - private boolean enabled; - private final ThymeleafProperties.Servlet servlet; - private final ThymeleafProperties.Reactive reactive; - ... -} -``` - -#### 实战 - -1. 引入依赖 - -```xml - - org.springframework.boot - spring-boot-starter-thymeleaf - -``` - -引入 Thymeleaf 之后,Controller才能根据逻辑视图名转发到相应的视图。 - -2. JavaBean - -```java -public class Person { - private String name; - private Integer age; - private String address; - - public Person() {} - - //setter和getter -} -``` - -3. 页面 - -```html - - - - - - - - index - - -
    -
    -

    welcome

    -
    -
    - -
    -
    - - -``` - -页面引入了Bootstrap和jQuery,这些静态文件放置在src/main/resources/static下面。 - -4. Controller - -```java -import com.tyson.springbootdemo.pojo.Person; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping("/person") -public class PersonController { - @RequestMapping("/hi") - public String index(Model model) { - Person p = new Person(); - p.setName("tyson"); - model.addAttribute("person", p); - return "index"; - } - - @RequestMapping("/") - public String home() { - return "home"; - } -} -``` - -### Web 相关配置 - -#### Spring Boot 提供的自动配置 - -WebMvcAutoConfiguration 及 WebMvcProperties 定义了 Web 相关的自动配置。 - -1. 自动配置的ViewResolver - -```java -@Bean -@ConditionalOnMissingBean -public InternalResourceViewResolver defaultViewResolver() { - InternalResourceViewResolver resolver = new InternalResourceViewResolver(); - resolver.setPrefix(this.mvcProperties.getView().getPrefix()); - resolver.setSuffix(this.mvcProperties.getView().getSuffix()); - return resolver; -} - -@Bean -@ConditionalOnBean({View.class}) -@ConditionalOnMissingBean -public BeanNameViewResolver beanNameViewResolver() { - BeanNameViewResolver resolver = new BeanNameViewResolver(); - resolver.setOrder(2147483637); - return resolver; -} -``` - -2. 自动配置的静态资源 - -将类路径下的/static、/resources、/public 和 /META-INF/resources 文件夹下的静态文件直接映射为/**,可以通过 http://localhost:8080/\*\*访问。 - -把 webjar 的 /META-INF/resources/webjars/ 下的静态文件映射为/webjar/**,可以通过 http://localhost:8080/webjar/\*\*访问。 - -3. 自动配置的 Formatter 和 Converter - -```java -public void addFormatters(FormatterRegistry registry) { - Iterator var2 = this.getBeansOfType(Converter.class).iterator(); - - while(var2.hasNext()) { - Converter converter = (Converter)var2.next(); - registry.addConverter(converter); - } - - var2 = this.getBeansOfType(GenericConverter.class).iterator(); - - while(var2.hasNext()) { - GenericConverter converter = (GenericConverter)var2.next(); - registry.addConverter(converter); - } - - var2 = this.getBeansOfType(Formatter.class).iterator(); - - while(var2.hasNext()) { - Formatter formatter = (Formatter)var2.next(); - registry.addFormatter(formatter); - } - -} -``` - -只要我们定义了 Converter、GenericConverter 和 Formatter 接口的实现类的 Bean,这些 Bean 就会自动注册到 Spring MVC 中。 - -3. 静态首页的支持 - -把静态首页放到如下目录: - -- classpath:/META-INF/resources/index.html -- classpath:/resources/index.html -- classpath:/static/index.html -- classpath:/public/index.html - -当我们访问应用根目录 http://localhost:8080/ 时,会直接映射。 - -#### 实现自己的 MVC 配置 - -当 Spring Boot 提供的 Spring MVC 不符合要求时,可以通过一个配置类(注解有@Configuration 的类)加上@EnableWebMvc 注解来实现完全自己控制的 MVC 配置。 - -要想保留 Spring Boot 提供的 MVC 配置,同时增加额外的配置,可以通过定义一个配置类并继承 WebMvcConfigurerAdapter,无需使用@EnableWebMvc注解。 - -```java -import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -public class WebMvcConfig implements WebMvcConfigurer { - @Override - public void addViewControllers(ViewControllerRegistry registry) { - registry.addViewController("/xx").setViewName("xx"); - } -} -``` - -重写的 addViewControllers 方法并不会覆盖 WebMvcAutocConfiguration 中的 addViewControllers(此方法中,Spring Boot 将“/”映射到 index.html),即我们自己的配置和 Spring Boot 的自动配置同时生效。 - -#### 注册 Servlet、Filter、Listener - -当使用嵌入式的 Servlet 容器时,通过将 Servlet、Filter 和 Listener 声明为 Spring Bean 达到注册的效果;或者注册 ServletRegistrationBean、FilterRegistrationBean 和 ServletRegistrationBean 的 Bean。 - -直接注册 bean: - -```java -@Bean -public xxServlet xxServlet() { - return new XxServlet(); -} -``` - -通过 RegistrationBean 示例: - -```java -@Bean -public ServletRegistrationBean servletRegistrationBean() { - return new ServletRegistrationBean(new XxServlet(), "/xx/*"); -} - -@Bean -public FilterRegistrationBean filterRegistrationBean() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean(); - registrationBean.setFilter(new YyFilter()); - registrationBean.setOrder(2); - return registrtionBean; -} - -@Bean -public ServletListenerRegistrationBean zzListenerServletRegistrationBean() { - return new ServletListenerRegistrationBean(new ZzListener()); -} -``` - -### Tomcat配置 - -#### 配置 Tomcat - -关于 Tomcat 的所有属性都在 org.springframework.boot.autoconfigure.web.ServerPr=8080operties 配置类中做了定义,只需在 application.properties 配置即可。通用的 Servlet 容器配置都以 "server" 作为前缀,而 Tomcat 特有的配置都以 "server.tomcat" 作为前缀。 - -```properties -#配置Servlet容器 -server.port=8080 -server.session-timeout=3600 #以秒为单位 -server.context-path=/ -#配置Tomcat -server.tomcat.uri-encoding=UTF-8 -server.tomcat.compression=off #是否开启压缩,默认是关闭 -``` - -#### 替换 Tomcat - -Spring Boot 默认使用 Tomcat 作为内嵌的 Servlet 容器,如果要使用 Jetty 或者 Undertow 为容器,只需修改 spring-boot-start-web 的依赖即可。 - -1. 替换为 Jetty - -在 pom.xml中,将 spring-boot-starter-web 的依赖由 spring-boot-starter-tomcat 替换为 spring-boot-starter-jetty: - -```xml - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - - org.springframework.boot - spring-boot-starter-jetty - -``` - -启动 Spring Boot,控制台输出效果如下: - -```java -INFO 21724 --- [ main] o.s.b.web.embedded.jetty.JettyWebServer : Jetty started on port(s) 8080 (http/1.1) with context path '/' -``` - -2. 替换为 Undertow - -将 spring-boot-starter-web 的依赖由 spring-boot-starter-tomcat 替换为 spring-boot-starter-undertow: - -```xml - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-tomcat - - - - - - org.springframework.boo - spring-boot-starter-undertow - -``` - -## Spring Boot 的数据访问 - -### Docker 常用命令及参数 - -#### Docker 镜像命令 - -(1)Docker 镜像检索 - -```powershell -docker search redis -``` - -(2)镜像下载 - -```powershell -docker pull redis -``` - -(3)镜像列表 - -```powershell -docker images -``` - -(4)删除镜像 - -删除指定镜像: - -```powershell -docker rmi image-id -``` - -删除所有镜像: - -```powershell -docker rmi ${docker images -q} -``` - -#### Docker 容器命令 - -(1)容器基本操作 - -运行容器,Docker 会为我们生成唯一的标识。 - -```powershell -docker run --name container-name -d image-name -``` - -(2)容器列表 - -查看运行中的容器列表: - -```powershell -docker ps -``` - -查看运行和停止状态的容器: - -```powershell -docker ps -a -``` - -(3)停止和启动容器 - -停止容器: - -```powershell -docker stop container-name/container-id -``` - -启动容器: - -```powershell -docker start container-name/container-id -``` - -端口映射:Docker 容器中运行的软件所使用的端口,在本机和本机的局域网是不能访问的,需要将 Docker 容器的端口映射到当前主机的端口上,这样我们在本机和本机的局域网才能访问该软件。 - -映射容器的6379端口到虚拟机的6378端口上: - -```powershell -docker run -d -p 6378:6379 --name port-redis resdis -``` - -删除单个容器: - -```powershell -docker rm container-id -``` - -删除所有容器: - -```powershell -docker rm ${docker ps -a -q} -``` - -查看容器日志: - -```powershell -docker logs container-name/container-id -``` - -登录容器: - -```powershell -docker exec -it container-id/container-name bash -``` - -登录后可以在容器中进行常规的 Linux 系统操作命令。 - -### Spring Boot 对 Spring Data JPA 的支持 - -JPA 是一个基于 O/R 映射的标准规范(不提供实现)。Spring Data JPA 是 Spring Data 的一个子项目,它通过提供基于 JPA 的Repository 极大地减少了 JPA 作为数据访问方案的代码量。 - -#### JDBC 的自动配置 - -spring-boot-starter-data-jpa 依赖于 spring-boot-starter-jdbc,Spring Boot 对 JDBC 做了一些自动配置,源码在 org.springframework.boot.autoconfigure.jdbc 下。 - -在 DataSourceAutoConfiguration 类中配置了对 DataSource 的自动配置,通过"spring.datasource"为前缀的属性自动配置 dataSource;Spring Boot 开启了注解事务的支持(@EnableTransactionManagement);还配置了一个jdbcTemplate。以下是 JdbcProperties 类的源码: - -```java -@ConfigurationProperties( - prefix = "spring.jdbc" -) -public class JdbcProperties { - private final JdbcProperties.Template template = new JdbcProperties.Template(); - - public JdbcProperties() { - } - - public JdbcProperties.Template getTemplate() { - return this.template; - } - - public static class Template { - private int fetchSize = -1; - private int maxRows = -1; - @DurationUnit(ChronoUnit.SECONDS) - private Duration queryTimeout; - ... - } -} -``` - - - -#### 对 JPA 的自动配置 - -Spring Boot 对 JPA 的自动配置放置在 org.springframework.boot.autoconfiguration.orm.jpa 下,从HibernateJpaAutoConfiguration 可以看出,Spring Boot 默认的 JPA 实现是Hibernate。 - -配置 JPA 可以在 application.properties 中使用 spring.jpa 为前缀的属性来配置。 - -以下是 JpaProperties 类的源码: - -```java -@ConfigurationProperties( - prefix = "spring.jpa" -) -public class JpaProperties { - private Map properties = new HashMap(); - private final List mappingResources = new ArrayList(); - private String databasePlatform; - private Database database; - private boolean generateDdl = false; - private boolean showSql = false; - private Boolean openInView; - ... -} -``` - -在 JpaBaseConfiguration 类中,Spring Boot 为我们创建了 transactionManager、jpaVendorAdapter、entityManagerFactory 等 bean。JpaBaseConfiguration 还有 getPackagesToScan 方法,可以自动扫描有@Entity 注解的实体类。 - -#### 对 Spring Data JPA 的自动配置 - -Spring Boot 对 Spring Data JPA 的自动配置放置在 org.springframework.boot.autoconfigure.data.jpa 中。JpaRepositoriesAutoConfiguration 是依赖于 HibernateJpaAutoConfiguration 配置的,且 Spring Boot 自动开启了对 Spring Data JPA 的支持,无需在配置类显式声明@EnableJpaRepositories。 - -在 Spring Boot 下使用 Spring Data JPA,首先在项目的 maven 依赖里添加 spring-boot-starter-data-jpa,然后只需定义DataSource、实体类和数据访问层,在需要使用数据访问的地方注入数据访问层的 Bean 即可。 - - - -## @Value原理 - -@Value的解析就是在bean初始化阶段。BeanPostProcessor定义了bean初始化前后用户可以对bean进行操作的接口方法,它的一个重要实现类AutowiredAnnotationBeanPostProcessor为bean中的@Autowired和@Value注解的注入功能提供支持。 - - - -## 启动过程 - -准备Environment——发布事件——创建上下文、bean——刷新上下文——结束。 - -构造SpringApplication的时候会进行初始化的工作,初始化的时候会做以下几件事: -判断运行环境类型,有三种运行环境:NONE 非 web 的运行环境、SERVLET 普通 web 的运行环境、REACTIVE 响应式 web 的运行环境 -加载 spring.factories 配置文件, 并设置 ApplicationContextInitializer -加载配置文件, 设置 ApplicationListener - - -SpringApplication构造完成之后调用run方法,启动SpringApplication,run方法执行的时候会做以下几件事: -构造一个StopWatch,观察SpringApplication的执行 -找出SpringApplicationRunListener,用于监听SpringApplication run方法的执行。监听的过程中会封装SpringApplicationEvent事件,然后使用ApplicationEventMulticaster广播出去,应用程序监听器ApplicationListener会监听到这些事件 -发布starting事件 -加载配置资源到environment,包括命令行参数、application.yml等 -发布environmentPrepared事件 -创建并初始化ApplicationContext,设置environment,加载配置 -refresh ApplicationContext -- 设置beanFactory -- 调用BeanFactoryPostProcessors -- 初始化消息源 -- 初始化事件广播器(initApplicationEventMulticaster) -- 调用onRefresh()方法,默认是空实现 -- 注册监听器 -- 实例化non-lazy-init单例 -- 完成refresh -- 发布ContextRefreshedEvent事件 - -发布started事件,启动结束 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/SpringMVC.md" "b/\346\241\206\346\236\266/SpringMVC.md" deleted file mode 100644 index 79c40cc..0000000 --- "a/\346\241\206\346\236\266/SpringMVC.md" +++ /dev/null @@ -1,1602 +0,0 @@ - - - - -- [简介](#%E7%AE%80%E4%BB%8B) -- [Spring MVC处理流程](#spring-mvc%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B) -- [Spring MVC和Struts的区别](#spring-mvc%E5%92%8Cstruts%E7%9A%84%E5%8C%BA%E5%88%AB) -- [Spring MVC环境搭建](#spring-mvc%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA) -- [处理器映射器和适配器](#%E5%A4%84%E7%90%86%E5%99%A8%E6%98%A0%E5%B0%84%E5%99%A8%E5%92%8C%E9%80%82%E9%85%8D%E5%99%A8) - - [非注解的处理器映射器和适配器](#%E9%9D%9E%E6%B3%A8%E8%A7%A3%E7%9A%84%E5%A4%84%E7%90%86%E5%99%A8%E6%98%A0%E5%B0%84%E5%99%A8%E5%92%8C%E9%80%82%E9%85%8D%E5%99%A8) - - [注解的处理器映射器和适配器](#%E6%B3%A8%E8%A7%A3%E7%9A%84%E5%A4%84%E7%90%86%E5%99%A8%E6%98%A0%E5%B0%84%E5%99%A8%E5%92%8C%E9%80%82%E9%85%8D%E5%99%A8) -- [前端控制器](#%E5%89%8D%E7%AB%AF%E6%8E%A7%E5%88%B6%E5%99%A8) - - [对静态资源的处理](#%E5%AF%B9%E9%9D%99%E6%80%81%E8%B5%84%E6%BA%90%E7%9A%84%E5%A4%84%E7%90%86) -- [视图解析器](#%E8%A7%86%E5%9B%BE%E8%A7%A3%E6%9E%90%E5%99%A8) - - [AbstractCachingViewResolver](#abstractcachingviewresolver) - - [UrlBasedViewResolver](#urlbasedviewresolver) - - [InternalResourceViewResolver](#internalresourceviewresolver) - - [XmlViewResolver](#xmlviewresolver) - - [BeanNameViewResolver](#beannameviewresolver) - - [ResourceBundleViewResolver](#resourcebundleviewresolver) - - [FreeMarkerViewResolver](#freemarkerviewresolver) -- [请求映射](#%E8%AF%B7%E6%B1%82%E6%98%A0%E5%B0%84) -- [参数绑定](#%E5%8F%82%E6%95%B0%E7%BB%91%E5%AE%9A) - - [简单类型参数绑定](#%E7%AE%80%E5%8D%95%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0%E7%BB%91%E5%AE%9A) - - [包装类型参数绑定](#%E5%8C%85%E8%A3%85%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0%E7%BB%91%E5%AE%9A) - - [集合类型参数绑定](#%E9%9B%86%E5%90%88%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0%E7%BB%91%E5%AE%9A) -- [Converter和Formatter](#converter%E5%92%8Cformatter) - - [Converter](#converter) - - [Formatter](#formatter) -- [验证器](#%E9%AA%8C%E8%AF%81%E5%99%A8) - - [使用Validator接口进行验证](#%E4%BD%BF%E7%94%A8validator%E6%8E%A5%E5%8F%A3%E8%BF%9B%E8%A1%8C%E9%AA%8C%E8%AF%81) - - [使用JSR-303 Validation进行验证](#%E4%BD%BF%E7%94%A8jsr-303-validation%E8%BF%9B%E8%A1%8C%E9%AA%8C%E8%AF%81) - - [自定义限制类型的注解](#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%99%90%E5%88%B6%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%B3%A8%E8%A7%A3) - - [分组校验](#%E5%88%86%E7%BB%84%E6%A0%A1%E9%AA%8C) -- [RequestBody和RequestParam](#requestbody%E5%92%8Crequestparam) - - - -## 简介 - -Spring MVC是一种基于MVC架构模式的轻量级Web框架。 - -## Spring MVC处理流程 - -Spring MVC的处理过程: - -1. DispatcherServlet 接收用户的请求 -2. 找到用于处理request的 handler 和 Interceptors,构造成 HandlerExecutionChain 执行链 -3. 找到 handler 相对应的 HandlerAdapter -4. 执行所有注册拦截器的preHandler方法 -5. 调用 HandlerAdapter 的 handle() 方法处理请求,返回 ModelAndView -6. 倒序执行所有注册拦截器的postHandler方法 -7. 请求视图解析和视图渲染 - -![Spring MVC处理流程](https://img-blog.csdnimg.cn/20190125180502787.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -处理流程中各个组件的功能: - -- 前端控制器(DispatcherServlet):接收用户请求,给用户返回结果。 -- 处理器映射器(HandlerMapping):根据请求的url路径,通过注解或者xml配置,寻找匹配的Handler。 -- 处理器适配器(HandlerAdapter):Handler 的适配器,调用 handler 的方法处理请求。 -- 处理器(Handler):执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装到ModelAndView对象中。 -- 视图解析器(ViewResolver):将逻辑视图名解析成真正的视图View。 -- 视图(View):接口类,实现类可支持不同的View类型(JSP、FreeMarker、Excel等)。 - - - -## Spring MVC和Struts的区别 - -1. Spring MVC是基于方法开发,Struts2是基于类开发的。 - - Spring MVC会将用户请求的URL路径信息与Controller的某个方法进行映射,所有请求参数会注入到对应方法的形参上,生成Handler对象,对象中只有一个方法; - - Struts每处理一次请求都会实例一个Action,Action类的所有方法使用的请求参数都是Action类中的成员变量,随着方法增多,整个Action也会变得混乱。 -2. Spring MVC支持单例开发模式,Struts只能使用多例 - - - Struts由于只能通过类的成员变量接收参数,故只能使用多例。 -3. Struts2 的核心是基于一个Filter即StrutsPreparedAndExcuteFilter,Spring MVC的核心是基于一个Servlet即DispatcherServlet(前端控制器)。 -4. Struts处理速度稍微比Spring MVC慢,Struts使用了Struts标签,加载数据较慢。 - - -## Spring MVC环境搭建 - -导入jar包: - -```xml - - - 4.0.0 - - com.tyson - springmvc-demo - 1.0-SNAPSHOT - - - UTF-8 - yyyyMMdd - 5.0.9.RELEASE - 1.2.3 - 1.7.12 - 2.0 - 2.9.1 - - - - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - ch.qos.logback - logback-access - ${logback.version} - - - - org.codehaus.janino - janino - 2.6.1 - - - - org.projectlombok - lombok - 1.12.4 - - - - - org.springframework - spring-core - ${spring.version} - - - - org.springframework - spring-beans - ${spring.version} - - - - org.springframework - spring-context - ${spring.version} - - - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-test - ${spring.version} - - - - - - org.springframework - spring-web - ${spring.version} - - - - org.springframework - spring-webmvc - ${spring.version} - - - - - - jstl - jstl - 1.2 - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - javax.servlet - jsp-api - ${jsp.version} - - - - - com.fasterxml.jackson.core - jackson-databind - ${json.version} - - - - - -``` - -新建logback.xml用来配置日志: - -```xml - - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - -``` - -web.xml文件中添加Spring MVC的前端控制器,用于拦截符合配置的url请求。 - -``` - - - - springmvc - - org.springframework.web.servlet.DispatcherServlet - - - - contextConfigLocation - classpath:config/springmvc.xml - - - - - - 1 - - - - springmvc - *.action - - - - - default - /resources/* - - - - - CharacherEncodingFilter - org.springframework.web.filter.CharacterEncodingFilter - - encoding - utf-8 - - - - CharacherEncodingFilter - /* - - -``` - -编写核心配置文件springmvc.xml。 - -``` - - - - - - - - - - - - - - -``` -Handler类 -``` -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.mvc.Controller; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class UserController implements Controller { - - public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { - ModelAndView mv = new ModelAndView(); - mv.setViewName("/WEB-INF/hello.html"); - return mv; - } -} -``` - - - -## 处理器映射器和适配器 - -在Spring MVC核心jar包中有一个默认的配置文件**DispatcherServlet.properties**(org.springframework.wen.servlet包下),当核心配置文件springmvc.xml没有配置处理器映射器和适配器时,会使用默认配置。 - - - -### 非注解的处理器映射器和适配器 -常用的处理器映射器有BeanNameUrlHandlerMapping,SimpleUrlHandlerMapping,ControllerClassNameHandlerMapping。 -``` - - -``` -``` - - - - - userController - - - - -``` -常用的处理器适配器有SimpleControllerHandlerAdapter,HttpRequestHandlerAdapter,AnnotationMethodHandlerAdapter。 -``` - - -``` - -### 注解的处理器映射器和适配器 -方式一(常用的配置方式):annotation-driven标签会自动注册RequestMappingHandlerMapping与RequestMappingHandlerAdapter两个Bean,这是Spring MVC为@Controller分发请求所必需的,并且提供了数据绑定支持,@NumberFormat支持,@DateTimeFormat支持,@Valid支持读写XML的支持(JAXB)和读写JSON的支持(默认Jackson)等功能。 -``` - -``` -方式二:必须保证基于注解的处理器映射器和适配器成对配置,否则没有效果。 -``` - - - - -``` - -使用了注解的处理器映射器和适配器,则Handler无需实现任何接口,也不用在xml中配置任何信息,只需在Handler处理器类上添加相应的注解即可(@Controller,@RequestMapping)。 - -为了让注解的处理器映射器和适配器找到注解的Handler,需要在springmvc.xml中声明相关的bean信息。有两种配置方式: -``` - -``` -扫描配置,对包下所有的类进行扫描,找出所有使用了@Controller注解的Handler控制器类。 -``` - - -``` -Handler类 -``` -import com.tyson.po.User; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -@RequestMapping("/user") -public class UserController { - @RequestMapping("/findUser") - @ResponseBody - public User findUser() { - return new User("666", "tyson"); - } -} -``` - - -## 前端控制器 -前端控制器DispatcherServlet类最核心的方法是doDispatch()。 - -在web.xml中配置了名为"springmvc"的Servlet,拦截以".action"结尾的url请求。当Web应用接收到这种请求时,会调用前端控制器。 -``` - protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { - HttpServletRequest processedRequest = request; - HandlerExecutionChain mappedHandler = null; - boolean multipartRequestParsed = false; - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - - try { - try { - ModelAndView mv = null; - Exception dispatchException = null; - - try { - //检测request是否包含多媒体类型(File文件),并将request转化为processedRequest - processedRequest = this.checkMultipart(request); - //判断processedRequest是否为原始的request - multipartRequestParsed = processedRequest != request; - mappedHandler = this.getHandler(processedRequest); - if (mappedHandler == null || mappedHandler.getHandler() == null) { - this.noHandlerFound(processedRequest, response); - return; - } - - HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); - String method = request.getMethod(); - boolean isGet = "GET".equals(method); - if (isGet || "HEAD".equals(method)) { - long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); - } - - if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { - return; - } - } - - if (!mappedHandler.applyPreHandle(processedRequest, response)) { - return; - } - - mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); - if (asyncManager.isConcurrentHandlingStarted()) { - return; - } - - this.applyDefaultViewName(processedRequest, mv); - mappedHandler.applyPostHandle(processedRequest, response, mv); - } catch (Exception var19) { - dispatchException = var19; - } - //处理ModelAndView,包含render()方法 - this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); - } catch (Exception var20) { - this.triggerAfterCompletion(processedRequest, response, mappedHandler, var20); - } catch (Error var21) { - this.triggerAfterCompletionWithError(processedRequest, response, mappedHandler, var21); - } - - } finally { - if (asyncManager.isConcurrentHandlingStarted()) { - if (mappedHandler != null) { - mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); - } - } else if (multipartRequestParsed) { - this.cleanupMultipart(processedRequest); - } - - } - } -``` - -### 对静态资源的处理 - -web.xml中DispatcherServlet的配置如下: - -```xml - - springmvc - - org.springframework.web.servlet.DispatcherServlet - - - - contextConfigLocation - classpath:config/springmvc.xml - - - - - - 1 - - - - springmvc - / - -``` - -经测试,`/`会拦截\*.html请求,不会拦截\*.jsp请求。"/"优先级最低,故\*.jsp请求会先被默认的jsp Servlet处理,不会被dispatcherServlet拦截。而\*.html没有默认的Servlet可以处理,会被dispatcherServlet拦截。 - -而`/*`则会拦截所有请求,包括\*.html和\*.jsp。 - -`*.action`则只会拦截后缀为action的请求,不会拦截静态资源的请求。 - -"/"和"/\*"的区别在于"/\*"的优先级高于"/路径"和"\*.后缀"的路径,而"/"在所有的匹配路径中,优先级最低,即当别的路径都无法匹配时,"/"所匹配的servlet才会进行相应的请求资源处理。 - -```xml - - default - org.apache.catalina.servlets.DefaultServlet - - debug - 0 - - - listings - false - - 1 - -``` - - - -```xml - - jsp - org.apache.jasper.servlet.JspServlet - - fork - false - - - xpoweredBy - false - - 3 - -``` - - - -```xml - - - default - / - - - - - jsp - *.jsp - *.jspx - -``` - - - - - -静态资源的处理: - -方法一:激活Tomcat的default Servlet来处理静态资源 - -```xml - - - default - *.jsp - *.html - *.js - - - - springmvc - / - -``` - -default Servlet无法解析jsp页面,直接输出html源码。 - -![default servlet处理静态资源](https://img-blog.csdnimg.cn/20190127162223870.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -方式二:使用`` - -请求的url若是静态资源请求,则转由org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler 处理并返回,否则才由DispatcherServlet处理。DefaultServletHttpRequestHandler使用的是各个Servlet容器自己默认的Servlet(如jsp servlet)。 - - - -方式三:Spring3.0.4以后版本`` - -```xml - - - -``` - -`` 将静态资源的处理经由 Spring MVC 框架交回 Web 应用服务器处理。而 ` `更进一步,由 Spring MVC 框架自行处理静态资源。 - -` `允许静态资源放在任何地方,如 WEB-INF 目录下、类路径,甚至 JAR 包中。可通过 cache-period 设置客户端数据缓存时间。 - -使用` `元素,把 mapping 的 URI 注册到 SimpleUrlHandlerMapping的urlMap 中, -key 为 mapping 的 URI pattern值,而 value为 ResourceHttpRequestHandler, -这样就巧妙的把对静态资源的访问由 HandlerMapping 转到 ResourceHttpRequestHandler 处理并返回,所以就支持classpath目录和jar包内静态资源的访问。 - - - - -## 视图解析器 -视图解析器ViewResolver的作用是把逻辑视图名称解析成具体的View对象,让View对象去解析视图,并将带有数据的视图反馈给客户端。 - -常用的视图解析器类有AbstractCachingViewResolver,UrlBasedViewResolver,InternalResourceViewResolver,XmlViewResolver,BeanNameViewResolver,ResorceBundleViewResolver,FreeMarkerViewResolver和VelocityViewResolver。 - - -### AbstractCachingViewResolver -抽象类,实现了该抽象类的视图解析器会将其曾经解析过的视图进行缓存。 - - -### UrlBasedViewResolver -继承了AbstractCachingViewResolver,通过拼接资源的uri路径来展示视图。 -``` - - - - - - - -``` -UrlBasedViewResolver支持返回的视图名称中含有"redirect:"和"forward:"前缀,支持视图的重定向和内部跳转设置。 - - -### InternalResourceViewResolver -内部资源视图解析器,最常用的视图解析器类型。它是UrlBasedViewResolver的子类。 - -特点:它会把返回的视图名称自动解析为InternalResourceView类型的对象,而InternalResourceView会把Controller处理器方法返回的模型属性都存放到对应的request属性中,然后通过RequestDispatcher在服务端把请求以forward的方式跳转到目标url。 -``` - - - - - -``` -当Controller处理器方法返回名为"login"的视图时,InternalResourceViewResolver会将"login"解析成一个InternalResourceView对象,然后将返回的模型数据存放到对应的HttpServletRequest属性中,最后利用RequestDispatcher把请求forward到"/WEB-INF/jsp/login.jsp"上。 - - -### XmlViewResolver -继承了AbstractCachingViewResolver,使用XmlViewResolver需要添加一个xml配置文件,用来定义视图的bean对象。当获得Controller方法返回的视图名称后,XmlViewResolver会在指定的配置文件中寻找对应名称的bean配置,解析并处理该视图。 -``` - - - - - - - -``` -/WEB-INF/config/view.xml,其遵循的DTD规则和Spring的bean工厂配置文件相同。 -``` - - - - - - -``` - - -### BeanNameViewResolver -通过把返回的逻辑视图名称去匹配定义好的视图bean对象。BeanNameViewResolver要求视图bean对象都定义在Spring的application context中。 -``` - - - - - - -``` - -MyView 定义: - -```java -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.Map; - -public class MyView implements View { - - public String getContentType() { - return "text/html"; - } - - public void render(Map map, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { - httpServletResponse.getWriter().print("my view"); - } -} -``` - -MyViewController - -```java -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -public class MyViewController { - @RequestMapping("/myView") - public String myView() { - return "myView"; - } -} -``` - -Spring MVC 根据返回的逻辑视图名去寻找视图 bean 对象。 - - -### ResourceBundleViewResolver -继承了AbstractCachingViewResolver,需要一个properties文件定义逻辑视图名和View对象的对应关系,配置文件需放在classpath根目录下。 -``` - - - - viewResource - - - -``` -classpath:viewResource.properties -``` -login.(class)=org.springframework.web.servlet.view.InternalResourceView -login.url=/hello.html -``` - - -### FreeMarkerViewResolver -FreeMarkViewResolver会将Controller返回的逻辑视图信息解析成FreeMarkerView类型。它是UrlBasedViewResolver的子类。 - -FreeMarkerViewResolver解析逻辑视图后,返回FreeMarker模板,该模板负责将模型数据合并到模板中,从而生成标准输出(html、xml等)。 - - -springmvc.xml配置 -``` - - - - - - - - - -``` -模板文件:web\WEB-INF\freemarker\template\fm_freemarker.ftl -``` - - - FreeMarker - - -

    hello, FreeMarker

    - - - -``` -建议在ViewResolver中,将InternalResourceViewResolver解析器优先级设置为最低,因为该解析器能解析所有类型的视图,并返回一个不为空的View对象。 - - -## 请求映射 -在使用annotation-driven标签时,处理器Handler的类型要符合annotation-driven标签指定的处理器映射器和适配器的类型。annotation-driven标签指定的默认处理器映射器和适配器在Spring3.1之前为DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter,在Spring3.1之后为RequestMappingHandlerMapping和RequestMappingHandlerAdapter。 - -每个处理器适配器都实现了HandlerAdapter接口。 -``` -public interface HandlerAdapter { - //检测Handler是否是支持的类型 - boolean supports(Object var1); - - ModelAndView handle(HttpServletRequest var1, HttpServletResponse var2, Object var3) throws Exception; - - long getLastModified(HttpServletRequest var1, Object var2); -} -``` -RequestMappingHandlerAdapter本身没有重写supports方法,它的supports方法定义在父类AbstractHandlerMethodAdapter中。 -``` - public final boolean supports(Object handler) { - //支持HandlerMethod类型的Handler - return handler instanceof HandlerMethod && this.supportsInternal((HandlerMethod)handler); - } -``` -RequestMappingHandlerAdapter支持HandlerMethod类型的Handler,HandlerMethod可以访问方法参数、方法返回值和方法的注解,所以它支持带有注解信息的Handler类。 - -@RequestMapping作用是为控制器指定可以处理那些url请求,该注解可以放在类上或者方法上。method用于指定处理哪些HTTP方法。 -``` -@RequestMapping(value = "/findUser", method={RequestMethod.GET, RequestMethod.POST}) -``` -@RequestMapping的param属性可以指定某一种参数名类型,当请求数据中包含该名称的请求参数时,才能进行相应,否则拒绝此次请求。 -``` -@RequestMapping(value = "/findUser", param = "username") -``` -@RequestMapping的headers属性可以指定某一种请求头类型。 -``` -@RequestMapping(value = "/findUser", headers = "Content-Type:text/html;charset=UTF-8") -``` -consumes属性表示处理请求的提交内容类型(Content-Type),例如"application/json,text/html"。而produces表示返回的内容类型,仅当request请求头中的Accept包含该指定类型时才会返回。 -``` -//仅处理reqeust的Content-Type为"application/json"类型的请求 -@RequestMapping(value = "/findUser", consumes = "application/json") - -@RequestMapping(value = "/findUser", produces = "application/json") -``` - - -## 参数绑定 -当用户发送请求时,前端控制器会请求处理器映射器返回一个处理器链,然后请求处理器适配器执行相应的Handler。此时,HandlerAdapter会调用Spring MVC提供的参数绑定组件将请求的key/value数据绑定到Controller处理器方法对应的形参上。 - - -### 简单类型参数绑定 -通过RequestParam将某个请求参数绑定到方法的形参上。value属性不指定时,则请求参数名称要与形参名称相同。required参数表示是否必须传入。defaultValue参数可以指定参数的默认值。 -``` - @RequestMapping("/findUser") - @ResponseBody - public User findUser(@RequestParam(value = "user_id", required = true, defaultValue = "1") long id) { - LOGGER.info("user_id: " + id); - return new User(id , "tyson"); - } -``` - -路径变量。 - -``` - @RequestMapping(value = "/insertUser/{id}") - public String insertUser(@PathVariable long id) { - return "success"; - } -``` - - - -### 包装类型参数绑定 - -``` -
    - id: - name: - -
    -``` -name属性名称与User类属性对应,Spring MVC的HandlerAdapter会解析请求参数生成具体的实体类,将相关的属性值通过set方法绑定到实体类中。 -``` - @RequestMapping("/findUserByCondition") - @ResponseBody - public User findUserByCondition(User user) { - return user; - } -``` - - -### 集合类型参数绑定 -``` - @RequestMapping("/findUsers") - @ResponseBody - public List findUsers(UserList userList) { - List users = userList.getUsers(); - for(User user : users) { - LOGGER.info("user_id: " + user.getId() + " " + "user_name: " + user.getName()); - } - return users; - } -``` - -``` -
    - - - - - -
    -``` -包装类中定义的List属性的名称要与前端页面的集合名一致。 -``` -public class UserList { - private List users; - - public List getUsers() { - return users; - } - - public void setUsers(List users) { - this.users = users; - } -} -``` - - - - -## Converter和Formatter - -### Converter - -将字符串转化成日期格式,可通过编写Converter接口的实现类来实现。 - -```java -import org.springframework.core.convert.converter.Converter; - -import java.text.SimpleDateFormat; -import java.util.Date; - -public class StringToDateConverter implements Converter { - private String dataPattern; - //利用传给构造器的日期样式,将String转化成Date - public StringToDateConverter(String dataPattern) { - this.dataPattern = dataPattern; - } - - public Date convert(String s) { - try { - SimpleDateFormat dateFormat = new SimpleDateFormat(dataPattern); - dateFormat.setLenient(false); - return dateFormat.parse(s); - } catch(Exception e) { - throw new IllegalArgumentException("invalid date format"); - } - } -} -``` - -在Spring MVC配置文件编写一个ConversionService bean。这个bean需包含一个converters属性。 - -```xml - - - - - - - - - -``` - -然后给annotation-driven标签的converter-service属性赋bean名称。 - -```xml - -``` - -### Formatter - -Formatter和Converter一样,也是将一种类型转化为另一种类型。但Formatter的源类型必须是String,而Converter适用于各种类型的源类型。Formatter更适合于web层。 - -```java -import org.springframework.format.Formatter; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -public class DateFormatter implements Formatter { - private static final Logger LOGGER = LoggerFactory.getLogger(Formatter.class); - private String datePattern; - private SimpleDateFormat dateFormat; - - public DateFormatter(String datePattern) { - this.datePattern = datePattern; - this.dateFormat = new SimpleDateFormat(datePattern); - dateFormat.setLenient(false); - } - - public Date parse(String s, Locale locale) throws ParseException { - try { - System.out.println(s); - return dateFormat.parse(s); - } catch(IllegalArgumentException ex) { - throw new IllegalArgumentException("Illegal date format. Please use \"" + datePattern + "\""); - } - } - - public String print(Date date, Locale locale) { - return dateFormat.format(date); - } -} -``` - -spring配置文件。 - -```xml - - - - - - - - - - - -``` - -## 验证器 - - -本节内容参考:[SpringMVC介绍之Validation](https://elim.iteye.com/blog/1812584) - -### 使用Validator接口进行验证 - -需要进行验证的实体类 - -```java -public class User { - private Long id; - - private String name; - - public User(Long id, String name) { - this.id = id; - this.name = name; - } - - public long getId() { - return id; - } - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} -``` - - - -Spring MVC提供了Validator接口,我们可以通过实现该接口来定义自己对实体对象的验证。 - -```java -import com.tyson.po.User; -import org.springframework.validation.Errors; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; - -public class UserValidator implements Validator { - - /** - * 判断当前Validator实现类是否支持校验当前需要校验的实体类 - * UserValidator只支持对User对象的校验 - */ - public boolean supports(Class aClass) { - return User.class.equals(aClass); - } - - /** - * @param errors 存放错误信息 - */ - public void validate(Object o, Errors errors) { - ValidationUtils.rejectIfEmpty(errors, "id", null, "id is empty"); - User user = (User)o; - if(user.getName().length() <= 4) { - errors.rejectValue("name", null, "name's length must be longer than 4"); - } - } -} -``` - -使用UserValidator校验User对象,使用DataBinder设定当前Controller由哪个Validator校验。 - -```java -import com.tyson.po.User; -import com.tyson.validator.UserValidator; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.validation.DataBinder; -import org.springframework.validation.ObjectError; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.RequestMapping; - -import javax.validation.Valid; -import java.util.List; - -@Slf4j -@Controller -public class UserController { - @InitBinder - public void initBinder(DataBinder binder) { - binder.setValidator(new UserValidator()); - } - - /** - *用@Valid标识需要校验的参数user,否则Spring不会对它进行校验 - * BindingResult参数告诉Spring数据校验单的错误由我们自己处理,否则Spring会直接抛出异常 - * BindingResult参数必须紧挨着@Valid参数,有多少个@Valid参数就有多少个BindingResult参数 - */ - @RequestMapping("/login") - public String login(@Valid User user, BindingResult bindingResult) { - if(bindingResult.hasErrors()) { - List errors = bindingResult.getAllErrors(); - for(ObjectError error : errors) { - log.info(error.toString()); - } - return "error"; - } - - return "success"; - } -} -``` - -在Controller类中通过@InitBinder标记的方法只有在请求当前Controller的时候才会被执行,所以其中定义的Validator也只能在当前Controller中使用,如果我们希望一个Validator对所有的Controller都起作用的话,我们可以通过WebBindingInitializer的initBinder方法来设定了。另外,在SpringMVC的配置文件中通过mvc:annotation-driven的validator属性也可以指定全局的Validator。 - -```xml - - - - - - - -``` - -非注解方式编写的适配器。 - -```xml - - - - - - - - - - -``` - - - -### 使用JSR-303 Validation进行验证 - -JSR-303是一个数据验证的规范,Spring没有对这一规范进行实现,当我们在Spring MVC使用JSR-303的时候需要提供一个对JSR-303规范的实现,Hibernate Validator实现了这一规范。 - -JSR303的校验是基于注解的,它的内部定义了一系列限制注解,我们只需要把这些注解标注在需要进行校验的实体类的属性或者对应的getter方法上面。 - -首先引入依赖。 - -```xml - - - org.hibernate - hibernate-validator - 5.0.2.Final - - - javax.validation - validation-api - 1.1.0.Final - -``` - -在SpringMVC的配置文件中引入MVC Namespace,并加上``,此时便可以使用JSR-303来进行实体对象的验证。 - -```xml - - - - - -``` - -实体类,其中@NotBlank是Hibernate Validator的扩展。 - -```java -import org.hibernate.validator.constraints.NotBlank; - -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; - -public class User { - private String name; - private String password; - private int age; - - @NotBlank(message = "用户名不能为空") - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @NotNull(message = "密码不能为null") - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - @Min(value = 10, message = "年龄最小为10") - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @Override - public String toString() { - return name + ":" + age; - } -} -``` - -Controller类 - -```java -import com.tyson.po.User; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.RequestMapping; - -import javax.validation.Valid; - -@Controller -public class UserController { - @RequestMapping("/login") - public String login(@Valid User user, BindingResult bindingResult) { - if(bindingResult.hasErrors()) { - return "error"; - } - return "success"; - } -} -``` - -**JSR-303原生支持的限制有如下几种:** - -| **限制** | 说明 | -| ----------------------------- | ------------------------------------------------------------ | -| **@Null** | 限制只能为null | -| **@NotNull** | 限制必须不为null | -| **@AssertFalse** | 限制必须为false | -| **@AssertTrue** | 限制必须为true | -| **@DecimalMax(value)** | 限制必须为一个不大于指定值的数字 | -| **@DecimalMin(value)** | 限制必须为一个不小于指定值的数字 | -| **@Digits(integer,fraction)** | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction | -| **@Future** | 限制必须是一个将来的日期 | -| **@Max(value)** | 限制必须为一个不大于指定值的数字 | -| **@Min(value)** | 限制必须为一个不小于指定值的数字 | -| **@Past** | 限制必须是一个过去的日期 | -| **@Pattern(value)** | 限制必须符合指定的正则表达式 | -| **@Size(max,min)** | 限制字符长度必须在min到max之间 | - -#### 自定义限制类型的注解 - -除了JSR-303原生支持的限制类型之外我们还可以定义自己的限制类型。定义自己的限制类型首先我们得定义一个该种限制类型的注解,而且该注解需要使用@Constraint标注。下面定义一个表示金额的限制类型。 - -```java -package com.tyson.validator; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.*; - -@Documented -@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = MoneyValidator.class) -public @interface Money { - String message() default "不是金额形式"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} -``` - -@Constraint注解的validatedBy属性用于指定我们定义的当前限制类型需要被哪个ConstraintValidator进行校验。在定义自己的限制类型的注解时有三个属性是必须定义的,message、groups和payload属性。 - -接下来定义限制类型校验类MoneyValidator,限制类型校验类必须实现接口javax.validation.ConstraintValidator,并实现它的initialize和isValid方法。 - -```java -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.regex.Pattern; - -public class MoneyValidator implements ConstraintValidator { - private String moneyReg = "^\\d+(\\.\\d{1,2})?$"; //表示金额的正则表达式 - private Pattern moneyPattern = Pattern.compile(moneyReg); - - /** - * 通过initialize可以获取限制类型 - */ - public void initialize(Money money) { - } - - public boolean isValid(Double value, ConstraintValidatorContext constraintValidatorContext) { - if(value == null) { - return false; - } - return moneyPattern.matcher(value.toString()).matches(); - } -} -``` - -同样的方法定义自己的@Min限制类型和对应的MinValidator校验器。 - -```java -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.*; - -@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Constraint( - validatedBy = {MinValidator.class} -) -public @interface Min { - int value() default 0; - - String message(); - - Class[] groups() default {}; - - Class[] payload() default {}; -} - -``` - -isValid方法的第一个参数正是对应的当前需要校验的数据的值,而它的类型也**正是对应的我们需要校验的数据的数据类型。**这两者的数据类型必须保持一致,否则Spring会提示找不到对应数据类型的ConstraintValidator。 - -```java -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -public class MinValidator implements ConstraintValidator { - private int minValue; - - public void initialize(Min min) { - minValue = min.value(); - } - - public boolean isValid(Integer val, ConstraintValidatorContext constraintValidatorContext) { - return val >= minValue; - } -} -``` - -下面是使用了@Min和@Money限制的一个实体类 - -```java -import com.tyson.validator.Min; -import com.tyson.validator.Money; - -public class Worker { - private int age; - private Double salary; - - @Min(value = 10, message = "最小年龄是10") - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @Money(message = "标准的金额格式是xxx.xx") - public Double getSalary() { - return salary; - } - - public void setSalary(Double salary) { - this.salary = salary; - } -} -``` - -WorkerController类 - -```java -import javax.validation.Valid; -import java.util.List; - -@Slf4j -@Controller -public class WorkerController { - @RequestMapping("/addWorker") - public String addWorker(@Valid Worker worker, BindingResult bindingResult) { - if(bindingResult.hasErrors()) { - List errors = bindingResult.getAllErrors(); - for(ObjectError error : errors) { - log.info(error.toString()); - } - return "error"; - } - return "success"; - } -} -``` - -另外Spring对自定义JSR-303限制类型支持的新特性,那就是Spring支持往ConstraintValidator里面注入bean对象。 - -```java -public class MoneyValidator implements ConstraintValidator { - - private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式 - private Pattern moneyPattern = Pattern.compile(moneyReg); - private UserController controller; - - public void initialize(Money money) {} - - public boolean isValid(Double value, ConstraintValidatorContext arg1) { - if (value == null) - return true; - return moneyPattern.matcher(value.toString()).matches(); - } - - public UserController getController() { - return controller; - } - - @Resource - public void setController(UserController controller) { - this.controller = controller; - } - -} -``` - -#### 分组校验 - -POJO有多个属性,分组校验可以使得Controller的方法只校验POJO的某个属性,而不是校验所有的属性。 - -```java -import com.tyson.validator.Min; -import com.tyson.validator.Money; -import com.tyson.validator.ValidationGroup1; -import com.tyson.validator.ValidationGroup2; - -public class Worker { - private int age; - private Double salary; - - @Min(value = 10, message = "最小年龄是10", groups = {ValidationGroup1.class}) - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - @Money(message = "标准的金额格式是xxx.xx", groups = {ValidationGroup2.class}) - public Double getSalary() { - return salary; - } - - public void setSalary(Double salary) { - this.salary = salary; - } -} -``` - -分组接口ValidationGroup1、ValidationGroup2,不需要写实现。 - -```java -public interface ValidationGroup1 {} -``` - -WorkerController类,@Validated(value = {ValidationGroup1.class})使得addWorker方法只校验ValidationGroup1这个分组的校验注解,即只校验年龄,不校验工资格式。 - -```java -import com.tyson.po.Worker; -import com.tyson.validator.ValidationGroup1; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Controller; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RequestMapping; -import java.util.List; - -@Slf4j -@Controller -public class WorkerController { - @RequestMapping("/addWorker") - public String addWorker(@Validated(value = {ValidationGroup1.class}) Worker worker, BindingResult bindingResult) { - if(bindingResult.hasErrors()) { - List errors = bindingResult.getAllErrors(); - for(ObjectError error : errors) { - log.info(error.toString()); - } - return "error"; - } - return "success"; - } -} -``` - - - -补充:@Valid和@Validated的区别:@Valid可以用于对象属性上,可嵌套验证,@Validated不可以嵌套验证;@Validated提供分组功能,而@Valid不支持分组功能。 - -```java -public class Item { - - @NotNull(message = "id不能为空") - @Min(value = 1, message = "id必须为正整数") - private Long id; - - @Valid // 嵌套验证必须用@Valid - @NotNull(message = "props不能为空") - @Size(min = 1, message = "props至少要有一个自定义属性") - private List props; -} -``` - -```java -public class Prop { - - @NotNull(message = "pid不能为空") - @Min(value = 1, message = "pid必须为正整数") - private Long pid; - - @NotNull(message = "vid不能为空") - @Min(value = 1, message = "vid必须为正整数") - private Long vid; - - @NotBlank(message = "pidName不能为空") - private String pidName; - - @NotBlank(message = "vidName不能为空") - private String vidName; -} -``` - - - -## RequestBody和RequestParam - -@RequestBody一般处理的是在ajax请求中声明contentType: "application/json; charset=utf-8"时候。也就是json数据或者xml数据。 - -@RequestParam一般就是在ajax里面没有声明contentType的时候,为默认的`x-www-form-urlencoded`格式时。 - - - diff --git "a/\346\241\206\346\236\266/SpringMVC\351\235\242\350\257\225\351\242\230.md" "b/\346\241\206\346\236\266/SpringMVC\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 0892077..0000000 --- "a/\346\241\206\346\236\266/SpringMVC\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,89 +0,0 @@ -## 说说你对 SpringMVC 的理解 - -SpringMVC是一种基于 Java 的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于Spring框架的一个模块。 - -它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful编程风格的请求。 - -## 什么是MVC模式? - -MVC的全名是`Model View Controller`,是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在需要改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间。 - -View,视图是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。MVC的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操纵的方式。 - -model,模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。 - -controller,控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。 - -## SpringMVC 有哪些优点? - -1. 与 Spring 集成使用非常方便,生态好。 -2. 配置简单,快速上手。 -3. 支持 RESTful 风格。 -4. 支持各种视图技术,支持各种请求资源映射策略。 - -## Spring MVC和Struts的区别 - -1. Spring MVC是基于方法开发,Struts2是基于类开发的。 - - Spring MVC会将用户请求的URL路径信息与Controller的某个方法进行映射,所有请求参数会注入到对应方法的形参上,生成Handler对象,对象中只有一个方法; - - Struts每处理一次请求都会实例一个Action,Action类的所有方法使用的请求参数都是Action类中的成员变量,随着方法增多,整个Action也会变得混乱。 -2. Spring MVC支持单例开发模式,Struts只能使用多例 - - - Struts由于只能通过类的成员变量接收参数,故只能使用多例。 -3. Struts2 的核心是基于一个Filter即StrutsPreparedAndExcuteFilter,Spring MVC的核心是基于一个Servlet即DispatcherServlet(前端控制器)。 -4. Struts处理速度稍微比Spring MVC慢,Struts使用了Struts标签,加载数据较慢。 - -## Spring MVC的工作原理 - -Spring MVC的工作原理如下: - -1. DispatcherServlet 接收用户的请求 -2. 找到用于处理request的 handler 和 Interceptors,构造成 HandlerExecutionChain 执行链 -3. 找到 handler 相对应的 HandlerAdapter -4. 执行所有注册拦截器的preHandler方法 -5. 调用 HandlerAdapter 的 handle() 方法处理请求,返回 ModelAndView -6. 倒序执行所有注册拦截器的postHandler方法 -7. 请求视图解析和视图渲染 - -![Spring MVC处理流程](https://img-blog.csdnimg.cn/20190125180502787.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -## Spring MVC的主要组件? - -- 前端控制器(DispatcherServlet):接收用户请求,给用户返回结果。 -- 处理器映射器(HandlerMapping):根据请求的url路径,通过注解或者xml配置,寻找匹配的Handler。 -- 处理器适配器(HandlerAdapter):Handler 的适配器,调用 handler 的方法处理请求。 -- 处理器(Handler):执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装到ModelAndView对象中。 -- 视图解析器(ViewResolver):将逻辑视图名解析成真正的视图View。 -- 视图(View):接口类,实现类可支持不同的View类型(JSP、FreeMarker、Excel等)。 - -## Spring MVC的常用注解由有哪些? -- @Controller:用于标识此类的实例是一个控制器。 -- @RequestMapping:映射Web请求(访问路径和参数)。 -- @ResponseBody:注解返回数据而不是返回页面 -- @RequestBody:注解实现接收 http 请求的 json 数据,将 json 数据转换为 java 对象。 -- @PathVariable:获得URL中路径变量中的值 -- @RestController:@Controller+@ResponseBody - -## Spring MVC的异常处理 - -可以将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可。 - -- 使用系统定义好的异常处理器 SimpleMappingExceptionResolver -- 使用自定义异常处理器 -- 使用异常处理注解 - -## SpringMVC 用什么对象从后台向前台传递数据的? - -1. 将数据绑定到 request; -2. 返回 ModelAndView; -3. 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到; -4. 绑定数据到 Session中。 - -## RequestBody和RequestParam的区别 - -@RequestBody一般处理的是在ajax请求中声明contentType: "application/json; charset=utf-8"时候。也就是json数据或者xml数据。 - -@RequestParam一般就是在ajax里面没有声明contentType的时候,为默认的`x-www-form-urlencoded`格式时。 - - - -![](http://img.dabin-coder.cn/image/20220612101342.png) diff --git "a/\346\241\206\346\236\266/Spring\345\256\236\346\210\230.md" "b/\346\241\206\346\236\266/Spring\345\256\236\346\210\230.md" deleted file mode 100644 index f56fe59..0000000 --- "a/\346\241\206\346\236\266/Spring\345\256\236\346\210\230.md" +++ /dev/null @@ -1,3228 +0,0 @@ -## Spring的核心 - -相对于EJB(Enterprise JaveBean),Spring提供了更加轻量级和简单的编程模型。 - -Spring可以简化Java开发:1.基于pojo的轻量级和最小侵入式编程;2.通过依赖注入和面向接口实现松耦合;3.基于切面和惯例进行声明式编程;4.通过切面和模板减少样板式代码。 - -1. 非侵入编程 - -很多框架通过强迫应用继承它们的类或实现它们的接口从而导致应用与框架绑死。Spring竭力避免自身API与应用代码的耦合。Spring不会强迫你实现Spring规范的接口或继承Spring规范的类。 - -2. 依赖注入 - -假如两个类相互协作完成特定的业务,按照传统的做法,每个对象负责管理与自己相互协作的对象的引用,这会导致高度耦合。 - -```java -public class Knight { - private RescueQuest quest; - - public Knight() { - this.quest = new RescueQuest(); //勇士只能进行救援工作 - } - - public void embarkOnQuest() { - quest.embark(); - } -} -``` - -通过DI,对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到对象当中。对象只通过接口来表明依赖关系,可以传入不同的具体实现。 - -```java -public class Knight { - private Quest quest; - - //通过构造器注入依赖,可以传入任何Quest接口的实现类,如RescueQuest,SlayDragonQuest等 - public Knight(Quest quest) { - this.quest = quest; - } - - public void embarkOnQuest() { - quest.embark(); - } -} -``` - -3. 面向切面编程 - -aop允许你将遍布应用各处的功能(日志,事务管理)分离出来形成可重用的组件。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216210355438.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -4. 模板技术 - -编程中会遇到许多样板式代码,如使用JDBC访问数据库查询数据时,首先需要创建数据库连接(Connection),然后再创建一个语句对象(PrepareStatement),然后进行查询,可能需要处理SQL异常,最后关闭数据库连接、语句和结果集。Spring通过模板封装来消除样板式代码。Spring的JDBCTemplate可以避免传统的JDBC样板代码。 - - - -## 装配bean - -Spring bean装配机制:1.自动装配;2.使用spring基于Java的配置(JavaConfig);3.xml配置。 - -我们应当尽可能使用自动装配的机制,显式配置越少越好。当自动装配行不通时,如使用第三方库的组件装配到应用,则需采用显式装配的方式,此时推荐使用类型安全并且比xml更为强大的JavaConfig。只有当需要使用便利的xml命名空间,并且在JavaConfig中没有同样的实现时,才应该使用xml。 - -### 自动装配bean - -实现自动装配bean 需要两点:1.开启组件扫描;2.给bean加自动装配的注解。 - -```java -public interface CD { - public void play(); -} - -@Component("cd") -public class EasonCD implements CD { - private String song = "Ten years"; - private String singer = "Eason"; - - @Override - public void play() { - System.out.println("singer--" + singer + " sings " + song); - } -} -``` - -@Component表明该类会作为组件类,并告知Spring为这个类创建bean。@Component("cd")可以为bean命名,没有配置默认是类名首字母小写。@Named注解与@Component作用类似。 - -CDPlayer类中,在构造器上加了注解@Autowired,表明当Spring创建CDPlayer bean的时候,会通过这个构造器进行实例化并传入一个类型为CD的bean。@Inject注解和@Autowired作用类似。 - -```java -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -/** - * @Autowired,通过byType的形式自动装配,可以用在属性,构造器和setter方法上 - * required属性设为false,没有符合条件的bean也不会抛异常 - */ -@Component -public class CDPlayer implements MediaPlayer { - private CD cd; - - @Autowired - public CDPlayer(CD cd) { - this.cd = cd; - } - - @Override - public void play() { - cd.play(); - } -} -``` - -组件扫描默认不启动,需要显式配置一下Spring,开启组件扫描。通过@ComponentScan注解启动了组件扫描,扫描包下是否有@Component标注的类,若有,则会在Spring中自动为其创建bean。 - -```java -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; - -/** - * @Configuration用于定义配置类。该类应该包含Spring应用上下文如何创建bean的细节。 - * 通过@ComponentScan注解启动了组件扫描 - * 配置basePackages则扫描指定的包,没配置则默认扫描当前包 - * 配置basePackageClasses则通过扫描配置的类所在的包 - */ -@Configuration -//@ComponentScan(basePackages = {"com.tyson.pojo", "com.tyson.service"}) -@ComponentScan(basePackageClasses = {EasonCD.class}) -public class CDPlayerConfig { -} -``` - -#### 验证自动装配 - -```java -import com.tyson.config.CDPlayerConfig; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -/** - * @RunWith(SpringJUnit4ClassRunner.class)在测试开始时自动创建Spring应用上下文 - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = {CDPlayerConfig.class}) //开启组件扫描 -public class CDPlayerTest { - @Autowired - private CD cd; - @Autowired - private MediaPlayer player; - - @Test - public void cdShouldNotBeNull() { - Assert.assertNotNull(cd); - } - - @Test - public void play() { - player.play(); - } -} -``` - -也可以在applicationContext.xml使用\来启动组件扫描。 - -```xml - - - - - -``` - -测试代码。 - -```java -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - - -/** - * @RunWith(SpringJUnit4ClassRunner.class)在测试开始时自动创建Spring应用上下文 - */ -@RunWith(SpringJUnit4ClassRunner.class) -//@ContextConfiguration(classes = {CDPlayerConfig.class}) //开启组件扫描 -@ContextConfiguration(locations = {"classpath:applicationContext.xml"}) //开启组件扫描 -public class CDPlayerTest { - @Autowired - private CD cd; - @Autowired - private MediaPlayer player; - - @Test - public void cdShouldNotBeNull() { - Assert.assertNotNull(cd); - } - - @Test - public void play() { - player.play(); - } -} -``` - -### 通过Java代码装配bean - -有些情况下无法使用自动装配,如要将第三方类库的组件装配到我们的应用,便无法使用自动装配。下面使用JavaConfig显式配置Spring。(JavaConfig是配置代码,应该放在单独的包中,与其他应用程序逻辑分离开。) - -CDPlayerConfig配置类 - -```java -import com.tyson.pojo.CD; -import com.tyson.pojo.CDPlayer; -import com.tyson.pojo.EasonCD; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * @Configuration用于定义配置类,可替换xml配置文件。该类应该包含Spring应用上下文如何创建bean的细节。 - */ -@Configuration -public class CDPlayerConfig { - @Bean(name = "easonCD") //@Bean告诉spring将对象注册到spring容器 - public CD easonCD() { - return new EasonCD(); - } - - /** - * 推荐这种方式,不要求将CD声明到同一个配置文件 - * CD可以采用各种方式创建(自动扫描、配置类、JavaConfig),Spring都能将其传入到配置方法中 - */ - @Bean(name = "cdPlayer") - public CDPlayer cdPlayer(CD cd) { - return new CDPlayer(cd); - } - -/* @Bean(name = "cdPlayer") - public CDPlayer cdPlayer() { - return new CDPlayer(easonCD()); - }*/ -} -``` - -通过AnnotationConfigApplicationContext从Java配置类加载Spring应用上下文。 - -```java -//AnnotationConfigApplicationContext:从一个或多个基于Java的配置类加载Spring应用上下文 -@Test -public void play() { - ApplicationContext cxt = new AnnotationConfigApplicationContext(CDPlayerConfig.class); - CDPlayer cdPlayer = (CDPlayer)cxt.getBean("cdPlayer"); - cdPlayer.play(); -} -``` - -### 通过xml装配bean - -#### 构造器注入 - -借助构造器注入初始化bean有两种方式:constructor-arg元素或者Spring3.0引入的c-命名空间。 - -1. 将引用注入构造器。 - -EasonCD.java - -```java -public class EasonCD implements CD { - @Override - public void play() { - System.out.println("haha"); - } -} -``` - -CDPlayer.java - -```java -import org.springframework.beans.factory.annotation.Autowired; - -public class CDPlayer implements MediaPlayer { - private CD cd; - - /** - * 构造器注入 - */ - public CDPlayer(CD cd) { - this.cd = cd; - } - - @Override - public void play() { - cd.play(); - } -} -``` - -applicationContext.xml使用构造器注入创建bean。 - -```xml - - - - - - - - - - - - - -``` - -测试类 - -```java -/** - * AnnotationConfigApplicationContext:从一个或多个基于Java的配置类加载Spring应用上下文 - * ClassPathXmlApplicationContext:从类路径上的一个或多个xml配置文件中加载Spring应用上下文 - */ -@Test -public void play() { - ApplicationContext cxt = new ClassPathXmlApplicationContext("applicationContext.xml"); - CDPlayer cdPlayer = (CDPlayer)cxt.getBean("cdPlayer"); - cdPlayer.play(); -} -``` - -c-命名空间元素组成如下图,可以看到使用了构造器参数。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190219211708943.png) - -也可以使用索引识别构造器参数。 - -```xml - -``` - -2. 将字面量注入构造器 - -```java -public class PuthCD implements CD { - private String song; - private String note; - - public PuthCD(String song, String note) { - this.song = song; - this.note = note; - } - - @Override - public void play() { - System.out.println("Puth sings \"" + song + "\""); - } -} -``` - -利用constructor-arg元素的value进字面量注入构造器。 - -```xml - - - - - -``` - -也可以使用c-空间。 - -```xml - - - - - -``` - -测试类 - -```java -@Test -public void puthPlay() { - ApplicationContext cxt = new ClassPathXmlApplicationContext("applicationContext.xml"); - PuthCD puthCD = (PuthCD)cxt.getBean("puthCD"); - puthCD.play(); -} -``` - -3. 装配集合 - -```java -public class EuropeCD implements CD { - private String name; - private List singers; - - public EuropeCD(String name, List singers) { - this.name = name; - this.singers = singers; - } - - @Override - public void play() { - System.out.println(name); - singers.forEach(singer -> { - System.out.print(singer + ", "); - }); - } -} -``` - -使用list元素装配集合。这里c-命名空间没有响应的实现,只能用constructor-arg元素。 - -```xml - - - - - - 断眉 - 梦龙 - 西域男孩 - 一体共和 - - - -``` - -#### 属性注入 - -除了可以通过构造器注入初始化bean以外,也可以通过属性注入初始化bean。通过属性注入,则类中不能有带参数的构造器,或者要显式声明无参的构造器。 - -1. 将引用注入属性中 - -```java -import org.springframework.beans.factory.annotation.Autowired; - -public class CDPlayer implements MediaPlayer { - private CD cd; - - /** - *属性注入 - */ - public void setCd(CD cd) { - this.cd = cd; - } - - @Override - public void play() { - cd.play(); - } -} -``` - -applicationContext.xml中进行属性注入创建bean。有两种方式,使用property元素或者p-命名空间。 - -```xml - - - - - - - -``` - -p命名空间元素组成如下: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190220133234267.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -2. 将字面量和集合注入属性中 - -EuropeCD.java - -```java -import java.util.List; - -public class EuropeCD implements CD { - private String name; - private List singers; - - //属性注入 - public void setName(String name) { - this.name = name; - } - - //属性注入 - public void setSingers(List singers) { - this.singers = singers; - } - - @Override - public void play() { - System.out.println(name); - singers.forEach(singer -> { - System.out.print(singer + ", "); - }); - } -} -``` - -applicationContext.xml - -```xml - - - - - - - - - 断眉 - 梦龙 - 西域男孩 - 一体共和 - - -``` - -引入util命名空间。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190220152747529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -### 混合配置 - -#### 在JavaConfig中引用xml配置 - -假如CD使用xml配置,CDPlayer使用JavaConfig配置。 - -```java -import com.tyson.pojo.CD; -import com.tyson.pojo.CDPlayer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class CDPlayerConfig { - @Bean(name = "cdPlayer") - public CDPlayer cdPlayer(CD cd) { - return new CDPlayer(cd); - } -} -``` - -装配CD定义在cd-config.xml中。 - -```xml - - - - - - - 断眉 - 梦龙 - 西域男孩 - 一体共和 - - -``` - -新建一个SoundSystemConfig.java类,通过@Import和@ImportResource引入JavaConfig配置类和xml配置文件。 - -```java -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportResource; - -//JavaConfig引用JavaConfig--@Import(CDPlayerConfig.class, CDConfig.class) -@Configuration -@Import(CDPlayerConfig.class) -@ImportResource("classpath:cd-config.xml") -public class SoundSystemConfig { -} -``` - -测试代码 - -```java -/** - * AnnotationConfigApplicationContext:从一个或多个基于Java的配置类加载Spring应用上下文 - * ClassPathXmlApplicationContext:从类路径上的一个或多个xml配置文件中加载Spring应用上下文 - */ -@Test -public void javaConfigImportXmlTest() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(SoundSystemConfig.class); - CDPlayer player = (CDPlayer) ctx.getBean("cdPlayer"); - player.play(); -} -``` - -#### 在xml配置中引用JavaConfig - -假如CDPlayer使用player-config.xml配置,CD使用cd-config.xml配置,则在player-config.xml中需要用import元素引用cd-config文件,从而将CD注入到CDPlayer。 - -player-config.xml - -```xml - - - - - - - -``` - -如果CDPlayer使用player-config.xml配置,而CD使用JavaConfig来配置,则需要这样声明bean: - -```xml - - - - - - - - - - - -``` - -CDConfig.java - -```java -import com.tyson.pojo.CD; -import com.tyson.pojo.EasonCD; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class CDConfig { - @Bean(name = "easonCD") - public CD getCD() { - return new EasonCD(); - } -} -``` - -测试代码: - -```java -@Test -public void xmlImportJavaConfigTest() { - ApplicationContext cxt = new ClassPathXmlApplicationContext("player-config.xml"); - CDPlayer player = (CDPlayer) cxt.getBean("cdPlayer"); - player.play(); -} -``` - -也可以使用第三个配置文件,将JavaConfig和xml配置组合起来。 - -soundSystem-config.xml - -```xml - - - - - - - - - -``` - -故无论是JavaConfig还是xml配置,都可以先创建一个根配置,在根配置里将多个装配类或者xml文件组合起来,同时可以在根配置打开组件扫描(通过\或者@ComponentScan)。 - -## 高级装配 - -### 自动装配的歧义性 - -编写Dessert接口,有三个实现Dessert的类:Cake、IceCream和Cookie。假如三个类都使用@Component注解,Spring在进行组件扫描时会为它们创建bean,这时如果Spring试图自动装配setDessert里面的Dessert参数时,这时Spring便不知道该使用哪个bean。 - -```java -@Autowired -public void setDessert(Dessert dessert) { - this.dessert = dessert; -} -``` - -#### 标识首选的bean - -通过设置其中某个bean为首选的bean可避免自动装配的歧义性。 - -定义Bean时使用@Primary注解。 - -```java -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Component; - -@Component -@Primary -public class Cookie implements Dessert { - @Override - public void make() { - System.out.println("making cookie..."); - } -} -``` - -或者在JavaConfig类中使用@Primary注解。 - -```java -import com.tyson.pojo.Cookie; -import com.tyson.pojo.Dessert; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -@Configuration -public class DessertConfig { - @Bean - @Primary - public Dessert dessert() { - return new Cookie(); - } -} -``` - -xml配置使用primary属性。 - -```xml - -``` - -#### 限定自动装配的bean - -@Primary只能标识一个首选的bean,当首选的bean存在有多个时,这种方法便失效。Spring的限定符可以在可选的bean进行缩小范围,最终使得只有一个bean符合限定条件。 - -@Qualifier注解是使用限定符的主要方式。它与@Autowired和@Inject协同使用,在注入的时候指定注入哪个bean。 - -```java -@Autowired -@Qualifier("cookie") -public void setDessert(Dessert dessert) { - this.dessert = dessert; -} -``` - -这种方式setDessert方法上所指定的限定符和要注入的bean名称是紧耦合的。如果类名称修改则会导致限定符失效。 - -#### 创建自定义的限定符 - -为bean设置自己的限定符。 - -```java -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.stereotype.Component; - -@Component -@Qualifier("cold") -public class IceCream implements Dessert { - - @Override - public void make() { - System.out.println("making ice cream......"); - } -} -``` - -在自动装配的地方引入自定义限定符。@Autowired和@Qualifier组合,按byName方式自动装配。 - -```java -@Autowired -@Qualifier("cold") -public void setDessert(Dessert dessert) { - this.dessert = dessert; -} -``` - -当使用JavaConfig装配bean时,@Qualifier可以和@Bean注解一起使用。 - -```java -import com.tyson.pojo.Cookie; -import com.tyson.pojo.Dessert; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -//@ComponentScan(basePackages = {"com.tyson.pojo"}) -public class DessertConfig { - @Bean - @Qualifier("cold") - public Dessert dessert() { - return new IceCream(); - } -} -``` - -使用自定义的限定符注解 - -当有多个Dessert都具有cold特征时,此时需要添加更多的限定符来避免歧义性的问题。 - -```java -import com.tyson.pojo.Dessert; -import com.tyson.pojo.IceCream; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class DessertConfig { - @Bean - @Qualifier("cold") - @Qualifier("creamy") - public Dessert dessert() { - return new IceCream(); - } -} -``` - -然而在一个类上出现多个相同类型的注解是不被允许的,会报编译错误。可以通过创建自定义的限定符注解解决这个问题。下面是自定义@Cold注解的例子: - -```java -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.*; - -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.CONSTRUCTOR}) -@Retention(RetentionPolicy.RUNTIME) -@Qualifier //具有@Qualifier的特性 -public @interface Cold { -} -``` - -自定义@Creamy注解。 - -```java -import org.springframework.beans.factory.annotation.Qualifier; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.CONSTRUCTOR}) -@Retention(RetentionPolicy.RUNTIME) -@Qualifier //具有@Qualifier的特性 -public @interface Creamy { -} -``` - -使用@Cold和@Creamy。 - -```java -import com.tyson.annotation.Cold; -import com.tyson.annotation.Creamy; -import org.springframework.stereotype.Component; - -@Component -@Cold -@Creamy -public class IceCream implements Dessert { - - @Override - public void make() { - System.out.println("making ice cream......"); - } -} -``` - -使用自定义注解更为安全。 - -### bean的作用域 - -默认情况下,Spring应用上下文所有的bean都是以单例的形式创建。不管给定的bean注入到其他bean多少次,每次注入的都是同一个实例。 - -Spring定义了多种定义域: - -单例(Singleton):在整个应用,只创建bean一个实例。 - -原型(prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。 - -会话(session):在web应用中,为每个会话创建一个bean实例。 - -请求(request):在web应用中,为每个请求创建一个bean实例。 - -单例是默认的作用域,使用@Scope选择其他的作用域,它可以跟@Component和@Bean一起使用。 - -使用@Scope声明CDPlayer为原型bean。 - -```java -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; - -@Component -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) -public class CDPlayer implements MediaPlayer { - private CD cd; - - /** - * 构造器注入 - */ - @Autowired - public CDPlayer(CD cd) { - this.cd = cd; - } - - @Override - public void play() { - cd.play(); - } -} -``` - -在JavaConfig中声明CDPlayer为原型bean。 - -```java -@Bean(name = "cdPlayer") -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) -public CDPlayer cdPlayer(CD cd) { - return new CDPlayer(cd); -} - -``` - -若是通过xml配置bean,可以通过元素的scope属性设置作用域。 - -```java - -``` - -### 运行时值注入 - -依赖注入,是将一个bean引用注入到另一个bean的属性或者构造器参数, 是一个对象和另一个对象进行关联。bean装配是将一个值注入到bean的属性或者构造器参数中。 - -#### 注入外部的值 - -使用JavaConfig的方式装配EasonCD bean。通过Environment类获得app.properties定义的属性值。 - -```java -import com.tyson.pojo.CD; -import com.tyson.pojo.EasonCD; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.core.env.Environment; - -@Configuration -@PropertySource("classpath:app.properties") -public class PropertySourcesPlaceholderConfig { - @Autowired - private Environment env; - - @Bean - public CD easonCD() { - return new EasonCD(env.getProperty("song"), - env.getProperty("singer") - ); - } -} -``` - -通过@PropertySource引用了类路径中一个名为app.properties的文件。 - -```properties -song = ten years -singer = Eason -``` - -测试代码 - -```java -@Test -public void test1() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(PropertySourcesPlaceholderConfig.class); - CD cd = ctx.getBean("easonCD", CD.class); - cd.play(); -} -``` - -如果使用组件扫描和自动装配创建bean,可以通过@Value注解引用外部的值。 - -```java -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.PropertySource; -import org.springframework.stereotype.Component; - -@Component("easonCD") -@PropertySource("classpath:app.properties") -public class EasonCD implements CD { - private String song; - private String singer; - -/* @Bean - public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { - PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer(); - ClassPathResource classPathResource = new ClassPathResource("app.properties"); - propertySourcesPlaceholderConfigurer.setLocation(classPathResource); - propertySourcesPlaceholderConfigurer.setLocalOverride(true); - return propertySourcesPlaceholderConfigurer; - }*/ - - public EasonCD(@Value("${song}") String song, - @Value("${singer}") String singer) { - this.song = song; - this.singer = singer; - } - - @Override - public void play() { - System.out.println("singer--" + singer + " sings " + song); - } -} -``` - -测试代码 - -```java -@Test -public void test1() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(CDConfig.class); - CD cd = ctx.getBean("easonCD", CD.class); - cd.play(); -} -``` - -使用xml配置的话,Spring的\元素可以为我们生成PropertySourcesPlaceholderConfigurer bean,进而可以使用占位符。 - -```xml - - - - - - - - - - - - -``` - -解析外部属性可以将值的处理推迟到运行时,但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。 - -> \ 只能导入一个 properties 文件,若需要导入多个 properties 文件,则使用下面的方法: - -```xml - - - - classpath:app.properties - - - -``` - - -#### 使用Spring表达式语言进行装配 - -Spring3引入了Spring表达式语言SpEL,它在将值装配到bean属性和构造器参数过程中所使用的表达式会在运行时计算得到值。 - -Spring表达式要放到"#{...}"之中。 - -```java -public EasonCD(@Value("#{systemProperties['song']}") String song, - @Value("#{systemProperties['singer']}") String singer) { - this.song = song; - this.singer = singer; -} -``` - -使用xml配置。 - -```xml - - - - -``` - -## 面向切面 - -依赖注入实现了对象之间的解耦。AOP可以实现横切关注点和它们所影响的对象之间的解耦。 - -切面可以帮我们模块化横切关注点。安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190223082943466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -横切关注点可以被模块化为特殊的类,这些类称之为切面。这样做有两个好处:每个关注点都集中到一个地方,不会分散到多处代码;服务模块更简洁,因为它们只包含核心功能的代码,而非核心功能的代码被转移到切面了。 - -### AOP术语 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190223083512446.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -通知(advice):切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题,它应该在某个方法被调用之前还是之后调用,或者在方法抛异常的时候才调用。 - -Spring切面定义了五种类型的通知:前置通知,后置通知,返回通知,异常通知,环绕通知。 - -连接点(join point):连接点是应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 - -切点(point cut):匹配通知所要织入的一个或多个连接点,缩小切面所通知的连接点的范围。通常使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以在运行时(通过方法的参数值等)决定是否应用通知。 - -切面(aspect):切面是通知和切点的结合,通知和切点共同定义了切面的全部内容。 - -引入(introduction):在无需修改原有类的情况下,引入允许我们向类添加新的方法或者属性(新的行为或状态)。 - -织入(weaving):织入是把切面应用到目标对象并创建代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期有多个点可以进行织入: - -- 编译期:切面在目标对象编译时被织入。这种方式需要特殊的编译器。Aspect的织入编译器就是以这种方式织入切面的。 -- 类加载期:切面在目标对象被加载到JVM时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强目标类的字节码。 -- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。Spring AOP就是以这种方式织入切面的。 - -### Spring对aop的支持 - -**Spring提供了4种类型的aop支持:** - -1.基于代理的经典Spring aop;2.纯pojo切面(借助Spring aop命名空间,xml配置);3.@AspectJ注解驱动的切面;4.注入式AspectJ切面(适用于Spring各版本) - -Spring aop构建在动态代理基础之上,因此Spring对aop的支持局限在方法拦截。如果应用的aop需求超过了简单的方法调用(构造器或属性拦截),就需要考虑使用AspectJ来实现切面。这种情况下,第四种类型能够帮助我们将值注入到AspectJ驱动的切面中。AspectJ通过特有的aop语言,我们可以获得更为强大和细粒度的控制,以及更丰富的aop工具集。 - -**Spring在运行时通知对象** - -通过在代理类中包裹切面,Spring在运行时将切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,执行额外的切面逻辑,并调用目标方法。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190227162637751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -### 通过切面选择连接点 - -在Spring aop中,要使用AspectJ的切点表达式语言来定义切点。Spring仅支持AspectJ切点指示器的一个子集。下表列出Spring AOP支持的AspectJ切点指示器。 - -| AspectJ指示器 | 描述 | -| ------------- | ------------------------------------------------------------ | -| arg() | 限制连接点匹配参数为指定类型的执行方法 | -| @args() | 限制连接点匹配参数由指定注解标注的执行方法 | -| execution() | 用于匹配是连接点的执行方法 | -| this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 | -| target | 限制连接点匹配目标对象为指定类型的类 | -| @target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 | -| within() | 限制连接点匹配指定的类型 | -| @within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) | -| @annotation | 限定匹配带有指定注解的连接点 | - -在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。 - -**编写切点** - -定义一个Performance接口: - -```java -public interface Performance { - public void perform(); -} -``` - -使用AspectJ切点表达式来选择Performance的perform()方法: - -```java -execution(* com.tyson.aop.Performance.perform(..)) -``` - -使用execution()指示器选择Performance的perform()方法。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190226163248197.png) - -使用within()指示器限制切点范围: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190226164058113.png) - -因为“&”在XML中有特殊含义,所以在Spring的XML配置里面描述切点时,我们可以使用and来代替“&&”。同样,or和not可以分别用来代替“||”和“!”。 - -**在切点中选择bean** - -Spring引入了一个新的bean()指示器,它允许我们在切点表达式使用bean的ID作为参数来限定切点只匹配特定的bean。 - -```java -execution(* com.tyson.aop.Performance.perform(..)) - and bean('drum') -``` - -在执行Performance的perform()方法时应用通知,当限定bean的ID为drum。 - -```java -execution(* com.tyson.aop.Performance.perform(..)) - and !bean('drum') -``` - -### 使用注解创建切面 - -使用注解创建切面是AspectJ 5引入的关键特性,通过少量的注解便可以把Java类转变为切面。 - -#### 定义切面 - -使用aop相关注解需先导入依赖。 - -```xml - - - - 4.0.0 - - com.tyson - SpringDemo - 1.0-SNAPSHOT - war - - SpringDemo Maven Webapp - - http://www.example.com - - - UTF-8 - 3.5.1 - 1.8 - 1.8 - UTF-8 - 5.0.5.RELEASE - 4.3.9.RELEASE - 1.6.11 - 2.1 - 4.12 - - - - - - - org.springframework - spring-core - ${spring.version} - - - - org.springframework - spring-test - ${spring.version} - - - - - org.springframework - spring-context - ${spring.version} - - - org.springframework - spring-aop - ${spring-aop.version} - - - org.aspectj - aspectjrt - ${aspectj-version} - - - - org.aspectj - aspectjweaver - ${aspectj-version} - - - - cglib - cglib - ${cglib.version} - - - org.springframework - spring-tx - ${spring.version} - - - - - junit - junit - ${junit.version} - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven.compiler.version} - - ${maven.compiler.source} - ${maven.compiler.target} - - - - - -``` - -Audience类定义了一个切面。 - -```java -import org.aspectj.lang.annotation.*; - -@Aspect -public class Audience { - @Pointcut("execution(* com.tyson.aop.Performance.perform(..))") - public void performance() {} - - @Before("performance()") - public void silenceCellPhones() { - System.out.println("silencing cell phones"); - } - - @Before("performance()") - public void takeSeats() { - System.out.println("taking seats"); - } - - @AfterReturning("performance()") - public void applause() { - System.out.println("clap clap clap!"); - } - - @AfterThrowing("performance()") - public void demandRefund() { - System.out.println("demanding a refund"); - } -} -``` - -启动自动代理功能。若使用JavaConfig,可以在配置类的类级别使用@EnableAspectJAutoProxy注解启动自动代理。 - -```java -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; - -@Configuration -@EnableAspectJAutoProxy -public class ConcertConfig { - @Bean - public Audience audience() { - return new Audience(); - } - - @Bean - public Performance magicPerformance() { - return new MagicPerformance(); - } -} -``` - -若使用xml装配bean,则可以用\元素启动自动代理。concert-config.xml如下: - -```xml - - - - - - - -``` - -测试代码: - -```java -import org.junit.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class ConcertTest { - @Test - public void consertTest() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(ConcertConfig.class); - Performance magicPerformance = (Performance) ctx.getBean("magicPerformance"); - magicPerformance.perform(); - } - - @Test - public void consertTest1() { - ApplicationContext ctx = new ClassPathXmlApplicationContext("concert-config.xml"); - Performance magicPerformance = (Performance) ctx.getBean("magicPerformance"); - magicPerformance.perform(); - } -} -``` - -测试结果: - -```java -silencing cell phones -taking seats -performing magic... -clap clap clap! -``` - - -#### 创建环绕通知 - -使用环绕通知重新实现Audience切面。 - -```java -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.*; - -@Aspect -public class Audience { - @Pointcut("execution(* com.tyson.aop.Performance.perform(..))") - public void performance() {} - - @Around("performance()") - public void watchPerformance(ProceedingJoinPoint joinPoint) { - try { - System.out.println("silencing cell phones"); - System.out.println("taking seats"); - //不调用proceed会阻塞对被通知方法performance的调用,也可以多次调用proceed方法 - joinPoint.proceed(); - System.out.println("clap clap clap!"); - } catch (Throwable e) { - System.out.println("demanding a refund"); - } - } -} -``` - -#### 处理通知中的参数 - -被通知方法含有参数,切面可以访问和使用传给被通知方法的参数。下面使用TrackCounter类记录磁道播放的次数。其中被通知方法playTrack方法有形参trackNum。 - -```java -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.aspectj.lang.annotation.Pointcut; - -import java.util.HashMap; -import java.util.Map; - -@Aspect -public class TrackCounter { - private Map trackCounts = new HashMap<>(); - - @Pointcut("execution(* com.tyson.pojo.BlankDisc.playTrack(int)) && args(trackNum)") - public void playTrack(int trackNum) {} - - @Before("playTrack(trackNum)") - public void countTrack(int trackNum) { - int currentCount = getTrackCount(trackNum); - trackCounts.put(trackNum, currentCount + 1); - } - - public int getTrackCount(int trackNum) { - return trackCounts.containsKey(trackNum) ? trackCounts.get(trackNum) : 0; - } -} -``` - -BlankDisc类: - -```java -public class BlankDisc { - private String title; - - public void setTitle(String title) { - this.title = title; - } - - public void playTrack(int trackNum) { - System.out.println("playing track: " + trackNum); - } -} -``` - -TrackCounterConfig开启自动代理,装配BlankDisc和TrackCounter bean。 - -```java -import com.tyson.pojo.BlankDisc; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; - -@Configuration -@EnableAspectJAutoProxy -public class TrackCounterConfig { - @Bean - public BlankDisc blankDisc() { - BlankDisc cd = new BlankDisc(); - cd.setTitle("apple band"); - - return cd; - } - - @Bean - public TrackCounter trackCounter() { - return new TrackCounter(); - } -} -``` - -测试代码: - -```java -import com.tyson.pojo.BlankDisc; -import org.junit.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -public class TrackCounterTest { - @Test - public void test() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(TrackCounterConfig.class); - BlankDisc blankDisc = (BlankDisc) ctx.getBean("blankDisc"); - TrackCounter counter = (TrackCounter) ctx.getBean("trackCounter"); - blankDisc.playTrack(1); - blankDisc.playTrack(2); - blankDisc.playTrack(2); - System.out.println(counter.getTrackCount(1)); - System.out.println(counter.getTrackCount(2)); - } -} -``` - -#### 通过注解引入新功能 - -利用引入的功能,切面可以为Spring bean添加新方法。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/2019022717044484.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -假如为Performance实现类引入Encoreable(再次表演)接口。 - -```java -public interface Encoreable { - public void performEncore(); -} -``` - -为了实现该功能,我们需要创建一个切面: - -```java -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.DeclareParents; - -@Aspect -public class EncoreableIntroducer { - @DeclareParents(value="com.tyson.aop.Performance+", - defaultImpl = DefaultEncoreable.class) - public static Encoreable encoreable; -} -``` - -ConcertConfig类如下,当Spring发现一个bean使用了@Aspect注解,Spring就会创建一个代理,在运行时会将调用委托给被代理的bean或者被引入的实现。 - -```java -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; - -@Configuration -@EnableAspectJAutoProxy -public class ConcertConfig { - @Bean - public Performance magicPerformance() { - return new MagicPerformance(); - } - - @Bean - public EncoreableIntroducer encoreableIntroducer() { - return new EncoreableIntroducer(); - } -} -``` - -测试代码: - -```java - @Test - public void consertTest2() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(ConcertConfig.class); - Encoreable magicPerformance = ctx.getBean("magicPerformance", Encoreable.class); - magicPerformance.performEncore(); - } -``` - -### 在xml中声明切面 - -在不能为通知类添加注解的时候,就只能使用xml配置。Spring的AOP配置元素如下: - -| aop配置元素 | 用途 | -| ---------------------- | ------------------------------------------------------------ | -| \ | 定义aop通知器 | -| \ | 定义aop切面 | -| \ | 定义切点 | -| \ | 以透明的方式为被通知对象引入新的接口 | -| \ | 顶层的aop配置元素,大多数\配置元素必须包含在\内 | -| \ | 前置通知 | -| \ | 后置通知 | -| \ | 返回通知 | -| \ | 异常通知 | -| \ | 环绕通知 | - -重新定义Audience类。 - -```java -public class Audience { - - public void silenceCellPhones() { - System.out.println("silencing cell phones"); - } - - public void takeSeats() { - System.out.println("taking seats"); - } - - public void applause() { - System.out.println("clap clap clap!"); - } - - public void demandRefund() { - System.out.println("demanding a refund"); - } -} -``` - -#### 声明通知 - -通过xml将无注解的Audience声明为切面。 - -```xml - - - - - - - - - - - - - - - - - - - -``` - -#### 创建环绕通知 - -```java -import org.aspectj.lang.ProceedingJoinPoint; - -public class Audience { - public void watchPerformance(ProceedingJoinPoint joinPoint) { - try { - System.out.println("silencing cell phones"); - System.out.println("taking seating"); - joinPoint.proceed(); - System.out.println("clap clap clap!"); - } catch (Throwable e) { - System.out.println("demanding a refund"); - } - } -} -``` - -声明Audience切面 - -```xml - - - - - - - - - - - - - - - - -``` - -#### 为通知传递参数 - -同样使用TrackCounter记录磁道播放的次数。无注解的TrackCounter如下: - -```java -import java.util.HashMap; -import java.util.Map; - -public class TrackCounter { - private Map trackCounts = new HashMap<>(); - public void countTrack(int trackNum) { - int currentCount = getTrackCount(trackNum); - trackCounts.put(trackNum, currentCount + 1); - } - - public int getTrackCount(int trackNum) { - return trackCounts.containsKey(trackNum) ? trackCounts.get(trackNum) : 0; - } -} -``` - -在xml中将TrackCounter配置为参数化的切面。xml中&符号会被解析成实体的开始,故用and代替。 - -```xml - - - - - - - - - - - - - - - - - -``` - -#### 通过切面引入新的功能 - -concert-config.xml - -```xml - - - - - - - - - - - - - - - - -``` - -测试代码: - -```java - @Test - public void consertTest4() { - ApplicationContext ctx = new ClassPathXmlApplicationContext("concert-config.xml"); - Encoreable magicPerformance = ctx.getBean("magicPerformance", Encoreable.class); - magicPerformance.performEncore(); - } -``` - -### 注入AspectJ切面 - -Spring AOP是功能比较弱的AOP解决方案,AspectJ提供了Spring AOP所不支持的许多类型的切点。如当我们需要在创建对象时应用通知,使用构造器切点很容易实现,而Spring AOP不支持构造器切点,所以基于代理的Spring AOP不能把通知应用于对象的创建过程。此时可以使用AspectJ实现。 - -使用AspectJ实现的表演评论员: - -``` -package com.tyson.aop; - -public aspect CriticAspect { - public CriticAspect() {} - - pointcut performance() : execution(* com.tyson.aop.Performance.perform(..)); - - pointcut construct() : execution(com.tyson.aop.CriticismEngineImpl.new()); - - before() : performance() { - System.out.println("performance构造器调用之前"); - } - - after() : performance() { - System.out.println("performance构造器调用之后"); - } - - after() returning : performance() { - System.out.println("criticism: " + criticismEngine.getCriticism()); - } - - private CriticismEngine criticismEngine; - - //注入CriticismEngine - public void setCriticismEngine(CriticismEngine criticismEngine) { - this.criticismEngine = criticismEngine; - } -} -``` - -CriticismEngine类以及实现类 - -```java -public class CriticismEngine { - private String criticism; - - public String getCriticism() { - return criticism; - } - - public void setCriticism(String criticism) { - this.criticism = criticism; - } -} - -public class CriticismEngineImpl extends CriticismEngine { - private String[] criticismPool; - - @Override - public String getCriticism() { - int i = (int) (Math.random() * criticismPool.length); - return criticismPool[i]; - } - - //Injected - public void setCriticismPool(String[] criticismPool) { - this.criticismPool = criticismPool; - } -} -``` - -在concert-config.xml中将CriticismEngine bean注入到CritisicAspect。CritisicAspect bean的声明使用了factory-method属性。通常情况下,Spring bean由Spring容器初始化,而Aspect切面是由AspectJ在运行期创建的。在Spring为CriticAspect注入CriticismEngine之前,CriticAspect已经被实例化了。故我们需要一种方式为Spring获得已经由AspectJ创建的CriticAspect实例的句柄,从而可以注入CriticismEngine。AspectJ切面提供了一个静态的aspectOf()方法,该方法返回切面的一个单例。Spring需要通过aspectOf()工厂方法获得切面的引用,然后进行依赖注入。 - -```xml - - - - - - - - - - - good - waste time - enjoy it - deserve - - - - - - - -``` - -测试代码: - -```java - @Test - public void consertTest5() { - ApplicationContext ctx = new ClassPathXmlApplicationContext("concert-config.xml"); - MagicPerformance magicPerformance = ctx.getBean("magicPerformance", MagicPerformance.class); - magicPerformance.perform(); - } -``` - -测试结果: - -```java -performance构造器调用之前 -performing magic... -performance构造器调用之后 -criticism: deserve -``` - -## 后端中的 Spring - -### 数据访问模板化 - -Spring 将数据访问过程分为两个部分:模板(template)和回调(callback),Spring 的模板类处理数据访问的固定部分,如事务控制、资源管理和处理异常;应用程序相关的数据访问,如语句、参数绑定以及处理结果集,在回调的实现中处理。 - -针对不同的持久化平台,Spring 提供了不同的持久化模板。如果直接使用 JDBC,则可以使用 JdbcTemplate。如果想要使用对象关系映射框架,可以使用 JpaTemplate 和 HibernateTemplate。 - -### 配置数据源 - -Spring 提供了配置数据源 bean 的多种方式: - -- 通过 JDBC 驱动程序定义的数据源 -- 通过 JNDI (Java Naming and Directory Interface)查找的数据源 -- 连接池的数据源 - -### 在 Spring 中集成 Hibernate - -本节参考自:[spring+springmvc+hibernate 整合](https://www.cnblogs.com/xuezhajun/p/7687230.html) - -Hibernate 是开源的持久化框架,不仅提供了基本的对象关系映射,还提供了 ORM 工具的复杂功能,如缓存、延迟加载、预先抓取(eager fetching)等。 - -引入依赖: - -```xml - - org.springframework - spring-tx - ${spring.version} - - - - org.springframework - spring-orm - 4.1.7.RELEASE - - - - - org.hibernate - hibernate-core - - - - - c3p0 - c3p0 - 0.9.1.2 - - - - - mysql - mysql-connector-java - 5.1.37 - -``` - -**声明 Hibernate 的 Session 工厂** - -Session 接口提供了基本的数据访问功能,获取 Session 对象的标准方式是借助于 Hibernate SessionFactory 接口的实现类。SessionFactory 主要负责 Hibernate Session 的打开、关闭以及管理。 - -从Spring 3.1版开始,Spring 提供了三个 SessionFactorybean : - -```Java -org.springframework.orm.hibernate3.LocalSessionFactoryBean -org.springframework.orm.hibernate4.LocalSessionFactoryBean -org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean -``` - -如果使用高于 Hibernate 3.1版本,低于4.0版本,并且使用 xml 定义映射的话,则需要定义org.springframework.orm.hibernate3.LocalSessionFactoryBean。 - -如果倾向于使用注解来定义映射的话,并且没有使用 Hibernate 4的话,那么需要使用 AnnotationSessionFactoryBean。 - -当使用 Hibernate 4时,就应该使用org.springframework.orm.hibernate4.LocalSessionFactoryBean,Spring 3.1引入的这个 SessionFactoryBean 类似于 Hibernate 3中的 LocalSessionFactoryBean 和 AnnotationSessionFactoryBean 的结合体。 - -datasource.xml 文件如下: - -```xml - - - - - - - - - - - - - - - - - - - - - - ${hibernate.hbm2ddl.auto} - ${hibernate.dialect} - ${hibernate.show_sql} - ${hibernate.format_sql} - - - - - - - - - - - -``` - -datasource.properties 如下: - -```properties -#database connection config -jdbc.driver = com.mysql.jdbc.Driver -jdbc.url = jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8 -jdbc.username = root -jdbc.password = 123456 - -#hibernate config -hibernate.dialect = org.hibernate.dialect.MySQLDialect -hibernate.show_sql = true -hibernate.format_sql = true -hibernate.hbm2ddl.auto = update -``` - - beans.xml 如下: - -```xml - - - - - - - - - - - - - - - - classpath:datasource.properties - - - - - - - -``` - -实体类(使用注解来定义映射): - -```java -import java.util.Set; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; - -@Entity -@Table(name="people") -public class People { - - @Id - @Column(name="ID") - private int id; - - @Column(name="name") - private String name; - - @Column(name="sex") - private String sex; - - public void setName(String name) { - this.name = name; - } - public String getSex() { - return sex; - } - public void setSex(String sex) { - this.sex = sex; - } - -} -``` - -### Spring 与 Java 持久化 API - -JPA 基于 POJO 的持久化机制。JPA 是一种规范,而 Hibernate 是它的一种实现。除了 Hibernate,还有 EclipseLink,OpenJPA 等可供选择,所以使用 JPA 的一个好处是,可以更换实现而不必改动太多代码。 - -**导入依赖包** - -```xml - - - org.springframework - spring-test - ${spring.version} - - - - org.springframework - spring-context - ${spring.version} - - - - org.springframework - spring-orm - ${spring.version} - - - - - org.hibernate - hibernate-entitymanager - ${hibernate.version} - -``` - -在 Spring 中使用 JPA 的第一步是要在 Spring 应用的上下文将实体管理器工厂(entity manager factory)按照 Bean 的形式进行配置。 - -#### 配置实体管理器工厂 - -基于 JPA 的应用程序需要通过 EntityManegerFactory 获取 EntityManager 实例。JPA 定义了两种类型的EntityManagerFactory: - -- 应用程序管理类型(Application-managed):当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器适合于不运行在 Java EE 容器中的独立应用程序。 -- 容器管理类型(container-managed):实体管理器由 Java EE 创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或 JNDI 来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适用于 Java EE 容器。 - -两种实体管理器实现了同一个 EntityManager 接口。这两种实体管理器工厂分别由对应的Spring工厂Bean创建: - -- LocalEntityManagerFactoryBean 生成应用程序管理类型的 EntityManager-Factory -- LocalContainerEntityManagerFactoryBean 生成容器管理类型的 Entity-ManagerFactory - -**使用容器管理的 EntityManagerFactory** - -配置容器管理类型的 EntityManagerFactory: - -```java -@Bean -public LocalContainerEntityManagerFactoryBean entityManagerFactory( - DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { - LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); - entityManagerFactory.setDataSource(dataSource); - entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter); - entityManagerFactory.setPackagesToScan("com.tyson.domain"); - - return entityManagerFactory; -} -``` - -LocalContainerEntityManager 的 DataSource 属性 dataSource 可以是 java.sql.DataSource 的任何实现,dataSource 还可以在 persistence.xml 中进行配置,但是这个属性指定的数据源具有更高的优先级。 - -jpaVendorAdapter 属性用于指定所使用的哪一个厂商的 JPA 实现。Spring提供了多个 JPA 厂商适配器。 - - - -- EclipseLinkJpaVendorAdapter -- HibernateJpaVendorAdapter -- OpenJpaVendorAdapter -- TopLinkJpaVendorAdapter - -配置容器管理类型的 JPA 完整代码: - -```java -import com.mchange.v2.c3p0.ComboPooledDataSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.core.env.Environment; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.JpaVendorAdapter; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.Database; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -import javax.sql.DataSource; -import java.beans.PropertyVetoException; - -@Configuration -@ComponentScan(basePackages = {"com.tyson.db"}) -@EnableTransactionManagement -@PropertySource(value = "db.properties") -public class JpaConfig { - @Autowired - Environment env; - - /** - * dataSource 注入到entityManagerFactory中,不用在persistence.xml配置数据库信息 - * jpaVendorAdapter 指定所使用的哪一个厂商的 JPA 实现 - * setPackagesToScan 可以在包下查找带有@Entity 注解的类 - * 可以不用使用 persistence.xml(主要作用是识别持久化单元的实体类) - */ - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory( - DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { - LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean(); - entityManagerFactory.setDataSource(dataSource); - entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter); - /** - * - */ - entityManagerFactory.setPackagesToScan("com.tyson.domain"); - - return entityManagerFactory; - } - - @Bean - public DataSource dataSource() { - ComboPooledDataSource dataSource = new ComboPooledDataSource(); - try { - dataSource.setUser(env.getProperty("c3p0.user")); - dataSource.setPassword(env.getProperty("c3p0.password")); - dataSource.setJdbcUrl(env.getProperty("c3p0.jdbcUrl")); - dataSource.setDriverClass(env.getProperty("c3p0.driverClass")); - } catch (PropertyVetoException e) { - e.printStackTrace(); - } - return dataSource; - } - - @Bean - public JpaVendorAdapter jpaVendorAdapter() { - HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); - jpaVendorAdapter.setDatabasePlatform("org.hibernate.dialect.MySQL5InnoDBDialect"); - jpaVendorAdapter.setDatabase(Database.MYSQL); - jpaVendorAdapter.setShowSql(true); - jpaVendorAdapter.setGenerateDdl(false); - - return jpaVendorAdapter; - } - - @Bean - public JpaTransactionManager transactionManager() { - JpaTransactionManager transactionManager = new JpaTransactionManager(); - transactionManager.setEntityManagerFactory( - entityManagerFactory(dataSource(), jpaVendorAdapter()).getObject()); - - return transactionManager; - } - - /* @Bean - public PersistenceAnnotationBeanPostProcessor postProcessor() { - return new PersistenceAnnotationBeanPostProcessor(); - }*/ - - /** - * 给 Repository 添加异常转换功能 - * 捕获平台相关的异常,然后使用 Spring 统一的非检查型异常的形式重新抛出 - */ - @Bean - public BeanPostProcessor persistenceTranslation() { - return new PersistenceExceptionTranslationPostProcessor(); - } -} -``` - -src/main/resources 目录下的 db.properties: - -```properties -c3p0.driverClass=com.mysql.jdbc.Driver -c3p0.jdbcUrl=jdbc:mysql://localhost:3306/springdemo?useUnicode=true&characterEncoding=utf-8 -c3p0.user= -c3p0.password= - -#连接池初始化时创建的连接数 -c3p0.initialPoolSize=3 -#连接池保持的最小连接数 -c3p0.minPoolSize=3 -#连接池在无空闲连接可用时一次性创建的新数据库连接数,default:3 -c3p0.acquireIncrement=3 -#连接池中拥有的最大连接数,如果获得新连接时会使连接总数超过这个值则不会再获取新连接,而是等待其他连接释放,所以这个值有可能会设计地很大,default : 15 -c3p0.maxPoolSize=15 -#连接的最大空闲时间,如果超过这个时间,某个数据库连接还没有被使用,则会断开掉这个连接,单位秒 -c3p0.maxIdleTime=100 -#连接池在获得新连接失败时重试的次数,如果小于等于0则无限重试直至连接获得成功 -c3p0.acquireRetryAttempts=30 -#连接池在获得新连接时的间隔时间 -c3p0.acquireRetryDelay=1000 -``` - -#### 编写 JPA Repository - -UserRepository 接口代码: - -```java -package com.tyson.db.jpa; - -import com.tyson.domain.User; - -import java.util.List; - -public interface UserRepository { - void saveUser(User user); - User findById(long id); - User findByUsername(String username); - List findAll(); -} -``` - -不使用 Spring 模板的纯 JPA Repository: - -```java -package com.tyson.db.jpa; - -import com.tyson.db.UserRepository; -import com.tyson.domain.User; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import java.util.List; - -@Repository("jpaUserRepository") -@Transactional -public class JpaUserRepository implements UserRepository { - - @PersistenceContext - private EntityManager entityManager; - - @Override - public void saveUser(User user) { - entityManager.persist(user); - } - - @Override - public User findById(long id) { - return entityManager.find(User.class, id); - } - - @Override - public User findByUsername(String username) { - return (User) entityManager.createQuery("select u from User u where u.username = ? ").setParameter(1, username).getSingleResult(); - } - - @Override - public List findAll() { - return entityManager.createQuery("select u from User u").getResultList(); - } -} -``` - -1. @Repository 可以减少显式配置,通过组件扫描自动创建。JpaTemplate 会捕获平台相关的异常,然后使用 Spring 统一的非检查型异常的形式重新抛出。为了给不使用模板的 Jpa Repository 添加异常转化功能,我们只需在 Spring 应用上下文添加一个 PersistenceExceptionTranslationPostProcessor bean,这是一个 bean 后置处理器,它会在所有拥有@Repository 注解的类上添加一个通知器(advisor),这样就会捕获平台相关的异常并以 Spring 非检查型数据访问异常的形式重新抛出。 - -2. @Transactional 表明这个 Repository 中的持久化方法是在事务上下文中执行的。不添加@Transactional 注解 会抛异常:javax.persistence.TransactionRequiredException: No transactional EntityManager available。 - -3. 使用@PersistenceContext 并不会真正注入 EntityManager,它没有把真正的 EntityManager 设置给 Repository,而是给了它一个 EntityManager 的代理。真正的 EntityManager 是与当前事务相关联的那个,如果不存在这样的 EntityManager 的话,就会创建一个新的。这样便能以线程安全的方式使用 EntityManager。 - -4. @PersistenceContext 并不是 Spring 的注解,它是由 JPA 规范提供的,为了让 Spring 理解这些注解,并注入EntityManager,我们需要配置 Spring 的 PersistenceAnnotationBeanPostProcessor。如果已经使用 \或者\,会自动注册 PersistenceAnnotationBeanPostProcessor bean。否则需要显式注册这个bean。 - -```java -@Bean -public PersistenceAnnotationBeanPostProcessor postProcessor() { - return new PersistenceAnnotationBeanPostProcessor(); -} -``` - -实体类 User: - -```java -package com.tyson.domain; - -import javax.persistence.*; - -@Entity -public class User { - public User() {} - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name="username") - private String username; - - //setter/getter/toString -} -``` - -测试代码: - -```java -import com.tyson.config.JpaConfig; -import com.tyson.db.UserRepository; -import com.tyson.domain.User; -import org.junit.Test; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -public class JpaTest { - @Test - public void test() { - ApplicationContext ctx = new AnnotationConfigApplicationContext(JpaConfig.class); - UserRepository userRepository = (UserRepository) ctx.getBean("jpaUserRepository"); - - User user = new User(); - user.setUsername("tyson"); - userRepository.saveUser(user); - } -} -``` - -### 借助 Spring Data 实现自动化的 JPA Repository - -Spring Data JPA 是Spring基于ORM框架、JPA规范封装的一套 JPA 应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。 - -#### Spring Data JPA 的核心接口 - -- Repository:最顶层的接口,是一个空的接口,目的是为了统一所有Repository的类型,且能让组件扫描的时候自动识别。 -- CrudRepository :是Repository的子接口,提供CRUD的功能 -- PagingAndSortingRepository:是CrudRepository的子接口,添加分页和排序的功能 -- JpaRepository:是PagingAndSortingRepository的子接口,增加了一些实用的功能,如批量操作等 -- JpaSpecificationExecutor:用来做负责查询的接口 -- Specification:是 Spring Data JPA 提供的一个查询规范,Criteria 查询 - -#### 定义数据访问层 - -引入依赖: - -```xml - - - org.springframework - spring-test - ${spring.version} - - - - org.springframework - spring-context - ${spring.version} - - - - org.springframework - spring-orm - ${spring.version} - - - - - org.springframework.data - spring-data-jpa - ${springdatajpa.version} - - - - - org.hibernate - hibernate-entitymanager - ${hibernate.vers\ion} - -``` - -只需定义一个继承 JpaRepository 的接口即可: - -```java -package com.tyson.db.springdatajpa; - -import com.tyson.domain.User; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRepository extends JpaRepository { -} -``` - -继承了 JpaRepository 接口默认已经有了下面的数据访问操作方法: - -```java -@NoRepositoryBean -public interface JpaRepository extends PagingAndSortingRepository, QueryByExampleExecutor { - List findAll(); - - List findAll(Sort var1); - - List findAll(Iterable var1); - - List save(Iterable var1); - - void flush(); - - S saveAndFlush(S var1); - - void deleteInBatch(Iterable var1); - - void deleteAllInBatch(); - - T getOne(ID var1); - - List findAll(Example var1); - - List findAll(Example var1, Sort var2); -} -``` - -Spring Data 自动为我们生成 UserRepository 的实现类。我们需要在 Spring 配置中添加一个元素启用 Spring Data JPA。下面使用 xml 配置方式启用Spring Data JPA: - -```xml - - - - - - ... - -``` - -\元素会扫描它的基础包来查找扩展自 Spring Data JPA Repository 接口的所有接口。如果发现有扩展自 Repository 的接口,它会在应用启动时自动生成该接口的实现类。 - -也可以使用 Java 配置方式启用 Spring Data JPA: - -```java -@Configuration -@EnableJpaRepositories(basePackages = {"com.tyson.db.springdatajpa"}) -public class SpringDataJpaConfig { -} -``` - -UserReposity 扩展了 Repository 接口,当Spring Data 扫描到它后,会为它创建 UserRepository 的实现类,其中包含了继承自 JpaRepository、PagingAndSortingRepository 和 CrudRepository 的18个方法。Repository 的 实现类是在应用启动时生成的,不是在构建时通过代码生成技术生成,也不是在接口调用时才创建的。 - -#### 定义查询方法 - -**根据属性名查询** - -将 UserRepository 接口修改如下: - -```java -import com.tyson.domain.User; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface UserRepository extends JpaRepository { - List findByUsername(String username); - - //限制结果数量 - public List findFirst10ByUsername(String username); -} -``` - -通过方法签名已经告诉 Spring Data JPA 怎样实现这个方法了。Repository 方法是由一个动词、一个可选的主题、关键字 By 和一个断言所组成的。 - -![](https://img2018.cnblogs.com/blog/1252910/201903/1252910-20190309201549758-1658225268.png) - - - -1. Spring Data 允许在方法名中使用四种名词:get/read/find/count。get/read/find是同义的。 - -2. 如果主题的名称以 Distinct 开头的话,那么返回的结果集不包含重复记录。 - -3. 断言指定了限制结果集的属性。断言中会有一个或者多个限制条件,并且每个条件可以指定一种比较操作,如果省略比较操作,则默认是相等比较操作。条件中可能包含 IgnoringCase 或者 IgnoresCase. - -```java -List findByFirstnameIgnoresCaseOrLastnameIgnoresCase(String first, String last); - -List findByFirstnameOrLastnameAllIgnoresCase(String first, String last); -``` - -4. 在方法名称结尾处添加 OrderBy,实现结果集排序。如果要根据多个属性排序的话,只需将其依序添加到 OrderBy 中即可。下面的代码会根据 Lastname 升序排列,然后根据 Firstname 降序排列。 - -```java -List readByFirstnameOrLastnameOrderByLastnameAscFirstnameDesc(String first, String last); -``` - -5. 限制结果数量,如 findFirst10ByUsername。 - -**排序和分页查询** - -```java -//分页 -public Page findByUsername(String username, Pageable pageable); - -//排序 -public List findByUsername(String username, Sort sort); -``` - -测试代码: - -```java -@Test -public void testPage() { - Page userPage = userRepository.findByUsername( - "tyson", new PageRequest(0, 10)); - //第0页的十条记录 - List users = userPage.getContent(); - users.forEach(user -> { - System.out.println(user); - }); -} - -@Test -public void testSort() { - List users = userRepository.findByUsername( - "tyson", new Sort(Sort.Direction.ASC, "id")); - users.forEach(user -> { - System.out.println(user); - }); -} -``` - -**使用@Query 查询** - -当所需的数据无法通过方法名描述时,如查找邮箱地址是不是 QQ 邮箱时,使用findQQMailUser方法不符合 Spring Data 方法命名约定,这时我们可以使用@Query 注解,为 Spring Data 提供要执行的查询。 - -```java -@Query("select u from User u where u.username like '%teacher%'") -public List findTeacher(); - -//使用参数索引 -@Query("select u from User u where u.username = ?1") -List findByUsername1(String username); - -//使用命名参数 -@Query("select u from User u where u.username = :username") -List findByUsername2(@Param("username") String username); - -//更新查询,Spring Data JPA支持@Modifying、@Query和@Transactional注解组合做更新查询 -@Modifying -@Transactional -@Query("update User u set u.username=?2 where u.id=?1") -public int setUsername(Long id, String username); -``` - -**Specification** - -有时我们在查询某个实体的时候,给定的条件是不固定的,这时我们就需要动态构建相应的查询语句,在JPA2.0中我们可以通过Criteria接口查询。在Spring data JPA中相应的接口是JpaSpecificationExecutor,这个接口基本是围绕着Specification 复杂查询接口来定义的。 - -(1)定义。接口类需实现 JpaSpecificationExecutor 接口 - -```java -public interface UserRepository extends JpaRepository , JpaSpecificationExecutor { -} -``` - -(2)定义 Criteria 查询(Criteria 查询采用面向对象的方式封装查询条件) - -```java -import com.tyson.domain.User; -import org.springframework.data.jpa.domain.Specification; - -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Predicate; -import javax.persistence.criteria.Root; - -public class UserSpecification { - public static Specification userNamedTyson() { - return new Specification() { - @Override - public Predicate toPredicate(Root root, CriteriaQuery criteriaQuery, CriteriaBuilder criteriaBuilder) { - //假设这是个很复杂的条件 - return criteriaBuilder.equal(root.get("username"), "tyson"); - } - }; - } -} -``` - -使用 Root 俩获得需要查询的属性,通过 CriteriaBuilder 构造条件。CriteriaBuilder 包含的条件有:exists、and、or、isTrue、isNull、greaterThan、greaterThanOrEqualTo、between等。 - -(3)使用。注入 userRepository 的 bean 后: - -```java -@Autowired -public UserRepository userRepository; - -@Test -public void testSpecification() { - List users = userRepository.findAll(userNamedTyson()); - users.forEach(user -> { - System.out.println(user); - }); -} -``` - -#### 混合自定义的功能 - -当所需的功能无法用 Spring Data 的方法命名约定来描述,并且无法用@Query 注解来实现,这时我们只能使用较低层级的 JPA 的方法。 - -当 Spring Data JPA 为 Repository 接口生成实现时,它会查找名字与接口相同,并且添加后缀 Impl 的一个类(本例是 UserRepositoryImpl),如果这个类存在,那么 Spring Data JPA 会将它的方法和 Spring Data JPA 生成的方法合并在一起。 - -UserRepositoryImpl 实现了 MixUserRepository 接口。 - -```java -package com.tyson.db.springdatajpa; - -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - -public class UserRepositoryImpl implements MixUserRepository { - - @PersistenceContext - EntityManager entityManager; - - /** - * 假设这是个很复杂的更新操作 - * 更新操作需要使用@Modifying 和 @Transactional 注解 - */ - @Override - @Modifying - @Transactional(rollbackFor = Exception.class) - public int updateUser() { - String update = "update User user " + - "set user.username='Bob' " + - "where user.id=1"; - - return entityManager.createQuery(update).executeUpdate(); - } -} -``` - -我们还需要确保 updateUser 方法被声明在 UserRepository 接口中,让 UserRepository 扩展 MixUserRepository。 - -```java -package com.tyson.db.springdatajpa; - -import com.tyson.domain.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import java.util.List; - -public interface UserRepository extends JpaRepository , MixUserRepository{ - List findByUsername(String username); - - @Query("select u from User u where u.username like '%teacher%'") - List findTeacher(); -} -``` - -Spring Data 将实现类与接口关联起来是基于接口的名称。Impl 后缀只是默认的做法,如果想到使用其他后缀,只需在配置@EnableJpaReposities 的时候,设置 repositoryImplementationPostfix 属性即可: - -```java -@EnableJpaRepositories(basePackages = {"com.tyson.db.springdatajpa"}, repositoryImplementationPostfix = "Helper") -``` - -如果在 xml 使用 \元素来配置 Spring Data JPA 的话,可以借助 repository-impl-postfix 属性指定后缀: - -``` - - -``` - -设置成 Helper 后缀的话,则 Spring Data JPA 会查找名为 UserRepositoryHelper 的类,用它来匹配 UserRepository 接口。 - - - -## Spring Security - -参考:[Spring Security](https://www.jianshu.com/p/e6655328b211) - -基于 Spring AOP 和 Servlet 规范中的 Filter 实现的安全框架。它能够在 web 请求级别和方法调用级别处理身份认证和授权。Spring Security 使用了依赖注入和面向切面的技术。 - -添加依赖: - -```xml - 3.2.3.RELEASE - - - - org.springframework.security - spring-security-web - ${spring.security.version} - - - - org.springframework.security - spring-security-config - ${spring.security.version} - -``` - -### 基于内存的用户存储 - -```java -package com.tyson.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; - -@Configuration -@EnableWebMvcSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - //添加两个用户,密码都是password,一个角色是user,一个有两个角色user和admin - //roles方法是authorities方法简写形式role("USER")等价于authorities("ROLE_USER") - auth.inMemoryAuthentication(). - withUser("user").password("password").roles("USER").and(). - withUser("admin").password("password").roles("USER", "ADMIN"); - } -} -``` - -### 基于数据库表进行认证 - -```java -@Configuration -@EnableWebMvcSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { - @Autowired - DataSource dataSource; - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - //passwordEncoder方法指定一个密码转换器 - auth.jdbcAuthentication() - .dataSource(dataSource) - .usersByUsernameQuery( - "select username, password from User where username=?" - ).authoritiesByUsernameQuery( - "select username, 'ROLE_USER' from User where username=?" - ).passwordEncoder((new StandardPasswordEncoder("53cr3t"))); - } -} -``` - -### 拦截请求 - -对每个请求进行细粒度安全性控制的关键在于重载configure(HttpSecurity)方法。 - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - //具体的访问路径放前面,最不具体的访问路径放最后(anyRequest) - http - .authorizeRequests() - .antMatchers("/").authenticated() - .antMatchers("/spitter/**", "/spitter/me").authenticated()//指定多个路径 - .antMatchers(HttpMethod.POST, "/spittles").authenticated().hasRole("SPLITTER")//不仅需要认证,还要有SPLITTER角色 - .anyRequest().permitAll();//其他请求无条件允许访问 - } -``` - -保护请求的配置方法: - -![img](../img/请求保护配置方法.png) - -### 使用Spring表达式进行安全保护 - -上表提供的方法都是一维的,如果使用了hasRole() 方法限制特定角色,就无法使用 hasIpAddress() 限制特定的 ip 地址。借助 access() 和 SpEL 表达式可以实现多维的访问限制: - -```java -.antMatchers("/splitter/me").access("hasRole('ROLE_SPLITTER') and hasIpAddress('192.168.2.1')") -``` - - -### 安全通道 - -requiresChannel()方法会为选定的 URL 强制使用 HTTPS。 - -```java - @Override - protected void configure(HttpSecurity http) throws Exception { - //具体的访问路径放前面,最不具体的访问路径放最后(anyRequest) - http - .authorizeRequests() - .antMatchers("/").authenticated() - .antMatchers("/spitter/**", "/spitter/me").authenticated()//指定多个路径 - .antMatchers(HttpMethod.POST, "/spittles").authenticated().hasRole("SPLITTER")//不仅需要认证,还要有SPLITTER角色 - .anyRequest().permitAll()//其他请求无条件允许访问 - .and() - .requiresChannel() - .antMatchers("/spitter/from").requiresSecure();//需要走HTTPS - } -``` - -有些页面不需要通过 HTTPS 传送,如首页(不包含敏感信息),可以声明始终通过 HTTP 传送。此时尽管通过 https 访问首页,也会重定向到 http 通道。 - -```java -.antMatchers("/").requiresInsecure(); -``` - -### 认证用户 - -1、启动默认的登录页。 - -2、自定义登录页 - -3、拦截/logout请求 - -4、logout成功后重定向到首页 - -5、rememberMe 功能,通过在 cookie 存储 一个 token 实现。 - -```java - http - .formLogin()//1 - .loginPage("/login")//2 - .and() - .logout()//3 - .logoutSuccessUrl("/")//4 - .and() - .rememberMe()//5 - .tokenRepository(new InMemoryTokenRepositoryImpl()) - .tokenValiditySeconds(2419200) - .key("spittrKey") - .and() - .httpBasic()//6 - .realmName("Spittr") - .and() - .authorizeRequests() - .antMatchers("/").authenticated() - .antMatchers("/spitter/me").authenticated() - .antMatchers(HttpMethod.POST, "/spittles").hasRole("SPLITTER") - .anyRequest().permitAll() - .and() - .requiresChannel() - .antMatchers("/spitter/from").requiresSecure()//需要走HTTPS - .antMatchers("/").requiresInsecure(); -``` - -### 方法级别安全 - -Spring Security 使用Spring AOP保护方法调用——借助于对象代理和使用通知,能够确保只有具备适当权限的用户才能访问安全保护的方法。 - -Spring Security提供了三种不同的安全注解: - -- Spring Security自带的@Secured注解; -- JSR-250的@RolesAllowed注解; -- 表达式驱动的注解,包括@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。 - -@Secured和@RolesAllowed方案类似,基于用户所授予的权限限制对方法的访问。@PreFilter/@和ostFilter能够过滤方法返回和传入方法的集合。 - -#### @Secured - -要启用基于注解的方法安全性,关键之处在于要在配置类上使用@EnableGlobalMethodSecurity。 - -```java -@Configuration -@EnableGlobalMethodSecurity(securedEnabled=true) -public class SecuredConfig extends GlobalMethodSecurityConfiguration { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth - .inMemoryAuthentication() - .withUser("user").password("password").roles("USER"); - } -} -``` - -GlobalMethodSecurityConfiguration 能够为方法级别的安全性提供更精细的配置。securedEnabled 属性的值为true,将会创建一个切点,Spring Security切面就会包装带有@Secured注解的方法。 - -用户必须具备ROLE_SPITTER或ROLE_ADMIN权限才能触发addSpittle: - -```java - @Secured({"ROLE_SPITTER", "ROLE_ADMIN"}) - public void addSpittle(Spittle spittle) { - System.out.println("Method was called successfully"); - } -``` - -如果方法被没有认证的用户或没有所需权限的用户调用,保护这个方法的切面将抛出一个Spring Security异常(非检查型异常)。 - -#### @RolesAllowed - -开启 @RolesAllowed 注解功能: - -```java -@Configuration -@EnableGlobalMethodSecurity(jsr250Enabled=true) -public class JSR250Config extends GlobalMethodSecurityConfiguration { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth - .inMemoryAuthentication() - .withUser("user").password("password").roles("USER"); - } - - @Bean - public SpittleService spitterService() { - return new JSR250SpittleService(); - } - -} -``` - -@Secured注解的不足之处在于它是Spring特定的注解。如果更倾向于使用Java标准定义的注解,应该考虑使用@RolesAllowed注解。 - -```java - @RolesAllowed("ROLE_SPITTER") - public void addSpittle(Spittle spittle) { - System.out.println("Method was called successfully"); - } -``` - -#### 使用表达式实现方法级别的安全性 - -开启方法调用前后注解: - -```java -@Configuration -@EnableGlobalMethodSecurity(prePostEnabled=true) -public class ExpressionSecurityConfig extends GlobalMethodSecurityConfiguration { - - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth - .inMemoryAuthentication() - .withUser("user").password("password").roles("USER"); - } - - @Bean - public SpittleService spitterService() { - return new ExpressionSecuredSpittleService(); - } - -} -``` - - - -![方法级别注解](..\img\spring\方法级别注解.png) - -方法调用前检查,`#spittle.text.length()`检查传入方法的参数: - -```java - @PreAuthorize("(hasRole('ROLE_SPITTER') and #spittle.text.length() le 140) or hasRole('ROLE_PREMIUM')") - public void addSpittle(Spittle spittle) { - System.out.println("Method was called successfully"); - } -``` - -进入方法以前过滤输入值: - -```java - @PreAuthorize("(hasRole('ROLE_SPITTER', 'ROLE_ADMIN')") - @PreFilter("hasRole('ROLE_ADMIN')" || "targetObject.spitter.username == principal.name") - public void addSpittle(Spittle spittle) { - } -``` - -@PreFilter注解能够保证传递给deleteSpittles()方法的列表中,只包含当前用户有权限删除的Spittle。targetObject是Spring Security提供的另外一个值,它代表了要进行计算的当前列表元素。 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/Spring\346\200\273\347\273\223.md" "b/\346\241\206\346\236\266/Spring\346\200\273\347\273\223.md" deleted file mode 100644 index cd822bf..0000000 --- "a/\346\241\206\346\236\266/Spring\346\200\273\347\273\223.md" +++ /dev/null @@ -1,471 +0,0 @@ - - - - -- [AOP](#aop) - - [静态代理](#%E9%9D%99%E6%80%81%E4%BB%A3%E7%90%86) - - [动态代理](#%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86) - - [Spring AOP动态代理](#spring-aop%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86) - - [实现原理](#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) -- [IOC](#ioc) - - [ioc容器](#ioc%E5%AE%B9%E5%99%A8) - - [BeanDefinition](#beandefinition) - - [容器初始化](#%E5%AE%B9%E5%99%A8%E5%88%9D%E5%A7%8B%E5%8C%96) - - [Bean生命周期](#bean%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F) - - [Aware接口](#aware%E6%8E%A5%E5%8F%A3) - - [BeanFactory和FactoryBean](#beanfactory%E5%92%8Cfactorybean) - - [FactoryBean使用](#factorybean%E4%BD%BF%E7%94%A8) - - [bean注入容器的方法](#bean%E6%B3%A8%E5%85%A5%E5%AE%B9%E5%99%A8%E7%9A%84%E6%96%B9%E6%B3%95) -- [bean的作用域](#bean%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F) -- [事务](#%E4%BA%8B%E5%8A%A1) - - [@Transactional](#transactional) - - [事务传播行为](#%E4%BA%8B%E5%8A%A1%E4%BC%A0%E6%92%AD%E8%A1%8C%E4%B8%BA) -- [循环依赖](#%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96) - - [初始化](#%E5%88%9D%E5%A7%8B%E5%8C%96) - - [三级缓存](#%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%98) -- [Spring启动过程](#spring%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B) -- [Spring Bean线程安全](#spring-bean%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8) - - - -## AOP - -面向切面编程,作为面向对象的一种补充,将公共逻辑(事务管理、日志、缓存等)封装成切面,跟业务代码进行分离,可以减少系统的重复代码和降低模块之间的耦合度。切面就是那些与业务无关,但所有业务模块都会调用的公共逻辑。 - -AOP有两种实现方式:静态代理和动态代理。 - -### 静态代理 - -静态代理:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,也称编译时增强。AspectJ使用的是静态代理。 - -缺点:代理对象需要与目标对象实现一样的接口,并且实现接口的方法,会有冗余代码。同时,一旦接口增加方法,目标对象与代理对象都要维护。 - -### 动态代理 - -动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。 - -### Spring AOP动态代理 - -Spring AOP中的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。 - -1. JDK动态代理(生成的代理类实现了接口)。如果目标类实现了接口,Spring AOP会选择使用JDK动态代理目标类。代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口。JDK动态代理的核心是`InvocationHandler`接口和`Proxy`类。 - - 缺点:目标类必须有实现的接口。如果某个类没有实现接口,那么这个类就不能用JDK动态代理。 - -2. CGLIB来动态代理(通过继承)。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。 - - CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为`final`,那么它是无法使用CGLIB做动态代理的。 - - 优点:目标类不需要实现特定的接口,更加灵活。 - -什么时候采用哪种动态代理? - -1. 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP -2. 如果目标对象实现了接口,可以强制使用CGLIB实现AOP -3. 如果目标对象没有实现了接口,必须采用CGLIB库 - -区别: - -1. jdk动态代理使用jdk中的类Proxy来创建代理对象,它使用反射技术来实现,不需要导入其他依赖。cglib需要引入相关依赖:asm.jar,它使用字节码增强技术来实现。 -2. 当目标类实现了接口的时候Spring Aop默认使用jdk动态代理方式来增强方法,没有实现接口的时候使用cglib动态代理方式增强方法。 - -#### 实现原理 - -[Spring aop实现原理](https://www.zhihu.com/question/23641679) - -Spring会为目标对象生成代理对象。当调用代理对象方法的时候,会触发CglibAopProxy.intercept(),然后将目标对象的增强包装成拦截器,形成拦截器链,最后执行全部拦截器和目标方法。 - - - -## IOC - -[IOC基础](https://juejin.im/entry/588083111b69e60059034e3d#comment) | [IOC demo](https://juejin.im/post/5b399eb1e51d4553156c0525#heading-3) - -IOC:控制反转,由Spring容器管理bean的整个生命周期。通过反射实现对其他对象的控制,包括初始化、创建、销毁等,解放手动创建对象的过程,同时降低类之间的耦合度。 - -IOC的好处:降低了类之间的耦合,对象创建和初始化交给Spring容器管理,在需要的时候只需向容器进行申请。 - -DI(依赖注入):在Spring创建对象的过程中,把对象依赖的属性注入到对象中。有两种方式:构造器注入和属性注入。 - -### ioc容器 - -Spring主要有两种ioc容器,实现了BeanFactory接口的简单容器和ApplicationContext高级容器。 - -- `BeanFactory` :延迟注入(使用到某个 bean 的时候才会注入),相比于`BeanFactory` 来说会占用更少的内存,程序启动速度更快。BeanFactory提供了最基本的ioc容器的功能(最基本的依赖注入支持)。 - -- `ApplicationContext` :容器启动的时候,一次性创建所有 bean 。`ApplicationContext` 扩展了 `BeanFactory` ,除了有`BeanFactory`的功能还有额外更多功能,所以一般开发人员使用`ApplicationContext`会更多。 - -ApplicationContext 提供了 BeanFactory 没有的新特性: - -1. 支持多语言版本; -2. 支持多种途径获取 Bean 定义信息; -3. 支持应用事件,方便管理 Bean; - -DefaultListableBeanFactory 实现了 ioc 容器的基本功能,其他 ioc 容器如 XmlBeanFactory 和 ApplicationContext 都是通过持有或扩展 DefaultListableBeanFactory 获得基本的 ioc 容器的功能。 - -### BeanDefinition - -BeanDefinition 用于管理Spring应用的对象和对象之间的依赖关系,是对象依赖关系的数据抽象。 - -### 容器初始化 - -ioc 容器初始化过程:BeanDefinition 的资源定位、解析和注册。 - -1. 从XML中读取配置文件。 -2. 将bean标签解析成 BeanDefinition,如解析 property 元素, 并注入到 BeanDefinition 实例中。 -3. 将 BeanDefinition 注册到容器 BeanDefinitionMap 中。 -4. BeanFactory 根据 BeanDefinition 的定义信息创建实例化和初始化 bean。 - -单例bean的初始化以及依赖注入一般都在容器初始化阶段进行,只有懒加载(lazy-init为true)的单例bean是在应用第一次调用getBean()时进行初始化和依赖注入。 - -```java -// AbstractApplicationContext -// Instantiate all remaining (non-lazy-init) singletons. -finishBeanFactoryInitialization(beanFactory); -``` - -多例bean 在容器启动时不实例化,即使设置 lazy-init 为 false 也没用,只有调用了getBean()才进行实例化。 - -loadBeanDefinitions 采用了模板模式,具体加载 BeanDefinition 的逻辑由各个子类完成。 - -### Bean生命周期 - -![](../img/bean-life-cycle.jpg) - -1.对Bean进行实例化 - -2.依赖注入 - -3.如果Bean实现了BeanNameAware接口,Spring将调用setBeanName(),设置 Bean id(xml文件中bean标签的id) - -4.如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory() - -5.如果Bean实现了ApplicationContextAware接口,Spring容器将调用setApplicationContext() - -6.如果存在BeanPostProcessor,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法,在Bean初始化前对其进行处理 - -7.如果Bean实现了InitializingBean接口,Spring将调用它的afterPropertiesSet方法,然后调用xml定义的 init-method 方法,两个方法作用类似,都是在初始化 bean 的时候执行 - -8.如果存在BeanPostProcessor,Spring将调用它们的postProcessAfterInitialization(后初始化)方法,在Bean初始化后对其进行处理 - -9.Bean初始化完成,供应用使用,直到应用被销毁 - -10.如果Bean实现了DisposableBean接口,Spring将调用它的destory方法,然后调用在xml中定义的 destory-method 方法,这两个方法作用类似,都是在Bean实例销毁前执行。 - -```java -public interface BeanPostProcessor { - @Nullable - default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Nullable - default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - -} - -public interface InitializingBean { - void afterPropertiesSet() throws Exception; -} -``` - -### Aware接口 - -对于应用程序来说,应该尽量减少对Sping Api的耦合程度,然而有些时候为了运用Spring所提供的一些功能,有必要让Bean了解Spring容器对其进行管理的细节信息,如让Bean知道在容器中是以那个名称被管理的,或者让Bean知道BeanFactory或者ApplicationContext的存在,也就是让该Bean可以取得BeanFactory或者ApplicationContext的实例,如果Bean可以意识到这些对象,那么就可以在Bean的某些动作发生时,做一些如事件发布等操作。 - -BeanNameAware:通过调用setBeanName()可以让bean获取自身的id属性。 - -ApplicationContextAware:通过调用 setApplicationContext() 设置应用上下文实例,从而可以直接在 Bean 中使用应用上下文的服务。 - -ApplicationEventPublisherAware:通过调用 setApplicationEventPublisher() 给 Bean 设置事件发布器,从而可以在 Bean 中发布应用上下文的事件。 - -### BeanFactory和FactoryBean - -BeanFactory:管理Bean的容器,Spring中生成的Bean都是由这个接口的实现来管理的。 - -FactoryBean:通常是用来创建比较复杂的bean,一般的bean 直接用xml配置即可,但如果一个bean的创建过程中涉及到很多其他的bean 和复杂的逻辑,直接用xml配置比较麻烦,这时可以考虑用FactoryBean,可以隐藏实例化复杂Bean的细节。 - -当配置文件中bean标签的class属性配置的实现类是FactoryBean时,通过 getBean()方法返回的不是FactoryBean本身,而是调用FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。如果想得到FactoryBean必须使用 '&' + beanName 的方式获取。 - -Mybatis 提供了 SqlSessionFactoryBean,可以简化 SqlSessionFactory 的配置: - -```java -public class SqlSessionFactoryBean implements FactoryBean, InitializingBean, ApplicationListener { - - @Override - public void afterPropertiesSet() throws Exception { - notNull(dataSource, "Property 'dataSource' is required"); - notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); - state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), - "Property 'configuration' and 'configLocation' can not specified with together"); - - this.sqlSessionFactory = buildSqlSessionFactory(); - } - - protected SqlSessionFactory buildSqlSessionFactory() throws IOException { - //复杂逻辑 - } - - @Override - public SqlSessionFactory getObject() throws Exception { - if (this.sqlSessionFactory == null) { - afterPropertiesSet(); - } - - return this.sqlSessionFactory; - } -} -``` - -在 xml 配置 SqlSessionFactoryBean: - -```xml - - - - - - -``` - -Spring 将会在应用启动时创建 `SqlSessionFactory`,并使用 `sqlSessionFactory` 这个名字存储起来。 - -#### FactoryBean使用 - -[FactoryBean使用](https://www.cnblogs.com/davidwang456/p/3688250.html) - -如果使用传统方式配置下面Car的\时,Car的每个属性分别对应一个\元素标签。 - -```java -public class Car { - private int maxSpeed ; - private String brand ; - private double price ; -} -``` - -如果用FactoryBean的方式实现就会灵活一些,下例通过逗号分割符的方式一次性地为Car的所有属性指定配置值: - -```java -public class CarFactoryBean implements FactoryBean { - private String carInfo ; - public Car getObject () throws Exception { - Car car = new Car () ; - String [] infos = carInfo .split ( "," ) ; - car.setBrand ( infos [ 0 ]) ; - car.setMaxSpeed ( Integer. valueOf ( infos [ 1 ])) ; - car.setPrice ( Double. valueOf ( infos [ 2 ])) ; - return car; - } - public Class getObjectType () { - return Car. class ; - } - public boolean isSingleton () { - return false ; - } - public String getCarInfo () { - return this . carInfo ; - } - - // 接受逗号分割符设置属性信息 - public void setCarInfo ( String carInfo ) { - this . carInfocarInfo = carInfo; - } -} -``` - -xml 配置 CarFactoryBean: - -```xml - -``` - -当调用getBean("car") 时,Spring通过反射机制发现CarFactoryBean实现了FactoryBean的接口,这时Spring容器就调用接口方法CarFactoryBean#getObject()方法返回。如果希望获取CarFactoryBean的实例,则需要在使getBean(beanName) 方法时在beanName前显示的加上 "&" 前缀,例如getBean("&car")。 - -### bean注入容器的方法 - -将普通类交给Spring容器管理,通常有以下方法: - -1、使用 @Configuration与@Bean 注解 - -2、使用@Controller @Service @Repository @Component 注解标注该类,然后启用@ComponentScan自动扫描 - -3、使用@Import 方法 - -@Import注解把bean导入到当前容器中。 - -```java -//@SpringBootApplication -@ComponentScan -/*把用到的资源导入到当前容器中*/ -@Import({Dog.class, Cat.class}) -public class App { - - public static void main(String[] args) throws Exception { - - ConfigurableApplicationContext context = SpringApplication.run(App.class, args); - System.out.println(context.getBean(Dog.class)); - System.out.println(context.getBean(Cat.class)); - context.close(); - } -} - -``` - - - -## bean的作用域 - -Spring创建bean默认是单例,每一个Bean的实例只会被创建一次,通过getBean()获取的是同一个Bean的实例。可使用<bean>标签的scope属性来指定一个Bean的作用域。 - -```xml - - -``` - -通过注解来声明作用域: - -```java -@Scope("prototype") -public class AccountDaoImpl { - //...... -} -``` - -容器在创建完一个prototype实例后,就不会去管理这个bean了,会把它交给应用自己去管理。 - -一般情况下,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。所谓有状态就是该bean保存有自己的信息,不能共享,否则会造成线程安全问题。而无状态则不保存信息,可以共享,spring中大部分bean都是单例的,整个生命周期过程只会存在一个。 - -request作用域:对于每次HTTP请求到达应用程序,Spring容器会创建一个全新的Request作用域的bean实例,且该bean实例仅在当前HTTP request内有效,整个请求过程也只会使用相同的bean实例,而其他请求HTTP请求则创建新bean的实例,当处理请求结束,request作用域的bean实例将被销毁。 - -session 作用域:每当创建一个新的HTTP Session时就会创建一个Session作用域的Bean,并该实例bean伴随着会话的结束(session过期)而销毁。 - - - -## 事务 - -事务就是一系列的操作原子执行。 - -Spring事务机制主要包括声明式事务和编程式事务。 - -声明式事务将事务管理代码从业务方法中分离出来,通过aop进行封装。用 @Transactional 注解开启声明式事务。 - -Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。 - -### @Transactional - -| 属性 | 类型 | 描述 | -| :--------------------- | ---------------------------------- | -------------------------------------- | -| value | String | 可选的限定描述符,指定使用的事务管理器 | -| propagation | enum: Propagation | 可选的事务传播行为设置 | -| isolation | enum: Isolation | 可选的事务隔离级别设置 | -| readOnly | boolean | 读写或只读事务,默认读写 | -| timeout | int (in seconds granularity) | 事务超时时间设置 | -| rollbackFor | Class对象数组,必须继承自Throwable | 导致事务回滚的异常类数组 | -| rollbackForClassName | 类名数组,必须继承自Throwable | 导致事务回滚的异常类名字数组 | -| noRollbackFor | Class对象数组,必须继承自Throwable | 不会导致事务回滚的异常类数组 | -| noRollbackForClassName | 类名数组,必须继承自Throwable | 不会导致事务回滚的异常类名字数组 | - -### 事务传播行为 - -在TransactionDefinition接口中定义了七个事务传播行为: - -1. PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。如果嵌套调用的两个方法都加了事务注解,并且运行在相同线程中,则这两个方法使用相同的事务中。如果运行在不同线程中,则会开启新的事务。 -2. PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。 -3. PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果不存在事务,则抛出异常`IllegalTransactionStateException`。 -4. PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。需要使用JtaTransactionManager作为事务管理器。 -5. PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。需要使用JtaTransactionManager作为事务管理器。 -6. PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。 -7. PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务, 则按PROPAGATION_REQUIRED 属性执行。 - -**PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:** - -使用PROPAGATION_REQUIRES_NEW时,内层事务与外层事务是两个独立的事务。一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。 - -使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。 - - - -## 循环依赖 - -[图解spring循环依赖](https://juejin.im/post/5e927e27f265da47c8012ed9#heading-5) | [循环依赖](https://blog.csdn.net/u010853261/article/details/77940767) - -构造器注入的循环依赖:spring处理不了,直接抛出BeanCurrentlylnCreationException异常。 - -单例模式下属性注入的循环依赖:通过三级缓存处理循环依赖。 - -非单例循环依赖:无法处理。 - -### 初始化 - -spring单例对象的初始化大略分为三步: - -1. createBeanInstance:实例化bean,使用构造方法创建对象,为对象分配内存。 -2. populateBean:进行依赖注入。 -3. initializeBean:初始化bean。 - -this.singletonsCurrentlyInCreation.add(String beanName)将当前正在创建的bean id记录在缓存中,如果在记录的过程中发现自己已经在缓存中,则说明存在循环依赖,将抛出BeanCurrentlylnCreationException 异常表示循环依赖。创建完成的bean将会从缓存中清除。 - -### 三级缓存 - -Spring为了解决单例的循环依赖问题,使用了三级缓存。 - -singletonObjects:完成了初始化的单例对象map,bean name --> bean instance - -earlySingletonObjects :完成实例化未初始化的单例对象map,bean name --> bean instance - -singletonFactories : 单例对象工厂map,bean name --> ObjectFactory,单例对象实例化完成之后会加入singletonFactories。 - -在调用createBeanInstance进行实例化之后,会调用addSingletonFactory,将单例对象放到singletonFactories中。 - -```java -protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { - Assert.notNull(singletonFactory, "Singleton factory must not be null"); - synchronized (this.singletonObjects) { - if (!this.singletonObjects.containsKey(beanName)) { - this.singletonFactories.put(beanName, singletonFactory); - this.earlySingletonObjects.remove(beanName); - this.registeredSingletons.add(beanName); - } - } -} -``` - -假如A依赖了B的实例对象,同时B也依赖A的实例对象。 - -1. A首先完成了实例化,并且将自己添加到singletonFactories中 -2. 接着进行依赖注入,发现自己依赖对象B,此时就尝试去get(B) -3. 发现B还没有被实例化,对B进行实例化 -4. 然后B在初始化的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects和二级缓存earlySingletonObjects没找到,尝试三级缓存singletonFactories,由于A初始化时将自己添加到了singletonFactories,所以B可以拿到A对象,然后将A从三级缓存中移到二级缓存中 -5. B拿到A对象后顺利完成了初始化,然后将自己放入到一级缓存singletonObjects中 -6. 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化 - -由此看出,属性注入的循环依赖主要是通过将实例化完成的bean添加到singletonFactories来实现的。而使用构造器依赖注入的bean在实例化的时候会进行依赖注入,不会被添加到singletonFactories中。比如A和B都是通过构造器依赖注入,A在调用构造器进行实例化的时候,发现自己依赖B,B没有被实例化,就会对B进行实例化,此时A未实例化完成,不会被添加到singtonFactories。而B依赖于A,B会去三级缓存寻找A对象,发现不存在,于是又会实例化A,A实例化了两次,从而导致抛异常。 - -总结:1、利用缓存识别已经遍历过的节点; 2、利用Java引用,先提前设置对象地址,后完善对象。 - - - -## Spring启动过程 - -1. 读取web.xml文件。 - -2. 创建 ServletContext,为 ioc 容器提供宿主环境。 - -3. 触发容器初始化事件,调用 contextLoaderListener.contextInitialized()方法,在这个方法会初始化一个应用上下文WebApplicationContext,即 Spring 的 ioc 容器。ioc 容器初始化完成之后,会被存储到 ServletContext 中。 - - ```java - public void contextInitialized(ServletContextEvent event) { - initWebApplicationContext(event.getServletContext()); - } - ``` - -4. 初始化web.xml中配置的Servlet。如DispatcherServlet,用于匹配、处理每个servlet请求。 - - - -## Spring Bean线程安全 - -Spring Bean默认是单例的,大部分的Spring bean没有可变的状态(比如Service类和DAO类),是线程安全的。如果Bean带有状态,可以将bean设置为prototype或者使用ThreadLocal确保线程安全。 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/Spring\347\224\250\345\210\260\345\223\252\344\272\233\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/\346\241\206\346\236\266/Spring\347\224\250\345\210\260\345\223\252\344\272\233\350\256\276\350\256\241\346\250\241\345\274\217.md" deleted file mode 100644 index dc0aacd..0000000 --- "a/\346\241\206\346\236\266/Spring\347\224\250\345\210\260\345\223\252\344\272\233\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ /dev/null @@ -1,67 +0,0 @@ - - -- 简单工厂:BeanFactory 就是简单工厂模式的体现,根据传入一个唯一标识来获得 Bean 对象。 - - ```java - @Override - public Object getBean(String name) throws BeansException { - assertBeanFactoryActive(); - return getBeanFactory().getBean(name); - } - ``` - -- 工厂方法:FactoryBean 就是典型的工厂方法模式。spring在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法。每个 Bean 都会对应一个 FactoryBean,如 SqlSessionFactory 对应 SqlSessionFactoryBean。 - -- 单例:一个类仅有一个实例,提供一个访问它的全局访问点。Spring 创建 Bean 实例默认是单例的。 - -- 适配器:SpringMVC中的适配器HandlerAdatper。由于应用会有多个Controller实现,如果需要直接调用Controller方法,那么需要先判断是由哪一个Controller处理请求,然后调用相应的方法。当增加新的 Controller,需要修改原来的逻辑,违反了开闭原则(对修改关闭,对扩展开放)。 - - ```java - if(mappedHandler.getHandler() instanceof MultiActionController){ - ((MultiActionController)mappedHandler.getHandler()).xxx - }else if(mappedHandler.getHandler() instanceof XXX){ - ... - }else if(...){ - ... - } - ``` - - 为此,Spring提供了一个适配器接口,每一种 Controller 对应一种 HandlerAdapter 实现类,当请求过来,SpringMVC会调用getHandler()获取相应的Controller,然后获取该Controller对应的 HandlerAdapter,最后调用HandlerAdapter的handle()方法处理请求,实际上调用的是Controller的handleRequest()。每次添加新的 Controller 时,只需要增加一个适配器类就可以,无需修改原有的逻辑。 - - 常用的处理器适配器:SimpleControllerHandlerAdapter,HttpRequestHandlerAdapter,AnnotationMethodHandlerAdapter。 - - ```java - // Determine handler for the current request. - mappedHandler = getHandler(processedRequest); - - HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); - - // Actually invoke the handler. - mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); - - public class HttpRequestHandlerAdapter implements HandlerAdapter { - - @Override - public boolean supports(Object handler) {//handler是被适配的对象,这里使用的是对象的适配器模式 - return (handler instanceof HttpRequestHandler); - } - - @Override - @Nullable - public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - - ((HttpRequestHandler) handler).handleRequest(request, response); - return null; - } - ``` - -- 代理:spring 的 aop 使用了动态代理,有两种方式JdkDynamicAopProxy 和Cglib2AopProxy。 - -- 观察者(observer):spring 中 observer 模式常用的地方是 listener 的实现,如ApplicationListener。 - -- 模板方法(template method):spring 中的 jdbctemplate。 - - - -> 参考链接:[spring设计模式](https://blog.csdn.net/caoxiaohong1005/article/details/80039656) | [SpringMVC适配器模式](https://blog.csdn.net/u010288264/article/details/53835185) \ No newline at end of file diff --git "a/\346\241\206\346\236\266/Spring\350\207\252\345\212\250\350\243\205\351\205\215.md" "b/\346\241\206\346\236\266/Spring\350\207\252\345\212\250\350\243\205\351\205\215.md" deleted file mode 100644 index 968c78c..0000000 --- "a/\346\241\206\346\236\266/Spring\350\207\252\345\212\250\350\243\205\351\205\215.md" +++ /dev/null @@ -1,187 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [byType](#bytype) -- [byName](#byname) -- [constructor](#constructor) -- [@Autowired和@Resource](#autowired%E5%92%8Cresource) -- [声明Bean注解](#%E5%A3%B0%E6%98%8Ebean%E6%B3%A8%E8%A7%A3) -- [@Bean和@Component](#bean%E5%92%8Ccomponent) - - - -Spring的自动装配有三种模式:byType(根据类型),byName(根据名称)、constructor(根据构造函数)。 - -## byType - -找到与依赖类型相同的bean注入到另外的bean中,这个过程需要借助setter注入来完成,因此必须存在set方法,否则注入失败。 - -当xml文件中存在多个相同类型名称不同的实例Bean时,Spring容器依赖注入仍然会失败,因为存在多种适合的选项,Spring容器无法知道该注入那种,此时我们需要为Spring容器提供帮助,指定注入那个Bean实例。可以通过<bean>标签的autowire-candidate设置为false来过滤那些不需要注入的实例Bean - -```xml - - - - - - - -``` - -## byName - -将属性名与bean名称进行匹配,如果找到则注入依赖bean。 - -```xml - - - - - -``` - -## constructor - -存在单个实例则优先按类型进行参数匹配(无论名称是否匹配),当存在多个类型相同实例时,按名称优先匹配,如果没有找到对应名称,则注入失败,此时可以使用autowire-candidate=”false” 过滤来解决。 - -```xml - - - - - - -``` - -@Autowired 可以传递了一个required=false的属性,false指明当userDao实例存在就注入不存就忽略,如果为true,就必须注入,若userDao实例不存在,就抛出异常。由于默认情况下@Autowired是按类型匹配的(byType),如果需要按名称(byName)匹配的话,可以使用@Qualifier注解与@Autowired结合。 - -```java -public class UserServiceImpl implements UserService { - //标注成员变量 - @Autowired - @Qualifier("userDao1") - private UserDao userDao; - } -``` - -byName模式 xml 配置: - -```xml - - - - - -``` - -@Resource,默认按 byName模式自动注入。@Resource有两个中重要的属性:name和type。Spring容器对于@Resource注解的name属性解析为bean的名字,type属性则解析为bean的类型。因此使用name属性,则按byName模式的自动注入策略,如果使用type属性则按 byType模式自动注入策略。倘若既不指定name也不指定type属性,Spring容器将通过反射技术默认按byName模式注入。 - -```java -@Resource(name=“userDao”) -private UserDao userDao;//用于成员变量 - -//也可以用于set方法标注 -@Resource(name=“userDao”) -public void setUserDao(UserDao userDao) { - this.userDao= userDao; -} -``` - -上述两种自动装配的依赖注入并不适合简单值类型,如int、boolean、long、String以及Enum等,对于这些类型,Spring容器也提供了@Value注入的方式。@Value接收一个String的值,该值指定了将要被注入到内置的java类型属性值,Spring 容器会做好类型转换。一般情况下@Value会与properties文件结合使用。 - -jdbc.properties文件如下: - -```properties -jdbc.driver=com.mysql.jdbc.Driver -jdbc.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&allowMultiQueries=true -jdbc.username=root -jdbc.password=root -``` - -利用注解@Value获取jdbc.url和jdbc.username的值,实现如下: - -```java -public class UserServiceImpl implements UserService { - //占位符方式 - @Value("${jdbc.url}") - private String url; - //SpEL表达方式,其中代表xml配置文件中的id值configProperties - @Value("#{configProperties['jdbc.username']}") - private String userName; - -} -``` - -xml配置文件: - -```xml - - - - - - - - - - - - classpath:/conf/jdbc.properties - - - -``` - -## @Autowired和@Resource - -@Autowired注解是按照类型(byType)装配依赖对象的,但是存在多个类型⼀致的bean,⽆法通过byType注⼊时,就会再使⽤byName来注⼊,如果还是⽆法判断注⼊哪个bean则会UnsatisfiedDependencyException。 -@Resource会⾸先按照byName来装配,如果找不到bean,会⾃动byType再找⼀次。 - -## 声明Bean注解 - -Spring 容器通过xml的bean标签配置和java注解两种方式声明的Bean对象。Spring的框架中提供了与@Component注解等效的用于声明bean的三个注解,@Repository 用于对DAO实现类进行标注,@Service 用于对Service实现类进行标注,@Controller 用于对Controller实现类进行标注。同时还可以给定一个bean名称,如果没有提供名称,那么默认情况下就是一个简单的类名(第一个字符小写)变成Bean名称。 - -```java -//@Component 相同效果 -@Service -public class AccountServiceImpl implements AccountService { - @Autowired - private AccountDao accountDao; -} -``` - -xml配置 bean: - -```xml - -``` - -## @Bean和@Component - -都是使用注解定义 Bean。@Bean 是使用 Java 代码装配 Bean,@Component 是自动装配 Bean。 - -@Component 注解用在类上,表明一个类会作为组件类,并告知Spring要为这个类创建bean,每个类对应一个 Bean。 - -@Bean 注解用在方法上,表示这个方法会返回一个 Bean。@Bean 需要在配置类中使用,即类上需要加上@Configuration注解。 - -```java -@Component -public class Student { - private String name = "lkm"; - - public String getName() { - return name; - } -} - -@Configuration -public class WebSocketConfig { - @Bean - public Student student(){ - return new Student(); - } -} -``` - -@Bean 注解更加灵活。当需要将第三方类装配到 Spring 容器中,因为没办法源代码上添加@Component注解,只能使用@Bean 注解的方式,当然也可以使用 xml 的方式。 \ No newline at end of file diff --git "a/\346\241\206\346\236\266/\346\267\261\345\205\245\346\265\205\345\207\272Mybatis\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230.md" "b/\346\241\206\346\236\266/\346\267\261\345\205\245\346\265\205\345\207\272Mybatis\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230.md" deleted file mode 100644 index bc1d6ca..0000000 --- "a/\346\241\206\346\236\266/\346\267\261\345\205\245\346\265\205\345\207\272Mybatis\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\345\256\236\346\210\230.md" +++ /dev/null @@ -1,2435 +0,0 @@ -## Mybatis简介 - -JDBC定义了连接数据库的接口规范,每个数据库厂商都会提供具体的实现,JDBC是一种典型的桥接模式。 - -### 传统的JDBC编程 - -- 获取数据库连接; -- 操作Connection,打开Statement对象; -- 通过Statement对象执行SQL,返回结果到ResultSet对象; -- 关闭数据库资源 - -```java -public class javaTest { - public static void main(String[] args) throws ClassNotFoundException, SQLException { - String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8"; - String USER="root"; - String PASSWORD="tiger"; - //1.加载驱动程序 - Class.forName("com.mysql.jdbc.Driver"); - //2.获得数据库链接 - Connection conn=DriverManager.getConnection(URL, USER, PASSWORD); - //3.通过数据库的连接操作数据库,实现增删改查(使用Statement类) - Statement st=conn.createStatement(); - ResultSet rs=st.executeQuery("select * from user"); - //4.处理数据库的返回结果(使用ResultSet类) - while(rs.next()){ - System.out.println(rs.getString("user_name")+" " - +rs.getString("user_password")); - } - - //关闭资源 - rs.close(); - st.close(); - conn.close(); - } -} -``` - -### Hibernate与Mybatis - -Hibernate建立在POJO和数据库表模型的直接映射关系上。通过POJO我们可以直接操作数据库的数据。相对而言,Hibernate对JDBC的封装程度比较高,我们不需要编写SQL,直接通过HQL去操作POJO进而操作数据库的数据。 - -Mybatis是半自动映射的orm框架,它需要我们提供POJO,SQL和映射关系,而全表映射的Hibernate只需要提供POJO和映射关系。 - -Hibernate编程简单,需要我们提供映射的规则,完全可以通过IDE实现,同时无需编写SQL,开发效率优于Mybatis。此外,它提供缓存、级联、日志等强大的功能, - -Hibernate与Mybatis区别: - -1. Hibernate是全自动,而Mybatis是半自动。 - Hibernate是全表映射,可以通过对象关系模型实现对数据库的操作,拥有完整的JavaBean对象与数据库的映射结构来自动生成sql。而Mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理。 - -2. Hibernate数据库移植性较好。 - Hibernate通过它强大的映射结构和hql语言,大大降低了对象与数据库的耦合性,而Mybatis由于需要手写sql,因此与数据库的耦合性直接取决于程序员写sql的方法,如果sql不具通用性而用了很多某数据库特性的sql语句的话,移植性也会随之降低很多,成本很高。 -3. Hibernate拥有完整的日志系统,Mybatis则欠缺一些。 - Hibernate日志系统非常健全,涉及广泛,包括:sql记录、关系异常、优化警告、缓存提示、脏数据警告等;而Mybatis则除了基本记录功能外,功能薄弱很多。 -4. sql直接优化上,Mybatis要比Hibernate方便很多。 - 由于Mybatis的sql都是写在xml里,因此优化sql比Hibernate方便很多,解除了sql与代码的耦合。而Hibernate的sql很多都是自动生成的,无法直接维护sql;写sql的灵活度上Hibernate不及Mybatis。 -5. Mybatis提供xml标签,支持编写动态sql。 - -## Mybatis入门 - -### SqlSessionFactory - -使用xml构建SqlSessionFactory,配置信息在mybatis-config.xml - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -```java -import org.apache.ibatis.io.Resources; -import org.apache.ibatis.session.SqlSession; -import org.apache.ibatis.session.SqlSessionFactory; -import org.apache.ibatis.session.SqlSessionFactoryBuilder; - -import java.io.IOException; -import java.io.InputStream; - -public class SqlSessionFactoryUtil { - private static SqlSessionFactory sqlSessionFactory = null; - //类线程锁 - private static final Class CLASS_LOCK = SqlSessionFactoryUtil.class; - - /** - * 私有化构造器 - */ - private SqlSessionFactoryUtil() {} - - public static SqlSessionFactory initSqlSessionFactory() { - String resource = "mybatis-config.xml"; - InputStream inputStream = null; - try { - inputStream = Resources.getResourceAsStream(resource); - } catch (IOException ex) { - ex.printStackTrace(); - } - synchronized (CLASS_LOCK) { - if(sqlSessionFactory == null) { - sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); - } - } - return sqlSessionFactory; - } - - public static SqlSession openSqlSession() { - if(sqlSessionFactory == null) { - initSqlSessionFactory(); - } - - return sqlSessionFactory.openSession(); - } -} -``` - -### SqlSession - -SqlSession是接口类,扮演门面的作用,真正处理数据的是Executor接口。在Mybatis中SqlSession的实现类有DefaultSqlSession和SqlSessionManager。 - - - -### 映射器Mapper - -映射器的实现方式有两种:通过xml方式实现,在mybatis-config.xml中定义Mapper;通过代码方式实现,在Configuration里面注册Mapper接口。建议使用xml配置方式,这种方式比较灵活,尤其当SQL语句很复杂时。 - -xml文件配置方式实现Mapper - -```java -public interface RoleMapper { - public Role getRole(@Param("id") Long id); -} -``` - -映射xml文件RoleMapper.xml - -```xml - - - - - - - -``` - -测试类 - -```java -import com.tyson.mapper.RoleMapper; -import com.tyson.pojo.Role; -import com.tyson.util.SqlSessionFactoryUtil; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.session.SqlSession; -import org.junit.Test; - -@Slf4j -public class RoleTest { - @Test - public void findRoleTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - Role role = roleMapper.getRole(1L); - if(role != null) { - log.info(role.toString()); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (sqlSession != null) { - sqlSession.close(); - } - } - } -} -``` - -Java方式实现Mapper - -```java -import com.tyson.pojo.Role; -import org.apache.ibatis.annotations.Select; - -public interface RoleMapper1 { - @Select(value="SELECT id, role_name as roleName, note from role where id = #{id}") - public Role getRole(@Param("id") Long id); -} -``` - -在Mybatis全局配置文件注册Mapper,有三种方式。 - -```xml - - - - - - - - - - - - - - - -``` - -### Mybatis组件的生命周期 - -1. SqlSessionFactoryBuilder - -作用是生成SqlSessionFactory,构建完毕则作用完结,生命周期只存在于方法的局部。 - -2. SqlSessionFactory - -创建SqlSession,每次访问数据库都需要通过SqlSessionFactory创建SqlSession。故SqlSessionFactory应存在于Mybatis应用的整个生命周期。 - -3. SqlSession - -会话,相当于JDBC的Connection对象,生命周期为请求数据库处理事务的过程。 - -4. Mapper - -作用是发送SQL,返回结果或执行SLQ修改数据库数据,它的生命周期在一个SqlSession事务方法之内。其最大的作用范围和SqlSession相同。 - -![Mybatis组件的生命周期](https://img-blog.csdnimg.cn/20190125180820866.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -## 配置 - -Mybatis全局配置文件mybatis-config.xml的层次结构顺序不能颠倒,否则在解析xml文件会产生异常。 - -```java - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### properties - -```xml - -``` - -```properties -driver=com.mysql.jdbc.Driver -url=jdbc:mysql://localhost:3306/mybatisdemo?serverTimezone=UTC -username=root -password=ad1234 -``` - -### typeAliases - -```xml - - - - - - -``` - -### typeHandler - -参考自:[关于mybatis中typeHandler的两个案例](https://blog.csdn.net/u012702547/article/details/54572679) - -类型处理器,作用是将参数从javaType转化为jdbcType,或者从数据库取出结果时把jdbcType转化成javaType。typeHandler和别名一样,分为系统定义和用户自定义。Mybatis系统定义的typeHandler就可以实现大部分的功能。 - -```java -public TypeHandlerRegistry() { - this.register((Class)Boolean.class, (TypeHandler)(new BooleanTypeHandler())); - this.register((Class)Boolean.TYPE, (TypeHandler)(new BooleanTypeHandler())); - this.register((JdbcType)JdbcType.BOOLEAN, (TypeHandler)(new BooleanTypeHandler())); - this.register((JdbcType)JdbcType.BIT, (TypeHandler)(new BooleanTypeHandler())); - this.register((Class)Byte.class, (TypeHandler)(new ByteTypeHandler())); - this.register((Class)Byte.TYPE, (TypeHandler)(new ByteTypeHandler())); - ...... -} -``` - -#### 自定义typeHandler - -假如需要将日期以字符串格式(转化成毫秒数)写进数据库,此时可以通过自定义typeHandler来实现此功能。 - -role表 - -```my -CREATE TABLE `role` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, - `role_name` varchar(20) DEFAULT NULL, - `note` varchar(20) DEFAULT NULL, - `reg_time` varchar(64) DEFAULT NULL, - `users` varchar(64) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1 -``` - -Role实体类 - -```java -import java.util.Date; -import java.util.List; - -public class Role { - private Long id; - private String roleName; - private String note; - private Date regTime; - - //setter和getter - - @Override - public String toString() { - return "id: " + id + ", roleName: " + roleName + ", note: " + note + ", regTime: " + regTime; //+ ", users: " + users.get(0); - } -} -``` - -1. 首先定义TypeHandler实现类(或继承BaseTypeHandler,BaseTypeHandler是TypeHandler的实现类)。在setNonNullParameter方法中,我们重新定义要写往数据库的数据。 在另外三个方法中我们将从数据库读出的数据进行类型转换。 - -```java -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.type.BaseTypeHandler; -import org.apache.ibatis.type.JdbcType; -import org.apache.ibatis.type.MappedJdbcTypes; -import org.apache.ibatis.type.MappedTypes; - -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Date; - -@Slf4j -@MappedJdbcTypes({JdbcType.VARCHAR}) -@MappedTypes({Date.class}) -public class MyDateTypeHandler extends BaseTypeHandler { - - @Override - public void setNonNullParameter(PreparedStatement preparedStatement, int i, Date date, JdbcType jdbcType) throws SQLException { - log.info("预编译语句设置参数: " + date.toString()); - preparedStatement.setString(i, String.valueOf(date.getTime())); - } - - @Override - public Date getNullableResult(ResultSet resultSet, String s) throws SQLException { - log.info("由列名 " + s + " 获取字符串:" + resultSet.getLong(s)); - return new Date(resultSet.getLong(s)); - } - - @Override - public Date getNullableResult(ResultSet resultSet, int i) throws SQLException { - log.info("由下标 " + i + " 获取字符串:" + resultSet.getLong(i)); - return new Date(resultSet.getLong(i)); - } - - @Override - public Date getNullableResult(CallableStatement callableStatement, int i) throws SQLException { - log.info("通过callbleStatement下标获取字符串"); - return callableStatement.getDate(i); - } -} -``` - -2. 在mybatis-config.xml中国注册自定义的typeHandler。单独配置或者扫描包的形式。注册之后数据的读写会被这个类过滤。 - -```java - - - - - - -``` - -3. 在RoleMapper.xml编写SQL语句,resultMap中的转换字段需要指定相应的jdbcType和javaType,启动我们自定义的typeHandler,本例中应设置reg_time字段jdbcType为VARCHAR,对应的regTime属性javaType设置为java.util.Date,与MyDateTypeHandler处理类型匹配,所以对reg_time字段的读取会先经过MyDateTypeHandler的处理。插入的时候不会启用我们自定义的typeHandler,需要在insert标签配置typeHandler(或指定jdbcType和javaType)。 - -```xml - - - - - - - - - - - - - - - - INSERT into role(id, role_name, note, reg_time) - VALUES(#{id}, #{roleName}, #{note}, #{regTime,javaType=Date, jdbcType=VARCHAR}) - - -``` - -#### 枚举类型typeHandler - -Mybatis内部提供了两个转化枚举类型的typeHandler:org.apache.ibatis.type.EnumTypeHandler和org.apache.apache.ibatis.type.EnumOrdinalTypeHandler。EnumTypeHandler使用枚举字符串名称作为参数传递,EnumOrdinalTypeHandler使用整数下标作为参数传递。 - -下面通过EnumOrdinalTypeHandler实现性别枚举。 - -```java -public enum Sex { - MALE(1, "男"), FEMALE(2, "女"); - - private int id; - private String name; - - private Sex(int id, String name) { - this.id = id; - this.name = name; - } - - public static Sex getSex(int id) { - if(id == 1) { - return MALE; - } else if(id == 2) { - return FEMALE; - } else { - return null; - } - } - - //setter和getter -} -``` - -StudentMapper.java - -```java -public interface StudentMapper { - public Student findStudent(int id); - public void insertStudent(Student student); -} -``` - -StudentMapper.xml - -```xml - - - - - - - - - - - - select last_insert_id() - - insert into student(name, sex) values(#{name}, - #{sex, typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}) - - - -``` - -测试类 - -```java -import com.tyson.entity.Sex; -import com.tyson.entity.Student; -import com.tyson.mapper.StudentMapper; -import com.tyson.util.SqlSessionFactoryUtil; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.session.SqlSession; -import org.junit.Test; - -@Slf4j -public class StudentTest { - @Test - public void findStudentTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - Student s = studentMapper.findStudent(3); - if(s != null) { - log.info(s.toString()); - } - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } - @Test - public void insertStudentTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - Student s = new Student(); - s.setName("tyson"); - s.setSex(Sex.MALE); - studentMapper.insertStudent(s); - sqlSession.commit(); - } catch (Exception ex) { - ex.printStackTrace(); - sqlSession.rollback(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } -} -``` - -插入的sex字段为INTEGER,测试结果如下: - -![枚举插入结果](https://img-blog.csdnimg.cn/20190202180633368.png) - -通过EnumTypeHandler实现性别枚举只需修改StudentMapper.xml相应的typeHandler,修改如下: - -```xml - - - - - - - - - - - - - select last_insert_id() - - insert into student(name, sex) values(#{name}, - #{sex, typeHandler=org.apache.ibatis.type.EnumTypeHandler}) - - - -``` - -插入的sex字段为VARCHAR类型,测试结果如下: - -![枚举插入结果](https://img-blog.csdnimg.cn/20190202180340893.png) - -##### 自定义枚举类typeHandler - -SexEnumTypeHandler类的定义。 - -```java -import com.tyson.entity.Sex; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.type.JdbcType; -import org.apache.ibatis.type.TypeHandler; - -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -@Slf4j -public class SexEnumTypeHandler implements TypeHandler { - - @Override - public void setParameter(PreparedStatement preparedStatement, int i, Sex sex, JdbcType jdbcType) throws SQLException { - log.info("预编译语句设置参数: " + i); - preparedStatement.setInt(i, sex.getId()); - } - - @Override - public Sex getResult(ResultSet resultSet, String s) throws SQLException { - log.info("由列名 " + s + " 获取字符串:" + resultSet.getString(s)); - int id = resultSet.getInt(s); - return Sex.getSex(id); - } - - @Override - public Sex getResult(ResultSet resultSet, int i) throws SQLException { - log.info("由下标 " + i + " 获取字符串:" + resultSet.getInt(i)); - int id = resultSet.getInt(i); - return Sex.getSex(id); - } - - @Override - public Sex getResult(CallableStatement callableStatement, int i) throws SQLException { - @Override - public Sex getResult(CallableStatement callableStatement, int i) throws SQLException { - int id = callableStatement.getInt(i); - return Sex.getSex(id); - } - } -} -``` - -mybatis-config.xml增加SexEnumTypeHandler的定义 - -```xml - - - - -``` - -StudentMapper.xml - -```xml - - - - - - - - - - - - - - select last_insert_id() - - insert into student(name, sex) values(#{name}, - #{sex, typeHandler=com.tyson.typeHandler.SexEnumTypeHandler}) - - - -``` - -测试结果如下: - -![测试结果](https://img-blog.csdnimg.cn/20190202182217878.png) - - - -### objectFactory - -当Mybatis在构建一个结果返回时,都会使用ObjectFactory去构建POJO,可以定制自己的对象工厂。一般使用默认的ObjectFactory即可,默认的ObjectFactory为org.apache.ibatis.reflection.factory.DefaultObjectFactory提。 - -### environments - -配置环境可以注册多个数据源,每一个数据源分为两部分的配置:数据库源的配置和数据库事务的配置。 - -```xml - - - - - - - - - - - - - - - - -``` - -- environments的default属性,表明在缺省的情况下,将使用哪个数据源配置。 -- transactionManager配置的是数据库事务,其type属性有三种配置方式。 - -(1)JDBC,使用JDBC管理事务,独立编码时常常使用; - -(2)MANAGED,使用容器方式管理事务,在JNDI数据源中常用; - -(3)自定义,由使用者自定义数据库管理方式,适用于特殊应用。 - -- dataSource标签,配置数据源连接的信息,type属性提供数据库连接方式的配置: - -(1)UNPOOLED,非连接池数据库 - -(2)POOLED,连接池数据库 - -(3)JNDI数据源 - -(4)自定义数据源 - -### 数据库事务 - -Mybatis数据库事务由SqlSession控制,我们可以通过SqlSession提交或回滚。 - -```java - @Test - public void insertRoleTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - - Role role = new Role(); - role.setId(2L); - role.setNote("hi"); - role.setRoleName("teacher"); - - roleMapper.insertRole(role); - sqlSession.commit(); - } catch (Exception e) { - e.printStackTrace(); - sqlSession.rollback(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -## 映射器 - -### select元素 - -| 元素 | 说明 | 备注 | -| ------------- | -------------------------------------------------------- | ------------------------------------------------------------ | -| id | 和Mapper命名空间的组合是唯一的 | 命名空间和id组合不唯一,则抛异常 | -| parameterType | 类的全路径或者别名 | 基本数据类型,JavaBean,Map等 | -| resultType | 基本数据类型或者类的全路径,可使用别名(需符合别名规范) | 允许自动匹配的请况下,结果集将通过JavaBean的规范映射,不能和resultMap同时使用 | -| resultMap | 自定义映射规则 | Mybatis最复杂的元素,可以配置映射规则、级联、typeHandler等 | - -#### 自动映射 - -autoMappingBehavior不为NONE时,Mybatis会提供自动映射的功能,只要返回的列名和JavaBean的属性一致,Mybatis就会帮助我们回填这些字段。实际上大部分数据库规范使用下划线分割单词,而Java则是用驼峰命名法,于是需要使用列的别名使得Mybatis能够自动映射,或者在配置文件中开启驼峰命名方式。 - -```xml - - -``` - -自动映射可以在setting元素中配置autoMappingBehavior属性值设定其策略。包含三个值: - -- NONE,取消自动映射 -- PARTIAL,只会自动映射,没有定义嵌套结果集映射的结果集 -- FULL,会自动映射任意复杂的结果集(无论是否嵌套) - -默认值是PARTIAL,默认情况下可以做到当前对象的映射,使用FULL是嵌套映射,性能会下降。 - -如果数据库是规范命名的,即每个单词用下划线分隔,而POJO是驼峰式命名的方式,此时可设置mapUnderscoreToCamelCase为true,这样就可以实现从数据库到POJO的自动映射了。 - -#### 传递多个参数 - -1. 使用注解方式传递参数 - -```java -public List findRoleByCondition(@Param("roleName") String roleName, @Param("note")String note); -``` - -RoleMapper.xml - -```xml - -``` - -2. 使用JavaBean传递参数 - -将参数组织成JavaBean,通过getter和setter方法设置参数。 - -```java -public class RoleParam { - private String roleName; - private String note; - - public String getRoleName() { - return roleName; - } - - public void setRoleName(String roleName) { - this.roleName = roleName; - } - - public String getNote() { - return note; - } - - public void setNote(String note) { - this.note = note; - } -} -``` - -接口RoleMapper - -```java -public List findRoleByParams2(RoleParam roleParam); -``` - -RoleMapper.xml - -```xml - -``` - -参数个数多于5,建议使用JavaBean方式。 - -#### 使用resultMap映射结果集 - -```xml - - - - - - - - -``` - -### insert元素 - -执行插入之后会返回一个整数,表示插入的记录数。parameterType 为 role(mybatis-config.xml 定义的别名)。 - -```xml - - INSERT into role(id, role_name, note) VALUES(#{id}, #{roleName}, #{note}) - -``` - -#### 主键回填 - -部分内容参考自:[insert主键返回 selectKey使用](https://blog.csdn.net/qq_29663071/article/details/79486048) - -设计表的时候有两种主键,一种自增主键,一般为int类型,一种为非自增的主键,例如用uuid等。 - -##### 自增主键 - -role表指定id字段为自增字段,对应的Role实体类提供getter和setter方法,便可以使用Mybatis的主键回填功能。通过keyProperty指定主键字段,并使用useGeneratedKeys告诉Mybatis这个主键是否使用数据库内置策略生成。 - -```xml - - - INSERT into role(role_name, note) VALUES(#{roleName}, #{note}) - -``` - -传入的role无需设置id,Mybatis在插入记录时会自动回填主键。 - -```java - @Test - public void insertRoleUseGeneratedKeysTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - - Role role = new Role(); - role.setNote("hello"); - role.setRoleName("worker"); - - roleMapper.insertRoleUseGeneratedKeys(role); - log.info(role.toString()); - sqlSession.commit(); - } catch (Exception e) { - e.printStackTrace(); - sqlSession.rollback(); - } finally { - if (sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -也可以通过selectKey设置主键回填。 - -```xml - - - - select LAST_INSERT_ID() - - INSERT into role(role_name, note) VALUES(#{roleName}, #{note}) - -``` - -##### 非自增主键 - -假设增加如下需求,当表role没有记录时,则插入第一条记录时id设为1,否则取最大的id加2,设置为新的主键,这个时候可以使用selectKey来处理。 - -```xml - - - - select if(max(id) is null, 1, max(id) + 2) as newId from role - - INSERT into role(id, role_name, note) VALUES(#{id}, #{roleName}, #{note}) - -``` - -selectKey标签的语句会被先执行,然后把查询到的id放到role对象。 - -```java - @Test - public void myInsertRoleTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - - Role role = new Role(); - role.setNote("hello"); - role.setRoleName("worker"); - - roleMapper.myInsertRole(role); - log.info(role.toString()); - sqlSession.commit(); - } catch (Exception e) { - e.printStackTrace(); - sqlSession.rollback(); - } finally { - if (sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -假设主键是VARCHAR类型,以uuid()方式生成主键。 - -```xml - - - - - - - select uuid() - - insert customer(id, name) values(#{id}, #{name}) - - -``` - -测试类。 - -```java -import com.tyson.mapper.CustomerMapper; -import com.tyson.pojo.Customer; -import com.tyson.util.SqlSessionFactoryUtil; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.session.SqlSession; -import org.junit.Test; - -@Slf4j -public class InsertCustomerTest { - @Test - public void insertCustomer() { - SqlSession session = null; - try { - session = SqlSessionFactoryUtil.openSqlSession(); - CustomerMapper customerMapper = session.getMapper(CustomerMapper.class); - Customer customer = new Customer(); - customer.setName("tyson"); - customerMapper.insertCustomer(customer); - session.commit(); - } catch (Exception ex) { - ex.printStackTrace(); - session.rollback(); - } finally { - if(session != null) { - session.close(); - } - } - } -} -``` - -### update和delete元素 - -update和delete元素用于更新记录和删除记录。插入和删除记录执行完成会返回一个整数,表示插入或删除几条记录。 - -```xml - - update role set - role_name = #{roleName}, - note = #{note} - where id = #{id} - - - - delete from role where id = #{id} - -``` - -测试类 - -```java - @Test - public void updateRole() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - Role role = new Role(); - role.setId(2L); - role.setRoleName("actor"); - role.setNote("fired"); - roleMapper.updateRole(role); - sqlSession.commit(); - } catch (Exception ex) { - ex.printStackTrace(); - sqlSession.rollback(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } - - @Test - public void deleteRole() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - roleMapper.deleteRole(8L); - sqlSession.commit(); - } catch (Exception ex) { - ex.printStackTrace(); - sqlSession.rollback(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -### sql元素 - -### resultMap元素 - -resultMap元素主要包括以下元素: - -```xml - - - - - - - - - - - - - -``` - -constructor元素用于配置构造方法。对于没有无参构造方法的POJO,可用constructor元素进行配置。 - -```xml - - - - -``` - -#### 级联 - -Mybatis中级联分为三种:association、collection和discriminator。 - -- association,代表一对一关系 -- collection,代表一对多关系 -- discriminator,鉴别器,它可以根据实际选择选用哪个类作为实例,允许根据特定的条件去关联不同的结果集。 - -##### association一对一级联 - -以学生和学生证为例,学生和学生证是一对一的关系,在Student建立一个类型为StudentCart的属性sc,这样便形成了级联。 - -```java -public class Student { - int id; - String name; - Sex sex; - StudentCard sc; - - //setter和getter -} -``` - -```java -public class StudentCard { - int id; - int sid; - String note; - - //setter和getter -} -``` - - - -在StudentCartMapper.xml中提供findStudentCardByStudentId方法。 - -```xml - - - - - - - - - - - -``` - -在StudentMapper里使用StudentCardMapper进行级联。 - -```xml - - - - - - - - - - - - - - - - select last_insert_id() - - insert into student(name, sex) values(#{name}, - #{sex, typeHandler=com.tyson.typeHandler.SexEnumTypeHandler}) - - -``` - -测试association级联。 - -```java - @Test - public void findStudentTest() { - SqlSession sqlSession = null; - try{ - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - Student s = studentMapper.findStudent(1); - log.info(s.toString()); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -测试结果: - -```java -22:37:54.931 [main] INFO com.tyson.StudentTest - id: 1, name: tyson, sex: MALE, cardId: 10086 -``` - -##### collection一对多级联 - -一个学生有多门课程,这是一对多的级联。建立Lecture的POJO记录课程和StudentLecture表示学生课程表,StudentLecture里有一个类型为Lecture的属性lecture,用来记录学生成绩。 - -StudentLecture和Lecture类: - -```java -public class StudentLecture { - int id; - int studentId; - Lecture lecture; - int grade; - - //setter和getter -} -``` - -```java -public class Lecture { - int id; - String lectureName; - - //setter和getter -} -``` - -Student类添加一个List类型的属性。 - -```java -public class Student { - int id; - String name; - Sex sex; - StudentCard sc; - List studentLectureList; - - //setter和getter -} -``` - -StudentMapper.xml使用collection对Student和StudentLecture做一对多的级联。 - -```xml - - - - - - - - - - - -``` - -StudentLectureMapper.xml需要使用association对StudentLecture和Lecture做一对一的级联。 - -```xml - - - - - - - - - - - -``` - -LectureMapper.xml - -```xml - - - - - - -``` - -测试类 - -```java - @Test - public void findStudentTest() { - SqlSession sqlSession = null; - try{ - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - Student s = studentMapper.findStudent(1); - log.info(s.toString()); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -测试结果如下: - -```java -10:46:33.916 [main] INFO com.tyson.StudentTest - id: 1, name: tyson, sex: MALE, cardId: 10086, studentLectureList: StudentLecture{studentId=1, lecture=id: 1, lectureName: math, grade=90}, StudentLecture{studentId=1, lecture=id: 2, lectureName: physics, grade=78}, -``` - -##### discriminator鉴别器级联 - -鉴别器级联是在特定的条件下去使用不用的POJO。比如可以通过学生信息表的sex属性进行判断关联男生健康指标或者女生的健康指标。新建MaleStudentHealth.java和FemaleStudentHealth.java,存储男生女生的健康信息。新建MaleStudent.java和FemaleStudent.java,继承自Student.java类。 - -```java -public class MaleStudentHealth { - int height; - //setter和getter -} - -public class MaleStudent extends Student { - List maleStudentHealthList; - //setter和getter -} -``` - -StudentMapper.xml如下,在discriminator元素通过sex字段的值判断是男生还是女生。当sex=1时,引入maleStudentMap的resultMap,这个resultMap继承自studentMap,使用collection对MaleStudent和MaleStudentHealth做一对多的级联。 - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -测试结果如下: - -```java -16:39:14.123 [main] INFO com.tyson.StudentTest - MaleStudent{maleStudentHealthList=MaleStudentHealth{height=170}, MaleStudentHealth{height=172}, id=1, name='tyson', sex=MALE, sc=studentCard: 10086, studentLectureList=[StudentLecture{studentId=1, lecture=id: 1, lectureName: math, grade=90}, StudentLecture{studentId=1, lecture=id: 2, lectureName: physics, grade=78}]} -``` - -##### 延迟加载 - -级联的优势在于能够方便快捷地获取数据,但是每次获取数据时,所有级联数据都会取出,每一个关联都会多执行一次SQL,这样会造成SQL执行过多性能下降。 - -假如我们通过传入id查找学生信息,然后打印出学生证信息,代码如下: - -```java -@Test -public void findStudentTest() { - SqlSession sqlSession = null; - try{ - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - Student s = studentMapper.findStudent(1); - log.info("***********获取学生证信息***********"); - log.info("学生的学生证信息:" + s.getSc().toString()); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } -} -``` - -测试结果如下,所有级联数据都会被加载出来。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216152606573.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -为了解决这个问题可以采用延迟加载的功能。首先打开延迟加载的开关。 - -```xml - - - - -``` - -重新运行测试代码,结果如下: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216151926516.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -由图可知,当我们查找学生信息时,会同时查出健康信息,当访问学生课程时,会同时把学生证信息查出。原因是Mybatis默认是按层级延迟加载的,如下图所示: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216115304804.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -当加载学生信息时,会根据鉴别器找到健康的信息。而当我们访问学生课程时,由于学生证和学生课程是一个层级,也会访问到学生证信息。通过设置全局参数aggressiveLazyLoading可以避免这种情况。aggressiveLazyLoading默认值是true,使用层级加载的策略,设置为false则会按照我们的需要去延迟加载数据。 - -```xml - - - - - - -``` - -测试结果如下,此时会根据我们的需要加载数据,不需要的数据不会被加载。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216153512266.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -aggressiveLazyLoading是全局的设置,不能指定哪个属性可以立即加载,哪个属性可以延迟加载。假如我们在查找学生信息时,很多情况下需要同时把学生课程成绩查出,此时采用即时加载比较好,多条SQL同时发出,性能高。我们可以在association和collection元素加入属性值fetchType(取值为lazy和eager),便可以实现局部延迟加载的功能。(需先设置aggressiveLazyLoading为false) - -StudentMapper.xml设置学生证和健康信息延时加载,学生课程即时加载。 - -```xml - - - - - - - -``` - -StudentLectureMapper.xml设置课程信息即时加载。 - -```xml - - - - - -``` - -测试代码: - -```java -public void findStudentTest() { - SqlSession sqlSession = null; - try{ - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - studentMapper.findStudent(1); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } -} -``` - -测试结果如下,可以看到有三条SQL被执行,查询学生信息,学生课程和课程信息。当我们访问延迟加载对象时,它才会发送SQL到数据库把数据加载回来。 - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190216160411511.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - - - -## 动态SQL - - Mybatis的动态SQL主要包括以下几种元素。 - -| 元素 | 作用 | -| ----------------------- | ------------------------ | -| if | 单条件分支判断 | -| choose(when、otherwise) | 相当于Java的switch、case | -| foreach | 在in语句等列举条件常用 | -| trim(where、set) | 用于处理SQL拼装问题 | - -### if元素 - -if元素和test属性联合使用。 - -```xml - -``` - -### choose元素 - -choose、when和otherwise类似于Java的switch、case和default。 - -```xml - -``` - -- 当roleName不为空,则只用roleName作为条件查询; -- 当roleName为空,note不为空,则用note作为条件进行查询; -- 当roleName和note都为空时,则以 id != 1 作为查询条件 - -### where元素 - -where元素解析时会自动将第一个字段的and去掉。 - -```xml - -``` - -测试结果: - -```java -11:42:16.301 [main] DEBUG c.tyson.mapper.RoleMapper.findRoles - ==> Preparing: select id, role_name, note, reg_time from role where note like concat('%', ?, '%') -11:42:16.404 [main] DEBUG c.tyson.mapper.RoleMapper.findRoles - ==> Parameters: hi(String) -``` - -使用trim也可以达到同样的效果。prefix代表语句前缀,prefixOverrides代表需要去掉的字符串。 - -```xml - -``` - -### set元素 - -当在 update 语句中使用if标签时,如果前面的if没有执行,则或导致逗号多余错误。使用set标签可以将动态的配置 SET 关键字,并剔除追加到条件末尾的任何不相关的逗号。使用 if+set 标签修改后,如果某项为 null 则不进行更新,而是保持数据库原值。 - -```xml - - update role - - - role_name = #{roleName}, - - - note = #{note}, - - - reg_time = #{regTime} - - - where id = #{id} - -``` - -测试结果如下,最后一个逗号被去掉了。 - -```java -11:39:37.975 [main] DEBUG c.tyson.mapper.RoleMapper.updateRole - ==> Preparing: update role SET role_name = ?, note = ? where id = ? -11:39:38.111 [main] DEBUG c.tyson.mapper.RoleMapper.updateRole - ==> Parameters: actor(String), fired(String), 2(Long) -``` - -### foreach元素 - -foreach用于遍历元素,支持数组、List和Set接口的集合。 - -```xml - - - select LAST_INSERT_ID() - - insert into role(role_name, note, reg_time) values - - - (#{role.roleName}, #{role.note}, #{role.regTime,javaType=Date, jdbcType=VARCHAR}) - - - - -``` - -RoleMapper.java - -```java -//List没有使用@Param指定参数名称,则对应Mapper.xml中的collection名称为list -public void batchInsertRole(@Param("roleList") List roleList); -public List findRolesInIds(@Param("ids") int[] ids); -``` - -| 参数 | 说明 | -| ---------- | ---------------------------- | -| collection | 数组、List或Set接口 | -| item | 当前元素 | -| index | 当前元素在集合的下标 | -| open/close | 用什么符号将集合元素包装起来 | -| separator | 间隔符 | - -### bind元素 - -当进行模糊查询时,对于MySQL数据库,我们经常会用concat函数用%和参数连接。对于Oracle则是用||符号连接。这样不同的数据库便需要不同的实现。有了bind元素,就不用考虑使用何种数据库语言,只要使用Mybatis的语言即可与所需参数相连,提高其移植性。 - -```xml - -``` - -测试结果: - -```java -15:05:21.798 [main] DEBUG c.tyson.mapper.RoleMapper.findRoles - ==> Preparing: select id, role_name, note, reg_time from role where role_name like ? and note like ? -15:05:21.946 [main] DEBUG c.tyson.mapper.RoleMapper.findRoles - ==> Parameters: %teacher%(String), %hi%(String) -``` - - - -## Mybatis-Spring应用 - -整合Mybatis-Spring可以通过xml的方式配置,也可以通过注解配置。配置Mybatis-Spring分为几个部分:配置数据源、配置SqlSessionFactory、配置SqlSessionTemplate、配置Mapper和事务处理。SqlSessionTemplate是对SqlSession操作的封装。 - -pom.xml导入依赖。 - -```xml - - - 4.0.0 - - com.tyson - mybatis-spring - 1.0-SNAPSHOT - - - UTF-8 - 4.3.2.RELEASE - 1.3.0 - 5.1.38 - 3.4.1 - 4.12 - 0.9.1.2 - - - - - - org.springframework - spring-context - ${spring.version} - - - - org.springframework - spring-test - ${spring.version} - - - - org.mybatis - mybatis-spring - ${mybatis-spring.version} - - - - org.springframework - spring-jdbc - ${spring.version} - - - - mysql - mysql-connector-java - ${mysql.version} - - - - org.mybatis - mybatis - 3.4.1 - - - - junit - junit - ${junit.version} - - - - c3p0 - c3p0 - ${c3p0.version} - - - - - - - - src/main/java - - **/*.xml - - - - src/main/resources - - - - -``` - -applicationContext.xml - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -sqlMapConfig.xml - -```xml - - - - - - - - - - - - - - - - -``` - -实体类Role.java - -```java -import java.util.Date; - -public class Role { - private Long id; - private String roleName; - private String note; - private Date regTime; - - //getter和setter -} -``` - -RoleMapper.java - -```java -import com.tyson.pojo.Role; -import org.springframework.stereotype.Repository; - -@Repository -public interface RoleMapper { - public void insertRole(Role role); -} -``` - -RoleMapper.xml - -```xml - - - - - insert into role(id, role_name, note) values(#{id}, #{roleName}, #{note}) - - -``` - -RoleService.java - -```java -import com.tyson.pojo.Role; - -public interface RoleService { - public void insertRole(Role role); -} -``` - -RoleServiceImpl.java - -```java -import com.tyson.mapper.RoleMapper; -import com.tyson.pojo.Role; -import com.tyson.service.RoleService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -@Service("roleService") -public class RoleServiceImpl implements RoleService { - @Autowired - RoleMapper roleMapper; - - @Transactional(propagation = Propagation.REQUIRED) - public void insertRole(Role role) { - roleMapper.insertRole(role); - } -} -``` - -测试 - -```java -import com.tyson.pojo.Role; -import com.tyson.service.RoleService; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(locations = {"classpath:applicationContext.xml"}) -public class RoleTest { - @Autowired - RoleService roleService; - - @Test - public void insertRolesTest() { - Role role = new Role(); - role.setId(1L); - role.setRoleName("stu"); - role.setNote("emm"); - - roleService.insertRole(role); - } -} -``` - - - -## 实用场景 - -### 批量更新 - -Mybatis内置的ExecutorType有3种,默认的是simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交sql;而batch模式重复使用已经预处理的语句,并且批量执行所有更新语句。 - -在数据库中使用批量更新有利于提高性能。在Mybatis中通过修改mybatis-config.xml配置文件中的settings的defaultExecutorType来制定其执行器为批量执行器。 - -```xml - - - -``` - -也可以通过Java代码实现批量执行器的使用。 - -```java -sqlSessionFactory.openSession(ExecutorType.BATCH); -``` - -在Spring中使用批量执行器。 - -```xml - - - - - -``` - -使用批量执行器,在默认情况下,它在sqlSession进行commit操作之后才会执行SQL语句。 - -测试代码如下: - -```java - @Test - public void roleTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - - Role role = new Role(); - role.setNote("emm"); - role.setRoleName("man"); - - roleMapper.insertRole(role); - roleMapper.insertRole(role); - roleMapper.insertRole(role); - - sqlSession.commit(); - } catch (Exception e) { - e.printStackTrace(); - sqlSession.rollback(); - } finally { - if (sqlSession != null) { - sqlSession.close(); - } - } - } -``` - -测试结果: - -未开启批量执行: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190214232057457.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -开启批量执行: - -![在这里插入图片描述](https://img-blog.csdnimg.cn/20190214232308469.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==,size_16,color_FFFFFF,t_70) - -设置ExecutorType.BATCH原理:把SQL语句发给数据库,数据库预编译好,数据库等待需要运行的参数,接收到参数后一次运行,ExecutorType.BATCH只打印一次SQL语句,多次设置参数。 - - - -### 存储过程 - -存储过程就是具有名字的一段代码,用来完成一个特定的功能。创建的存储过程保存在数据库的数据字典中。 - -优点(为什么要用存储过程?): - -  ①将重复性很高的一些操作,封装到一个存储过程中,简化了对这些SQL的调用 - -  ②批量处理:SQL+循环,减少流量 - -#### in和out参数 - -1.新建存储过程,按照传入的参数查询男女学生人数。 - -```my -#声明分隔符,默认为“;",编译器将两个$之间的内容当做存储过程的代码,不会执行这些代码 -DELIMITER $ -CREATE PROCEDURE gesture_count(IN sex INT, OUT ges_count INT) -BEGIN -IF sex=1 THEN -SELECT COUNT(*) FROM student AS s WHERE s.sex='male' INTO ges_count; -ELSE -SELECT COUNT(*) FROM student AS s WHERE s.sex='female' INTO ges_count; -END IF; -END -$ -#还原分隔符 -DELIMITER ; -``` - -调用存储过程。 - -```mysql -SET @ges_count=1; -CALL gesture_count(2, @ges_count); -SELECT @ges_count -``` - -2.定义一个Pojo反映存储过程的参数。 - -```java -public class ProcedureParam { - private int sex; - private int gesCount; - - //setter和getter -} -``` - -3.在xml映射器做配置,调用存储过程。 - -```xml - - -``` - -4.存储过程接口。 - -```java -public void gesCount(ProcedureParam procedureParam); -``` - -5.测试代码。 - -```java -@Test -public void getCountTest() { - SqlSession sqlSession = null; - try{ - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); - ProcedureParam procedureParam = new ProcedureParam(); - procedureParam.setSex(2); - studentMapper.gesCount(procedureParam); - String sex = procedureParam.getSex() == 1 ? "male" : "female"; - log.info("sex: " + sex + " count: " + procedureParam.getGesCount()); - } catch (Exception ex) { - ex.printStackTrace(); - } finally { - if(sqlSession != null) { - sqlSession.close(); - } - } -} -``` - -#### 游标 - -游标可以遍历返回的多行结果。Mysql中游标只适合存储过程和函数。 - -语法: - -1.定义游标:declare cur_name cursor for select 语句; - -2.打开游标:open cur_name; - -3.获取结果:fetch cur_name into param1, param2, ...; - -4.关闭游标:close cur_name; - -```mysql -DROP PROCEDURE IF EXISTS find_customer; -#声明分隔符,默认为“;",编译器将两个$之间的内容当做存储过程的代码,不会执行这些代码 -DELIMITER $ -CREATE PROCEDURE find_customer() -BEGIN - DECLARE no_more_record INT DEFAULT 0; - DECLARE id VARCHAR(64); - DECLARE cus_name VARCHAR(16); - #声明游标 - DECLARE cur CURSOR FOR SELECT * FROM customer; - DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_record = 1; - OPEN cur; - FETCH cur INTO id, cus_name; - - #不断循环到达表的末尾,继续fetch会报错 - WHILE no_more_record != 1 DO - INSERT INTO customer_tmp(id, `name`) - VALUES(id, cus_name); - FETCH cur INTO id, cus_name; - - END WHILE; - CLOSE cur; -END -$ -#还原分隔符 -DELIMITER ; -``` - -调用存储过程。 - -```mysql -TRUNCATE TABLE customer_tmp; -CALL find_customer(); -``` - -### 分页 - -#### RowBounds分页 - -RowBounds分页是Mybatis内置的基本功能,在任何的select语句中都可以使用,它是在SQL语句查询出所有结果之后,对结果进行截断,当SQL语句返回大量结果时,容易造成内存溢出。其适用于返回数据量小的查询。 - -RowBounds有两个重要的参数limit和offeset,offeset表示从哪一条记录开始读取,limit表示限制返回的记录数。 - -下面通过角色名称模糊查询角色信息。 - -```xml - - -``` - -RoleMapper接口定义。 - -```java -import com.tyson.pojo.Role; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.session.RowBounds; - -public interface RoleMapper { - public List getRoleByRoleName(@Param("roleName") String roleName, RowBounds rowBounds); -} -``` - -测试代码。 - -```java -@Test -public void getRoleByRoleNameTest() { - SqlSession sqlSession = null; - try { - sqlSession = SqlSessionFactoryUtil.openSqlSession(); - RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); - List roles = roleMapper.getRoleByRoleName("man", new RowBounds(0, 5)); - roles.forEach(role -> { - log.info(role.toString()); - }); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (sqlSession != null) { - sqlSession.close(); - } - } -} -``` - -测试结果返回五条记录。 - - - -## 预编译 - -#{ } 被解析成预编译语句,预编译之后可以直接执行,不需要重新编译sql。 - -```mysql -//sqlMap 中如下的 sql 语句 -select * from user where name = #{name}; -//解析成为预编译语句;编译好SQL语句再取值 -select * from user where name = ?; -``` - -${ } 仅仅为一个字符串替换,每次执行sql之前需要进行编译,存在 sql 注入问题。 - -```mysql -select * from user where name = '${name}' -//传递的参数为 "ruhua" 时,解析为如下,然后发送数据库服务器进行编译。取值以后再去编译SQL语句。 -select * from user where name = "ruhua"; -``` - - - -数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。如果一条sql语句需要反复执行,每次都进行语法检查和优化,会浪费很多时间。预编译语句就是将sql语句中的`值用占位符替代`,即将`sql语句模板化`。一次编译、多次运行,省去了解析优化等过程。 - -mybatis是通过PreparedStatement和占位符来实现预编译的。 - -mybatis底层使用PreparedStatement,默认情况下,将对所有的 sql 进行预编译,将#{}替换为?,然后将带有占位符?的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。 - -预编译的作用: - -1. 预编译阶段可以优化 sql 的执行。预编译之后的 sql 多数情况下可以直接执行,数据库服务器不需要再次编译,可以提升性能。 - -2. 预编译语句对象可以重复利用。把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。 - -3. 防止SQL注入。使用预编译,而其后注入的参数将`不会再进行SQL编译`。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一个参数。 - - - - ## 缓存 - -目前流行的缓存服务器有Redis、Ehcache、MangoDB等。缓存是计算机内存保存的数据,在读取数据的时候不用从磁盘读入,具备快速读取的特点,如果缓存命中率高,可以极大提升系统的性能。若缓存命中率低,则使用缓存意义不大,故使用缓存的关键在于存储内容访问的命中率。 - -### 一级缓存和二级缓存 - -Mybatis对缓存提供支持,默认情况下只开启一级缓存,一级缓存作用范围为同一个SqlSession。在SQL和参数相同的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只会执行一次SQL。因为在使用SqlSession第一次查询后,Mybatis会将结果放到缓存中,以后再次查询时,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只会取出当前缓存的数据,不会再次发送SQL到数据库。若使用不同的SqlSession,因为不同的SqlSession是相互隔离的,不会使用一级缓存。 - -二级缓存作用范围是Mapper(Namespace),可以使缓存在各个SqlSession之间共享。二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存: - -```xml - - - - -``` - -并在相应的Mapper.xml文件添加cache标签,表示对哪个mapper 开启缓存: - -```xml - -``` - -二级缓存要求返回的POJO必须是可序列化的,即要求实现Serializable接口。 - -当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。 - - - -## 原理 - -[Mybatis原理](https://blog.csdn.net/weixin_43184769/article/details/91126687) - -当调用Mapper接口方法的时候,Mybatis会使用JDK动态代理返回一个Mapper代理对象,代理对象会拦截接口方法,根据接口的全路径和方法名,定位到sql,使用executor执行sql语句,然后将sql执行结果返回。 - -因为mybatis动态代理寻找策略是 全限定名+方法名,不涉及参数,所以不支持重载。 - - - -## 优缺点 - -优点: - -1. SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 -2. 开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。 -3. 与各种数据库兼容。 - -缺点: - -1. SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 - diff --git "a/\346\266\210\346\201\257\351\230\237\345\210\227/\346\255\273\344\277\241\351\230\237\345\210\227.md" "b/\346\266\210\346\201\257\351\230\237\345\210\227/\346\255\273\344\277\241\351\230\237\345\210\227.md" deleted file mode 100644 index e09527c..0000000 --- "a/\346\266\210\346\201\257\351\230\237\345\210\227/\346\255\273\344\277\241\351\230\237\345\210\227.md" +++ /dev/null @@ -1,117 +0,0 @@ -死信队列:消费失败的消息存放的队列。消息消费失败的原因: - -- 消息被拒绝并且消息没有重新入队(requeue=false) -- 消息超时未消费 -- 达到最大队列长度 - -设置死信队列的 exchange 和 queue,然后进行绑定: - -```java - @Bean - public DirectExchange dlxExchange() { - return new DirectExchange(RabbitMqConfig.DLX_EXCHANGE); - } - - @Bean - public Queue dlxQueue() { - return new Queue(RabbitMqConfig.DLX_QUEUE, true); - } - - @Bean - public Binding bindingDeadExchange(Queue dlxQueue, DirectExchange deadExchange) { - return BindingBuilder.bind(dlxQueue).to(deadExchange).with(RabbitMqConfig.DLX_QUEUE); - } -``` - -在普通队列加上两个参数,绑定普通队列到死信队列。当消息消费失败时,消息会被路由到死信队列。 - -```java - @Bean - public Queue sendSmsQueue() { - Map arguments = new HashMap<>(2); - // 绑定该队列到私信交换机 - arguments.put("x-dead-letter-exchange", RabbitMqConfig.DLX_EXCHANGE); - arguments.put("x-dead-letter-routing-key", RabbitMqConfig.DLX_QUEUE); - return new Queue(RabbitMqConfig.MAIL_QUEUE, true, false, false, arguments); - } -``` - -生产者完整代码: - -```java -@Component -@Slf4j -public class MQProducer { - - @Autowired - RabbitTemplate rabbitTemplate; - - @Autowired - RandomUtil randomUtil; - - @Autowired - UserService userService; - - final RabbitTemplate.ConfirmCallback confirmCallback = (CorrelationData correlationData, boolean ack, String cause) -> { - log.info("correlationData: " + correlationData); - log.info("ack: " + ack); - if(!ack) { - log.info("异常处理...."); - } - }; - - - final RabbitTemplate.ReturnCallback returnCallback = (Message message, int replyCode, String replyText, String exchange, String routingKey) -> - log.info("return exchange: " + exchange + ", routingKey: " - + routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText); - - public void sendMail(String mail) { - //貌似线程不安全 范围100000 - 999999 - Integer random = randomUtil.nextInt(100000, 999999); - Map map = new HashMap<>(2); - String code = random.toString(); - map.put("mail", mail); - map.put("code", code); - - MessageProperties mp = new MessageProperties(); - //在生产环境中这里不用Message,而是使用 fastJson 等工具将对象转换为 json 格式发送 - Message msg = new Message("tyson".getBytes(), mp); - msg.getMessageProperties().setExpiration("3000"); - //如果消费端要设置为手工 ACK ,那么生产端发送消息的时候一定发送 correlationData ,并且全局唯一,用以唯一标识消息。 - CorrelationData correlationData = new CorrelationData("1234567890"+new Date()); - - rabbitTemplate.setMandatory(true); - rabbitTemplate.setConfirmCallback(confirmCallback); - rabbitTemplate.setReturnCallback(returnCallback); - rabbitTemplate.convertAndSend(RabbitMqConfig.MAIL_QUEUE, msg, correlationData); - - //存入redis - userService.updateMailSendState(mail, code, MailConfig.MAIL_STATE_WAIT); - } -} -``` - -消费者完整代码: - -```java -@Slf4j -@Component -public class DeadListener { - - @RabbitListener(queues = RabbitMqConfig.DLX_QUEUE) - public void onMessage(Message message, Channel channel) throws IOException { - - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - long deliveryTag = message.getMessageProperties().getDeliveryTag(); - //手工ack - channel.basicAck(deliveryTag,false); - System.out.println("receive--1: " + new String(message.getBody())); - } -} -``` - -当普通队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的死信交换机去,然后被路由到死信队列。可以监听死信队列中的消息做相应的处理。 diff --git "a/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\351\242\230.md" "b/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 90a7b0b..0000000 --- "a/\346\266\210\346\201\257\351\230\237\345\210\227/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,72 +0,0 @@ -## 为什么要使用消息队列? - -总结一下,主要三点原因:**解耦、异步、削峰**。 - -1、解耦。比如,用户下单后,订单系统需要通知库存系统,假如库存系统无法访问,则订单减库存将失败,从而导致订单操作失败。订单系统与库存系统耦合,这个时候如果使用消息队列,可以返回给用户成功,先把消息持久化,等库存系统恢复后,就可以正常消费减去库存了。 - -2、异步。将消息写入消息队列,非必要的业务逻辑以异步的方式运行,不影响主流程业务。 - -3、削峰。消费端慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。比如秒杀活动,一般会因为流量过大,从而导致流量暴增,应用挂掉。这个时候加上消息队列,服务器接收到用户的请求后,首先写入消息队列,如果消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。 - -## 使用了消息队列会有什么缺点 - -- 系统可用性降低。引入消息队列之后,如果消息队列挂了,可能会影响到业务系统的可用性。 -- 系统复杂性增加。加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。 - -## 常见的消息队列对比 - -| 对比方向 | 概要 | -| -------- | ------------------------------------------------------------ | -| 吞吐量 | 万级的 ActiveMQ 和 RabbitMQ 的吞吐量(ActiveMQ 的性能最差)要比 十万级甚至是百万级的 RocketMQ 和 Kafka 低一个数量级。 | -| 可用性 | 都可以实现高可用。ActiveMQ 和 RabbitMQ 都是基于主从架构实现高可用性。RocketMQ 基于分布式架构。 kafka 也是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 时效性 | RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。 | -| 功能支持 | 除了 Kafka,其他三个功能都较为完备。 Kafka 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准 | -| 消息丢失 | ActiveMQ 和 RabbitMQ 丢失的可能性非常低, RocketMQ 和 Kafka 理论上不会丢失。 | - -**总结:** - -- ActiveMQ 的社区算是比较成熟,但是较目前来说,ActiveMQ 的性能比较差,而且版本迭代很慢,不推荐使用。 -- RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 -- RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的 -- Kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 - -## MQ常用协议 - -- **AMQP协议** AMQP即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同开发语言等条件的限制。 - - > 优点:可靠、通用 - -- **MQTT协议** MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和致动器(比如通过Twitter让房屋联网)的通信协议。 - - > 优点:格式简洁、占用带宽小、移动端通信、PUSH、嵌入式系统 - -- **STOMP协议** STOMP(Streaming Text Orientated Message Protocol)是流文本定向消息协议,是一种为MOM(Message Oriented Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供一个可互操作的连接格式,允许客户端与任意STOMP消息代理(Broker)进行交互。 - - > 优点:命令模式(非topic/queue模式) - -- **XMPP协议** XMPP(可扩展消息处理现场协议,Extensible Messaging and Presence Protocol)是基于可扩展标记语言(XML)的协议,多用于即时消息(IM)以及在线现场探测。适用于服务器之间的准即时操作。核心是基于XML流传输,这个协议可能最终允许因特网用户向因特网上的其他任何人发送即时消息,即使其操作系统和浏览器不同。 - - > 优点:通用公开、兼容性强、可扩展、安全性高,但XML编码格式占用带宽大 - -- **其他基于TCP/IP自定义的协议**:有些特殊框架(如:redis、kafka、zeroMq等)根据自身需要未严格遵循MQ规范,而是基于TCP\IP自行封装了一套协议,通过网络socket接口进行传输,实现了MQ的功能。 - -## MQ的通讯模式 - -1. **点对点通讯**:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 -2. **多点广播**:MQ适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点(Destination List)。可以使用一条MQ指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ将消息的一个复制版本和该系统上接收者的名单发送到目标MQ系统。目标MQ系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 -3. **发布/订阅(Publish/Subscribe)模式**:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。在MQ家族产品中,MQ Event Broker是专门用于使用发布/订阅技术进行数据通讯的产品,它支持基于队列和直接基于TCP/IP两种方式的发布和订阅。 -4. **集群(Cluster)**:为了简化点对点通讯模式中的系统配置,MQ提供 Cluster 的解决方案。集群类似于一个 域(Domain) ,集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用 Cluster 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性 - -## 如何保证消息的顺序性? - -单线程消费保证消息的顺序性;对消息进行编号,消费者根据编号处理消息。 - -## 如何避免消息重复消费? - -在消息生产时,MQ内部针对每条生产者发送的消息生成一个唯一id,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列。 - -在消息消费时,要求消息体中也要有一全局唯一id作为去重和幂等的依据,避免同一条消息被重复消费。 - - - -![](http://img.dabin-coder.cn/image/20220612101342.png) \ No newline at end of file diff --git "a/\347\263\273\347\273\237\350\256\276\350\256\241/\346\211\253\347\240\201\347\231\273\345\275\225\345\216\237\347\220\206.md" "b/\347\263\273\347\273\237\350\256\276\350\256\241/\346\211\253\347\240\201\347\231\273\345\275\225\345\216\237\347\220\206.md" deleted file mode 100644 index 9b2fa63..0000000 --- "a/\347\263\273\347\273\237\350\256\276\350\256\241/\346\211\253\347\240\201\347\231\273\345\275\225\345\216\237\347\220\206.md" +++ /dev/null @@ -1,85 +0,0 @@ -本期是【**大厂面试**】系列文章的第**31**期 - -回复【**手册**】获取大彬精心整理的**大厂面试手册**。 - -## 面试开始 - -旁白:通过微信聊天方式模拟现场面试 - -**面试官**:**看你简历上写了做过扫码登录的功能** - -**面试官**:**详细介绍下怎么设计的?** - -大彬:嗯,扫码登录功能主要分为三个阶段:**待扫描、已扫描待确认、已确认**。 - -大彬:整体流程图如下图。 - -![](E:\b站\视频素材\图片\二维码扫描\整个流程.png) - -大彬:下面分阶段来看看设计原理。 - -**1、待扫描阶段** - -首先是待扫描阶段,这个阶段是 PC 端跟服务端的交互过程。 - -每次用户打开PC端登陆请求,系统返回一个**唯一的二维码ID**,并将二维码ID的信息绘制成二维码返回给用户。 - -这里的二维码ID一定是唯一的,后续流程会将二维码ID跟身份信息绑定,不唯一的话就会造成你登陆了其他用户的账号或者其他用户登陆你的账号。 - -此时在 PC 端会启动一个定时器,**轮询查询二维码是否被扫描**。 - -如果移动端未扫描的话,那么一段时间后二维码将会失效。 - -这个阶段的交互过程如下图所示。 - -![](E:\b站\视频素材\图片\二维码扫描\第一阶段.png) - -**2、已扫描待确认阶段** - -第二个阶段是已扫描待确认阶段,主要是移动端跟服务端交互的过程。 - -首先移动端扫描二维码,获取二维码 ID,然后**将手机端登录的凭证(token)和 二维码 ID 作为参数发送给服务端** - -此时的手机在之前已经是登录的,不存在没登录的情况。 - -服务端接受请求后,会将 token 与二维码 ID 关联,然后会生成一个临时token,这个 token 会返回给移动端,临时 token 用作确认登录的凭证。 - -PC 端的定时器,会轮询到二维码的状态已经发生变化,会将 PC 端的二维码更新为已扫描,请在手机端确认。 - -**面试官**:**打断一下,这里为什么要有手机端确认的操作?** - -大彬:假设没有确认这个环节,很容易就会被坏人拦截token去冒充登录。所以二维码扫描一定要有这个确认的页面,让用户去确认是否进行登录。 - -大彬:另外,二维码扫描确认之后,再往用户app或手机等发送登录提醒的通知,告知如果不是本人登录的,则建议用户立即修改密码。 - -大彬:这个阶段是交互过程如下图所示。 - -![](http://img.dabin-coder.cn/image/20220411002823.png) - -**3、已确认** - -扫码登录的最后阶段,用户点击确认登录,移动端携带上一步骤中获取的临时 token访问服务端。 - -服务端校对完成后,会更新二维码状态,并且给 PC 端生成一个正式的 token。 - -后续 PC 端就是持有这个 token 访问服务端。 - -这个阶段是交互过程如下图所示。 - -![](http://img.dabin-coder.cn/image/20220411002832.png) - -大彬:以上就是整个扫码登录功能的详细设计! - -**面试官**:**点赞!** - - - -## 点关注,不迷路 - -大彬,**非科班出身,自学Java**,校招斩获京东、携程、华为等offer。作为一名转码选手,深感这一路的不易。 - -希望我的分享能帮助到更多的小伙伴,**我踩过的坑你们不要再踩**。想与大彬交流的话,可以到公众号后台获取大彬的微信~ - -后台回复『 **笔记**』即可领取大彬斩获大厂offer的**面试笔记**。 - -![](http://img.dabin-coder.cn/image/公众号.jpg) diff --git "a/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241old.md" "b/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241old.md" deleted file mode 100644 index 4f92564..0000000 --- "a/\347\263\273\347\273\237\350\256\276\350\256\241/\347\263\273\347\273\237\350\256\276\350\256\241old.md" +++ /dev/null @@ -1,24 +0,0 @@ -1. 设计一个聊天服务 -2. 设计拼车服务 -3. 设计一个URL缩短服务 -4. 设计一个社交媒体服务 -5. 设计一个社交留言板 -6. 设计文件存储服务 -7. 设计一个视频流媒体服务 -8. 设计一个API速率限制器 -9. 设计一个邻近服务器 -10. 设计一个预输入服务 - - - -## 秒杀系统 - -rabbitmq是为了削峰,如果是有1000件商品参与秒杀,每个商品有10件,那么系统的最大并发就是1万,db扛不住这么大的并发的,如果商品数量更大,这个并发量会更大。 -通过Redis预减库存减少到DB的请求,通过消息队列异步写库缓解数据库的压力。用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步操作。 - -把判断库存扣减库存的操作封装成lua脚本,实现原子性操作,避免超卖。 - -![](http://img.dabin-coder.cn/image/20220509004840.jpg) - - - diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\256\227\346\263\225.md" deleted file mode 100644 index 7100651..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225/\347\256\227\346\263\225.md" +++ /dev/null @@ -1,374 +0,0 @@ -## 二叉树的遍历 - -二叉树的先序、中序和后序属于深度优先遍历DFS,层次遍历属于广度优先遍历BFS。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220619165820.png) - -### 前序遍历 - -```java -class Solution { - //方法1 - public List preorderTraversal1(TreeNode root) { - List result = new ArrayList<>(); - LinkedList ll = new LinkedList<>(); //类型声明List改为LinkedList,List没有addFirst()/removeFirst()方法 - - while (root != null || !ll.isEmpty()) { - while (root != null) { - result.add(root.val); - ll.addFirst(root); - root = root.left; - } - root = ll.removeFirst(); - root = root.right; - } - - return result; - } - //方法2 - public List preorderTraversal2(TreeNode root) { - List result = new ArrayList<>(); - if(root == null) { - return result; - } - - Stack s = new Stack<>(); - s.push(root); - while(!s.isEmpty()) { - TreeNode node = s.pop(); - result.add(node.val); - if(node.right != null) { - s.push(node.right);//先压右,再压左 - } - if(node.left != null) { - s.push(node.left); - } - } - - return result; - } -} -``` - -### 中序遍历 - -```java - public List inorderTraversal(TreeNode root) { - List res = new ArrayList<>(); - Deque deque = new ArrayDeque<>(); - - while (!deque.isEmpty() || root != null) { - while (root != null) { - deque.push(root); - root = root.left; - } - root = deque.pop(); - res.add(root.val); - root = root.right; - } - - return res; - } -``` - -### 后序遍历 - -使用 null 作为标志位,访问到 null 说明此次递归调用结束。 - -```java -class Solution { - public List postorderTraversal(TreeNode root) { - List res = new LinkedList<>(); - if (root == null) { - return res; - } - - Stack stack = new Stack<>(); - stack.push(root); - while (!stack.isEmpty()) { - root = stack.pop(); - if (root != null) { - stack.push(root);//最后访问 - stack.push(null); - if (root.right != null) { - stack.push(root.right); - } - if (root.left != null) { - stack.push(root.left); - } - } else { //值为null说明此次递归调用结束,将节点值存进结果 - res.add(stack.pop().val); - } - } - - return res; - } -} -``` - -### 层序遍历 - -``` -class Solution { - public List> levelOrder(TreeNode root) { - List> res = new ArrayList<>(); - LinkedList queue = new LinkedList<>(); - if (root == null) { - return res; - } - queue.addLast(root); - while (!queue.isEmpty()) { - List levelList = new LinkedList<>(); - int size = queue.size(); - while (size-- > 0) { - root = queue.removeFirst(); - levelList.add(root.val); - if (root.left != null) { - queue.addLast(root.left); - } - if (root.right != null) { - queue.addLast(root.right); - } - } - res.add(levelList); - } - return res; - } -} -``` - -## 排序算法 - -常见的排序算法主要有:冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序、基数排序。各种排序算法的时间空间复杂度、稳定性见下图。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/排序算法.png) - -### 冒泡排序 - -```java - public void bubbleSort(int[] arr) { - if (arr == null) { - return; - } - boolean flag; - for (int i = arr.length - 1; i > 0; i--) { - flag = false; - for (int j = 0; j < i; j++) { - if (arr[j] > arr[j + 1]) { - int tmp = arr[j]; - arr[j] = arr[j + 1]; - arr[j + 1] = tmp; - flag = true; - } - } - if (!flag) { - return; - } - } - } -``` - -### 插入排序 - -```java - public void insertSort(int[] arr) { - if (arr == null) { - return; - } - for (int i = 1; i < arr.length; i++) { - int tmp = arr[i]; - int j = i; - for (; j > 0 && tmp < arr[j - 1]; j--) { - arr[j] = arr[j - 1]; - } - arr[j] = tmp; - } - } -``` - -### 选择排序 - -```java - public void selectionSort(int[] arr) { - if (arr == null) { - return; - } - for (int i = 0; i < arr.length - 1; i++) { - for (int j = i + 1; j < arr.length; j++) { - if (arr[i] > arr[j]) { - int tmp = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp; - } - } - } - } -``` - -### 基数排序 - -在基数排序中,因为没有比较操作,所以在时间复杂上,最好的情况与最坏的情况在时间上是一致的,均为 O(d * (n + r))。d 为位数,r 为基数,n 为原数组个数。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWFnZXMyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTI1MjkxMC8yMDE4MDkvMTI1MjkxMC0yMDE4MDkxMzExMDYyOTY4Ni00MDU0Mjk5NjkucG5n?x-oss-process=image/format,png) - -### 快速排序 - -快速排序是由**冒泡排序**改进而得到的,是一种排序执行效率很高的排序算法,它利用**分治法**来对待排序序列进行分治排序,它的思想主要是通过一趟排序将待排记录分隔成独立的两部分,其中的一部分比关键字小,后面一部分比关键字大,然后再对这前后的两部分分别采用这种方式进行排序,通过递归的运算最终达到整个序列有序。 - -快速排序的过程如下: - -1. 在待排序的N个记录中任取一个元素(通常取第一个记录)作为基准,称为基准记录; -2. 定义两个索引 left 和 right 分别表示首索引和尾索引,key 表示基准值; -3. 首先,尾索引向前扫描,直到找到比基准值小的记录,并替换首索引对应的值; -4. 然后,首索引向后扫描,直到找到比基准值大于的记录,并替换尾索引对应的值; -5. 若在扫描过程中首索引等于尾索引(left = right),则一趟排序结束;将基准值(key)替换首索引所对应的值; -6. 再进行下一趟排序时,待排序列被分成两个区:[0,left-1]和[righ+1,end] -7. 对每一个分区重复以上步骤,直到所有分区中的记录都有序,排序完成 - -快排为什么比冒泡效率高? - -快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小于基准值的都交换到左边,大于基准值的都交换到右边,这样不会像冒泡一样每次都只交换相邻的两个数,因此比较和交换的此数都变少了,速度自然更高。 - -快速排序的平均时间复杂度是O(nlgn),最坏时间复杂度是O(n^2)。 - -```java - public void quickSort(int[] arr) { - if (arr == null) { - return; - } - quickSortHelper(arr, 0, arr.length - 1); - } - private void quickSortHelper(int[] arr, int left, int right) { - if (left > right) { - return; - } - int tmp = arr[left]; - int i = left; - int j = right; - while (i < j) { - //j先走,最终循环终止时,j停留的位置就是arr[left]的正确位置 - //改为i<=j,则会进入死循环,[1,5,5,5,5]->[1] 5 [5,5,5]->[5,5,5],会死循环 - while (i < j && arr[j] >= tmp) { - j--; - } - while (i < j && arr[i] <= tmp) { - i++; - } - if (i < j) { - int tmp1 = arr[i]; - arr[i] = arr[j]; - arr[j] = tmp1; - } else { - break; - } - } - - //当循环终止的时候,i=j,因为是j先走的,j所在位置的值小于arr[left],交换arr[j]和arr[left] - arr[left] = arr[j]; - arr[j] = tmp; - - quickSortHelper(arr, left, j - 1); - quickSortHelper(arr, j + 1, right); - } -``` - -### 归并排序 - -归并排序 (merge sort) 是一类与插入排序、交换排序、选择排序不同的另一种排序方法。归并的含义是将两个或两个以上的有序表合并成一个新的有序表。归并排序有多路归并排序、两路归并排序 , 可用于内排序,也可以用于外排序。 - -两路归并排序算法思路是递归处理。每个递归过程涉及三个步骤 - -- 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素 -- 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作 -- 合并: 合并两个排好序的子序列,生成排序结果 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220327151830.png) - -时间复杂度:对长度为n的序列,需进行logn次二路归并,每次归并的时间为O(n),故时间复杂度是O(nlgn)。 - -空间复杂度:归并排序需要辅助空间来暂存两个有序子序列归并的结果,故其辅助空间复杂度为O(n) - -```java -public class MergeSort { - public void mergeSort(int[] arr) { - if (arr == null || arr.length == 0) { - return; - } - //辅助数组 - int[] tmpArr = new int[arr.length]; - mergeSort(arr, tmpArr, 0, arr.length - 1); - } - - private void mergeSort(int[] arr, int[] tmpArr, int left, int right) { - if (left < right) { - int mid = (left + right) >> 1; - mergeSort(arr, tmpArr, left, mid); - mergeSort(arr, tmpArr, mid + 1, right); - merge(arr, tmpArr, left, mid, right); - } - } - - private void merge(int[] arr, int[] tmpArr, int left, int mid, int right) { - int i = left; - int j = mid + 1; - int tmpIndex = left; - while (i <= mid && j <= right) { - if (arr[i] < arr[j]) { - tmpArr[tmpIndex++] = arr[i]; - i++; - } else { - tmpArr[tmpIndex++] = arr[j]; - j++; - } - } - - while (i <= mid) { - tmpArr[tmpIndex++] = arr[i++]; - } - - while (j <= right) { - tmpArr[tmpIndex++] = arr[j++]; - } - - for (int m = left; m <= right; m++) { - arr[m] = tmpArr[m]; - } - } -} -``` - -### 堆排序 - -堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。 - -![](https://img-blog.csdn.net/20150312212515074) - -**Top大问题**解决思路:使用一个固定大小的**最小堆**,当堆满后,每次添加数据的时候与堆顶元素比较,若小于堆顶元素,则舍弃,若大于堆顶元素,则删除堆顶元素,添加新增元素,对堆进行重新排序。 - -对于n个数,取Top m个数,时间复杂度为O(nlogm),这样在n较大情况下,是优于nlogn(其他排序算法)的时间复杂度的。 - -PriorityQueue 是一种基于优先级堆的优先级队列。每次从队列中取出的是具有最高优先权的元素。如果不提供Comparator的话,优先队列中元素默认按自然顺序排列,也就是数字默认是小的在队列头。优先级队列用数组实现,但是数组大小可以动态增加,容量无限。 - -```java -//找出前k个最大数,采用小顶堆实现 -public static int[] findKMax(int[] nums, int k) { - PriorityQueue pq = new PriorityQueue<>(k);//队列默认自然顺序排列,小顶堆,不必重写compare - - for (int num : nums) { - if (pq.size() < k) { - pq.offer(num); - } else if (pq.peek() < num) {//如果堆顶元素 < 新数,则删除堆顶,加入新数入堆 - pq.poll(); - pq.offer(num); - } - } - - int[] result = new int[k]; - for (int i = 0; i < k&&!pq.isEmpty(); i++) { - result[i] = pq.poll(); - } - return result; -} -``` - - - diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/TCP IP\345\215\217\350\256\256.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/TCP IP\345\215\217\350\256\256.md" deleted file mode 100644 index 0415e38..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/TCP IP\345\215\217\350\256\256.md" +++ /dev/null @@ -1,349 +0,0 @@ - - - -- [概述](#%E6%A6%82%E8%BF%B0) - - [分层](#%E5%88%86%E5%B1%82) - - [封装](#%E5%B0%81%E8%A3%85) - - [分用](#%E5%88%86%E7%94%A8) - - [端口号](#%E7%AB%AF%E5%8F%A3%E5%8F%B7) -- [链路层](#%E9%93%BE%E8%B7%AF%E5%B1%82) - - [SLIP](#slip) - - [PPP 点对点协议](#ppp-%E7%82%B9%E5%AF%B9%E7%82%B9%E5%8D%8F%E8%AE%AE) - - [以太网](#%E4%BB%A5%E5%A4%AA%E7%BD%91) -- [网际协议](#%E7%BD%91%E9%99%85%E5%8D%8F%E8%AE%AE) - - [IP 首部](#ip-%E9%A6%96%E9%83%A8) - - [IP 路由选择](#ip-%E8%B7%AF%E7%94%B1%E9%80%89%E6%8B%A9) - - [子网寻址](#%E5%AD%90%E7%BD%91%E5%AF%BB%E5%9D%80) -- [ARP](#arp) - - [工作原理](#%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86) - - [分组格式](#%E5%88%86%E7%BB%84%E6%A0%BC%E5%BC%8F) - - [ARP 代理](#arp-%E4%BB%A3%E7%90%86) -- [RARP](#rarp) - - [分组格式](#%E5%88%86%E7%BB%84%E6%A0%BC%E5%BC%8F-1) -- [ICMP](#icmp) - - [ICMP 报文类型](#icmp-%E6%8A%A5%E6%96%87%E7%B1%BB%E5%9E%8B) - - [ICMP 地址掩码请求与应答](#icmp-%E5%9C%B0%E5%9D%80%E6%8E%A9%E7%A0%81%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%BA%94%E7%AD%94) - - [ICMP 时间戳请求与应答](#icmp-%E6%97%B6%E9%97%B4%E6%88%B3%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%BA%94%E7%AD%94) - - [ICMP 端口不可达差错](#icmp-%E7%AB%AF%E5%8F%A3%E4%B8%8D%E5%8F%AF%E8%BE%BE%E5%B7%AE%E9%94%99) -- [Traceroute](#traceroute) -- [IP 选路](#ip-%E9%80%89%E8%B7%AF) -- [广播和多播](#%E5%B9%BF%E6%92%AD%E5%92%8C%E5%A4%9A%E6%92%AD) -- [IGMP](#igmp) -- [UDP](#udp) - - [特点](#%E7%89%B9%E7%82%B9) - - [首部](#%E9%A6%96%E9%83%A8) -- [TCP](#tcp) - - [特点](#%E7%89%B9%E7%82%B9-1) - - [停止等待协议](#%E5%81%9C%E6%AD%A2%E7%AD%89%E5%BE%85%E5%8D%8F%E8%AE%AE) - - [首部](#%E9%A6%96%E9%83%A8-1) -- [DNS](#dns) -- [TFTP](#tftp) -- [BOOTP](#bootp) - - - -## 概述 - -### 分层 - -1. 数据链路层:有时称作网络接口层,通常包括操作系统的设备驱动程序和计算机中对应的网络接口卡。 -2. 网络层:处理分组在网络中的活动,如分组的选路。 -3. 运输层:主要为两台主机的应用程序进程之间的通信提供通用的数据传输服务。通用是指多种应用可以使用同一个运输层服务。 -4. 应用层:应用进程间通信和交互的规则。 - -### 封装 - -TCP 传给 IP 的数据单元称作 TCP报文段。IP 传给网络接口层的数据单元称作 IP 数据报。通过以太网传输的比特流称作帧。 - -![数据进入协议栈时的封装过程](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190817161255444-332455631.png) - -应用程序使用 TCP/UDP 传输数据,运输层协议在生成报文首部时要存入一个应用程序的标识。TCP、UDP 使用16 bit 的端口号来表示不同的应用程序,把源端口号和目的端口号分别存入报文首部。 - -TCP 报文进入 IP 层时,IP 层会在首部存入一个长度为8 bit 的数值,称作协议域。1表示为 ICMP 协议, 2表示为 IGMP 协议, 6表示为 TCP 协议, 17表示为 UDP 协议。 - -数据从网络层进入数据链路层时,需要在帧首部加入一个16 bit的帧类型域,以指明生成数据的网络层协议。 - -### 分用 - -当目的主机收到一个以太网数据帧时,数据就开始从协议栈由下而上进行分用,同时去掉各层协议的首部。每个协议层都会检查报文首部的协议标识,从而确定接收数据的上层协议。 - -![以太网数据帧分用过程](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190817163045743-889709219.png) - -### 端口号 - -服务器一般使用知名的端口号,FTP 服务器的 TCP 端口号是21,Telnet 服务器的 TCP 端口号是23。客户端端口号也称为临时端口号,只有用户运行了这个应用程序时端口号才存在。 - - - -## 链路层 - -TCP/IP 协议族中,链路层主要有三个目的:1.为 IP 模块发送和接收 IP 数据报;2.为 ARP 模块发送ARP 请求和接收 ARP 应答;3.为 RARP 发送 RARP 请求和接收 RARP 应答。 -数据链路层使用的信道主要有以下两种类型: -- 点对点信道 -- 广播信道 - -### SLIP - -SLIP 的全程是 Serial Line IP。它是一种在串行线路上对 ip 数据报进行封装的简单格式。 - -SLIP 是一种简单的帧封装方法,有以下缺陷: - -1. 每一端必须知道对方的I P地址; -2. 数据帧中没有类型字段(类似于以太网中的类型字段)。如果一条串行线路用于 SLIP,那么它不能同时使用其他协议; -3. 没有在数据帧中加上检验和。 - -### PPP 点对点协议 - -PPP 修改了 SLIP 协议中的所有缺陷,有三个组成部分: - -1. 在串行链路上封装I IP 数据报的方法; -2. 一个用来建立、配置和测试数据链路连接的链路控制协议LCP; -3. 一套网络控制协议NCP。 - -MTU:最大传输单元。如果 IP 层传输数据比链路层的 MTU 大,那么需要在 IP 层对数据进行分片。 - - - -环回接口(Loopback Interface)允许运行在同一台主机上的客户端和服务器通过 tcp/ip 进行通信。大多数系统把 ip 地址127.0.0.1 分配给这个接口,命名为 localhost。一个传给环回接口的数据报不能出现在任何网络上。 - -### 以太网 -在局域网中,硬件地址又称为物理地址或MAC地址。 -![MAC帧格式](https://img-blog.csdnimg.cn/20190824171916714.png) - - - -## 网际协议 - -IP 协议特点有: - -1. 不可靠。不能保证 IP 数据报能成功地到达目的地。IP 仅提供最好的传输服务。如果发生某种错误时,如某个路由器暂时用完了缓冲区, IP有一个简单的错误错误处理算法:丢弃该数据报,然后发送 ICMP 消息报给信源端。任何要求的可靠性必须由上层来提供。 -2. 无连接。IP 并不维护任何关于后续数据报的状态信息。 - -### IP 首部 - -IP 数据报的格式: - -![ip首部](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912193522827-1130088556.png) - -TTL 设置了数据报可以经过的最多路由器数目。TTL 初始值由主机设置,每经过一个路由器它的值会减一。当该字段的值为0时,数据报就被丢弃,并发送 ICMP 报文通知源主机。 - -### IP 路由选择 - -IP 层可以配置成路由器的功能,也可以配置成主机的功能。 - -路由表包含的信息:1.目的 ip 地址;2.下一跳路由器的 ip 地址;3.标志。其中一个标志指明目的 ip 地址是网络地址还是主机地址,另一个标志指明下一跳路由器是否是真正的路由器,还是一个直接连接的接口。 - -ip 路由选择主要完成以下功能: - -1. 搜索路由表,寻找能与目的 ip 地址完全匹配的表目(网络号和主机号都要匹配); -2. 搜索路由表,寻找能与目的网络号相匹配的表目; -3. 搜索路由表,寻找默认路由。 - -如果上述步骤都没有成功,那么该数据报不能被传送。 - -为一个网络指定一个路由器,而不是为每个主机指定路由器,可以极大的缩小路由表的规模。 - -### 子网寻址 - -B 类地址:16位网络号;8位子网号;8位主机号 - -子网对外部路由器来说隐藏了内部网络组织(一个校园或公司内部)的细节。 - -通过 ip 地址可以分辨属于 A/B/C/D 类地址。通过子网掩码可以知道子网号和主机号之间的分界线。 - -![ip地址分类](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190822111405226-1329649757.png) - - - -## ARP - -地址解析协议。ARP 的功能是在同一个局域网内为32 bit的IP地址和采用不同网络技术的硬件地址之间提供动态映射。 - -### 工作原理 - -首先,每台主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址的对应关系。当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP列表中是否存在该 IP地址对应的MAC地址,如果有,就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个 ARP响应数据包,告诉对方自己是它需要查找的MAC地址;源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。 - -### 分组格式 - -![ARP分组格式](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190819170719796-1815161798.png) - -目的地址为全1的特殊地址是广播地址。电缆上的所有以太网接口都要接收广播的数据帧。 - -两个字节长的以太网帧类型表示后面数据的类型。对于A R P请求或应答来说,该字段的值为0x0806。 - -硬件类型字段表示硬件地址的类型。它的值为1即表示以太网地址。协议类型字段表示要映射的协议地址类型。它的值为0x0800即表示 IP 地址。 - -操作字段 op 有四种类型,ARP 请求(值为1)、ARP 应答(2)、RARP 请求(3)和 RARP 应答(4)。这个字段是必须的,因为 ARP 请求和 ARP 应答的帧类型值是一样的。 - -对于一个 ARP 请求,除了目的端硬件地址(以太网地址)外的所有字段都需要填满。当系统收到一个目的主机为本机的ARP 请求后,它会把自己的硬件地址填写进去,然后将两个目的端硬件地址改成发送 ARP 请求的主机的硬件地址。 - -### ARP 代理 - -如果 APR 请求是从一个网络的主机发往另一个网络上的主机,那么连接这两个网络的路由器就可以回答该请求,这个过程称作 ARP 代理。这样可以欺骗发起 ARP 请求的发送端,使它误以为路由器就是目的主机,而事实上目的主机是在路由器的“另一边”。路由器的功能相当于目的主机的代理,把分组从其他主机转发给它。 - - - -## RARP - -逆地址解析协议。具有本地磁盘的系统引导时,一般从磁盘的配置文件读取 IP 地址。而无盘系统通过 RARP 协议来读取 ip:从接口卡上读取唯一的硬件地址,在分组中表明发送端的硬件地址,然后发送 RARP 请求,请求某个主机响应该无盘系统的 IP 地址(RARP 应答中)。 - -### 分组格式 - -RARP 的分组格式和 ARP 的分组格式类似,主要区别是 RARP 请求或应答的帧类型是0x8053,而且 RARP 请求的 op 操作代码是3,RARP 应答的 op 操作代码是4。 - - - -## ICMP - -Internet 控制报文协议。ICMP 通常被认为是 IP 层的组成部分。它被用来传递报文以及其他需要注意的信息。ICMP 报文通常被 IP 层或者更高层的协议使用。 - -### ICMP 报文类型 - -查询报文和差错报文。 - -### ICMP 地址掩码请求与应答 - -ICMP 地址掩码请求用于无盘系统在引导过程获取自己的子网掩码。 - -### ICMP 时间戳请求与应答 - -ICMP 时间戳请求系统向另一个系统查询当前时间。 - -### ICMP 端口不可达差错 - -如果收到一份 UDP 数据报而目的端口与某个正在使用的进程不相符,那么 UDP 就会返回一个端口不可达报文。 - - - -## Traceroute - -Traceroute 程序的操作过程。开始时发送一个 TTL 字段为1的 UDP 数据报,然后将 TTL 字段每次加1,以确定路径中的每个路由器。每个路由器在丢弃 UDP 数据报时都返回一个 ICMP 超时报文,而最终目的主机则产生一个ICMP 端口不可达的报文(Traceroute 程序发送数据报给目的主机时指定了一个不可能的值作为 UDP 端口,目的主机任何应用程序都不可能使用此端口)。 - -对于每个 TTL 值会发送三份数据报,每接收到一份 ICMP 报文,就计算并打印出往返时间。如果在5秒种内仍未收到3份数据报的任意一份的响应,则打印一个星号,并发送下一份数据报。 - -`tracert baid.com` - -![traceroute命令](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190820112635665-2041876448.png) - - - -## IP 选路 - -系统产生的或转发的每份 IP 数据报都要搜索路由表,它可以被路由守护程序或 ICMP 重定 -向报文修改。 - - - -## 广播和多播 - -三种 IP 地址:单播地址、广播地址和多播地址。广播和多播仅适用于 UDP。多播的数据帧仅发送给属于多播组的多个主机。 - -网卡查看由信道传送过来的帧,确定是否接收该帧,若接收后就将它传往设备驱动程序。通常网卡仅接收那些目的地址为网卡物理地址或广播地址的帧。 - -如果网卡收到一个帧,这个帧将被传送给设备驱动程序(如果帧检验和错,网卡将丢弃该帧)。设备驱动程序将进行另外的帧过滤。首先,帧类型中必须指定要使用的协议( IP、ARP等等)。其次,进行多播过滤来检测该主机是否属于多播地址说明的多播组。 - -广播是将数据报发送到网络中的所有主机(通常是本地相连的网络),而多播是将数据报发送到网络的一个主机组。。在许多应用中,多播比广播更好,因为多播降低了不参与通信的主机的负担。 - - - -## IGMP - -Internet 组管理协议用于支持主机和路由器进行多播。它让一个物理网络上的所有系统知道当前主机所在的多播组。。多播路由器需要这些信息以便知道多播数据报应该向哪些接口转发。跟 ICMP 一样,IGMP 也被当做 IP 层的一部分,IGMP 报文通过 IP 数据报进行传输。IGMP 有固定的报文长度,没有可选的数据。 - - - -## UDP - -UDP 是一个简单的面向数据报的运输层协议。 - -### 特点 - -无连接:发送数据之前不需要建立连接。 - -不可靠的服务:尽最大努力的交互,主机不需要维持复杂的连接状态表。 - -面向报文:发送方的 UDP 对应用程序传进来的报文,在添加首部后就向下交给 IP 层。 - -没有拥塞控制:网络出现拥塞不会使源主机的发送速率变慢。 - -支持一对一、一对多、多对多的交互通信。 - -UDP 的首部开销小。 - -### 首部 - -![UDP 首部](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823160458588-1745399117.png) - -伪首部既不向上传送也不向下提交,而仅仅为了计算检验和。伪首部第三字段是全0,第四个字段是 IP 首部中协议字段的值,对于 UDP,此协议字段值为17。 - - - -## TCP - -TCP 提供一种面向连接的、可靠的字节流服务。 - -![使用TCP/UDP的各种应用](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823155102643-1792038068.png) - -### 特点 - -1. 面向连接。应用程序传输数据之前需要先建立连接。 -2. 提供可靠交互的服务。 -3. 全双工通信。TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。 -4. 面向字节流。TCP 把应用程序交下来的数据仅仅看成一串无结构的字节流。 - -### 停止等待协议 - -每发送一个分组就会设置一个超时计数器,如果在计数器没有过期之前收到了对方的确认,就撤销超时计数器。 - -![停止等待协议](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823162130085-100928361.png) - -a 图 B 对 M1 的确认丢失了,A 会重传 M1,然后 B 会收到重复的 M1,将其丢弃,重传确认。 - -b 图 B 对 M1 的确认在网络滞留,A重传M1,最后会收到两次对M1确认,A会手下迟到的确认但是什么都不做。 - -![确认丢失和确认迟到](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823163614916-2026615611.png) - -### 首部 - -![TCP首部](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823164725621-1852408493.png) - -序号:TCP 连接中传送的字节流中的每一个字节都按顺序编号。 - -数据偏移:TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。 - -紧急 URG:当 URG = 1时,表明紧急指针字段有效,它告诉系统此报文段有紧急数据,应该尽快传送,不要按照之前的排队顺序来传送。 - -推送 PSH:接收方收到一个 PSH = 1 的报文段,就会尽快把数据交互到应用程序,而不再等到整个缓存填充满了再向上交互。 - -复位 RST:当 RST = 1 时,表明 TCP 连接中出现了严重差错,必须释放连接,重新建立连接。 - -同步 SYN:建立连接同步序号。 - -窗口:窗口值表明接收方能接受多少数据。窗口值作为接收方让发送方设置其发送窗口的依据。 - -检验和:检验的范围包括首部和数据。 - -紧急指针:URG = 1时有效,指明紧急字段的末尾在报文段的位置。在窗口为0时也可以发送紧急数据。 - -最大报文段长度 MSS:默认值是536字节。 - - - -## DNS - -域名系统是一种用于 TCP/IP 应用程序的分布式数据库,它提供主机名字和 IP 地址之间的转换及有关电子邮件的选路信息。 - - - -## TFTP - -简单文件传输协议是一个简单的协议,适合于只读存储器,仅用于无盘系统进行系统引导。TFTP 使用不可靠的 UDP,需要处理分组丢失和分组重复。 - -![tftp命令](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190822211859937-1502767473.png) - - - -## BOOTP - -BOOTP 使用 UDP,它为引导无盘系统获得它的 IP 地址提供了除 RARP 外的另外一种选择。 - diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/session\345\222\214cookie.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/session\345\222\214cookie.md" deleted file mode 100644 index 6cc4584..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/session\345\222\214cookie.md" +++ /dev/null @@ -1,23 +0,0 @@ -由于HTTP协议是无状态的协议,需要用某种机制来识具体的用户身份,用来跟踪用户的整个会话。常用的会话跟踪技术是cookie与session。 - -## cookie - -cookie就是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。说得更具体一些:当用户使用浏览器访问一个支持cookie的网站的时候,用户会提供包括用户名在内的个人信息并且提交至服务器;接着,服务器在向客户端回传相应的超文本的同时也会发回这些个人信息,当然这些信息并不是存放在HTTP响应体中的,而是存放于HTTP响应头;当客户端浏览器接收到来自服务器的响应之后,浏览器会将这些信息存放在一个统一的位置。 自此,客户端再向服务器发送请求的时候,都会把相应的cookie存放在HTTP请求头再次发回至服务器。服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。网站的登录界面中“请记住我”这样的选项,就是通过cookie实现的。 -![](https://img-blog.csdn.net/20180924195237286?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -cookie工作流程: - -1. servlet创建cookie,保存少量数据,发送给浏览器。 -2. 浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。 -3. 下次访问时,浏览器将自动携带cookie数据发送给服务器。 - -## session - -session原理:首先浏览器请求服务器访问web站点时,服务器首先会检查这个客户端请求是否已经包含了一个session标识、称为SESSIONID,如果已经包含了一个sessionid则说明以前已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用,如果客户端请求不包含session id,则服务器为此客户端创建一个session,并且生成一个与此session相关联的独一无二的sessionid存放到cookie中,这个sessionid将在本次响应中返回到客户端保存,这样在交互的过程中,浏览器端每次请求时,都会带着这个sessionid,服务器根据这个sessionid就可以找得到对应的session。以此来达到共享数据的目的。 这里需要注意的是,session不会随着浏览器的关闭而死亡,而是等待超时时间。 -session的工作原理就是依靠cookie来做支撑,第一次使用request.getsession()时session被创建 -![](https://img-blog.csdn.net/20180924195247106?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -## 区别 - -session是**服务器端**记录用户信息的一种机制,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;cookie是**客户端**保存用户信息的一种机制,用来记录用户的一些信息,也是实现session的一种方式。 - diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\347\275\221\347\273\234.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\347\275\221\347\273\234.md" deleted file mode 100644 index 60a94c1..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\347\275\221\347\273\234.md" +++ /dev/null @@ -1,362 +0,0 @@ - - - - -- [三次握手](#%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B) -- [四次挥手](#%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B) -- [滑动窗口](#%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3) -- [拥塞控制](#%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6) - - [慢开始](#%E6%85%A2%E5%BC%80%E5%A7%8B) - - [拥塞避免](#%E6%8B%A5%E5%A1%9E%E9%81%BF%E5%85%8D) - - [快重传](#%E5%BF%AB%E9%87%8D%E4%BC%A0) - - [快恢复](#%E5%BF%AB%E6%81%A2%E5%A4%8D) -- [TCP和UDP](#tcp%E5%92%8Cudp) - - [TCP的特点](#tcp%E7%9A%84%E7%89%B9%E7%82%B9) - - [TCP和UDP区别](#tcp%E5%92%8Cudp%E5%8C%BA%E5%88%AB) - - [协议](#%E5%8D%8F%E8%AE%AE) - - [TCP首部](#tcp%E9%A6%96%E9%83%A8) - - [UDP首部](#udp%E9%A6%96%E9%83%A8) -- [http](#http) - - [主要特点](#%E4%B8%BB%E8%A6%81%E7%89%B9%E7%82%B9) - - [http请求](#http%E8%AF%B7%E6%B1%82) - - [http响应](#http%E5%93%8D%E5%BA%94) - - [http状态码](#http%E7%8A%B6%E6%80%81%E7%A0%81) - - [post和get](#post%E5%92%8Cget) - - [HTTPS](#https) - - [数字证书](#%E6%95%B0%E5%AD%97%E8%AF%81%E4%B9%A6) - - [原理](#%E5%8E%9F%E7%90%86) - - [HTTPS和HTTP的区别](#https%E5%92%8Chttp%E7%9A%84%E5%8C%BA%E5%88%AB) - - [浏览器中输入URL返回页面过程](#%E6%B5%8F%E8%A7%88%E5%99%A8%E4%B8%AD%E8%BE%93%E5%85%A5url%E8%BF%94%E5%9B%9E%E9%A1%B5%E9%9D%A2%E8%BF%87%E7%A8%8B) - - [长连接](#%E9%95%BF%E8%BF%9E%E6%8E%A5) -- [网络分层结构](#%E7%BD%91%E7%BB%9C%E5%88%86%E5%B1%82%E7%BB%93%E6%9E%84) -- [DNS解析](#dns%E8%A7%A3%E6%9E%90) -- [加密算法](#%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95) -- [CDN](#cdn) -- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - - -# 三次握手 - -1. 第一次握手:客户端A向B发出连接请求报文段(首部的同步位SYN=1,初始序号seq=x),此时客户端A进入SYN-SENT(同步已发送)状态。 -2. 第二次握手:B收到连接请求报文段后,如同意建立连接,则向A发送确认(确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y),B进入SYN-RCVD(同步收到)状态; -3. 第三次握手:A收到B的确认后,向B发送确认报文段(ACK=1,确认号ack=y+1,序号seq=x+1)。然后A进入ESTABLISHED。 -4. B收到确认报文段,就会进入ESTABLISHED状态,TCP连接成功。 - -![三次握手](https://img2018.cnblogs.com/blog/1252910/201909/1252910-20190912193729969-2125931519.png) - -为什么A还要发送一次确认呢?可以二次握手吗? - -主要为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误。如A发出连接请求,可能因为网络阻塞原因,A没有收到确认报文,于是A再重传一次连接请求。连接成功,等待数据传输完毕后,就释放了连接。而A发出的第一个连接请求等到连接释放以后的某个时间才到达B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接,不采用三次握手,只要B发出确认,就建立新的连接了,此时A不理睬B的确认且不发送数据,则B一直等待A发送数据,浪费资源。 - - -# 四次挥手 - -1. A的应用进程先向其TCP发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN-WAIT-1(终止等待1)状态,等待B的确认。 -2. B收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),B进入CLOSE-WAIT(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。 -3. A收到B的确认后,进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段。 -4. B发送完数据,就会发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),B进入LAST-ACK(最后确认)状态,等待A的确认。 -5. A收到B的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),A进入TIME-WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL(最大报文段生存时间)后,A才进入CLOSED状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。 - -![四次挥手](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823193334395-1767472912.png) - -为什么A在TIME-WAIT状态必须等待2MSL(最大报文段生存时间)的时间? - -- 保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。 -- 防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。 - -为什么连接的时候是三次握手,关闭的时候却是四次握手? - -- 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到连接释放报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的连接释放报文我收到了"。只有等到Server端所有的报文都发送完了,才能发送连接释放报文,因此不能一起发送。故需要四步握手。 - -# 滑动窗口 - -TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。 - -![滑动窗口](https://img2018.cnblogs.com/blog/1252910/201908/1252910-20190823173235953-969655958.png) - - -TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。 - -# 拥塞控制 - -防止过多的数据注入到网络中。 几种拥塞控制方法:慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。 - -![](../img/拥塞控制.jpg) - -## 慢开始 - -把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。 - - 当 cwnd < ssthresh 时,使用慢开始算法。 - - 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 - - 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。 - -## 拥塞避免 - -让拥塞窗口cwnd缓慢地增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长。 - -无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。 - -## 快重传 - -有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。 - -快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。 - -发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。 - -## 快恢复 - -当发送方连续收到三个重复确认,就会把慢开始门限ssthresh减半,接着把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 - -在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得TCP的性能有明显的改进。 - - - -# TCP和UDP - -TCP:传输控制协议。 - -UDP:用户数据报协议。 - -## TCP的特点 - -- TCP是面向连接的运输层协议 -- 每一条TCP连接只能有两个端点(一对一) -- TCP提供可靠交付的服务 -- TCP提供全双工通信 -- 面向字节流 - -- TCP可靠传输、流量控制和拥塞控制的实现 -TCP的可靠性如何保证:对于收到的请求,给出确认响应。 -流量控制:让发送方的发送速率不要太快,要让接收方来得及接收。利用滑动窗口实现流量控制。 -拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。 -TCP可靠传输是因为有: 数据报校验, 失序数据重排序, 丢弃重复数据,应答机制,超时重发,流量控制等原因 - -## TCP和UDP区别 -1. TCP面向连接,UDP是无连接的,即发送数据之前不需要建立连接 -2. TCP提供可靠的服务;UDP不保证可靠交付 -3. TCP面向字节流,把数据看成一连串无结构的字节流;UDP是面向报文的 -4. TCP有拥塞控制;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等) -5. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信 -6. TCP首部开销20字节;UDP的首部开销小,只有8个字节 - -## 协议 - -TCP对应的协议: -(1)FTP:定义了文件传输协议,使用21端口。 -(2)Telnet:用于远程登陆的端口。 -(3)SMTP:定义了简单邮件传送协议,25端口。 -(4)POP3:它是和SMTP对应,POP3用于接收邮件,110端口。 -(5)HTTP协议:是从万维网服务器传输超文本到本地浏览器的传送协议,使用80端口。 - -UDP对应的协议: -(1)DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。 -(2)SNMP:简单网络管理协议,使用161号端口。 -(3)TFTP,Trival File Transfer Protocal,简单文件传输协议,使用端口69。 -(4)RIP,路由信息协议。 -(5)DHCP,动态主机配置协议。 - - -## TCP首部 - -![](https://img-blog.csdn.net/20180924195341168) - -## UDP首部 - -![](https://img-blog.csdn.net/20180924195345900?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -# http - -超文本传输协议。 - -## 主要特点 - -1. 灵活:HTTP允许传输任意类型的数据。传输的类型由Content-Type加以标记。 -2. HTTP 0.9和1.0使用非持续连接:限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接。HTTP 1.1支持使用持续连接:一个连接可以处理多个请求,不必为每个请求创建一个新的连接,采用这种方式可以节省传输时间。 -3. 无状态:是指服务端对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话没有联系;HTTP 协议这种特性有优点也有缺点,优点在于解放了服务器,不会造成不必要连接占用,缺点在于如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。 - -## http请求 - -http请求由请求行、请求头部、空行和请求体四个部分组成。 - -![](https://img-blog.csdn.net/20180924195305583?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -- 请求行:请求方法,访问的资源URL,使用的HTTP版本;GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。 -- 请求头包含一些属性,格式为“属性名:属性值”,服务端据此获取客户端的信息,主要有cookie、host、connection、accept-language、accept-encoding、user-agent。 -- 请求体:用户的请求数据如用户名,密码等。 - -## http响应 - -HTTP响应也由四个部分组成,分别是:状态行、响应头、空行和响应体。 - -![](https://img-blog.csdn.net/20180924195318593?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -- 状态行:协议版本,状态码及状态描述。 -- 响应头:connection、content-type、content-encoding、content-length、set-cookie、Last-Modified,、Cache-Control、Expires。 -- 响应体:服务器返回给客户端的内容。 - -## http状态码 - -![](https://img-blog.csdn.net/20180924195323664?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1R5c29uMDMxNA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) - -## post和get - -GET和POST是HTTP协议中的两种发送请求的方法。GET/POST请求都是TCP连接。 - -Get方法并没有限制提交的数据长度,HTTP协议规范没有对URL长度进行限制。但是浏览器及服务器可能会对URL长度有限制。 - -POST方法没有对请求体长度进行限制,但是服务端可以对POST数据大小进行限制,比如tomcat默认限制2M。可以修改conf/server.xml:`maxPostSize=0`,取消POST的大小限制。 - -post和get请求的区别: - -- GET请求参数通过URL传递,POST的参数放在请求体中。 -- GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把请求头和请求体一并发送出去;而对于POST,浏览器先发送请求头,服务器响应100 continue,浏览器再发送请求体。 -- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。 -- GET请求只能进行url编码,而POST支持多种编码方式。 -- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。 - -## HTTPS - -HTTP协议以明文方式发送内容,不提供任何方式的数据加密,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。而HTTPS协议是由SSL+HTTP协议构建的可进行身份认证(非对称加密)、加密传输(对称加密)的网络协议。SSL作用在应用层和运输层之间,在TCP之上建立起一个安全通道,确保数据传输安全。在 SSL 层完成身份认证、加解密操作。 - -### 数字证书 - -服务端可以向证书颁发机构CA申请证书,以避免中间人攻击(防止证书被篡改)。证书包含三部分内容:tbsCertificate(to be signed certificate)待签名证书内容、证书签名算法和CA给的签名(使用证书签名算法对tbsCertificate进行哈希运算得到哈希值,CA会用它的私钥对此哈希值进行签名,并放在签名部分)。签名是为了验证身份。 - -![](../img/net/https-certificate.png) - -服务端把证书传输给浏览器,浏览器从证书里取公钥。证书可以证明该公钥对应该网站。 - -数字签名的制作过程: - -1. CA使用证书签名算法对证书内容(待签名证书内容)进行hash。 -2. 对hash后的值用CA自己的私钥加密,得到数字签名。 - -浏览器验证过程: - -1. 拿到证书,得到证书内容、证书签名算法和数字签名。 -2. 用CA机构的公钥对数字签名解密(由于是浏览器信任的机构,所以浏览器会保存它的公钥)。 -3. 用证书里的签名算法对证书内容进行hash。 -4. 比较解密后的数字签名和对证书内容hash后的哈希值,相等则表明证书可信。 - -### 原理 - -首先是TCP三次握手,然后客户端(浏览器)发起一个HTTPS连接建立请求,客户端先发一个Client Hello的包,然后服务端响应一个Server Hello,接着再给客户端发送它的证书,然后双方经过密钥交换,最后使用交换的密钥加解密数据。 - -1. 协商加密算法 。在Client Hello里面客户端会告知服务端自己当前的一些信息,包括客户端要使用的TLS版本,支持的加密套装,要访问的域名,给服务端生成的一个随机数(Nonce)等。需要提前告知服务器想要访问的域名以便服务器发送相应的域名的证书过来。 - - ![](../img/net/https-client-hello.jpg) - -2. 服务端在Server Hello里面会做一些响应,告诉客户端服务端选中的加密套装。 - - ![](../img/net/https-server-hello.jpg) - -3. 接着服务给客户端发来了4个证书。第二个证书是第一个证书的签发机构(CA)的证书,它是Amazon,也就是说Amazon会用它的私钥给[http://developer.mozilla.org](https://link.zhihu.com/?target=http%3A//developer.mozilla.org)进行签名。依此类推,第三个证书会给第二个证书签名,第四个证书会给第三个证书签名,并且我们可以看到第四个证书是一个根(Root)证书。 - - ![](../img/net/https-certificate-chain.jpg) - -4. 客户端使用证书的认证机构CA公开发布的RSA公钥对该证书进行验证。 - -5. 验证通过之后,浏览器和服务器通过密钥交换算法产生共享的对称密钥。 - -6. 开始传输数据,使用同一个对称密钥来加解密。 - - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9KZExrRUk5c1pmZXIxV1NQWWljN0trWWJpYm1wTkYwVnVPWTdsbU1qb0U4SVZPejhpYzNBQUZFWkFFOGliUmVhSjc4dWxWMXU4TTdQU1pGMzJwWllNS0tZV0EvNjQw?x-oss-process=image/format,png) - -### HTTPS和HTTP的区别 - -1. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。 -2. http和https用的端口不一样,前者是80,后者是443。 -3. https协议需要到ca机构申请证书,一般免费证书较少,因而需要一定费用。 - -## 浏览器中输入URL返回页面过程 - -1. 解析域名,找到主机IP。 -2. 浏览器利用IP直接与网站主机通信,三次握手。 -3. 浏览器向主机发起一个HTTP-GET报文请求。 -4. 服务器响应请求,发回网页内容。 -5. 浏览器解析网页内容。 - -## 长连接 - -HTTP长连接,指的是复用TCP连接。多个HTTP请求可以复用同一个TCP连接,这就节省了TCP连接建立和断开的消耗。长连接在HTTP1.0时是实验性扩展,HTTP1.1默认设置connection:keep-alive,使用长连接。设置为connection:close可以关闭长连接。客户端和服务器的HTTP首部的Connection都要设置为keep-alive,才能支持长连接。 - -长连接的过期时间可以通过Keep-Alive: timeout=20或者max=xxx(max表示这个长连接最多接收xxx次请求就断开)设置。 - -长连接数据传输完成的标志: - -- 判断传输数据是否达到了`Content-Length`指示的大小;4 -- 分块传输(chunked)没有`Content-Length`,这时候就要根据chunked编码来判断,chunked编码的数据在最后有一个空chunked块,表明本次传输数据结束。 - -# 网络分层结构 - -计算机网络七层模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。 - -- 应用层的任务是通过应用进程之间的交互来完成特定的网络作用,常见的应用层协议有域名系统DNS,HTTP协议等。 - -- 表示层的主要作用是数据的表示、安全、压缩。可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。 -- 会话层的主要作用是建立通信链接,保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时决定从何处重新发送。。 - -- 传输层的主要作用是负责向两台主机进程之间的通信提供数据传输服务。传输层的协议主要有传输控制协议TCP和用户数据协议UDP。 - -- 网络层的主要作用是选择合适的网间路由和交换结点,确保数据及时送达。常见的协议有IP协议。 - -- 数据链路层的作用是在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。 常见的协议有SDLC、HDLC、PPP等。 - -- 物理层的主要作用是实现相邻计算机结点之间比特流的透明传输,并尽量屏蔽掉具体传输介质和物理设备的差异。 - -# DNS解析 - -1. 浏览器搜索自己的DNS缓存(维护一张域名与IP地址的对应表) -2. 若没有,则搜索操作系统中的DNS缓存和hosts文件 -3. 若没有,则操作系统将域名发送至本地域名服务器,本地域名服务器查询自己的DNS缓存,查找成功则返回结果,否则通过递归或者迭代的查询方式依次向根域名服务器、顶级域名服务器、权限域名服务器发起查询请求,最终返回IP地址给本地域名服务器 -4. 本地域名服务器将得到的IP地址返回给操作系统,同时自己也将IP地址缓存起来 -5. 操作系统将 IP 地址返回给浏览器,同时自己也将IP地址缓存起来 -6. 浏览器得到域名对应的IP地址 - -# 加密算法 - -对称加密:通信双方使用相同的密钥进行加密。特点是加密速度快,但是缺点是需要保护好密钥,如果密钥泄露的话,那么加密就会被别人破解。常见的对称加密有AES,DES算法。 - -非对称加密:它需要生成两个密钥:公钥和私钥。公钥是公开的,任何人都可以获得,而私钥是私人保管的。我们提交代码到github的时候,就可以使用SSH key:在本地生成私钥和公钥,私钥放在本地`.ssh`目录中,公钥放在github网站上,这样每次提交代码,就不用输入用户名和密码了,github会根据网站上存储的公钥来识别我们的身份。公钥负责加密,私钥负责解密;或者,私钥负责加密,公钥负责解密。这种加密算法安全性更高,但是计算量相比对称加密大很多,加密和解密都很慢。常见的非对称算法有RSA。 - -对称加密和非对称加密需要搭配使用更主要的原因是: - -1. 对称加密:两边需要使用相同的密钥,需要使用一种安全的方式交换密钥,单纯使用对称加密,无法实现密钥交换。 -2. 非对称加密:只使用非对称加密是可以满足安全性要求的,但是由于非对称加密的计算耗时高于对称加密的2-3个数量级(相同安全加密级别),所以才先使用非对称交换密钥,之后再使用对称加密通信。 - -# CDN - -CDN用户访问流程: - -![cdn](https://raw.githubusercontent.com/Tyson0314/img/master/cdn.png) - -1.用户向浏览器输入www.web.com这个域名,浏览器第一次发现本地没有dns缓存,则向网站的DNS服务器请求; - -2.网站的DNS域名解析器设置了CNAME,指向了www.web.51cdn.com,请求指向了CDN网络中的智能DNS负载均衡系统; - -3.智能DNS负载均衡系统解析域名,把对用户响应速度最快的IP节点返回给用户; - -4.用户向该IP节点(CDN服务器)发出请求; - -5.由于是第一次访问,CDN服务器会向原web站点请求,并缓存内容; - -6.请求结果发给用户。 - -最简单的CDN网络有一个负责全局负载均衡的DNS和各节点一台Cache,即可运行。DNS支持根据用户源IP地址解析不同的IP,实现就近访问CDN服务器。为了保证高可用性等,需要监视各节点的流量、健康状况等。一个节点的单台Cache承载数量不够时,才需要多台Cache,多台Cache同时 工作,才需要负载均衡器,使Cache群协同工作。 - -# 参考资料 - -计算机网络,谢希仁 - -[拥塞控制](https://www.cnblogs.com/losbyday/p/5847041.html) - -[常见http状态码](https://www.cnblogs.com/zt123123/p/8327706.html) - -[浏览器中输入URL到返回页面的全过程](https://www.cnblogs.com/jiaosq/p/5841366.html) - -[HTTPS原理](https://zhuanlan.zhihu.com/p/75461564) \ No newline at end of file diff --git "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" "b/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index 3d82083..0000000 --- "a/\350\256\241\347\256\227\346\234\272\345\237\272\347\241\200/\347\275\221\347\273\234/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\253\230\351\242\221\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,315 +0,0 @@ -## 网络分层结构 - -计算机网络体系大致分为三种,OSI七层模型、TCP/IP四层模型和五层模型。一般面试的时候考察比较多的是五层模型。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/tcp5layer2.png) - -五层模型:应用层、传输层、网络层、数据链路层、物理层。 - -- **应用层**:为应用程序提供交互服务。在互联网中的应用层协议很多,如域名系统DNS、HTTP协议、SMTP协议等。 -- **传输层**:负责向两台主机进程之间的通信提供数据传输服务。传输层的协议主要有传输控制协议TCP和用户数据协议UDP。 -- **网络层**:选择合适的路由和交换结点,确保数据及时传送。主要包括IP协议。 -- **数据链路层**:在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。 -- **物理层**:实现相邻节点间比特流的透明传输,尽可能屏蔽传输介质和物理设备的差异。 - -## 三次握手 - -假设发送端为客户端,接收端为服务端。开始时客户端和服务端的状态都是`CLOSED`。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/三次握手图解.png) - -1. 第一次握手:客户端向服务端发起建立连接请求,客户端会随机生成一个起始序列号x,客户端向服务端发送的字段中包含标志位`SYN=1`,序列号`seq=x`。第一次握手前客户端的状态为`CLOSE`,第一次握手后客户端的状态为`SYN-SENT`。此时服务端的状态为`LISTEN`。 -2. 第二次握手:服务端在收到客户端发来的报文后,会随机生成一个服务端的起始序列号y,然后给客户端回复一段报文,其中包括标志位`SYN=1`,`ACK=1`,序列号`seq=y`,确认号`ack=x+1`。第二次握手前服务端的状态为`LISTEN`,第二次握手后服务端的状态为`SYN-RCVD`,此时客户端的状态为`SYN-SENT`。(其中`SYN=1`表示要和客户端建立一个连接,`ACK=1`表示确认序号有效) -3. 第三次握手:客户端收到服务端发来的报文后,会再向服务端发送报文,其中包含标志位`ACK=1`,序列号`seq=x+1`,确认号`ack=y+1`。第三次握手前客户端的状态为`SYN-SENT`,第三次握手后客户端和服务端的状态都为`ESTABLISHED`。**此时连接建立完成。** - -## 两次握手可以吗? - -第三次握手主要为了**防止已失效的连接请求报文段**突然又传输到了服务端,导致产生问题。 - -- 比如客户端A发出连接请求,可能因为网络阻塞原因,A没有收到确认报文,于是A再重传一次连接请求。 -- 连接成功,等待数据传输完毕后,就释放了连接。 -- 然后A发出的第一个连接请求等到连接释放以后的某个时间才到达服务端B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段。 -- 如果不采用三次握手,只要B发出确认,就建立新的连接了,**此时A不会响应B的确认且不发送数据,则B一直等待A发送数据,浪费资源。** - -## 四次挥手 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/四次挥手0.png) - -1. A的应用进程先向其TCP发出连接释放报文段(`FIN=1,seq=u`),并停止再发送数据,主动关闭TCP连接,进入`FIN-WAIT-1`(终止等待1)状态,等待B的确认。 -2. B收到连接释放报文段后即发出确认报文段(`ACK=1,ack=u+1,seq=v`),B进入`CLOSE-WAIT`(关闭等待)状态,此时的TCP处于半关闭状态,A到B的连接释放。 -3. A收到B的确认后,进入`FIN-WAIT-2`(终止等待2)状态,等待B发出的连接释放报文段。 -4. B发送完数据,就会发出连接释放报文段(`FIN=1,ACK=1,seq=w,ack=u+1`),B进入`LAST-ACK`(最后确认)状态,等待A的确认。 -5. A收到B的连接释放报文段后,对此发出确认报文段(`ACK=1,seq=u+1,ack=w+1`),A进入`TIME-WAIT`(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间`2MSL`(最大报文段生存时间)后,A才进入`CLOSED`状态。B收到A发出的确认报文段后关闭连接,若没收到A发出的确认报文段,B就会重传连接释放报文段。 - -## 第四次挥手为什么要等待2MSL? - -- **保证A发送的最后一个ACK报文段能够到达B**。这个`ACK`报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在`2MSL`时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到`CLOSED`状态,若A在`TIME-WAIT`状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到`CLOSED`状态。 -- **防止已失效的连接请求报文段出现在本连接中**。A在发送完最后一个`ACK`报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。 - -## 为什么是四次挥手? - -因为当Server端收到Client端的`SYN`连接请求报文后,可以直接发送`SYN+ACK`报文。**但是在关闭连接时,当Server端收到Client端发出的连接释放报文时,很可能并不会立即关闭SOCKET**,所以Server端先回复一个`ACK`报文,告诉Client端我收到你的连接释放报文了。只有等到Server端所有的报文都发送完了,这时Server端才能发送连接释放报文,之后两边才会真正的断开连接。故需要四次挥手。 - -## TCP有哪些特点? - -- TCP是**面向连接**的运输层协议。 -- **点对点**,每一条TCP连接只能有两个端点。 -- TCP提供**可靠交付**的服务。 -- TCP提供**全双工通信**。 -- **面向字节流**。 - -## TCP和UDP的区别? - -1. TCP**面向连接**;UDP是无连接的,即发送数据之前不需要建立连接。 -2. TCP提供**可靠的服务**;UDP不保证可靠交付。 -3. TCP**面向字节流**,把数据看成一连串无结构的字节流;UDP是面向报文的。 -4. TCP有**拥塞控制**;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。 -5. 每一条TCP连接只能是**点到点**的;UDP支持一对一、一对多、多对一和多对多的通信方式。 -6. TCP首部开销20字节;UDP的首部开销小,只有8个字节。 - -## HTTP协议的特点? - -1. HTTP允许传输**任意类型**的数据。传输的类型由Content-Type加以标记。 -2. **无状态**。对于客户端每次发送的请求,服务器都认为是一个新的请求,上一次会话和下一次会话之间没有联系。 -3. 支持**客户端/服务器模式**。 - -## HTTP报文格式 - -HTTP请求由**请求行、请求头部、空行和请求体**四个部分组成。 - -- **请求行**:包括请求方法,访问的资源URL,使用的HTTP版本。`GET`和`POST`是最常见的HTTP方法,除此以外还包括`DELETE、HEAD、OPTIONS、PUT、TRACE`。 -- **请求头**:格式为“属性名:属性值”,服务端根据请求头获取客户端的信息,主要有`cookie、host、connection、accept-language、accept-encoding、user-agent`。 -- **请求体**:用户的请求数据如用户名,密码等。 - -**请求报文示例**: - -```java -POST /xxx HTTP/1.1 请求行 -Accept:image/gif.image/jpeg, 请求头部 -Accept-Language:zh-cn -Connection:Keep-Alive -Host:localhost -User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0) -Accept-Encoding:gzip,deflate - -username=dabin 请求体 -``` - -HTTP响应也由四个部分组成,分别是:**状态行、响应头、空行和响应体**。 - -- **状态行**:协议版本,状态码及状态描述。 -- **响应头**:响应头字段主要有`connection、content-type、content-encoding、content-length、set-cookie、Last-Modified,、Cache-Control、Expires`。 -- **响应体**:服务器返回给客户端的内容。 - -**响应报文示例**: - -```html -HTTP/1.1 200 OK -Server:Apache Tomcat/5.0.12 -Date:Mon,6Oct2003 13:23:42 GMT -Content-Length:112 - - - 响应体 - -``` - -## HTTP状态码有哪些? - -![](https://raw.githubusercontent.com/Tyson0314/img/master/http-status-code.png) - -## POST和GET的区别? - -- GET请求参数通过URL传递,POST的参数放在请求体中。 -- GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把请求头和请求体一并发送出去;而对于POST,浏览器先发送请求头,服务器响应100 continue,浏览器再发送请求体。 -- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。 -- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。 - -## HTTP长连接和短连接? - -HTTP短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。**HTTP1.0默认使用的是短连接**。 - -HTTP长连接:指的是**复用TCP连接**。多个HTTP请求可以复用同一个TCP连接,这就节省了TCP连接建立和断开的消耗。 - -**HTTP/1.1起,默认使用长连接**。要使用长连接,客户端和服务器的HTTP首部的Connection都要设置为keep-alive,才能支持长连接。 - - - -## HTTP1.1和 HTTP2.0的区别? - -HTTP2.0相比HTTP1.1支持的特性: - -* **新的二进制格式**:HTTP1.1 基于文本格式传输数据;HTTP2.0采用二进制格式传输数据,解析更高效。 - -* **多路复用**:在一个连接里,允许同时发送多个请求或响应,**并且这些请求或响应能够并行的传输而不被阻塞**,避免 HTTP1.1 出现的”队头堵塞”问题。 - -* **头部压缩**,HTTP1.1的header带有大量信息,而且每次都要重复发送;HTTP2.0 把header从数据中分离,并封装成头帧和数据帧,**使用特定算法压缩头帧**,有效减少头信息大小。并且HTTP2.0**在客户端和服务器端记录了之前发送的键值对,对于相同的数据,不会重复发送。**比如请求a发送了所有的头信息字段,请求b则**只需要发送差异数据**,这样可以减少冗余数据,降低开销。 - -* **服务端推送**:HTTP2.0允许服务器向客户端推送资源,无需客户端发送请求到服务器获取。 - -## HTTPS与HTTP的区别? - -1. HTTP是超文本传输协议,信息是**明文传输**;HTTPS则是具有**安全性**的ssl加密传输协议。 -2. HTTP和HTTPS用的端口不一样,HTTP端口是80,HTTPS是443。 -3. HTTPS协议**需要到CA机构申请证书**,一般需要一定的费用。 -4. HTTP运行在TCP协议之上;HTTPS运行在SSL协议之上,SSL运行在TCP协议之上。 - -## 什么是数字证书? - -服务端可以向证书颁发机构CA申请证书,以避免中间人攻击(防止证书被篡改)。证书包含三部分内容:**证书内容、证书签名算法和签名**,签名是为了验证身份。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20211004111441594.png) - -服务端把证书传输给浏览器,浏览器从证书里取公钥。证书可以证明该公钥对应本网站。 - -**数字签名的制作过程**: - -1. CA使用证书签名算法对证书内容进行**hash运算**。 -2. 对hash后的值**用CA的私钥加密**,得到数字签名。 - -**浏览器验证过程**: - -1. 获取证书,得到证书内容、证书签名算法和数字签名。 -2. 用CA机构的公钥**对数字签名解密**(由于是浏览器信任的机构,所以浏览器会保存它的公钥)。 -3. 用证书里的签名算法**对证书内容进行hash运算**。 -4. 比较解密后的数字签名和对证书内容做hash运算后得到的哈希值,相等则表明证书可信。 - -## HTTPS原理 - -首先是TCP三次握手,然后客户端发起一个HTTPS连接建立请求,客户端先发一个`Client Hello`的包,然后服务端响应`Server Hello`,接着再给客户端发送它的证书,然后双方经过密钥交换,最后使用交换的密钥加解密数据。 - -1. **协商加密算法** 。在`Client Hello`里面客户端会告知服务端自己当前的一些信息,包括客户端要使用的TLS版本,支持的加密算法,要访问的域名,给服务端生成的一个随机数(Nonce)等。需要提前告知服务器想要访问的域名以便服务器发送相应的域名的证书过来。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921104210833.png) - -2. 服务端响应`Server Hello`,告诉客户端服务端**选中的加密算法**。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921105450791.png) - -3. 接着服务端给客户端发来了2个证书。第二个证书是第一个证书的签发机构(CA)的证书。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20211004172007102.png) - -4. 客户端使用证书的认证机构CA公开发布的RSA公钥**对该证书进行验证**,下图表明证书认证成功。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921105929268.png) - -5. 验证通过之后,浏览器和服务器通过**密钥交换算法**产生共享的**对称密钥**。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921110025197.png) - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921110155075.png) - -6. 开始传输数据,使用同一个对称密钥来加解密。 - - ![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921110315068.png) - -## DNS 的解析过程? - -1. 浏览器搜索**自己的DNS缓存** -2. 若没有,则搜索**操作系统中的DNS缓存和hosts文件** -3. 若没有,则操作系统将域名发送至**本地域名服务器**,本地域名服务器查询自己的DNS缓存,查找成功则返回结果,否则依次向**根域名服务器、顶级域名服务器、权限域名服务器**发起查询请求,最终返回IP地址给本地域名服务器 -4. 本地域名服务器将得到的IP地址返回给**操作系统**,同时自己也**将IP地址缓存起来** -5. 操作系统将 IP 地址返回给浏览器,同时自己也将IP地址缓存起来 -6. 浏览器得到域名对应的IP地址 - -## 浏览器中输入URL返回页面过程? - -1. **解析域名**,找到主机 IP。 -2. 浏览器利用 IP 直接与网站主机通信,**三次握手**,建立 TCP 连接。浏览器会以一个随机端口向服务端的 web 程序 80 端口发起 TCP 的连接。 -3. 建立 TCP 连接后,浏览器向主机发起一个HTTP请求。 -4. 服务器**响应请求**,返回响应数据。 -5. 浏览器**解析响应内容,进行渲染**,呈现给用户。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/输入url返回页面过程1.png) - - -## 什么是cookie和session? - -由于HTTP协议是无状态的协议,需要用某种机制来识具体的用户身份,用来跟踪用户的整个会话。常用的会话跟踪技术是cookie与session。 - -**cookie**就是由服务器发给客户端的特殊信息,而这些信息以文本文件的方式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。说得更具体一些:当用户使用浏览器访问一个支持cookie的网站的时候,用户会提供包括用户名在内的个人信息并且提交至服务器;接着,服务器在向客户端回传相应的超文本的同时也会发回这些个人信息,当然这些信息并不是存放在HTTP响应体中的,而是存放于HTTP响应头;当客户端浏览器接收到来自服务器的响应之后,浏览器会将这些信息存放在一个统一的位置。 自此,客户端再向服务器发送请求的时候,都会把相应的cookie存放在HTTP请求头再次发回至服务器。服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。网站的登录界面中“请记住我”这样的选项,就是通过cookie实现的。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/cookie.png) - -**cookie工作流程**: - -1. servlet创建cookie,保存少量数据,发送给浏览器。 -2. 浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。 -3. 下次访问时,浏览器将自动携带cookie数据发送给服务器。 - -**session原理**:首先浏览器请求服务器访问web站点时,服务器首先会检查这个客户端请求是否已经包含了一个session标识、称为SESSIONID,如果已经包含了一个sessionid则说明以前已经为此客户端创建过session,服务器就按照sessionid把这个session检索出来使用,如果客户端请求不包含session id,则服务器为此客户端创建一个session,并且生成一个与此session相关联的独一无二的sessionid存放到cookie中,这个sessionid将在本次响应中返回到客户端保存,这样在交互的过程中,浏览器端每次请求时,都会带着这个sessionid,服务器根据这个sessionid就可以找得到对应的session。以此来达到共享数据的目的。 这里需要注意的是,session不会随着浏览器的关闭而死亡,而是等待超时时间。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/session.png) - -## Cookie和Session的区别? - -- **作用范围不同**,Cookie 保存在客户端,Session 保存在服务器端。 -- **有效期不同**,Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。 -- **隐私策略不同**,Cookie 存储在客户端,容易被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。 -- **存储大小不同**, 单个 Cookie 保存的数据不能超过 4K;对于 Session 来说存储没有上限,但出于对服务器的性能考虑,Session 内不要存放过多的数据,并且需要设置 Session 删除机制。 - -## 什么是对称加密和非对称加密? - -**对称加密**:通信双方使用**相同的密钥**进行加密。特点是加密速度快,但是缺点是密钥泄露会导致密文数据被破解。常见的对称加密有`AES`和`DES`算法。 - -**非对称加密**:它需要生成两个密钥,**公钥和私钥**。公钥是公开的,任何人都可以获得,而私钥是私人保管的。公钥负责加密,私钥负责解密;或者私钥负责加密,公钥负责解密。这种加密算法**安全性更高**,但是**计算量相比对称加密大很多**,加密和解密都很慢。常见的非对称算法有`RSA`和`DSA`。 - -## 滑动窗口机制 - -TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 TCP会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。接收方发送的确认报文中的window字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0,则发送方不能发送数据。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/image-20210921112213523.png) - - -TCP头包含window字段,16bit位,它代表的是窗口的字节容量,最大为65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。 - -## 详细讲一下拥塞控制? - -防止过多的数据注入到网络中。 几种拥塞控制方法:慢开始( slow-start )、拥塞避免( congestion avoidance )、快重传( fast retransmit )和快恢复( fast recovery )。 - -![](https://raw.githubusercontent.com/Tyson0314/img/master/拥塞控制.jpg) - -### 慢开始 - -把拥塞窗口 cwnd 设置为一个最大报文段MSS的数值。而在每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值。每经过一个传输轮次,拥塞窗口 cwnd 就加倍。 为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。 - - 当 cwnd < ssthresh 时,使用慢开始算法。 - - 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。 - - 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。 - -### 拥塞避免 - -让拥塞窗口cwnd缓慢地增大,每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口cwnd按线性规律缓慢增长。 - -无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生 拥塞的路由器有足够时间把队列中积压的分组处理完毕。 - -### 快重传 - -有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1,因而降低了传输效率。 - -快重传算法可以避免这个问题。快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,使发送方及早知道有报文段没有到达对方。 - -发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期。由于发送方尽早重传未被确认的报文段,因此采用快重传后可以使整个网络吞吐量提高约20%。 - -### 快恢复 - -当发送方连续收到三个重复确认,就会把慢开始门限ssthresh减半,接着把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增大。 - -在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用。 采用这样的拥塞控制方法使得TCP的性能有明显的改进。 - -## ARP协议 - -ARP解决了同一个局域网上的主机和路由器IP和MAC地址的解析。 - -- 每台主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址的对应关系。 -- 当源主机需要将一个数据包要发送到目的主机时,会首先检查自己 ARP列表中是否存在该 IP地址对应的MAC地址,如果有,就直接将数据包发送到这个MAC地址;如果没有,就向本地网段发起一个ARP请求的广播包,查询此目的主机对应的MAC地址。此ARP请求数据包里包括源主机的IP地址、硬件地址、以及目的主机的IP地址。 -- 网络中所有的主机收到这个ARP请求后,会检查数据包中的目的IP是否和自己的IP地址一致。如果不相同就忽略此数据包;如果相同,该主机首先将发送端的MAC地址和IP地址添加到自己的ARP列表中,如果ARP表中已经存在该IP的信息,则将其覆盖,然后给源主机发送一个 ARP响应数据包,告诉对方自己是它需要查找的MAC地址。 -- 源主机收到这个ARP响应数据包后,将得到的目的主机的IP地址和MAC地址添加到自己的ARP列表中,并利用此信息开始数据的传输。 -- 如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。 - - - -![](https://raw.githubusercontent.com/Tyson0314/img/master/20220612101342.png)