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

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


  • 首页

  • 归档

  • 搜索

linux小技巧:SSH免密登录和配置环境变量 linux小

发表于 2021-11-28

linux小技巧:SSH免密登录和配置环境变量

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

  1. 免密登录

免密登录原理

image.png

注意:秘钥对是在用户的~/.ssh目录下,意味着该机器的该秘钥用户才能免密登录,如果同样机器,切换了一个没有配置免密登录的用户,还是要重新按照下面配置的。

生成公钥和私钥对

ssh-keygen -t rsa

然后敲(三个回车),就会生成两个文件 id_rsa(私钥)、id_rsa.pub(公钥)

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
sh复制代码[root@k8s1 .ssh]# cd ~/.ssh/
[root@k8s1 .ssh]# pwd
/root/.ssh
[root@k8s1 .ssh]# ll
total 4
-rw-r--r--. 1 root root 869 Nov 26 02:14 known_hosts
[root@k8s1 .ssh]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:Wj90GVrbS+O/WOdEDUJj8uJD1mcsO+RmaddyZxBoRwI root@k8s1
The key's randomart image is:
+---[RSA 2048]----+
| E.=oo |
| *o+.. |
| +.O.* |
| + * % +.|
| S = % * B|
| o o * = B.|
| . o o.o|
| . o+.|
| . .+|
+----[SHA256]-----+
[root@k8s1 .ssh]# ll
total 12
-rw-------. 1 root root 1679 Nov 26 03:17 id_rsa
-rw-r--r--. 1 root root 391 Nov 26 03:17 id_rsa.pub
-rw-r--r--. 1 root root 869 Nov 26 02:14 known_hosts

把公钥拷贝到要免密登录的目标机器上

每台机器都运行这三条命令

1
2
3
sh复制代码ssh-copy-id k8s1 
ssh-copy-id k8s2
ssh-copy-id k8s3

image.png

ssh-copy-id命令就是复制公钥放到要免密登录服务器的~/.ssh/authorized_keys文件上。

手动复制也行的。

image.png

测试

ssh 机器主机名

image.png

注意事项

  1. 免密配置对应的是账号,不同账号要重新配置。
  2. 相关操作需要机器网络互通的情况下。
  1. 配置环境变量

创建环境变量文件

应该为每个服务创建单独环境变量文件,便于管理

vim /etc/profile.d/xxx.sh

1
2
bash复制代码export JAVA_HOME=/app/jdk/jdk1.8.0_301
export PATH=$PATH:$JAVA_HOME/bin

刷新profile文件信息

source /etc/profile

查看全局环境变量路径有哪些

在这些路径下的脚本也可以在全局运行

echo $PATH

本文转载自: 掘金

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

【k8s 系列】k8s 学习十二,volume 、names

发表于 2021-11-28

「这是我参与11月更文挑战的第 27 天,活动详情查看:2021最后一次更文挑战」

顺带说一下 volume 和 namespace ,咱们就开始分享一下 service 是什么

volume 是什么

还记得 docker 的 volume 吗,是一个数据卷

在 K8S 中,volume 是 pod 中能够被多个容器访问的共享目录 ,实际上和 docker 是一样的

volume 是被定义在 pod 上面的,因此,volume 的生命周期和 pod 是相同的

volume 会被该 pod 中的多个容器挂载到具体的文件目录下面,若某个容器挂掉了,是不会影响 volume 的,也就是说 volume 中的数据是不会丢失的

我们可以使用 volume:

  • 在 pod 中指定 volume 的类型 和内容,可以使用spec.volumes字段
  • 需要将 volume 映射到容器中,我们可以使用 spec.containers.volumeMounts 字段

上面有说到 volume 的类型,这就可多了 , K8S 中支持的 volume 类型有:

  • awsElasticBlockStore
  • azureFile
  • cephfs
  • emptyDir
  • hostPath
  • configMap
  • fc (光纤通道)
  • 。。 。 等等

可以查看这上面的文档,每一种 卷类型都有解释到 点我开始卷一下

简单看一下 emptyDir 卷类型

emptyDir 见名知意,emptyDir 在创建 pod 的时候就会被创建,而且是个空的 , 只要 pod 在运行,这个卷就会一直存在

但是,当 pod 从节点上删除的时候,这个 emptyDir 也会被随之永久删除了

使用 emptyDir 我们可以这样写

​

emptyDir 卷存储是依赖于我们节点的存储介质的,例如,磁盘,SSD 或者网络存储等等

再来看看 hostPath 类型

hostPath 类型的卷,能够让对应的容器访问当当前宿主机上指定的目录

对于 hostPath 类型,我们尽可能的少用,官方有一个这样的警告

例如,我们需要运行一个访问一个 docker 系统目录的容器,那么我们可以使用 /var/lib/docker 目录作为一个 hostPath 类型的卷

但是,只要这个 pod 离开宿主机的话,hostPath 中的数据不会被永久删除,但是数据也不会跟着 pod 迁移到别的宿主机上面

使用 hostPath 我们可以这样写

关于 卷 的,我们后续可以将高阶用法,以及具体的原理可以分享一波

namespace

namespace 即 命名空间

命名空间在多数情况下是用于实现多用户的资源隔离的,通过集群内部的资源对象分配到不同的 命名空间中,形成逻辑上的分组,

这样可以让不同的组在共享使用整个集群的资源的情况下,还能够被分组管理

在 K8S 初始状态下,K8S 会有 3 个命名空间

  • default

无名字空间对象的默认名字空间

在 K8S 中,集群启动后,会创建一个 default 的命名空间
如果我们创建的 rc ,service,pod,不指定命名空间的话,那么这些资源都将被系统创建为 default 的命名空间中

  • kube-system

K8S 系统创建的对象的名字空间

  • kube-public

是 K8S 自动创建且被所有用户可读的名字空间

关于命名空间共享集群我们可以查看这里: 点我 进入 命名空间

我们可以这样使用命名空间:

  • 创建一个 NameSpace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码apiVersion: v1
kind: NameSpace
metadata:
name: myns
--------------------
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: myns
spec:
containers:
- image: busybox
command:
- ls
name: busybox

如上,我们创建了一个名为 myns的命名空间,创建了一个 pod ,指定命名空间为 myns

我们可以这样查看命名空间:

kubectl get namesapces

image-20211128120410202

我们也可以直接通过命令创建命名空间

kubectl create namespace myns

创建一个 pod ,指定命名空间为 myns , 创建后,我们可以查看指定 命名空间下的 pod 资源

kubectl get pods --namespace=命名空间

image-20211128120919274

今天就到这里,学习所得,若有偏差,还请斧正

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

Linux常用命令

发表于 2021-11-28

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

文件和目录

cd命令

用于切换当前目录,它的参数是要切换到的目录的路径,可以是绝对路径,也可以是相对路径。

1
2
3
4
5
6
shell复制代码cd /home 进入 '/ home' 目录\
cd .. 返回上一级目录\
cd ../.. 返回上两级目录\
cd 进入个人的主目录\
cd ~user1 进入个人的主目录\
cd - 返回上次所在的目录

pwd命令

显示工作路径

1
2
shell复制代码[root@mailvip ~]# pwd\
/root

ls命令

查看文件与目录的命令,list之意

1
2
3
4
5
shell复制代码ls 查看目录中的文件\
ls -l 显示文件和目录的详细资料\
ls -a 列出全部文件,包含隐藏文件\
ls -R 连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来\
ls [0-9] 显示包含数字的文件名和目录名

cp命令

用于复制文件,copy之意,它还可以把多个文件一次性地复制到一个目录下

1
2
3
4
5
shell复制代码-a :将文件的特性一起复制\
-p :连同文件的属性一起复制,而非使用默认方式,与-a相似,常用于备份\
-i :若目标文件已经存在时,在覆盖时会先询问操作的进行\
-r :递归持续复制,用于目录的复制行为 //经常使用递归复制\
-u :目标文件与源文件有差异时才会复制

mv命令

用于移动文件、目录或更名,move之意

1
2
3
shell复制代码-f :force强制的意思,如果目标文件已经存在,不会询问而直接覆盖\
-i :若目标文件已经存在,就会询问是否覆盖\
-u :若目标文件已经存在,且比目标文件新,才会更新

rm命令

用于删除文件或目录,remove之意

1
2
3
shell复制代码-f :就是force的意思,忽略不存在的文件,不会出现警告消息\
-i :互动模式,在删除前会询问用户是否操作\
-r :递归删除,最常用于目录删除,它是一个非常危险的参数

查看文件内容

cat命令

用于查看文本文件的内容,后接要查看的文件名,通常可用管道与more和less一起使用

1
2
3
4
5
6
7
8
9
10
shell复制代码cat file1 从第一个字节开始正向查看文件的内容
tac file1 从最后一行开始反向查看一个文件的内容
cat -n file1 标示文件的行数
more file1 查看一个长文件的内容

head -n 2 file1 查看一个文件的前两行
tail -n 2 file1 查看一个文件的最后两行
tail -n +1000 file1 从1000行开始显示,显示1000行以后的
cat filename | head -n 3000 | tail -n +1000 显示1000行到3000行
cat filename | tail -n +3000 | head -n 1000 从第3000行开始,显示1000(即显示3000~3999行)

文件搜索

find命令

用来查找系统的

1
2
3
4
5
6
shell复制代码find / -name file1 从 '/' 开始进入根文件系统搜索文件和目录\
find / -user user1 搜索属于用户 'user1' 的文件和目录\
find /usr/bin -type f -atime +100 搜索在过去100天内未被使用过的执行文件\
find /usr/bin -type f -mtime -10 搜索在10天内被创建或者修改过的文件\
whereis halt 显示一个二进制文件、源码或man的位置\
which halt 显示一个二进制文件或可执行文件的完整路径

删除大于50M的文件:

