开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 万字长文基础知识总结(下)-王者笔记《收藏版》|8

发表于 2021-08-02

上一篇 Java基础知识学习总结之(上)

下一篇 Java 集合容器篇面试题 (上)

目录

三、计算机原理和操作系统

内存、CPU、硬盘?

Linux基本命令

线程和进程的区别?

四、数据库基础

关系型数据库和非关系型数据库有哪些区别?举例说明

数据库索引是做什么用的

什么情况需要给字段建立索引?

什么情况下索引会失效?

慢查询是什么?

主从库是什么?

五、Java开发相关基础

MVC框架?

js跨域请求是什么意思?

服务器500、502、503、504、505错误是什么?

负载均衡是什么?

Cookie和Session分别是什么?Cookie的容量限制是多少?说⼀下 Session 的⼯作原理?

如果客户端禁⽌ Cookie 能实现 Session 还能用吗?

Session共享原理是什么?怎么实现?

java的历史

正则表达式?

反射技术?

Java 语⾔有哪些特点?

重载和重写的区别?

接⼝和抽象类的区别是什么?

成员变量与局部变量的区别有哪些?

== 与 equals?

hashCode 与 equals ?

hashCode()介绍

为什么要有 hashCode

JVM

什么是字节码?采用字节码的好处是什么?

JDK 和 JRE


三、计算机原理和操作系统

内存、CPU、硬盘?

内存是计算机的重要部件之⼀。 它是外存与CPU进⾏沟通的桥梁,计算机中所有程序的运⾏都在内存中进⾏。内存性能的强弱影响计算机整体发挥的⽔平。 内存(Memory)也称内存储器和主存储器,它用于暂时存放CPU中的运 算数据,与硬盘等外部存储器交换的数据。 只要计算机开始运⾏,操作系统就会把需要运算的数据从内存调到 CPU中进⾏运算。当运算完成,CPU将结果传送出来。 内存的运⾏也决定计算机整体运⾏快慢的程度。 内存条由内存芯片、电路板、⾦⼿指等部分组成。 电脑硬盘是计算机最主要的存储设备。硬盘(港台称之为硬碟,英⽂ 名:Hard Disk Drive, 简称HDD 全名温彻斯特式硬盘)由⼀个或者多个 铝制或者玻璃制的碟片组成。这些碟片外覆盖有铁磁性材料。 绝⼤多数硬盘都是固定硬盘,被永久性地密封固定在硬盘驱动器中。 早期的硬盘存储媒介是可替换的,不过今日典型的硬盘是固定的存储媒 介,被封在硬盘里 (除了⼀个过滤孔,用来平衡空⽓压⼒)。随着发 展,可移动硬盘也出现了,⽽且越来越普及,种类也越来越多.⼤多数微 机上安装的硬盘,由于都采用温切斯特(winchester)技术⽽被称之为“温切 斯特硬盘”,或简称“温盘”。 中央处理器(CPU),是电⼦计算机的主要设备之⼀,电脑中的核⼼ 配件。其功能主要是解释计算机指令以及处理计算机软件中的数据。CPU 是计算机中负责读取指令,对指令译码并执⾏指令的核⼼部件。中央处理 器主要包括两个部分,即控制器、运算器,其中还包括⾼速缓冲存储器及 实现它们之间联系的数据、控制的总线。电⼦计算机三⼤核⼼部件就CPU、内部存储器、输⼊/输出设备。中央处理器的功效主要为处理指 令、执⾏操作、控制时间、处理数据。

Linux基本命令

1.sudo

这个SuperUserDo是Linux新⼿要使用的最重要的命令。需要根权限的 每⼀个命令都需要这个sudo命令。你可以在需要根权限的每个命令之前使 用sudo。$ sudo su

2.ls(list)

借助list命令,终端就会显示你正在处理的那个目录里面的所有⽂件和⽂件夹。假设我在/home⽂件夹里面,想查看/home里面的目录和⽂ 件。

/home$ ls

/home中的ls返回下列结果:

imad lost+found

3.cd

更改目录(cd)是始终在终端中使用的主要命令。它是最基本的Linux命 令之⼀。使用这个命令很简单。只要输⼊你想要从当前目录进⼊到的那个 ⽂件夹的名称。如果想要返回上⼀级,只要将双圆点(..)作为参数。假设我在/home目录中,想进⼊到始终在/home里面的usr目录。下面 是我可以使用cd命令的⽅法:

/home $ cd usr

/home/usr $

4.mkdir

仅仅更改目录还不全面。有时候,你想要创建⼀个新的⽂件夹或⼦⽂

件夹。可以使用mkdir命令来做到这⼀点。只要在终端中将你的⽂件夹名称放在mkdir命令的后面即可。

$ mkdir folderName

5.cp

拷贝粘贴是我们为了组织整理⽂件⽽需要完成的重要任务。使用cp将 帮助你从终端拷贝粘贴⽂件。首先,你确定想要拷贝的那个⽂件,然后输⼊目的地位置,即可粘贴⽂件。

$ cp src des

注意:如果你将⽂件拷贝到任何新⽂件都需要根权限的目录,那么你就需要使用sudo命令。

6.rm

rm这个命令可以移除你的⽂件,甚⾄移除你的目录。如果⽂件需要根权限才能移除,可以使用-f。你还可以使用-r来进⾏递归移除,从⽽移除你的⽂件夹。

$ rm myfile.txt

7.apt-get

就不同的发⾏版⽽⾔,这个命令各不相同。在基于Debian的Linux发 ⾏版中,想安装、移除和升级任何软件包,我们可以使用⾼级包装⼯具 (APT)软件包管理器。apt-get命令可帮助你安装需要在Linux中运⾏的软件。这是个功能强⼤的命令⾏⼯具,可以执⾏安装、升级、甚⾄移除软件这类任务。 在其他发⾏版(比如Fedora和Centos)中,有不同的软件包管理器。

Fedora过去有yum,但现在它有dnf。

$ sudo apt-get update

$sudo dnf update

8.grep

你需要找到⼀个⽂件,但是又记不得它的确切位置或路径。grep可以帮助你解决这个问题。你可以使用grep命令,根据给定的关键字帮助找到⽂件。

$ grep user /etc/passwd

9.cat

作为用户,你常常需要查看来自脚本的⼀些⽂档或代码。同样,其中⼀个Linux基本命令是cat命令。它会为你显示⽂件里面的⽂本。

$ cat CMakeLists.txt

10.poweroff

有时候,你需要直接从终端来关机。这个命令就能完成这项任务。别忘了在命令的开头添加sudo,因为它需要根权限才能执⾏poweroff。

$ sudo poweroff

线程和进程的区别?

进程和线程的根本区别是进程是操作系统资源分配的基本单位,⽽线 程是处理器任务调度和执⾏的基本单位。另外区别还有资源开销、包含关 系、内存分配、影响关系、执⾏过程等。

资源开销:每个进程都有独立的代码和数据空间(程序上下⽂),程 序之间的切换会有较⼤的开销;线程可以看做轻量级的进程,同⼀类线程 共享代码和数据空间,每个线程都有自⼰独立的运⾏栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果⼀个进程内有多个线程,则执⾏过程不是⼀条线的,

⽽是多条线(线程)共同完成的;线程是进程的⼀部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同⼀进程的线程共享本进程的地址空间和资源,⽽进程之间的地址空间和资源是相互独立的。

影响关系:⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执⾏过程:每个独立的进程有程序运⾏的⼊⼝、顺序执⾏序列和程序出⼝。但是线程不能独立执⾏,必须依存在应用程序中,由应用程序提供多个线程执⾏控制,两者均可并发执⾏。

进程和线程的根本区别是进程是操作系统资源分配的基本单位,⽽线程是处理器任务调度和执⾏的基本单位。

四、数据库基础

关系型数据库和非关系型数据库有哪些区别?举例说明

关系型数据库,是指采用了关系模型来组织数据的数据库,其以⾏和列的形式存储数据,以便于用户理解,关系型数据库这⼀系列的⾏和列被称为表,⼀组表组成了数据库。用户通过查询来检索数据库中的数据,⽽查询是⼀个用于限定数据库中某些区域的执⾏代码。关系模型可以简单理解为⼆维表格模型,⽽⼀个关系型数据库就是由⼆维表及其之间的关系组成的⼀个数据组织。例如:mysql、oracle、sql server NoSQL,泛指非关系型的数据库。随着互联⽹web2.0⽹站的兴起,传统的关系数据库在处理web2.0⽹站,特别是超⼤规模和⾼并发的SNS类型的web2.0纯动态⽹站已经显得⼒不从⼼,出现了很多难以克服的问题,⽽非关系型的数据库则由于其本身的特点得到了非常迅速的发展。NoSQL数据库的产⽣就是为了解决⼤规模数据集合多重数据种类带来的挑战,尤其是⼤数据应用难题。例如:redis

数据库索引是做什么用的

在关系数据库中,索引是⼀种单独的、物理的对数据库表中⼀列或多列的值进⾏排序的⼀种存储结构,它是某个表中⼀列或若⼲列值的集合和 相应的指向表中物理标识这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。

什么情况需要给字段建立索引?****

表的主键、外键必须有索引;

数据量超过300的表应该有索引;

经常与其他表进连接的表,在连接字段上应该建索引;

经常出现在Where句 order by中的字段,特别是表的字段,应该建索引;

索引应该建在选择性的字段上;

索引应该建在字段上,对于的本字段甚超字段,不要建索引;

什么情况下索引会失效?

1.单独引用复合索引里非第⼀位置的索引列 假如有INDEX(a,b,c), 当条件为a或a,b或a,b,c时都可以使用索引,

但是当条件为b,c时将不会使用索引。

复合索引遵守“最左前缀”原则,即在查询条件中使用了复合索引的第 ⼀个字段,索引才会被使用。因此,在复合索引中索引列的顺序⾄关重要。如果不是按照索引的最左列开始查找,则⽆法使用索引。

2.对索引列运算,运算包括(+、-、*、/、!、、%、like’%_’(%放在前面)、or、in、exist等),导致索引失效。

错误的例⼦:select * from test where id-1=9;

正确的例⼦:select * from test where id=10;

注意!!

mysql sql 中如果使用了 not in , not exists , ( 不等于 !=) 这些不⾛ < 小于 > ⼤于 = 这个根据实际查询数据来判断,如果全盘扫描速度比索引速度要快则不⾛索引 。

3.对索引应用内部函数,这种情况下应该建立基于函数的索引。

1
csharp复制代码select * from template t where ROUND(t.logicdb_id) = 1

此时应该建ROUND(t.logicdb_id)为索引。

4、类型错误,如字段类型为varchar,where条件用number。

例:template_id字段是varchar类型。

错误写法:select * from template t where t.template_id = 1

正确写法:select * from template t where t.template_id = ‘1’

5.如果MySQL预计使用全表扫描要比使用索引快,则不使用索引

6.like的模糊查询以%开头,索引失效

7.索引列没有限制 not null,索引不存储空值,如果不限制索引列是 not null,oracle会认为索引列有可能存在空值,所以不会按照索引计算

慢查询是什么?

分析MySQL语句查询性能的⽅法除了使用 EXPLAIN 输出执⾏计划,还可以让MySQL记录下查询超过指定时间的语句,我们将超过指定时间的SQL语句查询称为“慢查询”。

主从库是什么?

1、做数据的热备,作为后备数据库,主数据库服务器故障后,可切 换到从数据库继续⼯作,避免数据丢失。

2、架构的扩展。业务量越来越⼤,I/O访问频率过⾼,单机⽆法满⾜,此时做多库的存储,降低磁盘I/O访问的频率,提⾼单个机器的I/O性能。

3、读写分离,使数据库能支撑更⼤的并发。在报表中尤其重要。由于部分报表sql语句非常的慢,导致锁表,影响前台服务。如果前台使用master,报表使用slave,那么报表sql将不会造成前台锁,保证了前台速度。

五、Java开发相关基础

MVC框架?

MVC开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从⽽使同⼀个程序可以使用不同的表现形式。比如⼀批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,⼀旦M改变,V应该同步更新。 [1-2] 模型-视图-控制器(MVC)是Xerox PARC在⼆⼗世纪⼋⼗年代为编程语⾔Smalltalk-80发明的⼀种软件设计模式,已被⼴泛使用。后来被推荐为Oracle旗下Sun公司Java EE平台的设计模式,并且受到越来越多的使用ColdFusion和PHP的开发者的欢迎。模型-视图-控制器模式是⼀个有用的⼯具箱,它有很多好处,但也有⼀些缺点。

js跨域请求是什么意思?

跨域访问,简单来说就是 A ⽹站的 javascript 代码试图访问 B ⽹站,包括提交内容和获取内容。由于安全原因,跨域访问是被各⼤浏览器所默认禁⽌的。

服务器500、502、503、504、505错误是什么?

HTTP 500 - 内部服务器错误

HTTP 502 - ⽹关错误

503 服务器目前⽆法使用(由于超载或停机维护)。通常,这只是暂时状态。(服务不可用)

504 表示超时,也就是客户端所发出的请求没有到达⽹关

505 服务器不支持请求中所用的 HTTP 协议版本。(HTTP 版本不受支持)

负载均衡是什么?

负载均衡建立在现有⽹络结构之上,它提供了⼀种廉价有效透明的⽅法扩展⽹络设备和服务器的带宽、增加吞吐量、加强⽹络数据处理能⼒、提⾼⽹络的灵活性和可用性。

负载均衡(Load Balance)其意思就是分摊到多个操作单元上进⾏执⾏,例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等,从⽽共同完成⼯作任务。

Cookie和Session分别是什么?Cookie的容量限制是多少?说⼀下 Session 的⼯作原理?

Cookie是⼀段不超过4KB的小型⽂本数据,由⼀个名称(Name)、⼀个值(Value)和其它⼏个用于控制Cookie有效期、安全性、使用范围的可选属性组成。

Session:在计算机中,尤其是在⽹络应用中,称为“会话控制”。

Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,⽽是在整个用户会话中⼀直存在下去。当用户请求来自应用程序的 Web页时,如果该用户还没有会话,则Web服务器将自动创建⼀个 Session对象。当会话过期或被放弃后,服务器将终⽌该会话。Session 对象最常见的⼀个用法就是存储用户的首选项。

如果客户端禁⽌ Cookie 能实现 Session 还能用吗?

能用。需要特殊处理。

Session共享原理是什么?怎么实现?

应用服务器间的session复制共享

基于cache DB缓存的session共享

基于redis缓存的session共享目的是实现单点登录

java的历史

1991 年Sun公司的James Gosling等⼈开始开发名称为 Oak 的语⾔,希望用于控制嵌⼊在有线电视交换盒、PDA等的微处理器;

1994年将Oak语⾔更名为Java;

Java的三种技术架构:

JAVAEE:Java Platform Enterprise Edition,开发企业环境下的应用程序,主要针对web程序开发;

JAVASE:Java Platform Standard Edition,完成桌面应用程序的开发,是其它两者的基础;

JAVAME:Java Platform Micro Edition,开发电⼦消费产品和嵌⼊式设备,如⼿机中的程序;

正则表达式?

好处:正则的出现,对字符串的复杂操作变得更为简单。

特点:将对字符串操作的代码用⼀些符号来表示。只要使用了指定符号,就可以调用底层的代码对字符串进⾏操作。符号的出现,简化了代码的书写。

弊端:符号的出现虽然简化了书写,但是却降低了阅读性。

其实更多是用正则解决字符串操作的问题。

组:用小括号标示,每定义⼀个小括号,就是⼀个组,⽽且有自动编 号,从1开始。 只要使用组,对应的数字就是使用该组的内容。别忘了,数组要加 \。 (aaa(wwww(ccc))(eee))技巧,从左括号开始数即可。有⼏个左括号就是⼏组。

常见操作:

1,匹配:其实用的就是String类中的matches⽅法。

String reg = “[1—9][0—9]{4,14}”;

boolean b = qq.matches(reg);//将正则和字符串关联对字符串进⾏匹配。

2,切割:其实用的就是String类中的split⽅法。

3,替换:其实用的就是String类中的replaceAll();

4,获取:

1),先要将正则表达式编译成正则对象。使用的是Pattern中静态⽅法 compile(regex);

2),通过Pattern对象获取Matcher对象。

Pattern用于描述正则表达式,可以对正则表达式进⾏解析。 ⽽将规则操作字符串,需要从新封装到匹配器对象Matcher中。

然后使用Matcher对象的⽅法来操作字符串。

如何获取匹配器对象呢?

通过Pattern对象中的matcher⽅法。该⽅法可以正则规则和字符串想关联。并返回匹配器对象。

3),使用Matcher对象中的⽅法即可对字符串进⾏各种正则操作。

反射技术?

就是动态加载⼀个指定的类,并获取该类中的所有的内容。⽽且将字

节码⽂件封装成对象,并将字节码⽂件中的内容都封装成对象,这样便于操作这些成员。简单说:反射技术可以对⼀个类进⾏解剖。

反射的好处:⼤⼤的增强了程序的扩展性。

反射的基本步骤:

1、获得Class对象,就是获取到指定的名称的字节码⽂件对象。

2、实例化对象,获得类的属性、⽅法或构造函数。

3、访问属性、调用⽅法、调用构造函数创建对象。

Java 语⾔有哪些特点?

简单易学;

面向对象(封装,继承,多态);

平台⽆关性( Java 虚拟机实现平台⽆关性);

可靠性;

安全性;

支持多线程( C++ 语⾔没有内置的多线程机制,因此必须调用操作

系统的多线程功能来进⾏多线程程序设计,⽽ Java 语⾔却提供了多线程支持);

支持⽹络编程并且很⽅便( Java 语⾔诞⽣本身就是为简化⽹络编程

设计的,因此 Java 语⾔不仅支持⽹络编程⽽且很⽅便);

重载和重写的区别?