1
bash复制代码find /[var](http://www.fly63.com/)/mail/ -size +50M -exec rm {} \;

文件的权限 - 使用 “+” 设置权限,使用 “-“ 用于取消

chmod命令

改变文件/文件夹权限

1
2
3
scss复制代码ls -lh 显示权限
chmod ugo+rwx directory1 设置目录的所有人(u)、群组(g)以及其他人(o)以读(r,4 )、写(w,2)和执行(x,1)的权限
chmod go-rwx directory1 删除群组(g)与其他人(o)对目录的读写执行权限

chown命令

改变文件的所有者

1
2
3
bash复制代码chown user1 file1 改变一个文件的所有人属性
chown -R user1 directory1 改变一个目录的所有人属性并同时改变改目录下所有文件的属性
chown user1:group1 file1 改变一个文件的所有人和群组属性

chgrp命令

改变文件所属用户组

1
bash复制代码chgrp group1 file1 改变文件的群组

文本处理

grep命令

分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工等等

1
2
3
4
5
6
7
8
9
10
perl复制代码grep Aug /var/log/messages  在文件 '/var/log/messages'中查找关键词"Aug" 

grep ^Aug /var/log/messages 在文件 '/var/log/messages'中查找以"Aug"开始的词汇
grep [0-9] /var/log/messages 选择 '/var/log/messages' 文件中所有包含数字的行

grep Aug -R /var/log/* 在目录 '/var/log' 及随后的目录中搜索字符串"Aug"

sed 's/stringa1/stringa2/g' example.txt 将example.txt文件中的 "string1" 替换成 "string2"

sed '/^$/d' example.txt 从example.txt文件中删除所有空白行

paste命令

1
2
bash复制代码paste file1 file2 合并两个文件或两栏的内容
paste -d '+' file1 file2 合并两个文件或两栏的内容,中间用"+"区分

sort命令

1
2
3
4
bash复制代码sort file1 file2 排序两个文件的内容
sort file1 file2 | uniq 取出两个文件的并集(重复的行只保留一份)
sort file1 file2 | uniq -u 删除交集,留下其他的行
sort file1 file2 | uniq -d 取出两个文件的交集(只留下同时存在于两个文件中的文件)

comm命令

1
2
3
bash复制代码comm -1 file1 file2 比较两个文件的内容只删除 'file1' 所包含的内容
comm -2 file1 file2 比较两个文件的内容只删除 'file2' 所包含的内容
comm -3 file1 file2 比较两个文件的内容只删除两个文件共有的部分

打包和压缩文件

tar命令

对文件进行打包,默认情况并不会压缩,如果指定了相应的参数,它还会调用相应的压缩程序(如gzip和bzip等)进行压缩和解压

1
2
3
4
5
6
7
8
diff复制代码-c :新建打包文件
-t :查看打包文件的内容含有哪些文件名
-x :解打包或解压缩的功能,可以搭配-C(大写)指定解压的目录,注意-c,-t,-x不能同时出现在同一条命令中
-j :通过bzip2的支持进行压缩/解压缩
-z :通过gzip的支持进行压缩/解压缩
-v :在压缩/解压缩过程中,将正在处理的文件名显示出来
-f filename :filename为要处理的文件
-C dir :指定压缩/解压缩的目录dir

压缩:tar -jcv -f filename.tar.bz2 要被处理的文件或目录名称 查询:tar -jtv -f filename.tar.bz2 解压:tar -jxv -f filename.tar.bz2 -C 欲解压缩的目录

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码bunzip2 file1.bz2 解压一个叫做 'file1.bz2'的文件
bzip2 file1 压缩一个叫做 'file1' 的文件
gunzip file1.gz 解压一个叫做 'file1.gz'的文件
gzip file1 压缩一个叫做 'file1'的文件
gzip -9 file1 最大程度压缩
rar a file1.rar test_file 创建一个叫做 'file1.rar' 的包
rar a file1.rar file1 file2 dir1 同时压缩 'file1', 'file2' 以及目录 'dir1'
rar x file1.rar 解压rar包

zip file1.zip file1 创建一个zip格式的压缩包
unzip file1.zip 解压一个zip格式压缩包
zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包

系统和关机(关机、重启和登出)

1
2
3
4
5
6
7
8
9
perl复制代码shutdown -h now 关闭系统(1)
init 0 关闭系统(2)
telinit 0 关闭系统(3)
shutdown -h hours:minutes & 按预定时间关闭系统
shutdown -c 取消按预定时间关闭系统
shutdown -r now 重启(1)
reboot 重启(2)
logout 注销
time 测算一个命令(即程序)的执行时间

进程相关的命令

jps命令

显示当前系统的java进程情况,及其id号jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。ps命令,用于将某个时间点的进程运行情况选取下来并输出,process之意

1
2
3
4
5
6
7
8
9
10
diff复制代码-A :所有的进程均显示出来
-a :不与terminal有关的所有进程
-u :有效用户的相关进程
-x :一般与a参数一起使用,可列出较完整的信息
-l :较长,较详细地将PID的信息列出

ps aux # 查看系统所有的进程数据
ps ax # 查看不与terminal有关的所有进程
ps -lA # 查看系统所有的进程数据
ps axjf # 查看连同一部分进程树状态

kill命令

用于向某个工作(%jobnumber)或者是某个PID(数字)传送一个信号,它通常与ps和jobs命令一起使用命令格式 : kill[命令参数][进程id]命令参数:

1
2
3
4
5
diff复制代码-l 信号,若果不加信号的编号参数,则使用“-l”参数会列出全部的信号名称
-a 当处理当前进程时,不限制命令名和进程号的对应关系
-p 指定kill 命令只打印相关进程的进程号,而不发送任何信号
-s 指定发送信号
-u 指定用户

实例1:列出所有信号名称 命令:kill -l 输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码[root@localhost test6]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

说明:只有第9种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略。 下面是常用的信号:

1
2
3
4
5
6
7
bash复制代码HUP 1 终端断线
INT 2 中断(同 Ctrl + C)
QUIT 3 退出(同 Ctrl + \)
TERM 15 终止
KILL 9 强制终止
CONT 18 继续(与STOP相反, fg/bg命令)
STOP 19 暂停(同 Ctrl + Z)

实例2:得到指定信号的数值

1
2
3
4
5
csharp复制代码[root@localhost test6]# kill -l KILL
[root@localhost test6]# kill -l SIGKILL
[root@localhost test6]# kill -l TERM
[root@localhost test6]# kill -l SIGTERM
[root@localhost test6]#

实例3:先用ps查找进程,然后用kill杀掉

1
2
3
4
5
yaml复制代码命令:kill 3268
[root@localhost test6]# ps -ef|grep vim
root 3268 2884 0 16:21 pts/1 00:00:00 vim install.log
root 3370 2822 0 16:21 pts/0 00:00:00 grep vim
[root@localhost test6]# kill 3268

实例4:彻底杀死进程

1
arduino复制代码命令:kill –9 3268 // -9 强制杀掉进程

killall命令

向一个命令启动的进程发送一个信号,用于杀死指定名字的进程

命令格式 : killall[命令参数][进程名]

1
2
3
4
5
6
7
8
9
10
11
12
13
diff复制代码命令参数:
-Z 只杀死拥有scontext 的进程
-e 要求匹配进程名称
-I 忽略小写
-g 杀死进程组而不是进程
-i 交互模式,杀死进程前先询问用户
-l 列出所有的已知信号名称
-q 不输出警告信息
-s 发送指定的信号
-v 报告信号是否成功发送
-w 等待进程死亡
--help 显示帮助信息
--version 显示版本显示

示例

1
2
3
4
5
6
markdown复制代码1:杀死所有同名进程
killall nginx
killall -9 bash

2.向进程发送指定信号
killall -TERM ngixn 或者 killall -KILL nginx

top命令

是Linux下常用的性能分析,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。

如何杀死进程:

1
2
3
4
bash复制代码(1)图形化界面的方式
(2)kill -9 pid (-9表示强制关闭)
(3)killall -9 程序的名字
(4)pkill 程序的名字

查看进程端口号:

1
perl复制代码netstat -tunlp|grep 端口号

本文转载自: 掘金

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

通过 JAVA 提供的 API 实现一些反射操作

发表于 2021-11-28

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

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
java复制代码package reflecttest;

public class Person {
public int age;
private String name;

public Person() {
// TODO 自动生成的构造函数存根
}

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

private Person(String name,int age) {
super();
this.name = name;
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void show(String name,int age) {
System.out.println("迪迦,变身");
}

private void fly() {
System.out.println("迪迦,飞行");
}

private String Work(String name,int age) {
return name;
}

}

获取Class的实例的三种方式

Class的实例就是运行时的类

  1. 通过类字面常量Class.class获取
1
java复制代码		Class clazz = Person.class;
  1. 通过Object类中的getClass()方法返回一个Class类型的实例
1
2
java复制代码		Person person = new Person();
Class clazz = person.getClass();
  1. 通过静态方法Class.forName("全类名")获取类名对应的Class对象
1
java复制代码		Class clazz = Class.forName("reflecttest.Person");

通过反射创建对象

反射创建类对象主要有两种方式,通过Class对象的newInstance()方法、通过Constructor对象的 newInstance()方法。

  1. 第一种:通过Class对象的newInstance()方法。
1
2
3
4
5
java复制代码		// 获取类的 Class 对象实例
Class clazz = Person.class;

//创建Person对象
Person person = (Person)clazz.newInstance();
  1. 第二种:通过Constructor对象的newInstance()方法
1
2
3
4
5
6
java复制代码		// 获取类的 Class 对象实例
Class clazz = Person.class;

//创建Person对象
Constructor constructor = clazz.getConstructor();
Person person = (Person)constructor.newInstance();

通过Constructor对象创建类对象可以选择特定构造方法,而通过 Class对象则只能使用默认的无参数构造方法。

示例:(调用有参构造方法进行类对象的初始化)

1
2
3
4
5
6
Java复制代码		// 获取类的 Class 对象实例
Class clazz = Person.class;

//创建Person对象
Constructor constructor = clazz.getConstructor();
Person person = (Person) constructor.newInstance("迪迦",16);

通过反射调用对象属性

获取属性

获取所有属性的两种方式

  1. getFields()
1
2
java复制代码		// 通过getFields()获取Person.class及父类中所有 public 修饰的属性
Field[] fields = clazz.getFields();
  1. getDeclaredFields()
1
2
java复制代码		// 通过getDeclaredFields()获取Person.class中所有的属性
Field[] fields = clazz.getDeclaredFields();

注:

getFields() 可以获取本类及父类中所有 public 修饰的属性
getDeclaredFields() 可以获取本类所有的属性。包括 public、private、protected、default 修饰的属性,不包括父类中的属性

获取指定属性的两种方式

  1. getField(String name)
1
2
java复制代码		// 通过getField()获取Person.class中的age属性		
Field field = clazz.getField("age");
  1. getDeclaredField(String name)
1
2
java复制代码		// 通过getDeclaredField()获取Person.class中的name属性
Field field = clazz.getDeclaredField("name");

注:

getField(String name) 可以获取本类及父类中的 public 修饰的属性
getDeclaredField(String name) 可以获取本类的属性。包括 public、private、protected、default 修饰的属性,不包括父类中的属性

对属性的操作

设置属性值

1
2
3
4
5
6
7
8
java复制代码		// 通过getDeclaredField()获取Person.class中的name属性
Field field = clazz.getDeclaredField("name");

//如果属性是private修饰的,则需要设置访问权限才能赋值
field.setAccessible(true);

//通过set(Object obj, Object value)设置person对象的name属性为"迪迦"
field.set(person, "迪迦");

注:如果属性是private修饰的,则需要设置setAccessible(true)才能赋值

获取属性值

1
2
java复制代码		//通过get(Object obj)获取person对象的field属性
String name = (String) field.get(person);

获取属性的类型

1
2
3
4
5
java复制代码		//通过getType()获取属性的类型
Class type = field.getType();

//获取属性的类型名
String typename = field.getType().getName();

获取属性名

1
2
java复制代码		//调用getName()获取属性名
String fieldname = field.getName();

获取修饰类型

1
2
3
4
5
java复制代码		//调用getModifiers()获取修饰类型,
int modifier = field.getModifiers();

//Modifier.toString(int mod)转成public、private、protected、default
String modifiername = Modifier.toString(modifier);

注:0-default、1-public、2-private、4-protected

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码		// 获取类的 Class 对象实例
Class clazz = Person.class;

//创建Person对象
Person person = (Person) clazz.newInstance();

// 通过getDeclaredField()获取Person.class中的name属性
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
//通过set()设置person对象的age属性为"12"
field.set(person, 12);

//获取修饰符
int modifier = field.getModifiers();
String modifiername = Modifier.toString(modifier);
//获取属性类型名
String typename = field.getType().getName();
//获取属性名
String fieldname = field.getName();
//获取属性值
int fieldvalue = field.getInt(person);

System.out.println(modifiername+" "+typename+" "+fieldname+" = "+fieldvalue+";");

输出:public int age = 12;

通过反射调用对象方法

获取方法

获取所有方法的两种方式

  1. getMethods()
1
2
java复制代码		//通过getMethods()获取Person类及父类中所有的public修饰的方法		
Method[] methods =clazz.getMethods();
  1. getDeclaredMethods()
1
2
java复制代码		//通过getDeclaredMethods()获取Person类中所有的方法
Method[] methods = clazz.getDeclaredMethods();

注:

getMethods()可以获取本类及父类中所有 public 修饰的方法
getDeclaredMethods()可以获取本类所有的方法。包括 public、private、protected、default 修饰的方法,不包括父类中的方法

取指定方法的两种方式

用方法名和形参列表来确定获取的方法

  1. getMethod(String name, class<?>... parameterTypes)
1
2
java复制代码		//获取Person类中的show方法,参数为方法名,show()的形参列表
Method method =clazz.getMethod("show",String.class,int.class);
  1. getDeclaredMethod(String name, 类<?>... parameterTypes)
1
2
java复制代码		//获取Person类中的fly方法,参数为方法名,fly()无形参列表
Method method = clazz.getDeclaredMethod("fly");

注:

getMethod() 可以获取本类及父类中的 public 修饰的属性
getDeclaredMethod() 可以获取本类的属性。包括 public、private、protected、default 修饰的属性,不包括父类中的属性

对方法的操作

调用方法

1
2
3
4
5
6
7
8
9
10
java复制代码		//获取Person类中的fly方法,参数为方法名,fly()无形参列表
Method method = clazz.getDeclaredMethod("fly");

//如果方法是private修饰的,则需要设置访问权限
method.setAccessible(true);

//调用person中的fly方法
method.invoke(person);
//如果方法是静态方法static时,除了上面的方式,也可用下面的方法,当前类调用方法
//method.invoke(Person.class);

注:invoke()方法的返回值和调用的方法的返回值一样

获取方法的参数列表

1
2
java复制代码		//获取形参列表
Class[] paramTypes =method.getParameterTypes();

获取方法名

1
2
java复制代码		//获取方法名
String methodname = method.getName();

获取方法的修饰类型

1
2
3
java复制代码		//获取方法修饰符
int modifier = method.getModifiers();
String modifiername = Modifier.toString(modifier);

获取方法的返回值类型

1
2
3
4
5
java复制代码		//获取返回值类型
Class<?> returntype =method.getReturnType();

//获取返回值类型名
String returntypename = returntype.getName();

示例

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复制代码	public static void main(String[] args) throws Exception, IllegalAccessException {
// 获取类的 Class 对象实例
Class clazz = Person.class;

//创建Person对象
Person person = (Person) clazz.newInstance();

//获取Person类中的方法
Method method = clazz.getDeclaredMethod("Work",String.class,int.class);

//设置权限
method.setAccessible(true);

//获取方法名
String methodname = method.getName();

//获取方法修饰符
int modifier = method.getModifiers();
String modifiername = Modifier.toString(modifier);

//获取返回值类型
Class<?> returntype =method.getReturnType();
//获取返回值类型名
String returntypename = returntype.getName();

//获取形参列表
Class[] paramTypes =method.getParameterTypes();

System.out.print(modifiername+" "+returntypename+" "+methodname+"(");

//打印方法参数
printParamTypes(paramTypes);

}
/**
* 打印方法参数
* @param paramTypes
*/
private static void printParamTypes(Class[] paramTypes) {
for (int j = 0; j < paramTypes.length; ++j) {
if (j > 0) {
System.out.print(",");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}

输出:private java.lang.String Work(java.lang.String,int);


通过反射调用对象构造方法

获取构造方法

获取所有构造方法的两种方式

  1. getConstructors()
1
2
java复制代码		//获取所有public修饰的构造方法,不包括父类
Constructor[] constructors = clazz.getConstructors();
  1. getDeclaredConstructors()
1
2
java复制代码		//获取所有构造方法,包括public、private、protected、default 修饰,不包括父类
Constructor[] constructors = getDeclaredConstructors();

注:

getConstructors()可以获取本类中所有 public 修饰的构造方法,不包括父类
getgetDeclaredConstructors()可以获取本类所有的构造方法。包括 public、private、protected、default 修饰的方法,不包括父类

获取指定构造方法的两种方式

对构造器的选择是依赖于形参列表的类型

  1. clazz.getConstructor()
1
2
java复制代码		//获取无参数的构造方法
Constructor constructor = clazz.getConstructor();
  1. getDeclaredConstructor()
1
2
java复制代码		//获取形参为(String,int)类型的构造方法
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);

注:

getConstructor()只可以获取本类 public 修饰的构造方法,不包括父类
getDeclaredConstructor()可以获取本类所有的构造方法。包括 public、private、protected、default 修饰的方法,不包括父类

对构造方法的操作

创建对象

对构造器的选择是依赖于形参列表的类型

1
2
3
4
5
6
7
8
9
java复制代码		//获取空参的构造器
//Constructor constructor = clazz.getDeclaredConstructor();
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);

//如果构造方法是private修饰的,设置权限
constructor.setAccessible(true);

//创建person对象
Person person = (Person)constructor.newInstance("迪迦",20);

获取构造方法的参数列表

1
2
java复制代码		//获取构造方法形参列表
Class[] paramTypes = constructor.getParameterTypes();

获取构造方法名

1
2
java复制代码		//获取构造方法名
String constructorname = constructor.getName();

获取构造方法的修饰类型

1
2
3
java复制代码		//获取构造方法的修饰符
int modifier = constructor.getModifiers();
String modifiername = Modifier.toString(modifier);

示例

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复制代码public static void main(String[] args) throws Exception, IllegalAccessException {
// 获取类的 Class 对象实例
Class clazz = Person.class;

//获取带参的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);

//如果构造方法是private修饰的,设置权限
constructor.setAccessible(true);

//创建person对象
Person person = (Person)constructor.newInstance("迪迦",20);

//获取构造方法名
String constructorname = constructor.getName();

//获取构造方法的修饰符
int modifier = constructor.getModifiers();
String modifiername = Modifier.toString(modifier);

//获取构造方法形参列表
Class[] paramTypes = constructor.getParameterTypes();

System.out.print(modifiername+" "+constructorname+"(");

//打印方法参数
printParamTypes(paramTypes);

}
/**
* 打印方法参数
* @param paramTypes
*/
private static void printParamTypes(Class[] paramTypes) {
for (int j = 0; j < paramTypes.length; ++j) {
if (j > 0) {
System.out.print(",");
}
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}

输出:private test1.Person(java.lang.String,int);

本文转载自: 掘金

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

从源码层面分析Threadjoin的作用和原理 文章简介

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

文章简介

很多人对Thread.join的作用以及实现了解得很少,毕竟这个api我们很少使用。这篇文章仍然会结合使用及原理进行深度分析

内容导航

  1. Thread.join的作用
  2. Thread.join的实现原理
  3. 什么时候会使用Thread.join

Thread.join的作用

之前有人问过我一个这样的面试题

Java中如何让多线程按照自己指定的顺序执行?

这个问题最简单的回答是通过Thread.join来实现,久而久之就让很多人误以为Thread.join是用来保证线程的顺序性的。
下面这段代码演示了Thread.join的作用

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
java复制代码public class JoinDemo extends Thread{
int i;
Thread previousThread; //上一个线程
public JoinDemo(Thread previousThread,int i){
this.previousThread=previousThread;
this.i=i;
}
@Override
public void run() {
try {
//调用上一个线程的join方法,大家可以自己演示的时候可以把这行代码注释掉
previousThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num:"+i);
}
public static void main(String[] args) {
Thread previousThread=Thread.currentThread();
for(int i=0;i<10;i++){
JoinDemo joinDemo=new JoinDemo(previousThread,i);
joinDemo.start();
previousThread=joinDemo;
}
}
}

上面的代码,注意 previousThread.join部分,大家可以把这行代码注释以后看看运行效果,在没有加join的时候运行的结果是不确定的。加了join以后,运行结果按照递增的顺序展示出来。

thread.join的含义是当前线程需要等待previousThread线程终止之后才从thread.join返回。简单来说,就是线程没有执行完之前,会一直阻塞在join方法处。

下面的图表现了join对于线程的作用

image.png

Thread.join的实现原理

线程是如何被阻塞的?又是通过什么方法唤醒的呢?先来看看Thread.join方法做了什么事情

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复制代码public class Thread implements Runnable {
...
public final void join() throws InterruptedException {
join(0);
}
...
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { //判断是否携带阻塞的超时时间,等于0表示没有设置超时时间
while (isAlive()) {//isAlive获取线程状态,无线等待直到previousThread线程结束
wait(0); //调用Object中的wait方法实现线程的阻塞
}
} else { //阻塞直到超时
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
...

从join方法的源码来看,join方法的本质调用的是Object中的wait方法实现线程的阻塞,wait方法的实现原理我们在后续的文章再说详细阐述。但是我们需要知道的是,调用wait方法必须要获取锁,所以join方法是被synchronized修饰的,synchronized修饰在方法层面相当于synchronized(this),this就是previousThread本身的实例。

有很多人不理解join为什么阻塞的是主线程呢? 不理解的原因是阻塞主线程的方法是放在previousThread这个实例作用,让大家误以为应该阻塞previousThread线程。实际上主线程会持有previousThread这个对象的锁,然后调用wait方法去阻塞,而这个方法的调用者是在主线程中的。所以造成主线程阻塞。

第二个问题,为什么previousThread线程执行完毕就能够唤醒住线程呢?或者说是在什么时候唤醒的?

要了解这个问题,我们又得翻jdk的源码,但是如果大家对线程有一定的基本了解的话,通过wait方法阻塞的线程,需要通过notify或者notifyall来唤醒。所以在线程执行完毕以后会有一个唤醒的操作,只是我们不需要关心。
接下来在hotspot的源码中找到 thread.cpp,看看线程退出以后有没有做相关的事情来证明我们的猜想.

1
2
3
4
5
6
7
8
9
java复制代码void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
assert(this == JavaThread::current(), "thread consistency check");
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
assert(!this->has_pending_exception(), "ensure_join should have cleared");
...

观察一下 ensure_join(this)这行代码上的注释,唤醒处于等待的线程对象,这个是在线程终止之后做的清理工作,这个方法的定义代码片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
//这里是清除native线程,这个操作会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);//注意这里
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}

ensure_join方法中,调用 lock.notify_all(thread); 唤醒所有等待thread锁的线程,意味着调用了join方法被阻塞的主线程会被唤醒; 到目前为止,我们基本上对join的原理做了一个比较详细的分析

总结,Thread.join其实底层是通过wait/notifyall来实现线程的通信达到线程阻塞的目的;当线程执行结束以后,会触发两个事情,第一个是设置native线程对象为null、第二个是通过notifyall方法,让等待在previousThread对象锁上的wait方法被唤醒。

什么时候会使用Thread.join

在实际应用开发中,我们很少会使用thread.join。在实际使用过程中,我们可以通过join方法来等待线程执行的结果,其实有点类似future/callable的功能。
我们通过以下伪代码来说明join的使用场景

1
2
3
4
5
6
7
8
9
10
java复制代码public void joinDemo(){
//....
Thread t=new Thread(payService);
t.start();
//....
//其他业务逻辑处理,不需要确定t线程是否执行完
insertData();
//后续的处理,需要依赖t线程的执行结果,可以在这里调用join方法等待t线程执行结束
t.join();
}

本文转载自: 掘金

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

Mybatis源码分析

发表于 2021-11-28

MyBatis介绍

MyBatis是一个持久层的ORM框架,使用简单,学习成本较低。可以执行自己手写的SQL语句,比较灵活。但是MyBatis的自动化程度不高,移植性也不高,有时从一个数据库迁移到另外一个数据库的时候需要自己修改配置,所以称只为半自动ORM框架。

Mybaits整体体系图

image.png

例子

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
ini复制代码public class App {
public static void main(String[] args) {
String resource = "mybatis-config.xml";
Reader reader;
try {
//将XML配置文件构建为Configuration配置类
reader = Resources.getResourceAsReader(resource);
// 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory
SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
// 数据源 执行器 DefaultSqlSession
SqlSession session = sqlMapper.openSession();
try {
// 执行查询 底层执行jdbc
//User user = (User)session.selectOne("com.xxx.mapper.selectById", 1);

UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println(mapper.getClass());
User user = mapper.selectById(1L);
System.out.println(user.getUserName());
} catch (Exception e) {
e.printStackTrace();
}finally {
session.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

build方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码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.
}
}
}

image.png

可以看到除了XMLConfigBuilder继承于BaseBuilder,还有一些其他的解析配置文件的类。

XMLConfigBuilder.parse方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码public Configuration parse() {
/**
* 若已经解析过了 就抛出异常
*/
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
/**
* 设置解析标志位
*/
parsed = true;
/**
* 解析我们的mybatis-config.xml的
* 节点
* <configuration>
*
*
* </configuration>
*/
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

parseConfiguration方法

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
ini复制代码private void parseConfiguration(XNode root) {
try {
/**
* 解析 properties节点
* <properties resource="mybatis/db.properties" />
* 解析到org.apache.ibatis.parsing.XPathParser#variables
* org.apache.ibatis.session.Configuration#variables
*/
propertiesElement(root.evalNode("properties"));
/**
* 解析我们的mybatis-config.xml中的settings节点
* 具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings
* <settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
..............
</settings>
*
*/
Properties settings = settingsAsProperties(root.evalNode("settings"));
/**
* 基本没有用过该属性
* VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。
Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序
解析到:org.apache.ibatis.session.Configuration#vfsImpl
*/
loadCustomVfs(settings);
/**
* 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
* SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
* 解析到org.apache.ibatis.session.Configuration#logImpl
*/
loadCustomLogImpl(settings);
/**
* 解析我们的别名
* <typeAliases>
<typeAlias alias="Author" type="cn.xxx.pojo.Author"/>
</typeAliases>
<typeAliases>
<package name="cn.xxx.pojo"/>
</typeAliases>
解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases
*/
typeAliasesElement(root.evalNode("typeAliases"));
/**
* 解析我们的插件(比如分页插件)
* mybatis自带的
* Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors
*/
pluginElement(root.evalNode("plugins"));

/**
* todo
*/
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 设置settings 和默认值
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631

/**
* 解析我们的mybatis环境
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="root"/>
<property name="password" value="Zw726515"/>
</dataSource>
</environment>

<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
* 解析到:org.apache.ibatis.session.Configuration#environment
* 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂
*/
environmentsElement(root.evalNode("environments"));
/**
* 解析数据库厂商
* <databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
<property name="MySql" value="mysql" />
</databaseIdProvider>
* 解析到:org.apache.ibatis.session.Configuration#databaseId
*/
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
/**
* 解析我们的类型处理器节点
* <typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap
*/
typeHandlerElement(root.evalNode("typeHandlers"));
/**
* 最最最最最重要的就是解析我们的mapper
*
resource:来注册我们的class类路径下的
url:来指定我们磁盘下的或者网络资源的
class:
若注册Mapper不带xml文件的,这里可以直接注册
若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名 同路径
-->
<mappers>
<mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
<mapper class="com.xxx.mapper.DeptMapper"></mapper>


<package name="com.xxx.mapper"></package>
-->
</mappers>
* package 1.解析mapper接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers
2.
*/
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

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
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
ini复制代码private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
/**
* 获取我们mappers节点下的一个一个的mapper节点
*/
for (XNode child : parent.getChildren()) {
/**
* 判断我们mapper是不是通过批量注册的
* <package name="com.xxx.mapper"></package>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
/**
* 判断从classpath下读取我们的mapper
* <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
*/
String resource = child.getStringAttribute("resource");
/**
* 判断是不是从我们的网络资源读取(或者本地磁盘得)
* <mapper url="D:/mapper/EmployeeMapper.xml"/>
*/
String url = child.getStringAttribute("url");
/**
* 解析这种类型(要求接口和xml在同一个包下)
* <mapper class="com.xxx.mapper.DeptMapper"></mapper>
*
*/
String mapperClass = child.getStringAttribute("class");

/**
* 我们得mappers节点只配置了
* <mapper resource="mybatis/mapper/EmployeeMapper.xml"/>
*/
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
/**
* 把我们的文件读取出一个流
*/
InputStream inputStream = Resources.getResourceAsStream(resource);
/**
* 创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件
*/
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
/**
* 真正的解析我们的mapper.xml配置文件(说白了就是来解析我们的sql)
*/
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.");
}
}
}
}
}

addMapper方法

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
typescript复制代码/**
* 方法实现说明:把我们的Mapper class保存到我们的knownMappers map 中
* @param type:我们的Mapper接口
* @return:
* @exception:
* @date:2019/8/22 20:29
*/
public <T> void addMapper(Class<T> type) {
/**
* 判断我们传入进来的type类型是不是接口
*/
if (type.isInterface()) {
/**
* 判断我们的缓存中有没有该类型
*/
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
/**
* 创建一个MapperProxyFactory 把我们的Mapper接口保存到工厂类中
*/
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try. mapper注解构造器
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
/**
* 进行解析
*/
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

MapperAnnotationBuilder.parser方法

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
scss复制代码public void parse() {
String resource = type.toString();
// 是否已经解析mapper接口对应的xml
if (!configuration.isResourceLoaded(resource)) {
// 根据mapper接口名获取 xml文件并解析, 解析<mapper></mapper>里面所有东西放到configuration
loadXmlResource();
// 添加已解析的标记
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
// 获取所有方法 看是不是用了注解
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 是不是用了注解 用了注解会将注解解析成MappedStatement
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}

loadXmlResource方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}

XMLMapperBuilder.parse方法

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
scss复制代码/**
* 方法实现说明:真正的去解析我们的Mapper.xml(EmployeeMapper.xml)
*
* @return:
* @exception:
* @date:2019/8/30 16:43
*/
public void parse() {
/**
* 判断当前的Mapper是否被加载过
*/
if (!configuration.isResourceLoaded(resource)) {
/**
* 真正的解析我们的 <mapper namespace="com.xxx.mapper.EmployeeMapper">
*
*/
configurationElement(parser.evalNode("/mapper"));
/**
* 把资源保存到我们Configuration中
*/
configuration.addLoadedResource(resource);

bindMapperForNamespace();
}

parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}

configurationElement方法

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
csharp复制代码/**
* 方法实现说明:解析我们的<mapper></mapper>节点
* @author:xsls
* @param context document节点
* @return:
* @exception:
* @date:2019/8/31 13:34
*/
private void configurationElement(XNode context) {
try {
/**
* 解析我们的namespace属性
* <mapper namespace="com.xxx.mapper.EmployeeMapper">
*/
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
/**
* 保存我们当前的namespace 并且判断接口完全类名==namespace
*/
builderAssistant.setCurrentNamespace(namespace);
/**
* 解析我们的缓存引用
* 说明我当前的缓存引用和DeptMapper的缓存引用一致
* <cache-ref namespace="com.xxx.mapper.DeptMapper"></cache-ref>
解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>
异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs
*/
cacheRefElement(context.evalNode("cache-ref"));
/**
* 解析我们的cache节点
* <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
解析到:org.apache.ibatis.session.Configuration#caches
org.apache.ibatis.builder.MapperBuilderAssistant#currentCache
*/
cacheElement(context.evalNode("cache"));
/**
* 解析paramterMap节点(该节点mybaits3.5貌似不推荐使用了)
*/
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
/**
* 解析我们的resultMap节点
* 解析到:org.apache.ibatis.session.Configuration#resultMaps
* 异常 org.apache.ibatis.session.Configuration#incompleteResultMaps
*
*/
resultMapElements(context.evalNodes("/mapper/resultMap"));
/**
* 解析我们通过sql节点
* 解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments
* 其实等于 org.apache.ibatis.session.Configuration#sqlFragments
* 因为他们是同一引用,在构建XMLMapperBuilder 时把Configuration.getSqlFragments传进去了
*/
sqlElement(context.evalNodes("/mapper/sql"));
/**
* 解析我们的select | insert |update |delete节点
* 解析到org.apache.ibatis.session.Configuration#mappedStatements
*/
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}

cacheElement方法

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
ini复制代码/**
* 方法实现说明:解析缓存属性
* <cache type=""
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

*
* @param context:cache节点
* @return:
* @exception:
* @date:2019/8/31 14:13
*/
private void cacheElement(XNode context) {
if (context != null) {
//解析cache节点的type属性
String type = context.getStringAttribute("type", "PERPETUAL");
//根据type的String获取class类型
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取缓存过期策略:默认是LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
Long flushInterval = context.getLongAttribute("flushInterval");
//size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
Integer size = context.getIntAttribute("size");
//只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//把缓存节点加入到Configuration中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}

useNewCache方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}

CacheBuilder.build方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}

setStandardDecorators方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
cache = new ScheduledCache(cache);//ScheduledCache:调度缓存,负责定时清空缓存
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) { // 将LRU 装饰到Serialized
cache = new SerializedCache(cache); //SerializedCache:缓存序列化和反序列化存储
}
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}

这边使用装饰者模式对cache进行增强。

buildStatementFromContext方法

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
php复制代码/**
* 方法实现说明:解析我们得得select|update|delte|insert节点然后
* 创建我们得mapperStatment对象
*
* @param list:所有的select|update|delte|insert节点
* @param requiredDatabaseId:判断有没有数据库厂商Id
* @return:
* @exception:
* @date:2019/9/5 21:35
*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
/**
* 循环我们的select|delte|insert|update节点
*/
for (XNode context : list) {
/**
* 创建一个xmlStatement的构建器对象
*/
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}

XMLStatementBuilder.parseStatementNode方法

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
ini复制代码public void parseStatementNode() {
/**
* 我们的insert|delte|update|select 语句的sqlId
*/
String id = context.getStringAttribute("id");
/**
* 判断我们的insert|delte|update|select 节点是否配置了
* 数据库厂商标注
*/
String databaseId = context.getStringAttribute("databaseId");

/**
* 匹配当前的数据库厂商id是否匹配当前数据源的厂商id
*/
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

/**
* 获得节点名称:select|insert|update|delete
*/
String nodeName = context.getNode().getNodeName();
/**
* 根据nodeName 获得 SqlCommandType枚举
*/
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
/**
* 判断是不是select语句节点
*/
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
/**
* 获取flushCache属性
* 默认值为isSelect的反值:查询:默认flushCache=false 增删改:默认flushCache=true
*/
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
/**
* 获取useCache属性
* 默认值为isSelect:查询:默认useCache=true 增删改:默认useCache=false
*/
boolean useCache = context.getBooleanAttribute("useCache", isSelect);

/**
* resultOrdered: 是否需要处理嵌套查询结果 group by (使用极少)
* 可以将比如 30条数据的三组数据 组成一个嵌套的查询结果
*/
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

/**
* 解析我们的sql公用片段
* <select id="qryEmployeeById" resultType="Employee" parameterType="int">
<include refid="selectInfo"></include>
employee where id=#{id}
</select>
将 <include refid="selectInfo"></include> 解析成sql语句 放在<select>Node的子节点中
*/
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

/**
* 解析我们sql节点的参数类型
*/
String parameterType = context.getStringAttribute("parameterType");
// 把参数类型字符串转化为class
Class<?> parameterTypeClass = resolveClass(parameterType);

/**
* 查看sql是否支撑自定义语言
* <delete id="delEmployeeById" parameterType="int" lang="xxxLang">
<settings>
<setting name="defaultScriptingLanguage" value="xxxLang"/>
</settings>
*/
String lang = context.getStringAttribute("lang");
/**
* 获取自定义sql脚本语言驱动 默认:class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver
*/
LanguageDriver langDriver = getLanguageDriver(lang);

// Parse selectKey after includes and remove them.
/**
* 解析我们<insert 语句的的selectKey节点, 还记得吧,一般在oracle里面设置自增id
*/
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
/**
* 我们insert语句 用于主键生成组件
*/
KeyGenerator keyGenerator;
/**
* selectById!selectKey
* id+!selectKey
*/
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
/**
* 把我们的命名空间拼接到keyStatementId中
* com.xxx.mapper.Employee.saveEmployee!selectKey
*/
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
/**
*<insert id="saveEmployee" parameterType="com.xxx.entity.Employee" useGeneratedKeys="true" keyProperty="id">
*判断我们全局的配置类configuration中是否包含以及解析过的组件生成器对象
*/
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {

/**
* 若我们配置了useGeneratedKeys 那么就去除useGeneratedKeys的配置值,
* 否者就看我们的mybatis-config.xml配置文件中是配置了
* <setting name="useGeneratedKeys" value="true"></setting> 默认是false
* 并且判断sql操作类型是否为insert
* 若是的话,那么使用的生成策略就是Jdbc3KeyGenerator.INSTANCE
* 否则就是NoKeyGenerator.INSTANCE
*/
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

/**
* 通过class org.apache.ibatis.scripting.xmltags.XMLLanguageDriver来解析我们的
* sql脚本对象 . 解析SqlNode. 注意, 只是解析成一个个的SqlNode, 并不会完全解析sql,因为这个时候参数都没确定,动态sql无法解析
*/
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
/**
* STATEMENT,PREPARED 或 CALLABLE 中的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
*/
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
/**
* 这是一个给驱动的提示,尝试让驱动程序每次批量返回的结果行数和这个设置值相等。 默认值为未设置(unset)(依赖驱动)
*/
Integer fetchSize = context.getIntAttribute("fetchSize");
/**
* 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖驱动)。
*/
Integer timeout = context.getIntAttribute("timeout");
/**
* 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler) 推断出具体传入语句的参数,默认值为未设置
*/
String parameterMap = context.getStringAttribute("parameterMap");
/**
* 从这条语句中返回的期望类型的类的完全限定名或别名。 注意如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身。
* 可以使用 resultType 或 resultMap,但不能同时使用
*/
String resultType = context.getStringAttribute("resultType");
/**解析我们查询结果集返回的类型 */
Class<?> resultTypeClass = resolveClass(resultType);
/**
* 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂映射的情形都能迎刃而解。
* 可以使用 resultMap 或 resultType,但不能同时使用。
*/
String resultMap = context.getStringAttribute("resultMap");

String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}

/**
* 解析 keyProperty keyColumn 仅适用于 insert 和 update
*/
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");

/**
* 为我们的insert|delete|update|select节点构建成我们的mappedStatment对象
*/
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

以上即是mybatis解析配置文件的全流程源码过程。那么接下来就是openSession打开数据库会话操作。然后数据库操作过程的源码分析。

openSession方法

image.png

有两种实现,进入DefaultSqlSessionFactory类。
image.png

openSessionFromDataSource方法

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
php复制代码/**
* 方法实现说明:从session中开启一个数据源
* @param execType:执行器类型
* @param level:隔离级别
* @return:SqlSession
* @exception:
* @date:2019/9/9 13:38
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
/**
* 获取环境变量
*/
final Environment environment = configuration.getEnvironment();
/**
* 获取事务工厂
*/
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
/**
* 创建一个sql执行器对象
* 一般情况下 若我们的mybaits的全局配置文件的cacheEnabled默认为ture就返回
* 一个cacheExecutor,若关闭的话返回的就是一个SimpleExecutor
* 详看下面newExecutor方法
*/
final Executor executor = configuration.newExecutor(tx, execType);
/**
* 创建返回一个DeaultSqlSessoin对象返回
*/
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

newExecutor方法

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
ini复制代码/**
* 方法实现说明:创建一个sql语句执行器对象
* @param transaction:事务
* @param executorType:执行器类型
* @return:Executor执行器对象
* @exception:
* @date:2019/9/9 13:59
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
/**
* 判断执行器的类型
* 批量的执行器
*/
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
//可重复使用的执行器
executor = new ReuseExecutor(this, transaction);
} else {
//简单的sql执行器对象
executor = new SimpleExecutor(this, transaction);
}
//判断mybatis的全局配置文件是否开启缓存
if (cacheEnabled) {
//把当前的简单的执行器包装成一个CachingExecutor
executor = new CachingExecutor(executor);
}
/**
* TODO:调用所有的拦截器对象plugin方法
*/
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

selectOne方法

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
typescript复制代码/**
* 方法实现说明:查询我们当个对象
* @param statement:我们的statementId(com.xxx.mapper.EmployeeMapper.findOne)
* @param parameter:调用时候的参数
* @return: T 返回结果
* @exception:
* @date:2019/9/9 20:26
*/
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
/**
* 这里selectOne调用也是调用selectList方法
*/
List<T> list = this.selectList(statement, parameter);
//若查询出来有且有一个一个对象,直接返回要给
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
/**
* 查询的有多个,那么久抛出我们熟悉的异常
* Expected one result (or null) to be returned by selectOne(), but found: " + list.size()
*/
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}

selectList方法

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
typescript复制代码/**
* 方法实现说明
* @param statement: statementId
* @param parameter:参数对象
* @param rowBounds :mybiats的逻辑分页对象
* @return:
* @exception:
* @date:2019/9/9 20:33
*/
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
/**
* 第一步:通过我们的statement去我们的全局配置类中获取MappedStatement
*/
MappedStatement ms = configuration.getMappedStatement(statement);
/**
* 通过执行器去执行我们的sql对象
* 第一步:包装我们的集合类参数
* 第二步:一般情况下是executor为cacheExetory对象
*/
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

wrapCollection方法

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
typescript复制代码/**
* 方法实现说明:包装我们集合类的参数
* @param object:参数对象
* @return:Object:包装后的对象
* @exception:
* @date:2019/9/9 20:36
*/
private Object wrapCollection(final Object object) {
//若我们的参数类型是Collection
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<>();
//把他key为collection存放到map中
map.put("collection", object);
//若我们参数类型是list类型 把key为list作为集合存放到map中
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
//若是数组,存放key为array的map中
StrictMap<Object> map = new StrictMap<>();
map.put("array", object);
return map;
}
return object;
}

CachingExecutor.query方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码/**
* 方法实现说明:通过我们的sql执行器对象执行sql
* @param ms 用于封装我们一个个的insert|delete|update|select 对象
* @param parameterObject:参数对象
* @param rowBounds :mybaits的逻辑分页对象 TODO?????
* @param resultHandler:结果处理器对象
* @return:
* @exception:
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/**
* 通过参数对象解析我们的sql详细信息1339025938:1570540512:com.xxx.mapper.selectById:0:2147483647:select id,user_name,create_time from t_user where id=?:1:development
*/
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
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
scss复制代码@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/**
* 判断我们我们的mapper中是否开启了二级缓存<cache></cache>
*/
Cache cache = ms.getCache();
/**
* 判断是否配置了cache
*/
if (cache != null) {
//判断是否需要刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
/**
* 先去二级缓存中获取
*/
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
/**
* 二级缓存中没有获取到
*/
if (list == null) {
//通过查询数据库去查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//加入到二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有整合二级缓存,直接去查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

BaseExecutor.query方法

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
scss复制代码@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// <2> 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// <4.1> 从一级缓存中,获取查询结果
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// <4.2> 获取到,则进行处理
if (list != null) {
//处理存过的
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 获得不到,则从数据库中查询 详细看下面queryFromDatabase方法
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}

queryFromDatabase方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 详细看下面doQuery方法
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

SimpleExecutor.doQuery方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 详细看下面newStatementHandler方法
StatementHandler handler = configuration.newStatementHandler方法(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//详细看下面prepareStatement方法
stmt = prepareStatement(handler, ms.getStatementLog());
// 详细看下面PreparedStatementHandler的query方法
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

newStatementHandler方法

1
2
3
4
5
ini复制代码public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

prepareStatement方法

1
2
3
4
5
6
7
ini复制代码private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}

PreparedStatementHandler.query方法

1
2
3
4
5
6
java复制代码public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行数据库操作
ps.execute();
return resultSetHandler.handleResultSets(ps);
}

本文转载自: 掘金

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

可扩展标记语言 XML 1、XML 简介 2、XML 语法

发表于 2021-11-28

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战


1、XML 简介

什么是xml?

可扩展标记语言,标准通用标记语言的子集,简称XML。是一种用于标记电子文件使其具有结构性的标记语言。

在电子计算机中,标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种的信息比如文章等。它可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 它非常适合万维网传输,提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。是Internet环境中跨平台的、依赖于内容的技术,也是当今处理分布式结构信息的有效工具。早在1998年,W3C就发布了XML1.0规范,使用它来简化Internet的文档信息传输。

xml 的作用?

XML 被设计用来传输和存储数据。


2、XML 语法

  1. 文档声明。
  2. 元素(标签)
  3. xml 属性
  4. xml 注释
  5. 文本区域(CDATA 区)

注:

  1. 不同于html,所有XML 元素都须有关闭标签(也就是闭合)
  2. XML 标签大小写敏感
  3. XML 必须正确地嵌套
  4. XML 文档必须有唯一的根元素,也就是顶级元素
  5. XML 的属性值须加引号
  6. XML 中的特殊字符的表示方法与html一样

2.1文档声明

创建一个 xml 文件

1
2
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<!-- xml 声明 version 是版本的意思 encoding 是编码 -->

属性

  1. version 是版本号
  2. encoding 是 xml 的文件编码
  3. standalone=”yes/no” 表示这个 xml 文件是否是独立的 xml 文

2.2 元素(标签)

2.2.1 什么是 XML 元素?

XML 元素 指的是从(且包括)开始标签直到(且包括)结束标签的部分。

元素可包含其他元素、文本或者两者的混合物。元素也可以拥有属性。xml元素类似html标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<bookstore>
<book category="CHILDREN">
<title>Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title>Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>

在上例中, 和 都拥有元素内容,因为它们包含了其他元素。 只有文本内容,因为它仅包含文本。

在上例中,只有 元素拥有属性 (category=”CHILDREN”)。

2.2.2 XML 命名规则

XML 元素必须遵循以下命名规则:

  • 名称可以含字母、数字以及其他的字符
  • 名称不能以数字或者标点符号开始
  • 名称不能以字符 “xml”(或者 XML、Xml)开始
  • 名称不能包含空格

可使用任何名称,没有保留的字词。


2.3 xml 属性

XML 元素可以在开始标签中包含属性,类似 HTML。

属性 (Attribute) 提供关于元素的额外信息。

属性值必须被引号包围

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!-- sex 是一个属性 -->
<person sex="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>

<!-- sex 是一个子元素 -->
<person>
<sex>female</sex>
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>

在第一个例子中,sex 是一个属性。在第二个例子中,sex 则是一个子元素。两个例子均可提供相同的信息。


2.4 xml 注释

html 和 XML 注释 一样 :

1
xml复制代码<!-- 注释 -->

2.5 文本区域(CDATA 区)

CDATA 里的文本内容,只是纯文本,不会被xml语法解析

CDATA 格式:

1
xml复制代码 <![CDATA[纯文本原样显示]]]>

例:

1
2
3
4
5
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<!-- xml 声明 version 是版本的意思 encoding 是编码 -->
<BOOK>
<![CDATA[纯文本原样显示]]]>
</BOOK>

3、XML解析技术—dom4j★

3.1 下载【dom4j】jar包

下载地址:dom4j.github.io/

image-20210504192938835

下载之后,创建一个 lib 目录,并添加 dom4j 的 jar 包。并添加到类路径。【dom4j 】jar 包就导入完成了


3.2 dom4j 编程步骤

  1. 先加载 xml 文件创建 Document 对象
  2. 通过 Document 对象拿到根元素对象
  3. 通过根元素.elelemts(标签名); 可以返回一个集合,这个集合里放着。所有你指定的标签名的元素对象
  4. 找到你想要修改、删除的子元素,进行相应操作

【student.xml】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xml复制代码<?xml version="1.0" encoding="utf-8" ?>
<!-- xml 声明 version 是版本的意思 encoding 是编码 -->

<students>
<student sn="2020020197"> <!-- sn属性为学号-->
<name>冯诺依曼</name> <!-- name标签为姓名-->
<age>20</age> <!-- age标签为年龄-->
</student>

<student sn="2020020198"> <!-- sn属性为学号-->
<name>图灵</name> <!-- name标签为姓名-->
<age>22</age> <!-- age标签为年龄-->
</student>
</students>

【Student.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
java复制代码public class Student {
private String sn;
private String name;
private int age;

public Student(String sn, String name, int age) {
this.sn = sn;
this.name = name;
this.age = age;
}

public Student() {
}

@Override
public String toString() {
return "Student{" +
"sn='" + sn + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}

public String getSn() {
return sn;
}

public void setSn(String sn) {
this.sn = sn;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

解析XML文件

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复制代码    public void getDocument() throws DocumentException {
// 1.通过创建 SAXReader 对象。来读取 xml 文件,获取 Document 对象
SAXReader reader = new SAXReader();
Document document = reader.read("src/student.xml");

// 2.通过 Document 对象。拿到 XML 的根元素对象
Element root = document.getRootElement();

// 3.通过根元素对象。获取所有的 book 标签对象
List<Element> students = root.elements("student");

// 4.遍历每个 student 标签对象。然后获取到 student 标签对象内的每一个元素
for (Element student : students) {
//elementText("name") 获取name标签的文本内容
String nameText = student.elementText("name");
//elementText("age") 获取age标签的文本内容
String ageText = student.elementText("age");
//attributeValue() 获取sn属性的文本内容
String snValue = student.attributeValue("sn");

//实例化javabean类接收
System.out.println(new Student(snValue, nameText, Integer.parseInt(ageText)));
}

}

应对梦想道路上的困苦艰难坎坷,执著是最好的利刃,它会帮忙一个人劈开艰难,穿越困境,抵达铺满鲜花的梦想

本文转载自: 掘金

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

动手实现一个java中的散列表(HashTable) 1 散

发表于 2021-11-28

​

1 散列

散列表(HashTable),我们平时也会称它为“哈希表”,是一种经常使用的数据结构,也是典型的空间换时间的思路的体现。

散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。

假如我们有 19 名选手参加学校运动会。为了方便记录成绩,每个选手胸前都会贴上自己的参赛号码,用 6 位数字来表示。比如 051167,其中,前两位 05 表示年级,中间两位 11 表示班级,最后两位是依次递增的编号 1 到 19。

尽管不能直接把编号作为数组下标,但我们可以截取参赛编号的后两位作为数组下标来存取选手信息。这就是典型的散列思想。其中,参赛选手的编号叫做key,参赛编号转化为数组下标的映射方法就叫作散列函数,而散列函数计算得到的值就是散列值,时间复杂度是 O(1)。

)​

2 散列函数

散列函数在散列表中起着非常关键的作用,顾名思义就是一个函数,可以将其定义为 hash(key),key 表示元素的键值,hash(key) 的值表示经过散列函数计算得到的散列值。

上述例子中,编号的后两位就是数组下标,所以 hash(key) 就等于 key,如下:

1
2
3
4
5
6
7
typescript复制代码int hash(String key) {
// 获取后两位字符
string lastTwoChars = key.substr(length-2, length);
// 将后两位字符转换为整数
int hashValue = Integer.parseInt(lastTwoChars);
return hashValue;
}

如果参赛选手的编号是随机生成的 6 位数字,又或者用的是 a 到 z 之间的字符串,我们可以依据以下三点来设计散列函数:

  1. 散列函数计算得到的散列值是一个非负整数;
  2. 如果 key1 = key2,那 hash(key1) == hash(key2);
  3. 如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)。

第三点要求看起来合理,但是在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的。此外,因为数组的存储空间有限,也会加大散列冲突的概率。所以针对散列冲突问题,我们需要通过其他途径来解决。

3 散列冲突

3.1 开放寻址法

开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。比较简单的探测方法是线性探测。

当往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

)​

在散列表中查找元素的过程类似插入过程。通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。相等则说明就是要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。

)​

)​

散列表跟数组一样,不仅支持插入、查找操作,还支持删除操作。对于使用线性探测法解决冲突的散列表,删除操作稍微有些特别。我们不能单纯地把要删除的元素设置为空。在查找的时候,一旦我们通过线性探测方法,找到一个空闲位置,我们就可以认定散列表中不存在这个数据。但是,如果这个空闲位置是我们后来删除的,就会导致原来的查找算法失效。本来存在的数据,会被认定为不存在。

解决方案是将删除的元素,特殊标记一下。当线性探测查找的时候,遇到特殊标记的空间时,并不是停下来,而是继续往下探测。

)​

线性探测法其实存在很大问题。当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,我们可能需要探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。同理,在删除和查找时,也有可能会线性探测整张散列表,才能找到要查找或者删除的数据。

为了尽可能保证散列表的操作效率,一般情况下,会保证散列表中有一定比例的空闲槽位,空位的多少则用负载因子(load factor)表示。

负载因子 = 填入表中的元素个数 / 散列表的长度

负载因子越大,说明空闲位置越少,冲突越多,散列表的性能会下降。

3.2 链表法

链表法是一种更加常用的散列冲突解决办法。在散列表中,每个“槽(slot)”会对应一条链表,所有散列值相同的元素都放到相同槽位对应的链表中。

)​