重载: 发⽣在同⼀个类中,⽅法名必须相同,参数类型不同、个数

不同、顺序不同,⽅法返回值和访问修饰符可以不同,发⽣在编译时。

重写: 发⽣在⽗⼦类中,⽅法名、参数列表必须相同,返回值范围

小于等于⽗类,抛出的异常范围小于等于⽗类,访问修饰符范围⼤于等于⽗类;如果⽗类⽅法访问修饰符为 private 则⼦类就不能重写该⽅法。

接⼝和抽象类的区别是什么?

接⼝的⽅法默认是 public,所有⽅法在接⼝中不能有实现(Java 8 开始接⼝⽅法可以有默认实现),⽽抽象类可以有非抽象的⽅法。

接⼝中除了static、final变量,不能有其他变量,⽽抽象类中则不⼀

定。 ⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。接⼝自⼰本身可以通过extends关键字扩展多个接⼝。

接⼝⽅法默认修饰符是public,抽象⽅法可以有public、protected和 default这些修饰符(抽象⽅法就是为了被重写所以不能使用private关键字 修饰!)。 从设计层面来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。

备注:在JDK8中,接⼝也可以定义静态⽅法,可以直接用接⼝名调

用。实现类和实现是不可以调用的。如果同时实现两个接⼝,接⼝中定义了⼀样的默认⽅法,则必须重写,不然会报错。

成员变量与局部变量的区别有哪些?

从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义

的变量或是⽅法的参数;成员变量可以被 public,private,static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

从变量在内存中的存储⽅式来看:如果成员变量是使用static修饰的,

那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。⽽对象存在于堆内存,局部变量则存在于栈内存。

从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着

对象的创建⽽存在,⽽局部变量随着⽅法的调用⽽自动消失。

成员变量如果没有被赋初值:则会自动以类型的默认值⽽赋值(⼀种

情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会自动赋值。

== 与 equals?

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象 是不是同⼀个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它⼀般有两种使用情况:

情况1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。

情况2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

举个例⼦:

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
30
31
32
33
typescript复制代码public class test1 {

public static void main(String[] args) {

String a = new String("ab"); // a 为⼀个引用

String b = new String("ab"); // b为另⼀个引用,对象的内容⼀样

String aa = "ab"; // 放在常量池中

String bb = "ab"; // 从常量池中查找

if (aa == bb) // true

System.out.println("aa==bb");

if (a == b) // false,非同⼀对象

System.out.println("a==b");

if (a.equals(b)) // true

System.out.println("aEQb");

if (42 == 42.0) { // true

System.out.println("true");

}

}

}

说明:

String 中的 equals ⽅法是被重写过的,因为 object 的 equals ⽅法是比 较的对象的内存地址,⽽ String 的 equals ⽅法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建⼀个 String 对象。

hashCode 与 equals ?

hashCode()介绍

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回⼀ 个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包 含有hashCode() 函数。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有 hashCode

我们先以“HashSet 如何检查重复”为例⼦来说明为什么要有

hashCode: 当你把对象加⼊ HashSet 时,HashSet 会先计算对象的hashcode 值来判断对象加⼊的位置,同时也会与其他已经加⼊的对象的hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。

(摘自我的Java启蒙书《Head first java》第⼆版)。这样我们就⼤⼤减少了 equals 的次数,相应就⼤⼤提⾼了执⾏速度。

通过我们可以看出:hashCode() 的作用就是获取哈希码,也称为散列码;它实际上是返回⼀个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进⽽确定该对象在散列表中的位置。hashCode()与equals()的相关规定如果两个对象相等,则hashcode⼀定也是相同的 两个对象相等,对两个对象分别调用equals⽅法都返回true两个对象有相同的hashcode值,它们也不⼀定是相等的因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写hashCode(),则该 class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)

关于 JVM JDK 和 JRE

JVM

Java虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不面向任何特定的处理器,只面向虚拟机。Java 语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语⾔执⾏效率低的问题,同时又保留了解释型语⾔可移植的特点。所以 Java 程序运⾏时比较⾼效,⽽且,由于字节码并不针对⼀种特定的机器,因此,Java程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏。

我们需要格外注意的是 .class->机器码 这⼀步。在这⼀步 JVM 类加载器首先加载字节码⽂件,然后通过解释器逐⾏解释执⾏,这种⽅式的执⾏速度会相对比较慢。⽽且,有些⽅法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,⽽JIT 属于运⾏时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。⽽我们知道,机器码的运⾏效率肯定是⾼于 Java解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。

HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据⼆⼋定律,消耗⼤部分系统资源的只有那⼀小部分的代码(热点代码),⽽这也就是JIT所需要编译的部分。JVM会根据代码每次被执⾏的情况收集信息并相应地做出⼀些优化,因此执⾏的次数越多,它的速度就越快。JDK 9引⼊ 了⼀种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各⽅面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT编译器的。

总结:

Java虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,

它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语⾔“⼀次编译,随处可以运⾏”的关键所在。

JDK 和 JRE

JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如javadoc和jdb)。它能够创建和编译程序。

JRE 是 Java运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的⼀些基础构件。但是,它不能用于创建新程序。

如果你只是为了运⾏⼀下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进⾏⼀些 Java 编程⽅面的⼯作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进⾏任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运⾏Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。

静态⽅法和实例⽅法有何不同?

在外部调用静态⽅法时,可以使用”类名.⽅法名”的⽅式,也可以使用”对象名.⽅法名”的⽅式。⽽实例⽅法只有后面这种⽅式。也就是说,调用静态⽅法可以⽆需创建对象。

静态⽅法在访问本类的成员时,只允许访问静态成员(即静态成员变 量和静态⽅法),⽽不允许访问实例成员变量和实例⽅法;实例⽅法则⽆此限制

今天就到这儿吧,小伙伴们点赞、收藏、评论是对我最大的支持、下期见~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis系列(八)- MyBatis结果集嵌套映射|

发表于 2021-08-02

相关文章

MyBatis系列汇总:MyBatis系列


前言

  • 先看官网关于结果集的说明
+ ![image-20210720142547793.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/acd841cb8a5d2128fe163bb51cb18e62e3a6b069b66221442224b76749b9e274)
  • 我们一般使用最多的就是result,但在实际情况中,可能会遇到复杂类型的关联!这个时候就需要用到 association 和 collection
  • 下面是前置条件
  • student表
+ 
1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `student` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
`tid` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fktid` (`tid`),
CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • teacher表
+ 
1
2
3
4
5
sql复制代码CREATE TABLE `teacher` (
`id` int(10) NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 加点测试数据
+ ![image-20210720150033094.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/bbee46d1b20c301212a400d9f4030631c05d8388d64efeda0d11af2591c01839)
+ ![image-20210720150200345.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/85d3dc2a56b797afdde240fa316daab3692b80de5e1684ca23a721d4dca7f682)
  • 实体类
+ 
1
2
3
4
5
6
7
java复制代码@Data
public class Student {
private Integer id;
private String name;
//需要关联一个老师类
private Teacher teacher;
}
+
1
2
3
4
5
java复制代码@Data
public class Teacher {
private int id;
private String name;
}
  • 其他的跟前面文章保持一致即可!多种方式任君选择~

一、多对一

  • 首先我们正常查询一下学生试试看结果:
+ 
1
2
3
java复制代码public interface StudentMapper {
List<Student> getStudent();
}
+
1
2
3
java复制代码    <select id="getStudent" resultType="Student">
select * from student
</select>
+
1
2
3
4
5
6
7
8
9
10
java复制代码    @Test
public void getStudent(){
SqlSession session = MybatisUtils.getSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);
List<Student> students = mapper.getStudent();
students.forEach(student -> {
System.out.println(student);
});
session.close();
}
  • 执行结果:
+ ![image-20210730102419710.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/304010bd9570caf1d9d8c9408976224726aee2289ecdeecc25ff09de4b20ef56)
  • 结果显而易见,在查询中,如果有这种特殊的嵌套类,正常返还是null。下面我们就要使用嵌套映射来解决解决这个问题。

①、查询嵌套(子查询)

  • 注意点:resultMap和resultType只能有一个。不能同时存在。
  • 修改mapper.xml
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    <select id="getStudent" resultMap="Student">
select * from student
</select>

<resultMap id="Student" type="com.dy.pojo.Student">
<result property="id" column="id"></result>
<result property="name" column="name"></result>
<!--对象使用assiociation-->
<!--集合用collection-->
<association property="teacher" column="tid"
javaType="com.dy.pojo.Teacher"
select="getTeacher"></association>
</resultMap>

<select id="getTeacher" resultType="com.dy.pojo.Teacher">
select * from teacher where id = #{tid};
</select>
  • 执行结果:
+ ![image-20210730103403595.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/0850c284c31626cc140f5b94140db7166db436663dfdf307764b7fa4e610e770)
+ ![image-20210730103654594.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/f1be3bd92e9dc3f432d27c44ef0b90836132cef5da827006545f83eed1a29855)
  • Nice!完美解决问题!

②、结果嵌套(联表查询)

  • mapper.xml:
+ 
1
java复制代码    <select id="getStudent2" resultMap="Student">        select s.id sid,s.name sname,t.name tname        from student s,teacher t where s.tid=t.id;    </select>    <resultMap id="Student" type="com.dy.pojo.Student">        <result property="id" column="sid"></result>        <result property="name" column="sname"></result>        <association property="teacher" javaType="com.dy.pojo.Teacher">            <result property="name" column="tname"></result>        </association>    </resultMap>
  • 执行结果:
+ ![image-20210730105013436.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/85a671ca46b8cab88a746b8a3851bcd53c415a8dfe47a8a1a03917ba855df731)

二、一对多

  • 基本前提:
+ 
1
java复制代码@Data@Alias("Teacher2")public class Teacher2 {    private int id;    private String name;    private List<Student1> studentList;}
+
1
2
3
4
5
6
7
8
9
java复制代码@Datapublic class Student1 {    private Integer id;    private String name;    private int tid;}

```#### ①、结果嵌套(联表查询)


+ mapper.xml


-
java复制代码 <select id="getTeacher" resultMap="Teacher2"> select s.id sid, s.name sname, t.name tname, t.id tid from student s, teacher t where s.tid = t.id and t.id = #{id}; </select> <resultMap id="Teacher2" type="com.dy.pojo2.Teacher2"> <result property="id" column="tid"></result> <result property="name" column="tname"></result> <!--集合中的泛型信息,我们用oftype获取--> <collection property="studentList" ofType="com.dy.pojo2.Student1"> <result property="id" column="sid"></result> <result property="name" column="sname"></result> </collection> </resultMap>
1
2
3
4
5
6
7
8
9
10
11
12
13
	+ 执行结果:


- ![image-20210730112430923.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/6849cef59bb44d1a875ecc641a42fb264dd1a3043b97e6f716facbba3c637f64)


#### ②、查询嵌套(子查询)


* mapper.xml


+
java复制代码 <select id="getTeacher2" resultMap="Teacher2"> select * from teacher where id = #{id} </select> <resultMap id="Teacher2" type="com.dy.pojo2.Teacher2"> <collection property="studentList" column="id" javaType="ArrayList" ofType="com.dy.pojo2.Student1" select="getStudentByTeacherId"></collection> </resultMap> <select id="getStudentByTeacherId" resultType="com.dy.pojo2.Student1"> select * from student where tid = #{id} </select> ```
  • 执行结果:
+ ![image-20210730112812602.png](https://gitee.com/songjianzaina/juejin_p14/raw/master/img/733ca17b1b3d27fb544c36193def7f3a04edfd3c122280d2891b9bca113d4a04)
  • 完美!

三、总结

  • 关联 - association 多对一
  • 集合 - collection 一对多
  • javaType & ofType
+ JavaType用来指定实体中属性类型
+ ofType映射到list中的类型,泛型中的约束类型
  • 注意点:
+ 保证sql可读性,尽量保证通俗易懂
+ 如果问题不好排查错误,使用日志
+ resultMap 和 resultType 要区分清楚!

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

几百行代码写个Mybatis,原理搞的透透的!

发表于 2021-08-02

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

Mybatis 最核心的原理也是它最便于使用的体现,为什么这说?

因为我们在使用 Mybatis 的时候,只需要定义一个不需要写实现类的接口,就能通过注解或者配置SQL语句的方式,对数据库进行 CRUD 操作。

那么这是怎么做到的呢,其中有一点非常重要,就是在 Spring 中可以把你的代理对象交给 Spring 容器,这个代理对象就是可以当做是 DAO 接口的具体实现类,而这个被代理的实现类就可以完成对数据库的一个操作,也就是这个封装过程被称为 ORM 框架。

说了基本的流程,我们来做点测试,让大家可以动手操作起来!学知识,一定是上手,才能得到!你可以通过以下源码仓库进行练习

github.com/fuzhengwei/…


二、把Bean塞到Spring容器,分几步

Bean注册

  • 关于Bean注册的技术场景,在我们日常用到的技术框架中,MyBatis 是最为常见的。通过在使用 MyBatis 时都只是定义一个接口不需要写实现类,但是这个接口却可以和配置的 SQL 语句关联,执行相应的数据库操作时可以返回对应的结果。那么这个接口与数据库的操作就用到的 Bean 的代理和注册。
  • 我们都知道类的调用是不能直接调用没有实现的接口的,所以需要通过代理的方式给接口生成对应的实现类。接下来再通过把代理类放到 Spring 的 FactoryBean 的实现中,最后再把这个 FactoryBean 实现类注册到 Spring 容器。那么现在你的代理类就已经被注册到 Spring 容器了,接下来就可以通过注解的方式注入到属性中。

按照这个实现方式,我们来操作一下,看看一个 Bean 的注册过程在代码中是如何实现的。

1. 定义接口

1
2
3
4
5
java复制代码public interface IUserDao {

String queryUserInfo();

}
  • 先定义一个类似 DAO 的接口,基本这样的接口在使用 MyBatis 时还是非常常见的。后面我们会对这个接口做代理和注册。

2. 类代理实现

1
2
3
4
5
6
7
8
java复制代码ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};

InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);

String res = userDao.queryUserInfo();
logger.info("测试结果:{}", res);
  • Java 本身的代理方式使用起来还是比较简单的,用法也很固定。
  • InvocationHandler 是个接口类,它对应的实现内容就是代理对象的具体实现。
  • 最后就是把代理交给 Proxy 创建代理对象,Proxy.newProxyInstance。

3. 实现Bean工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class ProxyBeanFactory implements FactoryBean {

@Override
public Object getObject() throws Exception {

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class[] classes = {IUserDao.class};
InvocationHandler handler = (proxy, method, args) -> "你被代理了 " + method.getName();

return Proxy.newProxyInstance(classLoader, classes, handler);
}

@Override
public Class<?> getObjectType() {
return IUserDao.class;
}

}
  • FactoryBean 在 spring 起到着二当家的地位,它将近有70多个小弟(实现它的接口定义),那么它有三个方法;
    • T getObject() throws Exception; 返回bean实例对象
    • Class<?> getObjectType(); 返回实例类类型
    • boolean isSingleton(); 判断是否单例,单例会放到Spring容器中单实例缓存池中
  • 在这里我们把上面使用Java代理的对象放到了 getObject() 方法中,那么现在再从 Spring 中获取到的对象,就是我们的代理对象了。

4. Bean 注册

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(ProxyBeanFactory.class);

BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}

}

在 Spring 的 Bean 管理中,所有的 Bean 最终都会被注册到类 DefaultListableBeanFactory 中,以上这部分代码主要的内容包括:

  • 实现 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry方法,获取 Bean 注册对象。
  • 定义 Bean,GenericBeanDefinition,这里主要设置了我们的代理类工厂。
  • 创建 Bean 定义处理类,BeanDefinitionHolder,这里需要的主要参数;定义 Bean 和名称 setBeanClass(ProxyBeanFactory.class)。
  • 最后将我们自己的bean注册到spring容器中去,registry.registerBeanDefinition()

5. 测试验证

在上面我们已经把自定义代理的 Bean 注册到了 Spring 容器中,接下来我们来测试下这个代理的 Bean 被如何调用。

1. 定义 spring-config.xml

1
java复制代码<bean id="userDao" class="org.itstack.interview.bean.RegisterBeanFactory"/>
  • 这里我们把 RegisterBeanFactory 配置到 spring 的 xml 配置中,便于启动时加载。

2. 单元测试

1
2
3
4
5
6
7
java复制代码@Test
public void test_IUserDao() {
BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
String res = userDao.queryUserInfo();
logger.info("测试结果:{}", res);
}

测试结果

1
2
3
4
5
java复制代码22:53:14.759 [main] DEBUG o.s.c.e.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source
22:53:14.760 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userDao'
22:53:14.796 [main] INFO org.itstack.interview.test.ApiTest - 测试结果:你被代理了 queryUserInfo

Process finished with exit code 0
  • 从测试结果可以看到,我们已经可以通过注入到Spring的代理Bean对象,实现我们的预期结果。
  • 其实这个过程也是很多框架中用到的方式,尤其是在一些中间件开发,类似的 ORM 框架都需要使用到。

三、手写个Mybatis