当插入的时候,我们只需要通过散列函数计算出对应的散列槽位,将其插入到对应链表中即可,所以插入的时间复杂度是 O(1)。当查找、删除一个元素时,我们同样通过散列函数计算出对应的槽,然后遍历链表查找或者删除。

关于查找和删除操作的时间复杂度,和链表的长度k成正比,即O(k)。对于散列比较均匀的散列函数来说,理论上讲,k=n/m,其中 n 表示散列中数据的个数,m 表示散列表中“槽”的个数。

4 手写HashTable底层实现

哈希表存储的是键值对,所以需要两个泛型来分别表示key和value的数据类型,key的数据类型必须实现hashcode()方法,不过由于java中所有的类都继承了Object类,所以默认所有的类都实现了hashcode()方法。

关键在于hashcode()是不是我们想要的。

另外我们也可以考虑哈希表的动态空间处理,当每个位置的平均哈希冲突超过上界时则扩容,小于下界时则缩容。

上面介绍链表法时,整个槽slot可以看做是一个TreeMap数组,数组的每一个位置存储了一个 链表 或者 红黑树,所以构造函数可以实现如下:

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
arduino复制代码public class HashTable<K, V> {

/**
* 容忍度上界
*/
private static final int upperTol = 10;

/**
* 容忍度下界
*/
private static final int lowerTol = 2;

/**
* 初始容量
*/
private static final int initCapacity = 7;

private TreeMap<K, V>[] hashtable;
// 长度(素数)
private int M;
// 已经存储了多少元素
private int size;

public HashTable(int M){
this.M = M;
size = 0;
hashtable = new TreeMap[M];
// 对每一个TreeMap进行初始化
for (int i = 0; i < M; i ++)
hashtable[i] = new TreeMap<>();
}

public HashTable(){
this(initCapacity);
}

}

在实现哈希表的增删改查之前,我们需要定义辅助方法hash用于计算要存储的位置,由于数组的索引是从0开始的,所以我们要避免负数的出现:

1
2
3
4
5
arduino复制代码private int hash(K key){
return key.hashCode() & 0x7fffffff % M;
}

public int getSize(){ return size; }

向哈希表中添加键值对:

  1. 根据key计算要插入的位置(对应一个TreeMap)
  2. 如果TreeMap中已经存在这个key的话就进行value的更新
  3. 如果TreeMap中不存在这个key的话则进行数据添加
  4. 当元素过多时进行扩容操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码 public void add(K key, V value){
// 计算要插入的位置
TreeMap<K, V> map = hashtable[hash(key)];
// key已存在
if (map.containsKey(key))
map.put(key, value);
else {
// key不存在
map.put(key, value);
size ++;

// 满足条件则扩容
if (size >= upperTol * M)
resize(2 * M);
}
}

删除操作:

  1. 根据key计算要删除key对应的位置(对应一个TreeMap)
  2. 判断是否包含这个key,存在的话则删除
  3. 当元素过少时进行扩容操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码public V remove(K key){
// 计算要删除的位置
TreeMap<K, V> map = hashtable[hash(key)];
V ret = null;
// 存在则删除
if (map.containsKey(key)){
ret = map.remove(key);
size --;

// 一定情况下缩容
if (size < lowerTol * M && M / 2 >= initCapacity)
resize(M / 2);
}
return ret;
}