扩展上一篇源码分析工程;itstack-demo-mybatis,增加 like 包,模仿 Mybatis 工程。完整规程下载 github.com/fuzhengwei/…

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
30
31
32
33
34
35
36
37
38
39
40
java复制代码itstack-demo-mybatis
└── src
├── main
│ ├── java
│ │ └── org.itstack.demo
│ │ ├── dao
│ │ │ ├── ISchool.java
│ │ │ └── IUserDao.java
│ │ ├── like
│ │ │ ├── Configuration.java
│ │ │ ├── DefaultSqlSession.java
│ │ │ ├── DefaultSqlSessionFactory.java
│ │ │ ├── Resources.java
│ │ │ ├── SqlSession.java
│ │ │ ├── SqlSessionFactory.java
│ │ │ ├── SqlSessionFactoryBuilder.java
│ │ │ └── SqlSessionFactoryBuilder.java
│ │ └── interfaces
│ │ ├── School.java
│ │ └── User.java
│ ├── resources
│ │ ├── mapper
│ │ │ ├── School_Mapper.xml
│ │ │ └── User_Mapper.xml
│ │ ├── props
│ │ │ └── jdbc.properties
│ │ ├── spring
│ │ │ ├── mybatis-config-datasource.xml
│ │ │ └── spring-config-datasource.xml
│ │ ├── logback.xml
│ │ ├── mybatis-config.xml
│ │ └── spring-config.xml
│ └── webapp
│ └── WEB-INF
└── test
└── java
└── org.itstack.demo.test
├── ApiLikeTest.java
├── MybatisApiTest.java
└── SpringApiTest.java

关于整个 Demo 版本,并不是把所有 Mybatis 全部实现一遍,而是拨丝抽茧将最核心的内容展示给你,从使用上你会感受一模一样,但是实现类已经全部被替换,核心类包括;

  • Configuration
  • DefaultSqlSession
  • DefaultSqlSessionFactory
  • Resources
  • SqlSession
  • SqlSessionFactory
  • SqlSessionFactoryBuilder
  • XNode

1. 先测试下整个DemoJdbc框架

ApiLikeTest.test_queryUserInfoById()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Test
public void test_queryUserInfoById() {
String resource = "spring/mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();

try {
User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
System.out.println(JSON.toJSONString(user));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}

一切顺利结果如下(新人往往会遇到各种问题);

1
2
3
java复制代码{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0

可能乍一看这测试类完全和 MybatisApiTest.java 测试的代码一模一样呀,也看不出区别。其实他们的引入的包是不一样;

MybatisApiTest.java 里面引入的包

1
2
3
4
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;

ApiLikeTest.java 里面引入的包

1
2
3
4
java复制代码import org.itstack.demo.like.Resources;
import org.itstack.demo.like.SqlSession;
import org.itstack.demo.like.SqlSessionFactory;
import org.itstack.demo.like.SqlSessionFactoryBuilder;

好!接下来我们开始分析这部分核心代码。

2. 加载XML配置文件

这里我们采用 mybatis 的配置文件结构进行解析,在不破坏原有结构的情况下,最大可能的贴近源码。mybatis 单独使用的使用的时候使用了两个配置文件;数据源配置、Mapper 映射配置,如下;

mybatis-config-datasource.xml & 数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
<mapper resource="mapper/School_Mapper.xml"/>
</mappers>

</configuration>

User_Mapper.xml & Mapper 映射配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.itstack.demo.dao.IUserDao">

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>

<select id="queryUserList" parameterType="org.itstack.demo.po.User" resultType="org.itstack.demo.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where age = #{age}
</select>

</mapper>

这里的加载过程与 mybaits 不同,我们采用 dom4j 方式。在案例中会看到最开始获取资源,如下;

ApiLikeTest.test_queryUserInfoById() & 部分截取

1
2
3
4
5
java复制代码String resource = "spring/mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
...

从上可以看到这是通过配置文件地址获取到了读取流的过程,从而为后面解析做基础。首先我们先看 Resources 类,整个是我们的资源类。

Resources.java & 资源类

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
java复制代码/**
* 博 客 | https://bugstack.cn
* Create by 小傅哥 @2020
*/
public class Resources {

public static Reader getResourceAsReader(String resource) throws IOException {
return new InputStreamReader(getResourceAsStream(resource));
}

private static InputStream getResourceAsStream(String resource) throws IOException {
ClassLoader[] classLoaders = getClassLoaders();
for (ClassLoader classLoader : classLoaders) {
InputStream inputStream = classLoader.getResourceAsStream(resource);
if (null != inputStream) {
return inputStream;
}
}
throw new IOException("Could not find resource " + resource);
}

private static ClassLoader[] getClassLoaders() {
return new ClassLoader[]{
ClassLoader.getSystemClassLoader(),
Thread.currentThread().getContextClassLoader()};
}

}

这段代码方法的入口是getResourceAsReader,直到往下以此做了;

  1. 获取 ClassLoader 集合,最大限度搜索配置文件
  2. 通过 classLoader.getResourceAsStream 读取配置资源,找到后立即返回,否则抛出异常

3. 解析XML配置文件

配置文件加载后开始进行解析操作,这里我们也仿照 mybatis 但进行简化,如下;

1
java复制代码SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

SqlSessionFactoryBuilder.build() & 入口构建类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public DefaultSqlSessionFactory build(Reader reader) {
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new InputSource(reader));
Configuration configuration = parseConfiguration(document.getRootElement());
return new DefaultSqlSessionFactory(configuration);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}
  • 通过读取流创建 xml 解析的 Document 类
  • parseConfiguration 进行解析 xml 文件,并将结果设置到配置类中,包括;连接池、数据源、mapper关系

SqlSessionFactoryBuilder.parseConfiguration() & 解析过程

1
2
3
4
5
6
7
java复制代码private Configuration parseConfiguration(Element root) {
Configuration configuration = new Configuration();
configuration.setDataSource(dataSource(root.selectNodes("//dataSource")));
configuration.setConnection(connection(configuration.dataSource));
configuration.setMapperElement(mapperElement(root.selectNodes("mappers")));
return configuration;
}
  • 在前面的 xml 内容中可以看到,我们需要解析出数据库连接池信息 datasource,还有数据库语句映射关系 mappers

SqlSessionFactoryBuilder.dataSource() & 解析出数据源

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private Map<String, String> dataSource(List<Element> list) {
Map<String, String> dataSource = new HashMap<>(4);
Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String name = e.attributeValue("name");
String value = e.attributeValue("value");
dataSource.put(name, value);
}
return dataSource;
}
  • 这个过程比较简单,只需要将数据源信息获取即可

SqlSessionFactoryBuilder.connection() & 获取数据库连接

1
2
3
4
5
6
7
8
9
java复制代码private Connection connection(Map<String, String> dataSource) {
try {
Class.forName(dataSource.get("driver"));
return DriverManager.getConnection(dataSource.get("url"), dataSource.get("username"), dataSource.get("password"));
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return null;
}
  • 这个就是jdbc最原始的代码,获取了数据库连接池

SqlSessionFactoryBuilder.mapperElement() & 解析SQL语句

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码private Map<String, XNode> mapperElement(List<Element> list) {
Map<String, XNode> map = new HashMap<>();
Element element = list.get(0);
List content = element.content();
for (Object o : content) {
Element e = (Element) o;
String resource = e.attributeValue("resource");
try {
Reader reader = Resources.getResourceAsReader(resource);
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new InputSource(reader));
Element root = document.getRootElement();
//命名空间
String namespace = root.attributeValue("namespace");
// SELECT
List<Element> selectNodes = root.selectNodes("select");
for (Element node : selectNodes) {
String id = node.attributeValue("id");
String parameterType = node.attributeValue("parameterType");
String resultType = node.attributeValue("resultType");
String sql = node.getText();
// ? 匹配
Map<Integer, String> parameter = new HashMap<>();
Pattern pattern = Pattern.compile("(#\\{(.*?)})");
Matcher matcher = pattern.matcher(sql);
for (int i = 1; matcher.find(); i++) {
String g1 = matcher.group(1);
String g2 = matcher.group(2);
parameter.put(i, g2);
sql = sql.replace(g1, "?");
}
XNode xNode = new XNode();
xNode.setNamespace(namespace);
xNode.setId(id);
xNode.setParameterType(parameterType);
xNode.setResultType(resultType);
xNode.setSql(sql);
xNode.setParameter(parameter);

map.put(namespace + "." + id, xNode);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
return map;
}
  • 这个过程首先包括是解析所有的sql语句,目前为了测试只解析 select 相关
  • 所有的 sql 语句为了确认唯一,都是使用;namespace + select中的id进行拼接,作为 key,之后与sql一起存放到 map 中。
  • 在 mybaits 的 sql 语句配置中,都有占位符,用于传参。where id = #{id} 所以我们需要将占位符设置为问号,另外需要将占位符的顺序信息与名称存放到 map 结构,方便后续设置查询时候的入参。

4. 创建DefaultSqlSessionFactory

最后将初始化后的配置类 Configuration,作为参数进行创建 DefaultSqlSessionFactory,如下;

1
2
3
4
5
6
7
8
9
10
11
java复制代码public DefaultSqlSessionFactory build(Reader reader) {
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(new InputSource(reader));
Configuration configuration = parseConfiguration(document.getRootElement());
return new DefaultSqlSessionFactory(configuration);
} catch (DocumentException e) {
e.printStackTrace();
}
return null;
}

DefaultSqlSessionFactory.java & SqlSessionFactory的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class DefaultSqlSessionFactory implements SqlSessionFactory {

private final Configuration configuration;

public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}

@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}

}
  • 这个过程比较简单,构造函数只提供了配置类入参
  • 实现 SqlSessionFactory 的 openSession(),用于创建 DefaultSqlSession,也就可以执行 sql 操作

5. 开启SqlSession

1
java复制代码SqlSession session = sqlMapper.openSession();

上面这一步就是创建了DefaultSqlSession,比较简单。如下;

1
2
3
4
java复制代码@Override
public SqlSession openSession() {
return new DefaultSqlSession(configuration.connection, configuration.mapperElement);
}

6. 执行SQL语句

1
java复制代码User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);

在 DefaultSqlSession 中通过实现 SqlSession,提供数据库语句查询和关闭连接池,如下;

SqlSession.java & 定义

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public interface SqlSession {

<T> T selectOne(String statement);

<T> T selectOne(String statement, Object parameter);

<T> List<T> selectList(String statement);

<T> List<T> selectList(String statement, Object parameter);

void close();
}

接下来看具体的执行过程,session.selectOne