修改操作:

  1. 根据key计算要进行修改的位置(对应一个TreeMap)
  2. 存在该元素时则修改
1
2
3
4
5
6
vbnet复制代码public void set(K key, V value){
TreeMap<K, V> map = hashtable[hash(key)];
if (!map.containsKey(key))
throw new IllegalArgumentException(key + " doesn't exist!");
map.put(key, value);
}

查询操作:

1
2
3
4
5
6
7
vbnet复制代码    public boolean contains(K key){
return hashtable[hash(key)].containsKey(key);
}

public V get(K key){
return hashtable[hash(key)].get(key);
}

动态空间处理方法:

类似于动态数组,开辟一个新的TreeMap数组,将原始数据全都放入到新的TreeMap数组中即可,注意hash()函数计算时需要使用新的Map长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码
private void resize(int newM){
TreeMap<K, V>[] newHashTable = new TreeMap[newM];
for (int i = 0; i < newM; i ++)
newHashTable[i] = new TreeMap<>();

int oldM = M;
// hash()函数需要新的M值
this.M = newM;
for (int i = 0; i < oldM; i ++){
TreeMap<K, V> map = hashtable[i];
for (K key: map.keySet())
newHashTable[hash(key)].put(key, map.get(key));
}

this.hashtable = newHashTable;
}

}

测试:

读取一本英文著作《傲慢与偏见》,统计词频,查看性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码public static void main(String[] args) {
System.out.println("Pride and Prejudice");
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)){
// HashTable
startTime = System.nanoTime();
HashTable<String, Integer> ht = new HashTable<>(131071);
for (String word : words) {
if (ht.contains(word))
ht.set(word, ht.get(word) + 1);
else
ht.add(word, 1);
}
for (String word: words)
ht.contains(word);
endTime = System.nanoTime();
time = (endTime - startTime)/1000000000.0;
System.out.println("HashTable: " + time);
}
}

执行用时:

)​

5 *应用

有了哈希表我们可以应用于数据可以无序存储但需要快速查询的场景。比如:Word 文档中单词拼写检查功能。

常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间。所以我们可以用散列表来存储整个英文单词词典。

当用户输入某个英文单词时,我们拿用户输入的单词去散列表中查找。如果查到,则说明拼写正确;如果没有查到,则说明拼写可能有误,给予提示。

6 阿里面试题

HashMap的初始容量是16,并且容量都维持在n次方原因:性能考虑。只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞(hash值 & length-1)

负载因子越小,内存占用越多,查询时间越快;内存吃紧的话可以调大负载因子,冲突概率会增加。

HashMap对key的要求:不可变的,比如String被final修饰的。