DefaultSqlSession.selectOne() & 执行查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public <T> T selectOne(String statement, Object parameter) {
XNode xNode = mapperElement.get(statement);
Map<Integer, String> parameterMap = xNode.getParameter();
try {
PreparedStatement preparedStatement = connection.prepareStatement(xNode.getSql());
buildParameter(preparedStatement, parameter, parameterMap);
ResultSet resultSet = preparedStatement.executeQuery();
List<T> objects = resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
return objects.get(0);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
  • selectOne 就objects.get(0);,selectList 就全部返回
  • 通过 statement 获取最初解析 xml 时候的存储的 select 标签信息;
1
2
3
4
5
xml复制代码<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
SELECT id, name, age, createTime, updateTime
FROM user
where id = #{id}
</select>
  • 获取 sql 语句后交给 jdbc 的 PreparedStatement 类进行执行
  • 这里还需要设置入参,我们将入参设置进行抽取,如下;
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码private void buildParameter(PreparedStatement preparedStatement, Object parameter, Map<Integer, String> parameterMap) throws SQLException, IllegalAccessException {

int size = parameterMap.size();
// 单个参数
if (parameter instanceof Long) {
for (int i = 1; i <= size; i++) {
preparedStatement.setLong(i, Long.parseLong(parameter.toString()));
}
return;
}

if (parameter instanceof Integer) {
for (int i = 1; i <= size; i++) {
preparedStatement.setInt(i, Integer.parseInt(parameter.toString()));
}
return;
}

if (parameter instanceof String) {
for (int i = 1; i <= size; i++) {
preparedStatement.setString(i, parameter.toString());
}
return;
}

Map<String, Object> fieldMap = new HashMap<>();
// 对象参数
Field[] declaredFields = parameter.getClass().getDeclaredFields();
for (Field field : declaredFields) {
String name = field.getName();
field.setAccessible(true);
Object obj = field.get(parameter);
field.setAccessible(false);
fieldMap.put(name, obj);
}

for (int i = 1; i <= size; i++) {
String parameterDefine = parameterMap.get(i);
Object obj = fieldMap.get(parameterDefine);

if (obj instanceof Short) {
preparedStatement.setShort(i, Short.parseShort(obj.toString()));
continue;
}

if (obj instanceof Integer) {
preparedStatement.setInt(i, Integer.parseInt(obj.toString()));
continue;
}

if (obj instanceof Long) {
preparedStatement.setLong(i, Long.parseLong(obj.toString()));
continue;
}

if (obj instanceof String) {
preparedStatement.setString(i, obj.toString());
continue;
}

if (obj instanceof Date) {
preparedStatement.setDate(i, (java.sql.Date) obj);
}

}

}
+ 单个参数比较简单直接设置值即可,Long、Integer、String ...
+ 如果是一个类对象,需要通过获取 Field 属性,与参数 Map 进行匹配设置
  • 设置参数后执行查询 preparedStatement.executeQuery()
  • 接下来需要将查询结果转换为我们的类(主要是反射类的操作),resultSet2Obj(resultSet, Class.forName(xNode.getResultType()));
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
java复制代码private <T> List<T> resultSet2Obj(ResultSet resultSet, Class<?> clazz) {
List<T> list = new ArrayList<>();
try {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
// 每次遍历行值
while (resultSet.next()) {
T obj = (T) clazz.newInstance();
for (int i = 1; i <= columnCount; i++) {
Object value = resultSet.getObject(i);
String columnName = metaData.getColumnName(i);
String setMethod = "set" + columnName.substring(0, 1).toUpperCase() + columnName.substring(1);
Method method;
if (value instanceof Timestamp) {
method = clazz.getMethod(setMethod, Date.class);
} else {
method = clazz.getMethod(setMethod, value.getClass());
}
method.invoke(obj, value);
}
list.add(obj);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
+ 主要通过反射生成我们的类对象,这个类的类型定义在 sql 标签上
+ 时间类型需要判断后处理,Timestamp,与 java 不是一个类型

7. Sql查询补充说明

sql 查询有入参、有不需要入参、有查询一个、有查询集合,只需要合理包装即可,例如下面的查询集合,入参是对象类型;

ApiLikeTest.test_queryUserList()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Test
public void test_queryUserList() {
String resource = "spring/mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = sqlMapper.openSession();

try {
User req = new User();
req.setAge(18);
List<User> userList = session.selectList("org.itstack.demo.dao.IUserDao.queryUserList", req);
System.out.println(JSON.toJSONString(userList));
} finally {
session.close();
reader.close();
}
} catch (Exception e) {
e.printStackTrace();
}

}

**测试结果:

1
2
3
java复制代码[{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000},{"age":18,"createTime":1576944000000,"id":2,"name":"豆豆","updateTime":1576944000000}]

Process finished with exit code 0

四、源码分析(mybatis)

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

Mybatis的整个源码还是很大的,以下主要将部分核心内容进行整理分析,以便于后续分析Mybatis与Spring整合的源码部分。简要包括;容器初始化、配置文件解析、Mapper加载与动态代理。

1. 从一个简单的案例开始

要学习Mybatis源码,最好的方式一定是从一个简单的点进入,而不是从Spring整合开始分析。SqlSessionFactory是整个Mybatis的核心实例对象,SqlSessionFactory对象的实例又通过SqlSessionFactoryBuilder对象来获得。SqlSessionFactoryBuilder对象可以从XML配置文件加载配置信息,然后创建SqlSessionFactory。如下例子:

MybatisApiTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class MybatisApiTest {

@Test
public void test_queryUserInfoById() {
String resource = "spring/mybatis-config-datasource.xml";
Reader reader;
try {
reader = Resources.getResourceAsReader(resource);
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);

SqlSession session = sqlMapper.openSession();
try {
User user = session.selectOne("org.itstack.demo.dao.IUserDao.queryUserInfoById", 1L);
System.out.println(JSON.toJSONString(user));
} finally {
session.close();
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

dao/IUserDao.java

1
2
3
4
5
java复制代码public interface IUserDao {

User queryUserInfoById(Long id);

}

spring/mybatis-config-datasource.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
</mappers>

</configuration>

如果一切顺利,那么会有如下结果:

1
java复制代码{"age":18,"createTime":1571376957000,"id":1,"name":"花花","updateTime":1571376957000}

从上面的代码块可以看到,核心代码;SqlSessionFactoryBuilder().build(reader),负责Mybatis配置文件的加载、解析、构建等职责,直到最终可以通过SqlSession来执行并返回结果。

2. 容器初始化

从上面代码可以看到,SqlSessionFactory是通过SqlSessionFactoryBuilder工厂类创建的,而不是直接使用构造器。容器的配置文件加载和初始化流程如下:

微信公众号:bugstack虫洞栈 & 初始化流程

  • 流程核心类
    • SqlSessionFactoryBuilder
    • XMLConfigBuilder
    • XPathParser
    • Configuration

SqlSessionFactoryBuilder.java

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码public class SqlSessionFactoryBuilder {

public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}

public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}

public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

}

从上面的源码可以看到,SqlSessionFactory提供三种方式build构建对象;

  • 字节流:java.io.InputStream
  • 字符流:java.io.Reader
  • 配置类:org.apache.ibatis.session.Configuration

那么,字节流、字符流都会创建配置文件解析类:XMLConfigBuilder,并通过parser.parse()生成Configuration,最后调用配置类构建方法生成SqlSessionFactory。

XMLConfigBuilder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class XMLConfigBuilder extends BaseBuilder {

private boolean parsed;
private final XPathParser parser;
private String environment;
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

...
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
...
}
  1. XMLConfigBuilder对于XML文件的加载和解析都委托于XPathParser,最终使用JDK自带的javax.xml进行XML解析(XPath)
  2. XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver)
    1. reader:使用字符流创建新的输入源,用于对XML文件的读取
    2. validation:是否进行DTD校验
    3. variables:属性配置信息
    4. entityResolver:Mybatis硬编码了new XMLMapperEntityResolver()提供XML默认解析器

XMLMapperEntityResolver.java

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码public class XMLMapperEntityResolver implements EntityResolver {

private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";

private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

/*
* Converts a public DTD into a local one
*
* @param publicId The public id that is what comes after "PUBLIC"
* @param systemId The system id that is what comes after the public id.
* @return The InputSource for the DTD
*
* @throws org.xml.sax.SAXException If anything goes wrong
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}

private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}

}
  1. Mybatis依赖于dtd文件进行进行解析,其中的ibatis-3-config.dtd主要是用于兼容用途
  2. getInputSource(String path, String publicId, String systemId)的调用里面有两个参数publicId(公共标识符)和systemId(系统标示符)

XPathParser.java

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}

private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(entityResolver);
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}

}
  1. 从上到下可以看到主要是为了创建一个Mybatis的文档解析器,最后根据builder.parse(inputSource)返回Document
  2. 得到XPathParser实例后,接下来在调用方法:this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
1
2
3
4
5
6
7
8
9
10
java复制代码 XMLConfigBuilder.this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);

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;
this.parser = parser;
}
  1. 其中调用了父类的构造函数
1
2
3
4
5
6
7
8
9
10
11
java复制代码public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;

public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
}
  1. XMLConfigBuilder创建完成后,sqlSessionFactoryBuild调用parser.parse()创建Configuration
1
2
3
4
5
6
7
8
9
10
java复制代码public class XMLConfigBuilder extends BaseBuilder {    
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
}

3. 配置文件解析

这一部分是整个XML文件解析和装载的核心内容,其中包括;

  1. 属性解析propertiesElement
  2. 加载settings节点settingsAsProperties
  3. 载自定义VFS loadCustomVfs
  4. 解析类型别名typeAliasesElement
  5. 加载插件pluginElement
  6. 加载对象工厂objectFactoryElement
  7. 创建对象包装器工厂objectWrapperFactoryElement
  8. 加载反射工厂reflectorFactoryElement
  9. 元素设置settingsElement
  10. 加载环境配置environmentsElement
  11. 数据库厂商标识加载databaseIdProviderElement
  12. 加载类型处理器typeHandlerElement
  13. (核心)加载mapper文件mapperElement
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
30
31
32
33
34
35
36
java复制代码parseConfiguration(parser.evalNode("/configuration"));

private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
//属性解析propertiesElement
propertiesElement(root.evalNode("properties"));
//加载settings节点settingsAsProperties
Properties settings = settingsAsProperties(root.evalNode("settings"));
//加载自定义VFS loadCustomVfs
loadCustomVfs(settings);
//解析类型别名typeAliasesElement
typeAliasesElement(root.evalNode("typeAliases"));
//加载插件pluginElement
pluginElement(root.evalNode("plugins"));
//加载对象工厂objectFactoryElement
objectFactoryElement(root.evalNode("objectFactory"));
//创建对象包装器工厂objectWrapperFactoryElement
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//加载反射工厂reflectorFactoryElement
reflectorFactoryElement(root.evalNode("reflectorFactory"));
//元素设置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
//加载环境配置environmentsElement
environmentsElement(root.evalNode("environments"));
//数据库厂商标识加载databaseIdProviderElement
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//加载类型处理器typeHandlerElement
typeHandlerElement(root.evalNode("typeHandlers"));
//加载mapper文件mapperElement
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

所有的root.evalNode()底层都是调用XML DOM方法:Object evaluate(String expression, Object item, QName returnType),表达式参数expression,通过XObject resultObject = eval( expression, item )返回最终节点内容,可以参考mybatis.org/dtd/mybatis…

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
xml复制代码<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>

<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
>

<!ELEMENT properties (property*)>
<!ATTLIST properties
resource CDATA #IMPLIED
url CDATA #IMPLIED
>

<!ELEMENT property EMPTY>
<!ATTLIST property
name CDATA #REQUIRED
value CDATA #REQUIRED
>

<!ELEMENT settings (setting+)>

<!ELEMENT setting EMPTY>
<!ATTLIST setting
name CDATA #REQUIRED
value CDATA #REQUIRED
>

<!ELEMENT typeAliases (typeAlias*,package*)>

<!ELEMENT typeAlias EMPTY>
<!ATTLIST typeAlias
type CDATA #REQUIRED
alias CDATA #IMPLIED
>

<!ELEMENT typeHandlers (typeHandler*,package*)>

<!ELEMENT typeHandler EMPTY>
<!ATTLIST typeHandler
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
handler CDATA #REQUIRED
>

<!ELEMENT objectFactory (property*)>
<!ATTLIST objectFactory
type CDATA #REQUIRED
>

<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
>

<!ELEMENT reflectorFactory EMPTY>
<!ATTLIST reflectorFactory
type CDATA #REQUIRED
>

<!ELEMENT plugins (plugin+)>

<!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
>

<!ELEMENT environments (environment+)>
<!ATTLIST environments
default CDATA #REQUIRED
>

<!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment
id CDATA #REQUIRED
>

<!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager
type CDATA #REQUIRED
>

<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
>

<!ELEMENT mappers (mapper*,package*)>

<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>

mybatis-3-config.dtd 定义文件中有11个配置文件,如下;

  1. properties?,
  2. settings?,
  3. typeAliases?,
  4. typeHandlers?,
  5. objectFactory?,
  6. objectWrapperFactory?,
  7. reflectorFactory?,
  8. plugins?,
  9. environments?,
  10. databaseIdProvider?,
  11. mappers?

以上每个配置都是可选。最终配置内容会保存到org.apache.ibatis.session.Configuration,如下;

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
java复制代码public class Configuration {

protected Environment environment;
// 允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false。默认为false
protected boolean safeRowBoundsEnabled;
// 允许在嵌套语句中使用分页(ResultHandler)。如果允许使用则设置为false。
protected boolean safeResultHandlerEnabled = true;
// 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射。默认false
protected boolean mapUnderscoreToCamelCase;
// 当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载。默认值false (true in ≤3.4.1)
protected boolean aggressiveLazyLoading;
// 是否允许单一语句返回多结果集(需要兼容驱动)。
protected boolean multipleResultSetsEnabled = true;
// 允许 JDBC 支持自动生成主键,需要驱动兼容。这就是insert时获取mysql自增主键/oracle sequence的开关。注:一般来说,这是希望的结果,应该默认值为true比较合适。
protected boolean useGeneratedKeys;
// 使用列标签代替列名,一般来说,这是希望的结果
protected boolean useColumnLabel = true;
// 是否启用缓存 {默认是开启的,可能这也是你的面试题}
protected boolean cacheEnabled = true;
// 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet() 依赖或 null 值初始化的时候是有用的。
protected boolean callSettersOnNulls;
// 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的工程必须采用Java 8编译,并且加上-parameters选项。(从3.4.1开始)
protected boolean useActualParamName = true;
//当返回行的所有列都是空时,MyBatis默认返回null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集 (i.e. collectioin and association)。(从3.4.2开始) 注:这里应该拆分为两个参数比较合适, 一个用于结果集,一个用于单记录。通常来说,我们会希望结果集不是null,单记录仍然是null
protected boolean returnInstanceForEmptyRow;
// 指定 MyBatis 增加到日志名称的前缀。
protected String logPrefix;
// 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。一般建议指定为slf4j或log4j
protected Class <? extends Log> logImpl;
// 指定VFS的实现, VFS是mybatis提供的用于访问AS内资源的一个简便接口
protected Class <? extends VFS> vfsImpl;
// MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
// 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。
protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
// 指定对象的哪个方法触发一次延迟加载。
protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new String[] { "equals", "clone", "hashCode", "toString" }));
// 设置超时时间,它决定驱动等待数据库响应的秒数。默认不超时
protected Integer defaultStatementTimeout;
// 为驱动的结果集设置默认获取数量。
protected Integer defaultFetchSize;
// SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。
protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
// 指定发现自动映射目标未知列(或者未知属性类型)的行为。这个值应该设置为WARNING比较合适
protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
// settings下的properties属性
protected Properties variables = new Properties();
// 默认的反射器工厂,用于操作属性、构造器方便
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
// 对象工厂, 所有的类resultMap类都需要依赖于对象工厂来实例化
protected ObjectFactory objectFactory = new DefaultObjectFactory();
// 对象包装器工厂,主要用来在创建非原生对象,比如增加了某些监控或者特殊属性的代理类
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
// 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。
protected boolean lazyLoadingEnabled = false;
// 指定 Mybatis 创建具有延迟加载能力的对象所用到的代理工具。MyBatis 3.3+使用JAVASSIST
protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL
// MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。
protected String databaseId;
...
}

以上可以看到,Mybatis把所有的配置;resultMap、Sql语句、插件、缓存等都维护在Configuration中。这里还有一个小技巧,在Configuration还有一个StrictMap内部类,它继承于HashMap完善了put时防重、get时取不到值的异常处理,如下;

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
java复制代码protected static class StrictMap<V> extends HashMap<String, V> {

private static final long serialVersionUID = -4950446264854982944L;
private final String name;

public StrictMap(String name, int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
this.name = name;
}

public StrictMap(String name, int initialCapacity) {
super(initialCapacity);
this.name = name;
}

public StrictMap(String name) {
super();
this.name = name;
}

public StrictMap(String name, Map<String, ? extends V> m) {
super(m);
this.name = name;
}
}

(核心)加载mapper文件mapperElement

Mapper文件处理是Mybatis框架的核心服务,所有的SQL语句都编写在Mapper中,这块也是我们分析的重点,其他模块可以后续讲解。

XMLConfigBuilder.parseConfiguration()->mapperElement(root.evalNode(“mappers”));

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
30
31
java复制代码private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 如果要同时使用package自动扫描和通过mapper明确指定要加载的mapper,一定要确保package自动扫描的范围不包含明确指定的mapper,否则在通过package扫描的interface的时候,尝试加载对应xml文件的loadXmlResource()的逻辑中出现判重出错,报org.apache.ibatis.binding.BindingException异常,即使xml文件中包含的内容和mapper接口中包含的语句不重复也会出错,包括加载mapper接口时自动加载的xml mapper也一样会出错。
if ("package".equals(child.getName())) {
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);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
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.");
}
}
}
}
}
  • Mybatis提供了两类配置Mapper的方法,第一类是使用package自动搜索的模式,这样指定package下所有接口都会被注册为mapper,也是在Spring中比较常用的方式,例如:
1
2
3
java复制代码<mappers>
<package name="org.itstack.demo"/>
</mappers>
  • 另外一类是明确指定Mapper,这又可以通过resource、url或者class进行细分,例如;
1
2
3
4
5
java复制代码<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
<mapper class=""/>
<mapper url=""/>
</mappers>

4. Mapper加载与动态代理

通过package方式自动搜索加载,生成对应的mapper代理类,代码块和流程,如下;

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
...
}
}
}
}

微信公众号:bugstack虫洞栈 & 动态代理过程

Mapper加载到生成代理对象的流程中,主要的核心类包括;

  1. XMLConfigBuilder
  2. Configuration
  3. MapperRegistry
  4. MapperAnnotationBuilder
  5. MapperProxyFactory

MapperRegistry.java

解析加载Mapper

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void addMappers(String packageName, Class<?> superType) {
// mybatis框架提供的搜索classpath下指定package以及子package中符合条件(注解或者继承于某个类/接口)的类,默认使用Thread.currentThread().getContextClassLoader()返回的加载器,和spring的工具类殊途同归。
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
// 无条件的加载所有的类,因为调用方传递了Object.class作为父类,这也给以后的指定mapper接口预留了余地
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 所有匹配的calss都被存储在ResolverUtil.matches字段中
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
//调用addMapper方法进行具体的mapper类/接口解析
addMapper(mapperClass);
}
}

生成代理类:MapperProxyFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public <T> void addMapper(Class<T> type) {    
// 对于mybatis mapper接口文件,必须是interface,不能是class
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 为mapper接口创建一个MapperProxyFactory代理
knownMappers.put(type, new MapperProxyFactory<T>(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);
}
}
}
}

在MapperRegistry中维护了接口类与代理工程的映射关系,knownMappers;

1
java复制代码private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

MapperProxyFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}

如上是Mapper的代理类工程,构造函数中的mapperInterface就是对应的接口类,当实例化时候会获得具体的MapperProxy代理,里面主要包含了SqlSession。

五、综上总结

  • 分析过程较长篇幅也很大,不一定一天就能看懂整个流程,但当耐下心来一点点研究,还是可以获得很多的收获的。以后在遇到这类的异常就可以迎刃而解了,同时也有助于面试、招聘!
  • 之所以分析Mybatis最开始是想在Dao上加自定义注解,发现切面拦截不到。想到这是被动态代理的类,之后层层往往下扒直到MapperProxy.invoke!当然,Mybatis提供了自定义插件开发。
  • 以上的源码分析只是对部分核心内容进行分析,如果希望了解全部可以参考资料;MyBatis 3源码深度解析,并调试代码。IDEA中还是很方便看源码的,包括可以查看类图、调用顺序等。
  • mybatis、mybatis-spring中其实最重要的是将Mapper配置文件解析与接口类组装成代理类进行映射,以此来方便对数据库的CRUD操作。从源码分析后,可以获得更多的编程经验(套路)。
  • Mybatis相关链接;
    • github.com/mybatis/myb…
    • mybatis.org/mybatis-3/z…
    • github.com/fuzhengwei

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

docker部署redis记录,楼主亲测无异常| 8月更文挑

发表于 2021-08-02

项目中使用redis很常见,所以安装redis是每个小伙伴都应该熟练的技能。

这里定义两个概念 ,请记住。

  1. 宿主:你正在操作的linux系统(或是云服务器或者是实体服务)
  2. 容器:也就是在docker中虚拟出来的linux

1.安装redis

1.拉取镜像

这里也可以加上版本号,如果不加,默认最新。

1
复制代码docker pull redis

2.查看本地镜像

查看本地镜像是否成功。

1
复制代码docker images

image.png

3.修改配置

在linux新建任意一文件夹,这里楼主新建了/usr/local/redis 。在官网或者其他redis工程中,获取redis.conf,并以下配置修改好的后放入上文新建的文件夹中。

1
2
3
4
yaml复制代码#bind 0.0.0.0 #带bind的注释掉
protected-mode yes #开启密码 云服务上一定要开启密码,防火墙 不信你就试试!!!!!
daemonize no #一定改为no 否则redis不能启动 而且没有日志!!!!!!!!!!
requirepass 123 #密码

4.新建数据文件夹

新建/usr/local/docker/data文件夹,放置容易映射过来的数据(对应着下文参数,可以修改,如果修改自己对应一下)

docker容器会与宿主机建立连接,然后将数据同步到宿主机中,这样就不用每次都进入docker容器操作文件了。

下图为安装后,宿主映射出来的文件数据。

image.png

5.运行容器

1
bash复制代码docker run -p 6379:6379 --name redis -v /usr/local/redis/redis.conf:/etc/redis/redis.conf  -v /usr/local/docker/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes

命令中的文件夹路径是可以更改的,请按照个人环境自行修改。

6.命令解释

  • -p 6379:6379:第一个6379是宿主机的端口,第二个6379是reids容器的向外提供的端口。这个意思也就是宿主使用6379端口接收redis容器6379发出的请求。
  • –name redis :容器名称
  • -v /usr/local/redis/redis.conf:/etc/redis/redis.conf :将上文指定宿主/usr/local/redis/redis.conf文件映射到容器的/etc/redis/redis.conf
  • -v /usr/local/docker/data:/data :将容器的数据映射过来
  • -d redis redis-server /etc/redis/redis.conf :将容器下/etc/redis/redis.conf文件作为容器redis的配置启动项
  • –appendonly yes :标识开启持久化 这里只是演示可以通过这种方式设置参数

7.查询运行信息

1
复制代码docker ps

image.png

2.常见异常

1
bash复制代码changing ownership of '.': Permission denied或者chmod: changing permissions of'/var/lib/postgresql/data': Permission denied

以上错误时执行 -v /usr/local/redis/redis.conf:/etc/redis/redis.conf 出现的,因为docker容器没有宿主机的权限所以报错!

结论

关闭SELinux

1
2
3
4
5
6
7
ini复制代码getenforce #查看SELinux状态 permissive关闭状态 enforcing开启状态
setenforce 0 #临时关闭
setenforce 1 #临时开启

//永久关闭
修改 /etc/selinux/config
SELINUX=enforcing改为SELINUX=disabled

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

徒手撸一个记账本(附源码)| 8月更文挑战

发表于 2021-08-02

前言

之前已经写过关于 学生成绩管理系统 以及 点菜系统 的文章,大家如果感兴趣,可以点击各自的传送门去看看呀!

接下来开始我们今天的正题,我们日常生活中,想必有很多人都有记账的习惯,那今天,我们就来看看,如何设计并实现一个记账本。

需求分析

打开我们手机里的记账本,可以发现主要提供如下几个功能:

  1. 添加账目
  2. 删除账目
  3. 修改账目
  4. 查询账目
    • 查询所有账目
    • 按时间区间查询
    • 按账目类型查询
  5. 退出记账本

记账本

功能预览及代码实现

主菜单

主菜单中,主要用于打印提示我们进行选择,然后根据我们的输入再进入不同的子功能模块中。

  • 预览

  • 代码实现

代码实现很简单,只需要打印出提示信息即可,之后后续输入以及进入不同子功能模块,我们可以使用 switch 来进行选择。

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
30
31
32
33
34
35
36
37
38
java复制代码package com.cunyu;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

/**
* Created with IntelliJ IDEA.
*
* @author : 村雨
* @version : 1.0
* @project : Java 实战
* @package : com.cunyu
* @className : MainApp
* @createTime : 2021/8/1 7:22
* @email : 747731461@qq.com
* @公众号 : 村雨遥
* @website : https://cunyu1943.github.io
* @description :
*/
public class MainApp {

public static void main(String[] args) {

boolean flag = true;
while (flag) {
System.out.println("----------欢迎使用记账系统--------");
System.out.println("----------【1】添加账务----------");
System.out.println("----------【2】删除账务----------");
System.out.println("----------【3】修改账务----------");
System.out.println("----------【4】查询账务----------");
System.out.println("----------【0】退出-------------");
System.out.println("请输入功能序号【0-4】");
}
System.out.println("退出系统,期待下次见面 ~");
}
}

添加账目

当我们需要添加一笔新的账目时,此时就可以进入 添加账目 子模块,这里的功能主要是根据我们自己输入的 ID、类别、账户、类型、金额、时间以及备注将其添加到我们的总账目列表中进行汇总。

  • 预览

添加账目

  • 代码实现

要实现添加功能也很简单,根据我们的输入,我们利用账目类的构造函数创建一个对象,然后将其加入总账目列表当中就可以了。

1
2
3
4
5
java复制代码public void add(List<Bill> billList, int id, String category, String account, String type, double amount, String time, String desc) {
//创建一个账单对象,然后将其加入列表
Bill bill = new Bill(id, category, account, type, amount, time, desc);
billList.add(bill);
}

删除账目

假如我们的账目要公开给别人看,而自己有的账目又不想让别人看到,咋办呢?很简单!我们只需要将这笔账目干掉即可!这里我们只要输入我们所要删除的账单 ID,然后将其从总账目列表中删除即可。

  • 预览

  • 代码实现

要删除我们的账目,只需要根据我们输入的账单 id,找到对应 id 的账目,然后将其删除即可,这里之所以没有使用 remove(index) 的方式,是因为我们的 id 是自己输入的,这其实是不规则的,如果我们使用 remove(index) 的方式,就有可能导致越界的问题。

1
2
3
4
5
6
7
8
java复制代码public void del(List<Bill> billList, int id) {
//找到对应 id 的账单,并将其删除
for (Bill bill : billList) {
if (bill.getId() == id) {
billList.remove(bill);
}
}
}

修改账目

我们偷偷拿了私房钱买了猛男必备显卡(RTX 3090)想趁空了打打游戏,遇到老婆查账时,这时候可不能慌,我们只需要把账单偷梁换柱,改换成其他东西就可以,而且此事神不知鬼不觉,美滋滋呀!

  • 预览

修改账目

  • 代码实现

利用代码实现也很简单,找到对应 ID 的账单,然后重新输入账单各个细节进行修改即可!

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
30
31
32
33
34
35
36
37
java复制代码public void modify(List<Bill> billList) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入你要修改对应账务的 ID");
int id = scanner.nextInt();

System.out.println("请输入修改后的账务类别");
String category = scanner.next();

System.out.println("请输入修改后的账户");
String account = scanner.next();

System.out.println("请输入修改后的类型(收入/支出)");
String type = scanner.next();

System.out.println("请输入修改后的金额");
double amount = scanner.nextDouble();

System.out.println("请输入修改后的时间");
String time = scanner.next();

System.out.println("请输入修改后的备注");
String description = scanner.next();
//找到对应 id 的账单,然后修改对应信息
for (Bill bill : billList) {

if (bill.getId() == id) {

bill.setCategory(category);
bill.setAmount(amount);
bill.setAccount(account);
bill.setTime(time);
bill.setType(type);
bill.setDesc(description);
}
}

}

查询账目

查询账目这里,我们又分为 3 个不同的功能,既可以查看所有账单,也可以根据我们所输入的时间区间进行筛选,最后,还能够根据我们的输入查询出收入和支出的详细情况。

查询所有账目

  • 预览

所有账目

  • 代码实现

查询所有账单很简单,只需要遍历我们总账单列表即可。

1
2
3
4
5
6
java复制代码public void queryAll(List<Bill> billList) {
System.out.println("ID\t\t类别\t\t\t账户\t\t类型\t\t金额\t\t\t时间\t\t\t\t备注");
for (Bill bill : billList) {
System.out.println(bill.getId() + "\t\t" + bill.getCategory() + "\t\t" + bill.getAccount() + "\t\t" + bill.getType() + "\t\t" + bill.getAmount() + "\t\t" + bill.getTime() + "\t\t" + bill.getDesc());
}
}

按时间区间查询

  • 预览

  • 代码实现

按照时间跨度来查询稍微要麻烦一些,这里要处理两个输入,一个是开始时间,一个是结束时间,我们需要将账单的时间和这两个时间进行比较,筛选出介于这两者之间的账单。所以这里要涉及到 Date 类,然后就是如何从总账单列表中过滤出满足时间跨度的账单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public void queryByTime(List<Bill> billList, String startTime, String endTime) {
// 时间格式化
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
List<Bill> bills = billList.stream().filter(bill -> {
String tmpTime = bill.getTime();
try {
// 将输入的时间字符串转换为格式化的 Date 类型
Date tmpDate = simpleDateFormat.parse(tmpTime);
Date startDate = simpleDateFormat.parse(startTime);
Date endDate = simpleDateFormat.parse(endTime);
// 将介于开始时间和结束时间的账单进行过滤
if (tmpDate.before(endDate) && tmpDate.after(startDate)) {
return true;
}
} catch (ParseException parseException) {
parseException.printStackTrace();
}
return false;
// 将其转换为列表
}).collect(Collectors.toList());

queryAll(bills);
}

按类型查询

  • 预览

  • 代码实现

这里按类型查询其实就是在查询所有账单的基础上加上一个限制条件,只要其类型等于我们的输入即可,这里我写了两种筛选的方法。一种是直接使用 if 语句进行过滤,而另一种则是同按时间跨度一样使用集合的 Stream 流来进行过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void queryByType(List<Bill> billList, String type) {
//1. 第一种方式,利用条件判断
//for (Bill bill : billList) {
// if (bill.getType().equals(type)) {
// System.out.println(bill.getId() + "\t\t" + bill.getCategory() + "\t\t" + bill.getAccount() + "\t\t" + bill.getType() + "\t\t" + bill.getAmount() + "\t\t" + bill.getTime() + "\t\t" + bill.getDesc());
// }
//}

//2. 第二种方式,利用集合 Stream 流
List<Bill> bills = billList.stream().filter(bill -> {
String tmpType = bill.getType();
return tmpType.equals(type);
}).collect(Collectors.toList());
queryAll(bills);
}

退出系统

  • 预览

退出系统

  • 代码实现

在主菜单中,我们已经设置了一个标志位 flag,当 flag 为 true 时,我们每完成一项功能则循环打印主菜单在控制台中,而如果我们想要退出系统,只需要将标志位 flag 设置为 false 即可,此时不满足 while 循环的条件,所以跳出循环,因此退出系统的关键在于标志位状态的转换。

1
2
3
java复制代码case "0":
flag = false;
break;

整体程序

将各个子模块功能实现之后,剩下的就是整合工作了,最后得到我们最后的总体程序结构如下,主要代码均位于 com.cunyu 包下,然后分别是:

  • 实体类 Bill
  • 接口类 IBill
  • 主程序 MainApp

总结

好了,以上就是我们简单的记账本的实现了。实现的功能比较简陋,程序的健壮性也不够强,还有很大的优化空间。大家如果感兴趣或者有优化的地方,欢迎到本设计的 Github 仓库地址:github.com/cunyu1943/j… 提交 issue。当然了,如果你只是需要用来简单的做个示例,也可以自取。

最后,如果本文对大家有用的话,希望给我一个⭐ star,满足一下我的虚荣心呀 ~ 🙊🙊🙊

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Docker部署GitLab|8月更文挑战

发表于 2021-08-01

参考:

使用Docker安装GitLab

Docker搭建Gitlab私服(完整教程)

  1. docker拉取gitlab社区版

1
sh复制代码docker pull gitlab/gitlab-ce:latest

查看是否拉取成功

1
2
3
sh复制代码[root@test gitlab] docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
gitlab/gitlab-ce latest 75d591b81fd7 4 days ago 2.23GB
  1. 使用容器卷将数据映射到本地并运行

宿主机位置 容器位置 作用
/usr/local/gitlab/config /etc/gitlab 用于存储 GitLab 配置文件
/usr/local/gitlab/logs /var/log/gitlab 用于存储日志
/usr/local/gitlab/data /var/opt/gitlab 用于存储应用数据

在宿主机创建映射目录

1
2
3
4
5
6
sh复制代码[root@test ~] mkdir -p /usr/local/gitlab/config	#递归创建目录,即使上级目录不存在,会按目录层级自动创建目录
[root@test ~] mkdir -p /usr/local/gitlab/logs
[root@test ~] mkdir -p /usr/local/gitlab/data
[root@test ~] cd /usr/local/gitlab
[root@test gitlab] ls
config data logs

3.1 创建方法一

创建gitlab_start.sh文件

1
2
sh复制代码[root@test gitlab] touch gitlab_start.sh
[root@test gitlab] vim gitlab_start.sh

编写内容

1
2
3
4
5
6
7
8
9
10
11
sh复制代码#!/bin/sh
GITLAB_HOME=/usr/local/gitlab
sudo docker run --detach
--hostname 部署的服务器ip
--publish 443:443 --publish 80:80 --publish 10080:22
--name gitlab
--restart always
--volume $GITLAB_HOME/config:/etc/gitlab
--volume $GITLAB_HOME/logs:/var/log/gitlab
--volume $GITLAB_HOME/data:/var/opt/gitlab
gitlab/gitlab-ce:latest

参数说明:

–detach: 设置容器后台运行

–hostname: 设置容器的 hostname,如果是本地localhost ,否则使用外网ip

–publish: 端口转发规则(80:Http 访问端口,443:Https 访问端口,10080:主机的 ssh 访问端口,22:Docker 容器中 ssh 访问端口)

–name:容器名称

–restart always:每次启动容器就重启GitLab

–volume: 共享目录挂载,即 docker 容器内外数据共享

–e:配置 Gitlab 运行的环境变量

在该文件目录下,授予gitlab_start.sh执行权限

1
sh复制代码chmod +x gitlab_start.sh
1
sh复制代码./gitlab_start.sh		#执行脚本

可能遇到的错误

1
2
3
sh复制代码[root@test gitlab] ./gitlab_start.sh
000dc7b1b6e35d94171be203c49ef7a57a1ffb8ea76c72b6765cbed9b5de347b
docker: Error response from daemon: driver failed programming external connectivity on endpoint gitlab (757b5ee12c5202b00ff312c9a927621ebb63e3e5272c827ba36baf19614ee7d7): Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use.

要注意端口是否被占用

1
2
3
sh复制代码[root@test gitlab] netstat -ntulp | grep 80  #查看所有80端口使用情况
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 13389/nginx: master
tcp6 0 0 :::8080 :::* LISTEN 5233/java

修改gitlab_start.sh文件,将映射到宿主机的端口改为8088,或者其它没被占用的端口

1
sh复制代码    --publish 443:443 --publish 8088:80 --publish 10080:22 \

重新运行./gitlab_start.sh文件,报错如下

1
2
3
sh复制代码[root@test gitlab] ./gitlab_start.sh
docker: Error response from daemon: Conflict. The container name "/gitlab" is already in use by container "000dc7b1b6e35d94171be203c49ef7a57a1ffb8ea76c72b6765cbed9b5de347b". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

这是因为之前的容器虽然没有运行起来,但是已经创建了,把它删除掉

1
2
3
sh复制代码[root@test gitlab] docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
000dc7b1b6e3 gitlab/gitlab-ce:latest "/assets/wrapper" 7 minutes ago Created gitlab
1
2
3
4
sh复制代码[root@test gitlab] docker rm 000d
000d
[root@test gitlab] docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

重新运行gitlab_start.sh文件

1
2
3
4
5
sh复制代码[root@test gitlab] ./gitlab_start.sh
70b9da8332b287b9c154988e03eb6b92ba6c360f985d704b1b703367ffe30732
[root@test gitlab] docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
70b9da8332b2 gitlab/gitlab-ce:latest "/assets/wrapper" 4 seconds ago Up 3 seconds (health: starting) 0.0.0.0:443->443/tcp, :::443->443/tcp, 0.0.0.0:10080->22/tcp, :::10080->22/tcp, 0.0.0.0:8088->80/tcp, :::8088->80/tcp gitlab

可以看到容器成功启动

3.2创建方法二

创建docker-compose.yml文件,并在该文件所在的文件夹目录下运行docker-compose up -d

需要事先安装docker-compose

安装docker-compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sh复制代码version: '2'
services:
gitlab:
image: 'gitlab/gitlab-ce:latest'
container_name: "gitlab"
restart: always
hostname: '主机ip'
environment:
TZ: 'Asia/Shanghai'
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://主机ip:8088'
gitlab_rails['gitlab_shell_ssh_port'] = 10080
gitlab_rails['time_zone'] = 'Asia/Shanghai'
ports:
- '8088:8088'
- '10080:22'
- '443:443'
volumes:
- /usr/local/gitlab/config:/etc/gitlab
- /usr/local/gitlab/logs:/var/log/gitlab
- /usr/local/gitlab/data:/var/opt/gitlab

注:端口和映射目录可根据需要修改

若开放访问端口为80,external_url可不加端口号,默认80

  1. 修改gitlab配置文件(方法一需要)

若以3.1方法创建,需要修改相关配置文件

1
sh复制代码vim /usr/local/gitlab/config/gitlab.rb
1
2
3
4
5
6
7
8
sh复制代码# 改SSH端口为10080,以便不和宿主机22端口冲突
gitlab_rails['gitlab_shell_ssh_port'] = 10080

# 配置外部访问地址
# 旧版本
# external_url 'ip地址'
# 新版本
external_url 'http://ip地址'

4.1 应用配置,重启服务

在Gitlab容器运行状态时,重启服务,并远程访问网站测试

1
2
3
4
5
6
7
8
sh复制代码# 进入gitlab bash
docker exec -it gitlab bash
# 重新应用gitlab的配置
gitlab-ctl reconfigure
# 重启gitlab服务
gitlab-ctl restart
# 查看gitlab运行状态
gitlab-ctl status

*若访问Gitlab出现502等错误,使用命令检查错误原因

1
sh复制代码gitlab-rake gitlab:check

e.g.

1
2
3
4
5
sh复制代码Try fixing it:
Make sure GitLab is running;
Check the gitlab-shell configuration file:
sudo -u git -H editor /opt/gitlab/embedded/service/gitlab-shell/config.yml
Please fix the error above and rerun the checks.

查看上述配置文件,查找错误原因

image-20210711224805313.png

访问GitLab失败错误

虽然容器启动成功,但是却没办法根据ip:port访问gitlab

启动一个tomcat容器测试端口

拉取tomcat镜像

1
sh复制代码docker pull tomcat:jdk8-openjdk

为了测试8088端口是否可用,先把gitlab的容器暂停

1
sh复制代码docker stop gitlab

启动tomcat容器,并将tomcat容器的8080端口映射到宿主机的8088端口

1
2
3
4
5
6
7
8
9
10
sh复制代码[root@test ~] docker run -d --name tomcat -p 8088:8080 tomcat:jdk8-openjdk
aa29b816196ae32f12915a74e447f01bedae64cd200aaa5cab0dedcc383710f7
[root@test ~] docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
NAMES
aa29b816196a tomcat:jdk8-openjdk "catalina.sh run" 3 seconds ago Up 3 seconds 0.0.0.0:8088->8080/tcp, :::8088->8080/tcp tomcat
[root@test ~] lsof -i:8088
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
docker-pr 2257 root 4u IPv4 2690446 0t0 TCP *:radan-http (LISTEN)
docker-pr 2262 root 4u IPv6 2689770 0t0 TCP *:radan-http (LISTEN)

一切正常,访问ip:8088,还是和以前一样访问不了

开启防火墙并打开端口

因为之前系统的防火墙一直是关闭的并且其它的服务能正常访问,所以没有怀疑是防火墙的问题

1
2
3
4
sh复制代码[root@test ~] firewall-cmd --state
not running
[root@test ~] firewall-cmd --permanent --add-port=8088/tcp
FirewallD is not running

把防火墙打开

1
sh复制代码[root@test ~] systemctl start firewalld.service

访问之前能正常访问的服务,果然没法正常访问。

把该服务的端口开发,正常访问。

打开8088端口

1
2
3
4
sh复制代码[root@test ~] firewall-cmd --permanent --add-port=8088/tcp
success
[root@test ~] firewall-cmd --reload
success

再次访问

返回tomcat的404页面:HTTP Status 404 – Not Found

虽然是404页面,但是说明此时的端口是能正常访问的

再次启动gitlab服务

把tomcat的容器停止运行,重新运行gitlab容器

1
2
3
4
5
6
7
8
sh复制代码[root@test ~] docker stop tomcat
tomcat
[root@test ~] docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@test ~] docker start gitlab
Error response from daemon: driver failed programming external connectivity on endpoint gitlab (1d8b474e20fe113724f1c429c75b6ac3ece200e9c0beacb980907c15470c7d3e): (iptables failed: iptables --wait -t nat -A DOCKER -p tcp -d 0/0 --dport 10080 -j DNAT --to-destination 172.18.0.2:22 ! -i br-12aa369ee4a6: iptables: No chain/target/match by that name.
(exit status 1))
Error: failed to start containers: gitlab

虽然没启动成功,但离成功更进一步了:)

尝试把这个容器删除掉,再创建一次后还是得到这个错误。

1
2
3
4
5
6
sh复制代码[root@test gitlab] systemctl restart docker
[root@test gitlab] docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@test gitlab] docker-compose up -d
Starting gitlab ... done
[root@test gitlab]

还是访问不了,检查一下GitLab内部是否有问题

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
sh复制代码[root@test gitlab] docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
NAMES
7528489258e0 gitlab/gitlab-ce:latest "/assets/wrapper" 18 minutes ago Up 2 minutes (healthy) 0.0.0.0:443->443/tcp, :::443->443/tcp, 80/tcp, 0.0.0.0:8088->8088/tcp, :::8088->8088/tcp, 0.0.0.0:10080->22/tcp, :::10080->22/tcp gitlab
[root@test gitlab] docker exec -it gitlab bash
root@8:/ gitlab-ctl status
run: alertmanager: (pid 769) 250s; run: log: (pid 621) 281s
run: gitaly: (pid 281) 339s; run: log: (pid 316) 337s
run: gitlab-exporter: (pid 746) 251s; run: log: (pid 564) 302s
run: gitlab-workhorse: (pid 737) 251s; run: log: (pid 525) 314s
run: grafana: (pid 784) 249s; run: log: (pid 677) 270s
run: logrotate: (pid 253) 351s; run: log: (pid 261) 350s
run: nginx: (pid 539) 309s; run: log: (pid 548) 308s
run: postgres-exporter: (pid 777) 249s; run: log: (pid 636) 278s
run: postgresql: (pid 396) 333s; run: log: (pid 484) 330s
run: prometheus: (pid 759) 250s; run: log: (pid 607) 288s
run: puma: (pid 487) 327s; run: log: (pid 495) 324s
run: redis: (pid 265) 345s; run: log: (pid 273) 344s
run: redis-exporter: (pid 748) 251s; run: log: (pid 587) 294s
run: sidekiq: (pid 500) 321s; run: log: (pid 511) 318s
run: sshd: (pid 31) 361s; run: log: (pid 30) 361s
root@8:/ gitlab-rake gitlab:check
Checking GitLab subtasks ...

Checking GitLab Shell ...

GitLab Shell: ... GitLab Shell version >= 13.19.0 ? ... OK (13.19.0)
Running /opt/gitlab/embedded/service/gitlab-shell/bin/check
Internal API available: OK
Redis available via internal API: OK
gitlab-shell self-check successful

Checking GitLab Shell ... Finished

Checking Gitaly ...

Gitaly: ... default ... OK

Checking Gitaly ... Finished

Checking Sidekiq ...

Sidekiq: ... Running? ... yes
Number of Sidekiq processes (cluster/worker) ... 1/1

Checking Sidekiq ... Finished

Checking Incoming Email ...

Incoming Email: ... Reply by email is disabled in config/gitlab.yml

Checking Incoming Email ... Finished

Checking LDAP ...

LDAP: ... LDAP is disabled in config/gitlab.yml

Checking LDAP ... Finished

Checking GitLab App ...

Git configured correctly? ... yes
Database config exists? ... yes
All migrations up? ... yes
Database contains orphaned GroupMembers? ... no
GitLab config exists? ... yes
GitLab config up to date? ... yes
Log directory writable? ... yes
Tmp directory writable? ... yes
Uploads directory exists? ... yes
Uploads directory has correct permissions? ... yes
Uploads directory tmp has correct permissions? ... skipped (no tmp uploads folder yet)
Init script exists? ... skipped (omnibus-gitlab has no init script)
Init script up-to-date? ... skipped (omnibus-gitlab has no init script)
Projects have namespace: ...
GitLab Instance / Monitoring ... yes
Redis version >= 5.0.0? ... yes
Ruby version >= 2.7.2 ? ... yes (2.7.2)
Git version >= 2.31.0 ? ... yes (2.32.0)
Git user has default SSH configuration? ... yes
Active users: ... 1
Is authorized keys file accessible? ... yes
GitLab configured to store new projects in hashed storage? ... yes
All projects are in hashed storage? ... yes

Checking GitLab App ... Finished


Checking GitLab subtasks ... Finished

一切正常

我再次换成tomcat容器进行测试,发现8088端口访问不了了。

但是在宿主机内ping本机是可以的

1
2
sh复制代码[root@test tomcat] curl localhost:8088
<!doctype html><html lang="en"><head><title>HTTP Status 404 – Not Found</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 404 – Not Found</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Description</b> The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.</p><hr class="line" /><h3>Apache Tomcat/9.0.50</h3></body></html>

但是,换成ip:8088则不行

问题转移:为什么访问不了容器内的服务

参考

解决Docker端口映射无法访问问题

阿里云 ECS 的Docker为什么无法端口映射?

根据以上两篇文章,提出阿里云的内网eth0 网段正好跟Docker 的虚拟网卡都是 172 网段,有冲突

观察发现我使用的云服务器也是阿里云,并且通过ifconfig查看

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
sh复制代码[root@test config] ifconfig
br-12aa369ee4a6: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
inet6 fe80::42:61ff:fedf:d41f prefixlen 64 scopeid 0x20<link>
ether 02:42:61:df:d4:1f txqueuelen 0 (Ethernet)
RX packets 6 bytes 1174 (1.1 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 22 bytes 1682 (1.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:5dff:fe6c:be47 prefixlen 64 scopeid 0x20<link>
ether 02:42:5d:6c:be:47 txqueuelen 0 (Ethernet)
RX packets 283 bytes 18925 (18.4 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 254 bytes 19725 (19.2 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.xx.x.xxx netmask 255.255.240.0 broadcast 172.16.15.255
inet6 fe80::216:3eff:fe01:2f48 prefixlen 64 scopeid 0x20<link>
ether 00:16:3e:01:2f:48 txqueuelen 1000 (Ethernet)
RX packets 504273 bytes 329083359 (313.8 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 273686 bytes 220530043 (210.3 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

并且查看阿里云的内网ip确实也是172开头。

编辑配置文件/etc/docker/daemon.json ,若daemon.json文件不存在新建即可。

1
2
3
json复制代码{ 
"bip": "192.168.1.5/24"
}

重新启动docker服务

1
sh复制代码systemctl restart docker
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
sh复制代码[root@test docker]# ifconfig
br-12aa369ee4a6: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.18.0.1 netmask 255.255.0.0 broadcast 172.18.255.255
inet6 fe80::42:61ff:fedf:d41f prefixlen 64 scopeid 0x20<link>
ether 02:42:61:df:d4:1f txqueuelen 0 (Ethernet)
RX packets 508550 bytes 329587116 (314.3 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 276951 bytes 223924395 (213.5 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.5 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::42:5dff:fe6c:be47 prefixlen 64 scopeid 0x20<link>
ether 02:42:5d:6c:be:47 txqueuelen 0 (Ethernet)
RX packets 283 bytes 18925 (18.4 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 258 bytes 20085 (19.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.xx.x.xxx netmask 255.255.240.0 broadcast 172.16.15.255
inet6 fe80::216:3eff:fe01:2f48 prefixlen 64 scopeid 0x20<link>
ether 00:16:3e:01:2f:48 txqueuelen 1000 (Ethernet)
RX packets 508550 bytes 329587116 (314.3 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 276958 bytes 223925673 (213.5 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

可以看到docker0 的地址变了。

但是要注意,使用docker启动的服务会默认使用docker0,如果是用docker-compose启动的服务则不会。上面的br-12aa369ee4a6对应的是docker-compose启动的服务,可以看到还是172的网段。

docker-compose up使用自定义的网段的两种方式(从其根源指定)

根据该文使用方法2,即修改daemon.json。但并不能成功访问,我的环境是:docker:20.10.7,docker-compose:1.24.1

1
2
3
4
5
6
7
8
9
10
sh复制代码{
"bip": "192.168.1.5/24",
"debug": true,
"default-address-pools" : [
{
"base" : "192.168.1.5/16",
"size" : 24
}
]
}

后面发现访问不成功的原因在于docker与防火墙之间的关系,可以关掉防火墙,开启docker,不能访问则打开防火墙。在它们之间来回试探 = =。

后面尝试过关闭firewalld,打开iptables。

在启用iptables时,一般关掉iptables可以正常访问,打开则不能正常访问。

在启用firewalld时,按照docker和firewalld之间启动关闭的顺序不同,有时是开着防火墙能访问,有时是关了防火墙能访问。

但是容器内不能访问外部网络,该问题现在还未解决。
有一种迂回的解决方法就是容器使用host网络模式

问题:怀疑是docker,firewalld与iptables之间的设置问题

Docker与IPtables

docker 端口映射 及外部无法访问问题

Docker and IPtables

还有一种访问不了GitLab的情况是端口映射和配置文件有误。

快速的解决方法:将宿主机端口号和容器号设置相同,可参考下面的博客。

利用GitLab Docker images安装GitLab(填坑)

image-20210728162107752.png

总结

如果系统环境正常,安装会十分顺利,一般不会有奇怪的问题。

我在不同的服务器安装过

阿里云 CentOS 7.6: 访问正常,但是因内存不够出现502错误,容器内可正常连接外部网络

阿里云 Alibaba Cloud Linux 2: 可能不能正常访问容器,容器内不能正常连接外部网络。怀疑是防火墙与docker的问题。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Netty 大纲及笔记

发表于 2021-08-01

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

文档目的 :

  • 大纲型文档 , 持续更新
  • 梳理 Netty 的主要概念
  • 梳理 Netty 的核心类
  • 对 Netty 主要结构有一个基础的认知

后续会进行深入的源码分析 , 作为一个快查手册 ,将会逐步完善该文档

1.1 概念

Netty 是一个基于 JAVA NIO 类库的异步通信框架,用于创建异步非阻塞、基于事件驱动、高性能、高可靠性和高可定制性的网络客户端和服务器端

下面依次对这些概念做一个解析 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// JAVA NIO 类库
通过 Selector 实现的基于缓冲区 (Buffer) 的非阻塞式 IO
详情可以参考这一篇文章 : https://juejin.cn/post/6988884927525683230

// 异步通信框架
异步是由于 NIO 模型 , 通信说明了 Netty 的实质, 这是一个用于多端通信的框架

// 基于事件驱动
Netty 中是针对网络事件来处理的 , 这种模式类似于策略模式

// 高性能
Netty 的性能极其优异 , 很多框架的底层都是通过 Netty 来实现 , 高性能的一大原因是得益于 NIO 的无阻塞 IO , 使其支持高并发的场景

// 高可定制
Netty 提供了很多现成的使用类 ,但是同时他可以通过实现接口来扩展功能 , 来适应多种业务场景
Netty 除了支持 HTTP ,WebSocket 等常见协议的开发 ,同时支持私有协议的开发

// 网络客户端端服务器
Netty 需要构建 Client 端和 Server 端来构建网络通信功能

1.2 Netty 模型

Netty 的线程模型是基于 Reactor 模式的一个实现 , 那么就首先要了解什么是 Reactor .

I/O 多路复用模式通常有2种实现 :

  • Reactor : 采用同步IO
  • Proactor : 采用异步IO

在Reactor中,当读写操作准备就绪后 ,将会就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作

Reactor 中有三种线程模型 :

  • 单线程模型 : 所有的IO操作都在同一个NIO线程上面完成
  • 多线程模型 : 有一组NIO线程处理IO操作
  • 主从多线程模型 : 服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池

PS : 注意 , Netty 采用的是主从多线程模型 , 这个后面再专门深入

主从多线程的角色 :

  • Main Reactor : 主要负责连接事件,并将IO读写请求转发到 SubReactor 线程池
  • Acceptor : 不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现 , 类似于转发器
  • Sub Reactor : Main Reactor将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写

主从多线程主要流程 :

  1. MainReactor 监听 Server Socket
  2. 客户端连接 (OP_ACCEPT 事件)
  3. MainReactor 将通道的读写转发给 Sub Reactor
  4. 创建 NioSocketChannel对象 , 从SubReactor内挑选一个线程 , 发起 OP_READ事件
  5. 服务端通过 NioSocketChannel 从网卡中读取数据

后面的具体流程这一篇就不深入了

1.3 Netty 的功能特点

  • 异步、非阻塞、基于事件驱动的NIO框架
  • 支持多种传输层通信协议,包括TCP、UDP等
  • 开发异步HTTP服务端和客户端应用程序
  • 提供对多种应用层协议的支持,包括TCP私有协议、HTTP协议、WebSocket协议、文件传输等
  • 默认提供多种编解码能力,包括Java序列化、Google的ProtoBuf、二进制编解码、Jboss marshalling、文本字符串、base64、简单XML等,这些编解码框架可以被用户直接使用
  • 提供形式多样的编解码基础类库,可以非常方便的实现私有协议栈编解码框架的二次定制和开发
  • 经典的ChannelFuture-listener机制,所有的异步IO操作都可以设置listener进行监听和获取操作结果
  • 基于ChannelPipeline-ChannelHandler的责任链模式,可以方便的自定义业务拦截器用于业务逻辑定制
  • 安全性:支持SSL、HTTPS
  • 可靠性:流量整形、读写超时控制机制、缓冲区最大容量限制、资源的优雅释放等
  • 简洁的API和启动辅助类,简化开发难度,减少代码量

Netty 中的常见事件
Netty 使用事件驱动的应用程序范例,因此数据处理 pipeline 是经过处理程序的事件链 , 入站事件可以是 :

  • Channel activation and deactivation 通道激活和失活
  • Read operation events 阅读操作事件
  • Exception events 异常事件
  • User events 用户事件

1.4 业务分层

第一层 : Reactor 通信调度层

由一系列辅助类组成,包括 Reactor 线程NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内部子类等

第二层 :职责链 ChannelPipeLine

负责调度事件在职责链中的传播,支持动态的编排职责链,职责链可以选择性的拦截自己关心的事件,对于其它IO操作和事件忽略,Handler同时支持inbound和outbound事件

第三层 : 业务逻辑编排层

业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是应用层协议插件,用于协议相关的编解码和链路管理,例如 CMPP 协议插件

1.5 项目结构

system-Module.png

项目结构

  • common : 通用的工具类项目
  • buffer : 对应 Zero-Copy-Capable Rich Byte Buffer , 自行实现的一个 Byte Buffer 字节缓冲区
  • transport : 主要是 Transport Services、Universal Communication API 和 Extensible Event Model , 该项目是网络传输通道的抽象和实现
  • codec : 对应 Protocol Support , 该项目是协议编解码的抽象与部分实现
  • handler : 该项目是提供内置的连接通道处理器( ChannelHandler )实现类
  • example : 该项目是提供各种 Netty 使用示例

1.6 功能特点

零拷贝

零拷贝 : CPU不需要为数据在内存之间的拷贝消耗资源 ,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式

1
2
3
4
java复制代码Netty :
> Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝
> Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer
> Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题

内存池

Netty提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制
采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右

Netty提供四种ByteBuf :

  • 基于内存池可重复利用的非堆内存:PooledDirectByteBuf
  • 基于内存池可重复利用的堆内存:PooledHeapByteBuf
  • 朝生夕灭的非堆内存:UnpooledDirectByteBuf
  • 朝生夕灭的堆内存:UnpooledHeapByteBuf

二 . Netty 基本功能点

2.1 核心组件

三个NIO核心概念 :

  • Channel : 频道
  • Buffer : 缓冲区
  • Selector : 多路选择器

Netty 核心启动组件 :

  • AbstractBootstrap
  • Bootstrap & ServerBootstrap
  • Channel : 它代表一个到实体的开放连接,如读操作和写操作 , 可以把Channel 看作是传入(入站)或者传出(出站)数据的载体
  • ChannelFuture : 通过该接口的 #addListener(…) 方法,注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果
  • EventLoop & EventLoopGroup
  • ChannelHandler : 连接通道处理器 , ChannelHandler 主要用来处理各种事件
    • ChannelInboundHandler 的实现类还包括一系列的 Decoder 类,对输入字节流进行解码。
    • ChannelOutboundHandler 的实现类还包括一系列的 Encoder 类,对输入字节流进行编码。
  • ChannelPipeline : ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API

组件关联 :

功能详述

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// EventLoop & EventLoopGroup

// ChannelPipeline
当一个数据流进入 ChannelPipeline 时,它会从 ChannelPipeline 头部开始,传给第一个 ChannelInboundHandler 。当第一个处理完后再传给下一个,一直传递到管道的尾部。
与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的“最后”一个ChannelOutboundHandler ,当它处理完成后会传递给前一个 ChannelOutboundHandler 。

// Netty 的事件
事件是 Netty 的核心部分 , Netty 使用不同的事件来通知我们状态的改变或者是操作的状态 .

Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的 , 例如 :
> 出站事件 : 连接已被激活或者连接失活 , 数据读取 , 用户事件 , 错误事件
> 入站事件 : 打开或者关闭到远程节点的连接 , 将数据写到或者冲刷到套接字

三 . Netty 主要的成员

以下来看一下 Netty 的主要成员 , 主要围绕三个维度 :

  • Channel : Socket;
  • EventLoop : 控制流、多线程处理、并发;
  • ChannelFuture : 异步通知。

关联关系 @ Netty 实战

system-relative.png

  • 一个EventLoopGroup 包含一个或者多个EventLoop;
  • 一个EventLoop 在它的生命周期内只和一个Thread 绑定;
  • 所有由EventLoop 处理的I/O 事件都将在它专有的Thread 上被处理;
  • 一个Channel 在它的生命周期内只注册于一个EventLoop;
  • 一个EventLoop 可能会被分配给一个或多个Channel。

组件关系

image.png

3.1 EventLoop

作用 : EventLoop 是 Netty 基于 Reactor 模型的思想进行实现 , EventLoop 定义了Netty 的核心抽象,用于处理连接的生命周期中所发生的事件

EventLoop 处理所有注册到本线程多路复用器 Selector上的Channel, Selector的轮询操作由绑定的EventLoop线程run方法驱动, 在一个循环体内循环执行

3.2 ChannelPipeline

作用 : 为 ChannelHandler 的链,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。

流程 : 在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler

1
2
3
4
5
6
7
8
java复制代码// 处理流程 : 
- 一个ChannelInitializer的实现被注册到了ServerBootstrap中
- 当ChannelInitializer.initChannel()方法被调用时,ChannelInitializer将在ChannelPipeline 中安装一组自定义的ChannelHandler
- ChannelInitializer 将它自己从ChannelPipeline 中移除


// 方法
- 其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知

3. ChannelHandler

作用 : Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器

初始化 : 在应用程序的初始化或者引导阶段被注册

流程 : 接收事件、执行它们所实现的处理逻辑 , 并将数据传递给链中的下一个ChannelHandler

排序 : 它们的执行顺序是由它们被添加的顺序所决定的

流程
入站和出站ChannelHandler 可以被安装到同一个ChannelPipeline中

如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,并被传递给第一个ChannelInboundHandler。这个ChannelHandler 不一定会实际地修改数据,具体取决于它的具体功能,在这之后,数据将会被传递给链中的下一个ChannelInboundHandler。

最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。


数据的出站运动(即正在被写的数据)在概念上也是一样的。

在这种情况下,数据将从 ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站
数据将会到达网络传输层,通常情况下,会触发一个写操作。

ChannelHandler 的容器

当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定

在Netty 中,有两种发送消息的方式。可以直接写到Channel 中,也可以写到和ChannelHandler相关联的ChannelHandlerContext 对象中。

  • 前一种方式将会导致消息从Channel-Pipeline 的尾端开始流动
  • 后者将导致消息从ChannelPipeline 中的下一个Channel-Handler 开始流动。

ChannelInboundHandler 和 ChannelOutboundHandler

Netty 定义了下面两个重要的ChannelHandler 子接口:

  • ChannelInboundHandler——处理入站数据以及各种状态变化
  • ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作

相关类

  • C- ChannelInitializer : 特殊的ChannelInboundHandler 实现类,用于 Channel 注册到 EventLoop 后,执行自定义的初始化操作
  • C- SimpleChannelInboundHandler : 处理指定类型的消息 , 实现 SimpleChannelInboundHandler 后,实现对指定类型的消息的自定义处理
  • C- LoggerHandler
  • C- IdleStateHandler : 当 Channel 的读或者写空闲时间太长时,将会触发一个 IdleStateEvent 事件。
  • C- ReadTimeoutHandler : 当 Channel 的读空闲时间( 读或者写 )太长时,抛出 ReadTimeoutException 异常,并自动关闭该Channel
  • C- WriteTimeoutHandler : 当一个写操作不能在指定时间内完成时,抛出 WriteTimeoutException 异常,并自动关闭对应 Channel

3. Channel

作用 : Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作

在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供

常见操作 : bind()、connect()、read()和write()

常见的实现类 :

  • EmbeddedChannel
  • LocalServerChannel
  • NioDatagramChannel
  • NioSctpChannel
  • NioSocketChannel

3. ByteBuf

ByteBuf 是一个抽象类 , Java NIO 提供了ByteBuffer 作为它的字节容器 , 而 Netty 的ByteBuffer 替代品是ByteBuf , 既解决了JDK API 的局限性,
又为网络应用程序的开发者提供了更好的API。

ByteBuf 的特点

  • 它可以被用户自定义的缓冲区类型扩展
  • 通过内置的复合缓冲区类型实现了透明的零拷贝
  • 容量可以按需增长(类似于JDK 的StringBuilder)
  • 在读和写这两种模式之间切换不需要调用ByteBuffer 的flip()方法
  • 读和写使用了不同的索引
  • 支持方法的链式调用
  • 支持引用计数;
  • 支持池化

涉及组件

1
2
java复制代码abstract class ByteBuf
interface ByteBufHolder

基础原理

ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf 读取时,它的readerIndex 将会被递增已经被读取的字节数。同样地,当你写入ByteBuf 时,它的 writerIndex 也会被递增。图5-1 展示了一个空ByteBuf 的布局结构和状态。

ByteBuf 使用模式

  • 堆缓冲区 : 将数据存储在JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放
  • 直接缓冲区 : 允许JVM 实现通过本地调用来分配内存 , 直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外
  • 复合缓冲区 : 为多个ByteBuf 提供一个聚合视图
  • 派生缓冲区 : 派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图
  • Unpooled 缓冲区 : 提供了静态的辅助方法来创建未池化的ByteBuf实例

PS : 如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中
PS : 问题在于分配和释放都较为昂贵

ByteBufHolder

1
2
3
4
java复制代码// ByteBuf 分配
- 按需分配:ByteBufAllocator 接口 : Netty 通过interface ByteBufAllocator 实现了(ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的ByteBuf 实例

// ByteBufAllocator 实现 :

ByteBufHolder 的主要方法 :

  • content() : 返回由这个ByteBufHolder 所持有的ByteBuf
  • copy() : 返回这个ByteBufHolder 的一个深拷贝,包括一个其所包含的ByteBuf 的非共享拷贝
  • duplicate() : 返回这个ByteBufHolder 的一个浅拷贝,包括一个其所包含的ByteBuf 的共享拷贝

ByteBufAllocator

作用 : ByteBuf 的分配器,负责创建 ByteBuf 对象 , ByteBufAllocator 实现了池化 , 可以用来分配我们所描述过的任意类型的ByteBuf 实例


子类 :

  • PreferHeapByteBufAllocator ,倾向创建 Heap ByteBuf 的分配器。
  • PooledByteBufAllocator ,基于内存池的 ByteBuf 的分配器。
  • UnpooledByteBufAllocator ,普通的 ByteBuf 的分配器。

主要方法 :

  • buffer() : 返回一个基于堆或者直接内存存储的 ByteBuf
  • heapBuffer() : 返回一个基于堆内存存储的 ByteBuf
  • directBuffer() : 返回一个基于直接内存存储的 ByteBuf
  • compositeBuffer() : 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf

ByteBufUtil 类
作用 : 提供了用于操作ByteBuf 的静态的辅助方法

3. Jemalloc

作用 : Jemalloc 是内存管理算法

3. Decoder

正向处理 : 将 Byte 转换为 Message

1
2
3
4
5
6
java复制代码Decoder 中最核心的对象是 ByteToMessageDecoder , 它可以将字节解码为消息(或者另一个字节序列) , 该对象提供了2个方法 : 
- decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)
- decodeLast( ChannelHandlerContext ctx, ByteBuf in,List<Object> out)

// 其他核心对象 :
ReplayingDecoder : 扩展了ByteToMessageDecoder类 , 通过使用一个自定义的ByteBuf实现

逆行处理 : 将 Message 转换为 Byte

1
2
3
4
5
jaa复制代码该操作基于 MessageToByteEncoder ,     
- encode( ChannelHandlerContext ctx, I msg, ByteBuf out)

MessageToMessageEncoder ,将消息编码成另一种消息。
- encode( ChannelHandlerContext ctx, I msg, List<Object> out)

互相转换 : 在两个消息格式之间进行转换

1
java复制代码MessageToMessageDecoder ,将消息解码成另一种消息。

未知转换, 我们需要将字节解码为某种形式的消息

1
2
3
4
5
6
7
8
9
10
11
java复制代码• ByteToMessageCodec : ByteToMessageDecoder + MessageByteEncoder 的组合
?- 任何的请求/响应协议都可以作为使用ByteToMessageCodec的理想选择
- decode(ChannelHandlerContext ctx,ByteBuf in,List<Object>)
- decodeLast(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)
- encode(ChannelHandlerContext ctx,I msg,ByteBuf out)

其对应的 message 类为 MessageToMessageCodec (MessageToMessageDecoder + MessageToMessageEncoder 的组合)


• ByteToMessageDecoder ,将字节解码成消息。
• MessageByteEncoder ,将消息编码成字节。
1
2
3
4
java复制代码所有由Netty 提供的编码器/解码器适配器类都实现了ChannelOutboundHandler 或者ChannelInboundHandler 接口

// TooLongFrameException : 其将由解码器在帧超出指定的大小限制时抛出 , 可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException
实现方式 : 实现 ByteToMessageDecoder , 在其中设置一个最大值 , 遍历的时候会比较这个最大值 , 同时根据情况抛出 TooLongFrameException

3. Util

1
2
3
4
5
JAVA复制代码C- Future
C- FastThreadLocal
C- Recycler
C- HashedWheelTimer
C- MpscUnboundedArrayQueue

四 . Netty 核心引导类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码// Bootstrap 类
引导类的层次结构包括一个抽象的父类和两个具体的引导子类
- AbstractBootstrap
|- Bootstrap
|- ServerBootstrap

> Bootstrap 类的API
- Bootstrap group(EventLoopGroup) : 设置用于处理Channel 所有事件的EventLoopGroup
- Bootstrap channel(Class<? extends C>) : channel()方法指定了Channel的实现类。
- Bootstrap channelFactory(ChannelFactory<? extends C>) : 指定一个工厂类,被bind()方法调用
- Bootstrap localAddress(SocketAddress) : 指定Channel 应该绑定到的本地地址
- <T> Bootstrap option(ChannelOption<T> option,T value) : 设置ChannelOption,其将被应用到每个新创建的 Channel 的ChannelConfig
- <T> Bootstrap attr(Attribute<T> key, T value) : 指定新创建的Channel 的属性值。
- Bootstrap handler(ChannelHandler) : 设置将被添加到ChannelPipeline 以接收事件通知的ChannelHandler
- Bootstrap clone() : 创建一个当前Bootstrap 的克隆,其具有和原始的 Bootstrap 相同的设置信息

> Bootstrap 类负责为客户端和使用无连接协议的应用程序创建Channel

4.1 Netty 服务端的创建流程

  1. 创建 ServerBootStrap 实例 , ServerBootStrap 是 服务端的启动辅助类
  2. 设置并绑定 Reactor 线程池 , Netty 的 Reactor 线程池 为 EventLoopGroup (EventLoop 数组)
  3. 设置并绑定服务端 Channel, Netty对原生的NIO类库进行了封装,对应实现是NioServerSocketChannel
  4. TCP 链路建立时创建 ChannelPipeline , 它本质就是一个负责处理网络事件的职责链, 负责管理和执行 ChannelHandler
  5. 添加并设置 ChannelHandler , 利用ChannelHandler用户可以完成大多数的 功能定制 , 例如消息编解码、 心跳、 安全认证、 TSL/SSL认证、 流量控制和流撮整形等
  6. 绑定监听端口并且启动服务端 , 在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后 ,会启动监听端口, 并将ServerSocketChannel注册到Selector上监听客户端连接
  7. Selector 轮询 , 由Reactor线程NioEventLoop负责调度和执行Selector轮询操作, 选择准备就绪的Channel其合
  8. 网络事件通知 , 由Reactor线程NioEventLoop执行 ChannelPipel ine的相应方法 , 最终调度并执行ChannelHandler
  9. 执行 Netty 系统和业务 ChannelHandler

4.2 Netty 客户端创建流程

  1. 用户线程创建 Bootstrap 实例, 通过 API 设置创建客户端相关的参数, 异步发起客户端连接
  2. 创建 NioEventLoopGroup 线程组, 用于 创建处理客户端连接 、I/O 读写的 Reactor
  3. 过 Bootstrap 的 ChannelFactory 和用户指定的 Channel 类型创建用于客户端连接的 NioSocketChannel
  4. 创建默认的ChannelHandlerPipeline,用于调度和执行网络事件
  5. 异步发起 TCP 连接, 判断连接是否成功。如果成功, 则直接将 NioSocketCbannel 注册到多路复用器上,监听读操作位, 用于数据报读取和消息发送 (如果没有立即连接成功, 则注册连接监听位到多路复用器, 等待连接结果)
  6. 注册对应的网络监听状态位到多路复用器
  7. 由多路复用器在I/O现场中轮询各Channel, 处理连接结果
  8. 如果连接成功 , 设置Future结果 , 发送连接成功事件,触发ChannelPipeline执行
  9. 由ChanneLPipeline调度执行系统和用户的ChannelHandler, 执行业务逻辑

@ Netty 权威指南

总结

这只是一个基础大纲 , 很多细节等后面代码深入分析后再逐步完善

参考文档

@ Introduction to Netty | Baeldung

@ HTTP/2 in Netty | Baeldung

@ HTTP Server with Netty | Baeldung

@ A Quick Guide to Java on Netty | Okta Developer

@ 主从多Reactor! - 知乎 (zhihu.com)

@ A Quick Guide to Java on Netty | Okta Developer

@ Netty | 并发编程网 – ifeve.com

@ <Netty 实战>

@ <Netty 权威指南>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Spring声明式事务】 最全的Transactiona

发表于 2021-08-01

一、序言

哈喽小伙伴们,俺是啤酒熊,今天想来和大家聊聊Spring中涉及数据库事务的一些坑。

Spring为开发人员提供了声明式事务的使用方式,即在方法上标记@Transactional注解来开启事务。

我们都清楚,在业务代码对数据进行操作的时候一定是希望有事务控制的。

比如在写商家卖东西的业务代码时,代码的逻辑是商家先生成一个订单(订单信息插入到数据库中),再将钱收入到自己的账户中(数据库中的money增加)。如果后面这个操作失败了,那么前者也一定不能插入成功,这时候就会用到事务的回滚。

虽然大部分做后端开发同学们都有这方面的概念,但是在使用@Transactional注解时依旧会出现一些错误。

前几天在对公司新来的同学们写的代码做code review的时候,看到了他们在Spring的项目中,关于@Transactional注解的一些错误使用。在给他们纠正错误的同时也不禁想到自己也曾掉进过这些坑之中 ヽ(ー_ー)ノ

img

于是便想对该注解的使用做个避坑指南~,分享到社区中。

本文将介绍平常的业务开发中关于@Transactional的常见的几种错误使用,并给出相应的错误代码示例。针对每种错误类型,解释其原因,并给出使用@Transactional注解的正确使用姿势。接下来就让我们一起来看一下吧!

img

二、一些准备

2.1 数据库

我们在数据库中定义一个goods_stock货物库存表,并赋予一些初始数据:

image-20210801102106697

表明目前商品id为good_0001的商品库存为10件。

2.2 Spring Boot+Mybatis

我们在Java的方法中利用Mybatis进行减库存的操作,并在方法上标注@Transactional的注解,看该注解是否能使事务失效,遇到错误就回滚。

项目结构如下:

image-20210801105126126

我们将利用Swagger调用Controller层中的接口,在接口中调用Service层GoodsStockServiceImp中的具体业务代码,即减库存的操作。

具体的sql语句,执行库存减10操作,即若业务代码执行成功,该商品库存变为0:

1
2
3
4
5
6
7
XML复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.beerbear.springboottransaction.dao.GoodsStockMapper">
<update id="updateStock">
update goods_stock set stock = stock - 10
</update>
</mapper>

三、抛出异常

3.1 异常传播未出@Transactional标记的方法

很多时候,在真实的业务开发中,总希望接口能返回一个固定的类实例——这叫做统一返回结果。在本文中,以Result类作为统一返回结果,具体可看本文附带代码。

于是就有可能为了方便就直接在Service的方法中return一个Result类对象,为了避免受异常的影响而无法返回该结果集,就会使用try-catch语句,当业务代码出现错误而抛出异常时会捕获此异常,将异常信息写入Result的相关字段中,返回给调用者。下面给出该类型的实例:

Controller层:

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
java复制代码@Controller
@RestController
@Api( tags = "测试事务是否生效")
@RequestMapping("/test/transactionalTest")
@Slf4j
public class GoodsStockController {

@Autowired
private GoodsStockService goodsStockService;
/**
* create by: Beer Bear
* description: 第一个方法。
* create time: 2021/7/25 21:38
*/
@GetMapping("/exception/first")
@ApiOperation(value = "关于异常的第一个方法,不能够回滚", notes = "因为异常未能被事务发现,所以没有回滚")
@ResponseBody
public Result firstFunctionAboutException(){
try{
return goodsStockService.firstFunctionAboutException();
}catch (Exception e){
return Result.server_error().Message("操作失败:"+e.getMessage());
}
}
}

Service中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Autowired
private GoodsStockMapper goodsStockMapper;

@Override
@Transactional
public Result firstFunctionAboutException() {
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
return Result.ok();
}catch (Exception e){
log.info("减库存失败!" + e.getMessage());
return Result.server_error().Message("减库存失败!" + e.getMessage());
}
}

在firstFunctionAboutException方法的try代码块中,一定会抛出一个RuntimeException的异常,但这样是否能回滚呢?我们不妨通过实验来看一下:

利用Swagger调用接口:

image-20210801110934089

调用接口后,按理说事务应该回滚,库存数量不会变为0,但结果却是:

image-20210801112221671

为了节缩篇幅,下文中不再将出现这些截图,而是以文字代替

显然事务没有回滚。我们都知道当程序执行时出现错误而抛出异常时,事务才会回滚,这里虽然出现了异常但却被方法本身消化了(catch掉了),异常没有被事务所发现,所以这样子是不会出现回滚的。

下面我们我们给出相关正确的解决方法——将service中的try-catch语句去掉:

1
2
3
4
5
6
7
java复制代码@Override
@Transactional
public void secondFunctionAboutException() {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new RuntimeException();
}

这样子就能实现事务回滚了。(不过这样的话,异常怎么办呢,总不能直接报个异常吧,很简单,将异常放在Controller层去处理就行了。)

在此总结避坑指南的第一坑:

当标记了@Transactional注解的方法中出现异常时,如果该异常未传播到该方法外,则事务不会回滚;反之,只有异常传播到该方法之外,事务才会回滚。

3.2 明明抛出了异常却不回滚

现在我们都知道了当程序执行时出现错误而抛出异常时,只要别去处理该异常,让异常突破@Transactional所标注的方法,就能实现期望的回滚。

但是事实真的如此么?下面我们再来看个案例:

1
2
3
4
5
6
7
java复制代码@Override
@Transactional
public void thirdFunctionAboutException() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}

实际上,这个方法中的事务并不会回滚。

这也是我们在实际开发时最经常犯的错误,觉得只有抛出异常了就一定会回滚,结果被现实啪啪啪的打脸。

img

但我并不觉得这是件丢人的事情,因为我们去用一个工具的时候,或许一开始确实没精力和能力去学习它的一些原理,从而掉入了一些我们不易发现的坑。只要后面坚持学习,就一定会慢慢把这些坑填满,自己也就越来越强了。

好,言归正传,为啥在此事务不会回滚呢。我们将该方法与上面的secondFunctionAboutException一对比,发现只不过是RuntimeException和Exception的区别。确实是这样,就是因为Spring的@Transactional注解就是默认只有当抛出RuntimeException运行时异常时,才会回滚。

Spring通常采用RuntimeException表示不可恢复的错误条件。也就是说对于其他异常,Spring觉得无所谓所以就不回滚。

下面我给出两种解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Override
@Transactional
public void thirdFunctionAboutException1(){
try{
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}catch (Exception e){
log.info("出现异常"+e.getMessage());
throw new RuntimeException("手动抛出RuntimeException");
}
}

@Override
@Transactional(rollbackFor = Exception.class)
public void thirdFunctionAboutException2() throws Exception {
log.info("减库存开始");
goodsStockMapper.updateStock();
if(1 == 1) throw new Exception();
}

第一种我们手动来抛出RuntimeException异常,第二种是改变默认的@Transactional回滚的异常设置(RuntimeException是继承了Exception异常的)。

@Transactional(rollbackFor = Exception.class)

在此总结避坑指南的第二坑:

默认情况下,如果我们抛出的异常不是RuntimeException时,事务依旧不会回滚;需要手动抛出RuntimeException异常或者更改Spring中@Transactional默认配置。

四、事务还是不生效

就算我们注意到了异常与@Transactional的关系,并正确地避开了这些坑,但是我们还是会掉入一些更不容易发现和理解的坑中。在这一小节中我们将继续举出反例,并说明这些例子中事务未生效的原因以及给出解决方法。在这一小节中你还将学到@Transactional事务与Spring AOP的联系。

4.1 示例一

在service中添加这样两个方法:

1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public void privateFunctionCaller (){
privateCallee();
}

@Transactional
private void privateCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}

在Controller中调用service的privateFunctionCaller方法从而间接调用标注了@Transactional注解的方法privateCallee。

执行代码后,发现事务并没有回滚。这又是为什么呢?

我们在Service的类上标注了@Service注解表示该类作为Bean注入AOP容器,而Spring是通过动态代理来实现AOP的。也就说AOP容器中的Bean实际上都是代理对象。

Spring也正是通过该方式对@Transactional进行支持的,Spring会对原对象中的方法进行封装(即检查到标有该注解的方法时,就会为它加上事务).

这个行为就叫做为目标方法进行增强。虽然Spring实现动态代理的方式是CGLIB,但在此我想以JDK动态代理的实现方式来解释,因为这更便于理解。

image-20210801175619550

由service.function()可以看出,要是走代理的增强的方式,那么必然function不能是private的。所以private的方法上的事务并不能生效,自然就不能回滚了。

实际上当你写出这种上述的代码时,如果你使用的编译器是IDEA,编译器就会提示报错,当然只是报红,而不会影响编译和执行。

image-20210801180654695

Java 中实现动态代理的方式中就有JDK的实现方式和CGLIB。不理解动态代理的同学可以去学习一下代理模式,以及MaBtais在Spring中的实现。

4.2 示例二

那我们是不是只要把private换成public就可以了呢?下面的代码是很多同学刚使用@Transactional时经常掉入一个坑。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
public void publicFunctionCaller (){
publicCallee();
}

@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}

我们在Controller中调用Service中的publicFunctionCaller时,发现事务还是不能回滚,这又是为什么呢?

35301CCAEFC02FBE42F746B3969B233F.png

上文我们提到,在Controller中,被注入的Service对象其实是他的代理对象,当调用publicCallee方法时,上面是没有@Transactional注解的。

故只是简单的执行service.function()而已,即在代理对象的方法publicFunctionCaller中,先由Service的原对象来调用自己的publicFunctionCaller方法,再由其调用自己的publicCallee方法。压根就不会走代理对象增强过(带有事务)的publicCallee方法。自然事务也就不会回滚。

img

解决办法,我想大家就能自己找到了,那就是在Controller中由注入service的bean直接调用标注了@Transactional的方法,例如前文中的secondFunctionAboutException的被调用。

当然,我们还可以曲线救国,在service中注入自己,这样就能实现代理对象来调用增强的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Override
@Transactional
public void publicCallee(){
goodsStockMapper.updateStock();
throw new RuntimeException();
}

@Autowired
private GoodsStockService self;

@Override
public void aopSelfCaller (){
self.publicCallee();
}

不过显然这不符合分层结构,也不优雅。

在此总结避坑指南的第三坑:

标记了@Transactioal注解的方法必须是public的且必须由注入bean来直接调用才能事务回滚。


到此为止,@Transactional的避坑指南就算是结束了,大家有啥疑问,请评论留言,咱们相互交流。

也希望大家多多点赞,关注俺的公众号,以后还会继续输出更多优质文章的!搜索:BeerBear啤酒熊

img

本文所有代码,放在Gitee上,需要的小伙伴自取。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MybatisPlus经典示例:使用Wrapper查询指定字

发表于 2021-08-01

🍬🍬🍬文章目录

  • 01. 只查询指定字段
  • 02. 对查询字段进行函数处理
  • 03. 排除指定(过滤)字段查询
  • 拓:自定义where后的所有内容,直接写sql
  • 拓:拓 单独set某个字段

前言:新公司老大不让写sql,郁闷了好几天。我总觉得xml里写sql效率应该很高呀。没办法,只能听话用MybatisPlus的CURD和Wrapper。

begin:平时Wrapper大家都知道一般是加在where后的条件,但是我想灵活地写select后的字段怎么办呢?

  • 本文基于MybatisPlus==3.0.6==
  • 首先推荐大家学习一定要看文档,点我
  • ==大前提==:CURD的执行接口需要有Wrapper传参,比如list(Wrapper<T> queryWrapper) -- page(IPage<T> page -- Wrapper<T> queryWrapper) -- listObjs(Wrapper<T> queryWrapper)等(我并没有一个一个试,用的时候大家试一下)
  • 这里没有写的很详细,因为时间有限,如果有看不大明白的,文章最后有我的群号欢迎讨论
  • 以下代码我LambdaQueryWrapper和QueryWrapper混用的,这里用Lambda的好处就是降低代码的耦合性,不需要把字段名写死,但是不利于添加函数处理。懂得不要奇怪,不懂得留言或者进群讨论。
  • 文档:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值! 指定字段查询和过滤字段查询这两类方法重复调用以最后一次为准,指定查询和过滤查询不会同时存在

🍬 01. 只查询指定字段

  • 使用select(字段1,字段2….) .select(ClientBanner::isDelState,ClientBanner::getBusinessId);
1
2
3
4
5
6
7
8
9
10
java复制代码    public List<ClientBanner> getListById(String businessId) {
LambdaQueryWrapper<ClientBanner> wrapper = new LambdaQueryWrapper();
// 商家图片,未删除,可用状态
wrapper.eq(ClientBanner::getBusinessId,businessId)
.eq(ClientBanner::isDelState,false)
.eq(ClientBanner::isUsable,true)
.select(ClientBanner::isDelState,ClientBanner::getBusinessId);
List<ClientBanner> list = this.list(wrapper);
return list;
}
  • 控制台打印
1
2
3
java复制代码: ==>  Preparing: SELECT del_state,business_id FROM client_banner WHERE business_id = ? AND del_state = ? AND usable = ? 
: ==> Parameters: 55456(String), false(Boolean), true(Boolean)
: <== Total: 2
  • 不用LambdaQueryWrapper的方式为: 02. 对查询字段进行函数处理

🍬 02. 对查询字段进行函数处理

  • 使用QueryWrapper,传入数据库字段的时候加上函数"left(content,2) content" 这种函数处理,==如果你是用封装的对象接收,别忘了起别名==
1
2
3
4
5
6
7
8
java复制代码    public IPage<ClientEncyArticles> listBySplitPage(SplitPageDTO dto) {
QueryWrapper<ClientEncyArticles> wrapper = new QueryWrapper<ClientEncyArticles>();
wrapper.eq("del_state",false)
.orderByDesc("create_time")
.select("id","author","left(content,2) content","cover_picture","create_time");
IPage<ClientEncyArticles> page = this.page(new Page<>(dto.getPage(), dto.getPageSize()), wrapper);
return page;
}
  • 控制台打印
1
2
3
java复制代码 : ==>  Preparing: SELECT id,author,left(content,2),cover_picture,create_time FROM client_ency_articles WHERE del_state = ? ORDER BY create_time DESC LIMIT ?,? 
: ==> Parameters: false(Boolean), 0(Long), 10(Long)
: <== Total: 10

🍬 03. 排除指定(过滤)字段查询

  • 文档:过滤查询字段(主键除外),入参不包含 class 的调用前需要wrapper内的entity属性有值! 指定字段查询和过滤字段查询这两类方法重复调用以最后一次为准,指定查询和过滤查询不会同时存在

    • 即过滤查询不能过滤主键
    • 使用过滤查询时需要传clsss参数或者用setEntity方法,目的就是把实体传过去。看下面代码
  • 使用select(i->!i.()) .select(i->!i.getProperty().equals("delState")); 或者.select(i->!i.getColumn().equals("del_state"));

    • 别忘了!非号,如果不加! 就是指定字段查询,这种指定字段查询不如 01. 只查询指定字段 & 02. 对查询字段进行函数处理 简便
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      java复制代码    public IPage<ClientEncyArticles> listBySplitPage(SplitPageDTO dto) {
      ClientEncyArticles cc = new ClientEncyArticles();
      QueryWrapper<ClientEncyArticles> wrapper = new QueryWrapper<ClientEncyArticles>();
      wrapper.eq("del_state",false)
      //.setEntity(new ClientEncyArticles()) //如果下边参数不写ClientEncyArticles.class,这里要加setEntity
      .orderByDesc("create_time")
      // .select(ClientEncyArticles.class,i->!i.getColumn().equals("del_state")); // 填的是数据库字段名
      .select(ClientEncyArticles.class,i->!i.getProperty().equals("delState")); // 填的是实体类字段名,个人认为用这个好一些
      IPage<ClientEncyArticles> page = this.page(new Page<>(dto.getPage(), dto.getPageSize()), wrapper);
      return page;
      }
  • 控制台打印

1
2
3
java复制代码==>  Preparing: SELECT id,author,type,content,cover_picture,create_time FROM client_ency_articles WHERE del_state=? AND del_state = ? ORDER BY create_time DESC LIMIT ?,? 
==> Parameters: false(Boolean), false(Boolean), 0(Long), 10(Long)
<== Total: 10

🍬拓:自定义where后的所有内容,直接写sql

  • 文档:last函数 无视优化规则直接拼接到sql的最后,这样select按照本文上边的方法自定义,剩下的直接last函数开搞(也不懂源码,不知道无视他的优化规则会发生啥,自己注意sql的效率应该没啥问题)
    在这里插入图片描述

🍬单独set某个字段

文档链接,官方源码:

例:
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谷粒商城--分布式高级篇P102~P128

发表于 2021-08-01

谷粒商城–分布式高级篇P102~P128

由于学习的时间也比较少,只有周六周末才有时间出来学习总结,所以一篇一篇慢慢更新吧,本次总结内容为Elasticsearch(相关内容:kibana,es,nginx,ik分词器)

csdn csdn csdn csdn csdn

【谷粒商城–分布式基础篇P1~P27】: blog.csdn.net/Empire_ing/…

【谷粒商城–分布式基础篇P28~P101】mp.weixin.qq.com/s/5kvXjLNyV…


@

目录

  • 🔥1.ElasticSearch与kibana
+ [1.1.基本概念](https://i.cnblogs.com/mvc/mdPreview#11%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5)
+ [1.2 安装es与kibana](https://i.cnblogs.com/mvc/mdPreview#12-%E5%AE%89%E8%A3%85es%E4%B8%8Ekibana)
+ [1.3 Nginx安装](https://i.cnblogs.com/mvc/mdPreview#13-nginx%E5%AE%89%E8%A3%85)
+ [1.4 Ik中文分词器](https://i.cnblogs.com/mvc/mdPreview#14-ik%E4%B8%AD%E6%96%87%E5%88%86%E8%AF%8D%E5%99%A8)
  • 🔥2.SpringBoot整合Es
+ [2.1.环境配置](https://i.cnblogs.com/mvc/mdPreview#21%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE)
+ [2.2.搜索实现](https://i.cnblogs.com/mvc/mdPreview#22%E6%90%9C%E7%B4%A2%E5%AE%9E%E7%8E%B0)

🔥1.ElasticSearch与kibana

​ 内容概况:es、kibana、ik相关软件于centos中docker的下载与配置使用。本次安装最大感悟就是docker太好用了,真的好强。

​ 之前也写过一篇关于es文章实例:

​ SpringBoot整合es实现高亮搜索:www.cnblogs.com/meditation5…

1.1.基本概念

​ 索引index:类似于数据库

​ 类型type:类似于库中的表

​ 文档document:类似于表的每一行

​ 方法/属性:类似于每一行的字段

img

1.2 安装es与kibana

​ 1. ES访问地址:http://公网IP:9200/

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
bash复制代码# 1、下载包
docker pull elasticsearch:7.4.2


# 2、配置es文件映射
# 将docker里的目录挂载到linux的/mydata目录中
# 修改/mydata就可以改掉docker里的
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
# es可以被远程任何机器访问
echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml
# 递归更改权限,es需要访问
chmod -R 777 /mydata/elasticsearch/

# 3、启动es
# 9200是用户交互端口 9300是集群心跳端口 -e指定是单阶段运行 -e指定占用的内存大小,生产时可以设置32G
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

# 4、设置开机启动elasticsearch
docker update elasticsearch --restart=always

请添加图片描述

  1. kibana下载、配置、启动

Kibana访问地址:http://公网IP:5601/

1
2
3
4
5
6
7
8
ini复制代码# 1、下载包
docker pull kibana:7.4.2

# 2、启动es(这里填自己的IP地址)
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://公网IP:9200 -p 5601:5601 -d kibana:7.4.2

# 3、设置开机启动kibana
docker update kibana --restart=always

请添加图片描述

​ 访问地址:

查看es的node节点:http://公网IP:9200/_cat/nodes

查看es的helth健康状态:http://公网IP:9200/_cat/health

查看es的master主节点:http://公网IP:9200/_cat/master

查看es的索引主节点:http://公网IP:9200/_cat/indices

1.3 Nginx安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码docker run -p80:80 --name nginx -d nginx:1.10   

mkdir -p /mydata/nginx/html
mkdir -p /mydata/nginx/logs
mkdir -p /mydata/nginx/conf
docker container cp nginx:/etc/nginx/* /mydata/nginx/conf/
#由于拷贝完成后会在config中存在一个nginx文件夹,所以需要将它的内容移动到conf中
mv /mydata/nginx/conf/nginx/* /mydata/nginx/conf/
rm -rf /mydata/nginx/conf/nginx

docker stop nginx
docker rm nginx
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf/:/etc/nginx \
-d nginx:1.10
docker update nginx --restart=always
1.4 Ik中文分词器

​ 在elasticsearch/plugins目录下下载解压ik分词器后重启即可

1
2
3
4
5
6
7
8
bash复制代码#下载ik分词器
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
#解压并重命名ik
unzip elasticsearch-analysis-ik-7.4.2.zip -d ik

chmod -R 777 plugins/ik
rm -rf elasticsearch-analysis-ik-7.4.2.zip #这里要保证plugins目录下纯净,否则启动会报错
docker restart elasticsearch

\

🔥2.SpringBoot整合Es

2.1.环境配置
  • ELK版本对应并去掉springboot中自带的es包
1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>

<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
  • Config配置
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
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码package com.empirefree.gulimall.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @program: gulimall-search
* @description:
* @author: huyuqiao
* @create: 2021/08/01 13:54
*/


@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;

static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}



@Bean
public RestHighLevelClient restHighLevelClient(){
RestClientBuilder restClientBuilder = null;
restClientBuilder = RestClient.builder(new HttpHost("公网IP", 9200, "http"));

RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
return client;
}
}
2.2.搜索实现

​ 用过两款,一种是狂神的整合es高亮,一种是现在的,做的一些平均值,有点难理解,但是还是先贴出来

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
ini复制代码       @Test
public void testEs() throws IOException {
SearchRequest searchRequest = new SearchRequest();

// 指定索引
searchRequest.indices("newbank");

// 1.指定检索条件 DSL
SearchSourceBuilder builder = new SearchSourceBuilder();
// 1.1 构造检索条件
builder.query(QueryBuilders.matchQuery("address","mill"));

// 1.2 按照年龄值聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
builder.aggregation(ageAgg);
// 1.3 计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
builder.aggregation(balanceAvg);

System.out.println("检索条件:" + builder.toString());
searchRequest.source(builder);

// 2.执行检索
SearchResponse search = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

// 3.分析结果
// Map map = JSON.parseObject(search.toString(), Map.class);
// System.out.println(map);
// 3.1 索取所有记录
SearchHits hits = search.getHits();
// 详细记录
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// String index = hit.getIndex();
// String id = hit.getId();
String source = hit.getSourceAsString();
Account account = JSON.parseObject(source, Account.class);
System.out.println(account);
}
// 获取分析数据
Aggregations aggregations = search.getAggregations();
// List<Aggregation> list = aggregations.asList();
// for (Aggregation aggregation : list) {
// Terms agg = aggregations.get(aggregation.getName());
// System.out.println(agg.getBuckets());
// }
Terms agg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : agg.getBuckets()) {
System.out.println("年龄: " + bucket.getKeyAsString() + "-->" + bucket.getDocCount() + "人");
}

Avg avg = aggregations.get("balanceAvg");
System.out.println("平均薪资: " + avg.getValue());
}

\

书山有路勤为径,学海无涯苦作舟。程序员不仅要懂代码,更要懂生活,关注我,一起进步。\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…584585586…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%