HashMap存在线程安全问题,1.8之前使用头插法会产生环。我们可以使用ConcurrentHashMap来避免线程安全问题。

1.7之前:采用了分段锁机制来储存数据。在集合对象中储存一个segment数组,把集合的元素分为16个分段,每个分段上储存一个HashEntry数组及对应的链表,每个HashEntry数组就相当于一个HashMap。

)​

当触发锁时每一个segment中都会有一个独立的锁,而不会将整个对象锁住,所以多线程操作时每个segment中的数据不会相互影响,从而保证了效率,当然,这样储存的版本还有着不少的问题,如最多并发只有16个,结构过于臃肿等等,随着版本的迭代,在jdk1.8时更改了ConcurrentHashMap的结构,使之更加完善。

1.8之后:利用CAS原理(比较交换原理)解决并发问题,其中有三个数:所存的值V,预期的值A,要变换成为的新值B。当A和V相同时,V会修改成B,否则不进行操作。这样就能较好的解决线程安全的问题。

\

​

本文转载自: 掘金

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

Rust - 结构体基本使用 基础代码示例 使用元组重构代码

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

基础代码示例

为了理解何时需要使用结构体,官方文档给了一个案例,就是计算长方形的面积,这里我们会一步一步的重构代码直到使用结构体为止。

计算长方形的面积的具体逻辑就是获取长方形的宽度和高度,然后通过公式计算出长方形的面积,如下代码示例:

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let width = 10;
let height = 20;
println!("长方形的面积是{}", area(width, height))
}

fn area(width: u32, height: u32) -> u32{
width * height
}

通过cargo run运行程序后可以得到如下结果:

1
复制代码长方形的面积是200

虽然上述代码可以运行,并且在调用area函数时通过传入长方形的长和宽可以计算出长方形的面积,但是宽度和高度是相关联的,这个问题尤其体现在area函数的签名上,虽然area可以计算长方形的面积,但是参数却有两个。这两个参数本应相互关联,但是程序本身并没有体现出这一点,如果可以将长度和宽度组合在一起就可以更易懂也更易处理了。因此我们可以使用元组进行代码重构。

1
rust复制代码fn area(width: u32, height: u32) -> u32

使用元组重构代码

使用元组重构的代码示例如下:

1
2
3
4
5
6
7
8
9
rust复制代码fn main() {
let tup_w_h = (10, 20); // 索引0对应宽度

println!("长方形的面积是{}", area(tup_w_h))
}

fn area(tup_w_h: (u32, u32)) -> u32{
tup_w_h.0 * tup_w_h.1
}

元组版本的代码比基础代码更好一些,帮助我们增加一些结构性,而且area只需要传递一个参数即可,但是这个版本的代码有一点不是非常明确:就是元组并没有给出元素的名称,只能通过索引获取元组的每一部分,虽然在计算面积时将长宽弄混无关紧要但是如果在屏幕上绘制就会出现很大问题了,就必须搞清楚元组中哪个元素是长哪个是宽,这样就很容易混淆而造成错误,原因就是没有在代码中传达数据所代表的意义。

使用结构体重构代码

现在我们就可以使用结构体为数据命名赋予意义,可以将元组转换成一个有整体名称而且每个部分也有对应名字的数据类型-结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rust复制代码struct WidthHeight {
width: u32,
height: u32,
}

fn main() {
let tup_w_h = WidthHeight {width: 10, height: 20}; // 索引0对应宽度

println!("长方形的面积是{}", area(&tup_w_h))
}

fn area(width_height: &WidthHeight) -> u32{
width_height.height * width_height.width
}

上面的示例代码中定义了结构体WidthHeight,在在大括号中定义了字段 width 和 height,类型都是 u32。接着在 main 中,我们创建了一个具体的 WidthHeight 实例。

函数 area 现在被定义为接收一个名叫 width_height 的参数,其类型是一个结构体 WidthHeight 实例的不可变借用。前面的文章提到,我们希望借用结构体而不是获取它的所有权,这样 main 函数就可以保持 tup_w_h 的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &。

area 函数访问 WidthHeight 实例的 width 和 height 字段。area 的函数签名现在明确的阐述了我们的意图:使用 WidthHeight 的 width 和 height 字段,计算 WidthHeight 的面积。这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 0 和 1 。结构体胜在更清晰明了。

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

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

bash shell基本语法之函数 前言 函数

发表于 2021-11-28

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

前言

本文对shell中函数的定义、使用、返回值等作一个示例说明.

函数

定义

bash shell函数的定义需要使用function来声明,格式如下:

function function_name

{

....相关代码/命令

}

上面的写法是在bash shell中,其它shell可能不支持,比如dash就不能识别function。

除了上面那种写法,还有另一种写法:

function_name()

{

...相关代码/命令

}

在函数名后面加一对英文括号()来声明(这个在bash和dash通用,其它的脚本没用过)。

function_name是函数名,必须是唯一的。函数需要先定义后使用。

使用

使用的时候,就像执行一个命令一样,调用函数名即可(不要像其它语言如c一样加括号)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码#! /bin/bash

#定义函数hello
function hello
{
echo "hello, world!"
}
#调用函数hello
hello

#定义函数hello2
hello2()
{
echo "hello, world2!"
}
#调用函数hello2
hello2

执行结果如下:

函数名重复

上面函数定义提到函数名必须唯一,如果不唯一呢,作个验证,将上面的示例代码改下,两个函数名改成一样:hello,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码#! /bin/bash

#定义函数hello
function hello
{
echo "hello, world!"
}
#调用函数hello
hello

#定义函数hello2
function hello
{
echo "hello, world2!"
}
#调用函数hello2
hello

看下执行结果:

重名的话,后面定义的函数把前面的定义覆盖了

函数参数用法

即使函数要传递参数,函数的定义还是上面定义的格式。调用函数时想要传递参数,用法就和执行脚本一样,在调用函数后以空格分隔设置参数,如下:

function_name arg1 arg2 arg3 …

如果想要在函数体内获得参数值,使用位置变量获得。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码#! /bin/bash

#定义函数hello
hello()
{
echo "脚本名: $0"
#打印前3个参数的值
echo "hello, $1 $2 $3"
}
#调用函数hello
hello 小明 小红

注意一下,这里$0的值是执行的脚本的名字,可不是调用的函数名。获取3个参数,但是我只传递了两个,那个后面的就是空了,执行结果如下:

注意一下,虽然脚本第一行我声明的#!使用bash来解释,但是执行的时候,我是直接使用bash解释执行的,也就是说#!声明不生效的,这是为了$0可以获得执行的当前脚本的名字。

函数返回值

函数的返回值需要注意以下3点:

  1. 函数的返回值可以使用$?获取,来表示函数的调用状态:成功/失败。默认返回值情况下:0表示成功,非0表示失败,就像命令一样,如下:
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码#! /bin/bash

#定义函数hello
hello()
{
echo "脚本名: $0"
#打印前3个参数的值
echo "hello, $1 $2 $3"
}
#调用函数hello
hello 小明 小红
echo "函数执行结果:$?"

执行结果:

  1. 使用return返回特定的状态码,但是这个值必须在0-255之间,如下:
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
bash复制代码#! /bin/bash

less_than_0()
{
return -1
}

between_0_255()
{
return 100
}

eq_255()
{
return 255
}

greater_than_255()
{
return 256
}
less_than_0
echo "less_than_0: $?"
between_0_255
echo "between_0_255: $?"
eq_255
echo "eq_255: $?"
greater_than_255
echo "greater_than_255: $?"

执行结果:

不在0-255的用其它值代替。

  1. 如果这个返回值不是一个数字,而我们用$?获取这个结果,如下:
1
2
3
4
5
6
7
8
bash复制代码#! /bin/bash

fun_string_test()
{
return "hello,world"
}
fun_string_test
echo "fun_string_test: $?"

执行结果:

调用失败,返回值必须是数字类型

本文转载自: 掘金

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

1…142143144…956

开发者博客

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